Technical details of App Gatekeeper version 1.1

This post explains the technical details of how App Gatekeeper supports Android 5 & above devices, and the changes involved with the implementation of monitoring service to run as separate process.

In the initial version, App Gatekeeper monitoring service used the API ActivityManager.getRecentTasks
This method was deprecated in API level 21 (Android 5).

In the new version, it still uses ActivityManager.getRecentTasks for devices earlier than Android 5. For Android 5 & above devices, it uses
This is added in API level 21.

As the monitoring service needs to keep track of the app that the user is using at the moment, so as to check against its allowed usage and also its allowed periods of times, the UsageStatsManager.queryEvents satisfies this requirement by reporting UsageEvents.Events which include moved to foreground and moved to background events.

The usage time of an app will then be approximately the difference in time when the app fades to background after it moves to foreground, and in between, the user is assumed to be using the app.

The monitoring service queries in 1 second interval, so if the current time ( = System.currentTimeMillis() ) is used, it would be something like:
UsageStatsManager.queryEvents(time - 1000, time);

However, when this is being tested on Nexus 7, it would usually report no events, even when an app is being launched. So, it seems as if the API is not working.

After some trials and errors, it 's discovered that the API actually works, if the query time is a bit towards the past. I call this a system lag in reporting the events, as if the system requires a bit of time to collect the user interface events before it can report these events to others.

To ensure the monitoring service can receive all events, then its final query would be something like:
UsageStatsManager.queryEvents(time - 1000 - systemLag, time - systemLag);

A conservative systemLag that is found to work reliably when tested on Nexus 7 2012 is 2500 ms (or 2.5 second). The final implementation includes a new preference setting: System lag (secs) with default value of 2.5 . This System lag setting is only enabled on Android 5 & above devices. In the Setting UI, the System lag description is "For developer/tester only". So, this post also explains what the System lag setting is for. (Hopefully, the default value also works reliably for other devices)

So, this obviously means that the monitoring service only detects an app launch about at least 2.5 seconds later. It thus does not work perfectly especially if checking against allowed periods of times.

UsageStatsManager offers another API
UsageStatsManager.queryUsageStats which can be used to show all apps current usage for the day , where the app usage = UsageStats.getTotalTimeInForeground

UsageStats.getLastTimeUsed can also be used to detect the most recent app by finding the app with the maximum getLastTimeUsed.

The UsageStatsManager.queryUsageStats is however more expensive to use, it takes much longer time to execute as compared to UsageStatsManager.queryEvents. It is suitable to use it to retrieve all apps usages.

From testing, it seems that when UsageStatsManager.queryEvents reports no events, then UsageStatsManager.queryUsageStats also reports no usage records. (when using a small interval)

From apps usage reporting point of view, the monitoring service collects apps usage in units of seconds. The results will be less accurate than the Android system, and it also does not collect any usage if the service is not running.

If user disables usage access for App Gatekeeper in Settings -> Security -> Apps with usage access, when the monitoring service is running, the service will still continue to run unaware of what happens, but it will not receive any results from the Android API until the user re-enables usage access. In the meanwhile, it is unable to collect any usage. (Note: a special check is performed against Android Settings so that it does not assume the user is using the Android Settings in the meanwhile)

In the new version, the monitoring service is implemented to run as separate process from the main user interface program.

The monitoring service does not initiate any communication to the UI, and the UI only need to communicate to the service to retrieve the apps usage for today. In other cases, the UI simply reads the apps usage from the database. So, the changes for this implementation is not substantial.

In the initial version, because the UI and the service reside in the same process, the UI simply gets the service object and calls a service method directly to retrieve the current apps usage.

In the new version, the simplest implementation is used. The UI calls startService with an Intent with a Messenger instance. The monitoring service then send back the current apps usage via Messenger.send method.

This message-based communication across processes is still fast.

The other notable change is that the UI requires to know when it is running and also when it is not running (meaning the user has switches to another app). Previously, the easiest implementation is to rely on the monitoring service to inform the UI.

In the new version, the UI performs this checking itself, by checking the sequences of all activities onResume and onStop. Using the fact that if activity A launches activity B, B.onResume is called before A.onStop , which means that on B.onResume tells us that B is the current activity, and on A.onStop is not stopping the current activity, and thus our UI is still running. When activity A resumes, the same sequence happens: A.onResume is called before B.onStop . In the case when an activity onStop is called and it is the current activity, then it means the UI is no longer running.

An exception to this is when an activity onStop is called because it is being recreated when there is a configuration changed like when user changes orientation of device. This condition can be checked with Activity.isChangingConfigurations.

For released notes: see App Gatekeeper updated to version 1.1.


blog comments powered by Disqus