Skip to content

Commit

Permalink
[Usage] Attempt to fix the usage time calculation method
Browse files Browse the repository at this point in the history
The usage events returned by the UsageStatsManager was assumed to be in order of
their timestamp which does not seem to be true and resulted in missing a few
events due to the standard calculation method, that is, calculating the time
difference between activity resume and pause time. In addition, it appears that
the system may log activity stop time without logging any pause time (a typical
cycle would be resume --> pause --> stop) causing further miss of events. All of
these issues are addressed by sorting the events in order of their timestamp as
well as measure the time difference between resume and stop times instead of
resume and pause times. However, this does not solve the problems with access
count since it is still is a summation of the resume-stop cycles.

Signed-off-by: Muntashir Al-Islam <[email protected]>
  • Loading branch information
MuntashirAkon committed Nov 6, 2024
1 parent d3995a7 commit e6e325a
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,18 @@
import android.os.RemoteException;
import android.os.UserHandleHidden;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresPermission;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import io.github.muntashirakon.AppManager.ipc.ProxyBinder;
import io.github.muntashirakon.AppManager.self.SelfPermissions;
import io.github.muntashirakon.AppManager.users.Users;
import io.github.muntashirakon.AppManager.utils.ArrayUtils;
import io.github.muntashirakon.AppManager.utils.BroadcastUtils;
import io.github.muntashirakon.AppManager.utils.ContextUtils;
import io.github.muntashirakon.AppManager.utils.ExUtils;
Expand All @@ -30,21 +39,52 @@ public final class UsageStatsManagerCompat {
}
}

public static UsageEvents queryEvents(long beginTime, long endTime, int userId) throws RemoteException {
IUsageStatsManager usm = getUsageStatsManager();
String callingPackage = SelfPermissions.getCallingPackage(Users.getSelfOrRemoteUid());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return usm.queryEventsForUser(beginTime, endTime, userId, callingPackage);
@RequiresPermission("android.permission.PACKAGE_USAGE_STATS")
@Nullable
public static UsageEvents queryEvents(long beginTime, long endTime, int userId) {
try {
IUsageStatsManager usm = getUsageStatsManager();
String callingPackage = SelfPermissions.getCallingPackage(Users.getSelfOrRemoteUid());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return usm.queryEventsForUser(beginTime, endTime, userId, callingPackage);
}
return usm.queryEvents(beginTime, endTime, callingPackage);
} catch (RemoteException e) {
return ExUtils.rethrowFromSystemServer(e);
}
}

/**
* Note: This method should only be used when sorted entries are required as the operations done
* here are expensive.
*/
@RequiresPermission("android.permission.PACKAGE_USAGE_STATS")
@NonNull
public static List<UsageEvents.Event> queryEventsSorted(long beginTime, long endTime, int userId, int[] filterEvents) {
List<UsageEvents.Event> filteredEvents = new ArrayList<>();
UsageEvents events = queryEvents(beginTime, endTime, userId);
if (events != null) {
while (events.hasNextEvent()) {
UsageEvents.Event event = new UsageEvents.Event();
events.getNextEvent(event);
if (ArrayUtils.contains(filterEvents, event.getEventType())) {
filteredEvents.add(event);
}
}
Collections.sort(filteredEvents, (o1, o2) -> -Long.compare(o1.getTimeStamp(), o2.getTimeStamp()));
}
return usm.queryEvents(beginTime, endTime, callingPackage);
return filteredEvents;
}

public static void setAppInactive(String packageName, @UserIdInt int userId, boolean inactive)
throws RemoteException {
public static void setAppInactive(String packageName, @UserIdInt int userId, boolean inactive) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
getUsageStatsManager().setAppInactive(packageName, inactive, userId);
if (userId != UserHandleHidden.myUserId()) {
BroadcastUtils.sendPackageAltered(ContextUtils.getContext(), new String[]{packageName});
try {
getUsageStatsManager().setAppInactive(packageName, inactive, userId);
if (userId != UserHandleHidden.myUserId()) {
BroadcastUtils.sendPackageAltered(ContextUtils.getContext(), new String[]{packageName});
}
} catch (RemoteException e) {
ExUtils.rethrowFromSystemServer(e);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import io.github.muntashirakon.AppManager.compat.ManifestCompat;
import io.github.muntashirakon.AppManager.compat.NetworkStatsCompat;
Expand Down Expand Up @@ -163,17 +164,17 @@ public List<PackageUsageInfo> getUsageStats(@UsageUtils.IntervalType int usageIn
throws RemoteException, SecurityException {
List<PackageUsageInfo> packageUsageInfoList = new ArrayList<>();
int _try = 5; // try to get usage stats at most 5 times
RemoteException re;
Throwable re;
do {
try {
packageUsageInfoList.addAll(getUsageStatsInternal(usageInterval, userId));
re = null;
} catch (RemoteException e) {
} catch (Throwable e) {
re = e;
}
} while (0 != --_try && packageUsageInfoList.isEmpty());
if (re != null) {
throw re;
throw (RemoteException) (new RemoteException(re.getMessage()).initCause(re));
}
return packageUsageInfoList;
}
Expand All @@ -188,27 +189,31 @@ public PackageUsageInfo getUsageStatsForPackage(@NonNull String packageName,
ApplicationInfo applicationInfo = PackageManagerCompat.getApplicationInfo(packageName, MATCH_UNINSTALLED_PACKAGES
| PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userId);
PackageUsageInfo packageUsageInfo = new PackageUsageInfo(mContext, packageName, userId, applicationInfo);
UsageEvents events = UsageStatsManagerCompat.queryEvents(range.getStartTime(), range.getEndTime(), userId);
if (events == null) return packageUsageInfo;
UsageEvents.Event event = new UsageEvents.Event();
List<UsageEvents.Event> events = UsageStatsManagerCompat.queryEventsSorted(range.getStartTime(), range.getEndTime(), userId, new int[]{UsageEvents.Event.ACTIVITY_RESUMED, UsageEvents.Event.ACTIVITY_PAUSED, UsageEvents.Event.ACTIVITY_STOPPED});
List<PackageUsageInfo.Entry> usEntries = new ArrayList<>();
long startTime = 0;
long endTime = 0;
while (events.hasNextEvent()) {
events.getNextEvent(event);
String currentPackageName = event.getPackageName();
int eventType = event.getEventType();
long eventTime = event.getTimeStamp();
if (currentPackageName.equals(packageName)) {
if (eventType == UsageEvents.Event.ACTIVITY_RESUMED) {
if (startTime == 0) startTime = eventTime;
} else if (eventType == UsageEvents.Event.ACTIVITY_PAUSED) {
if (startTime > 0) endTime = eventTime;
for (UsageEvents.Event event : events) {
if (Objects.equals(packageName, event.getPackageName())) {
int eventType = event.getEventType();
// Queries are sorted in descending order, so a not-running activity should be paused
// or stopped first and then resumed (i.e., reversed logic)
if (eventType == UsageEvents.Event.ACTIVITY_STOPPED || eventType == UsageEvents.Event.ACTIVITY_PAUSED) {
if (endTime > 0) {
// Log.d(TAG, "Start time non-zero (%d) for package %s", endTime, packageName);
// Prefer stop times over pause. So, ignore all the subsequent events until an
// resume event is found. This may result in inaccurate access count. However,
// this inaccuracy is acceptable.
continue;
}
endTime = event.getTimeStamp();
} else if (eventType == UsageEvents.Event.ACTIVITY_RESUMED) {
if (endTime == 0) {
Log.d(TAG, "Start time is zero for package %s, skipping...", packageName);
continue;
}
usEntries.add(new PackageUsageInfo.Entry(event.getTimeStamp(), endTime));
endTime = 0;
}
} else if (startTime > 0 && endTime > 0) {
usEntries.add(new PackageUsageInfo.Entry(startTime, endTime));
startTime = 0;
endTime = 0;
}
}
packageUsageInfo.entries = usEntries;
Expand All @@ -226,53 +231,50 @@ public PackageUsageInfo getUsageStatsForPackage(@NonNull String packageName,
*/
@NonNull
private List<PackageUsageInfo> getUsageStatsInternal(@UsageUtils.IntervalType int usageInterval,
@UserIdInt int userId)
throws RemoteException {
@UserIdInt int userId) {
List<PackageUsageInfo> screenTimeList = new ArrayList<>();
Map<String, Long> endTimes = new HashMap<>();
Map<String, Long> screenTimes = new HashMap<>();
Map<String, Long> lastUse = new HashMap<>();
Map<String, Integer> accessCount = new HashMap<>();
// Get events
UsageUtils.TimeInterval interval = UsageUtils.getTimeInterval(usageInterval);
UsageEvents events = UsageStatsManagerCompat.queryEvents(interval.getStartTime(), interval.getEndTime(), userId);
if (events == null) {
return Collections.emptyList();
}
UsageEvents.Event event = new UsageEvents.Event();
long startTime;
long endTime;
boolean skip_new = false;
while (events.hasNextEvent()) {
if (!skip_new) events.getNextEvent(event);
List<UsageEvents.Event> events = UsageStatsManagerCompat.queryEventsSorted(interval.getStartTime(), interval.getEndTime(), userId, new int[]{UsageEvents.Event.ACTIVITY_RESUMED, UsageEvents.Event.ACTIVITY_PAUSED, UsageEvents.Event.ACTIVITY_STOPPED});
for (UsageEvents.Event event : events) {
int eventType = event.getEventType();
long eventTime = event.getTimeStamp();
String packageName = event.getPackageName();
if (eventType == UsageEvents.Event.ACTIVITY_RESUMED) { // App opened: MOVE_TO_FOREGROUND
startTime = eventTime;
while (events.hasNextEvent()) {
events.getNextEvent(event);
eventType = event.getEventType();
eventTime = event.getTimeStamp();
if (eventType == UsageEvents.Event.ACTIVITY_RESUMED) {
skip_new = true;
break;
} else if (eventType == UsageEvents.Event.ACTIVITY_PAUSED) {
endTime = eventTime;
skip_new = false;
if (packageName.equals(event.getPackageName())) {
long time = endTime - startTime + 1;
if (screenTimes.containsKey(packageName)) {
screenTimes.put(packageName, NonNullUtils.defeatNullable(screenTimes
.get(packageName)) + time);
} else screenTimes.put(packageName, time);
lastUse.put(packageName, endTime);
if (accessCount.containsKey(packageName)) {
accessCount.put(packageName, NonNullUtils.defeatNullable(accessCount
.get(packageName)) + 1);
} else accessCount.put(packageName, 1);
}
break;
}
// Queries are sorted in descending order, so a not-running activity should be paused or
// stopped first and then resumed (i.e., reversed logic).
if (eventType == UsageEvents.Event.ACTIVITY_STOPPED || eventType == UsageEvents.Event.ACTIVITY_PAUSED) {
if (endTimes.get(packageName) != null) {
// Log.d(TAG, "(%2d) Start time non-zero (%d) for package %s", eventType, endTimes.get(packageName), packageName);
// Prefer stop times over pause. So, ignore all the subsequent events until an
// resume event is found. This may result in inaccurate access count. However,
// this inaccuracy is acceptable.
continue;
}
// Override previous pause/stops
endTimes.put(event.getPackageName(), event.getTimeStamp());
} else if (eventType == UsageEvents.Event.ACTIVITY_RESUMED) {
Long endTime = endTimes.remove(packageName);
if (endTime == null) {
Log.d(TAG, "Start time is zero for package %s", packageName);
continue;
}
long time = endTime - event.getTimeStamp();
if (screenTimes.containsKey(packageName)) {
screenTimes.put(packageName, NonNullUtils.defeatNullable(screenTimes
.get(packageName)) + time);
} else screenTimes.put(packageName, time);
// FIXME: 11/6/24 Access count is still not perfect since it counts access through
// activities. Instead, access counts should be measured for the entire time the
// app is running after it's opened.
if (accessCount.containsKey(packageName)) {
accessCount.put(packageName, NonNullUtils.defeatNullable(accessCount
.get(packageName)) + 1);
} else accessCount.put(packageName, 1);
if (lastUse.get(packageName) == null) {
lastUse.put(packageName, event.getTimeStamp());
}
}
}
Expand Down Expand Up @@ -303,20 +305,18 @@ private List<PackageUsageInfo> getUsageStatsInternal(@UsageUtils.IntervalType in

@RequiresPermission("android.permission.PACKAGE_USAGE_STATS")
public static long getLastActivityTime(String packageName, @NonNull UsageUtils.TimeInterval interval) {
try {
UsageEvents events = UsageStatsManagerCompat.queryEvents(interval.getStartTime(), interval.getEndTime(),
UserHandleHidden.myUserId());
if (events == null) return 0L;
UsageEvents.Event event = new UsageEvents.Event();
while (events.hasNextEvent()) {
events.getNextEvent(event);
if (event.getPackageName().equals(packageName)) {
return event.getTimeStamp();
}
UsageEvents events = UsageStatsManagerCompat.queryEvents(interval.getStartTime(), interval.getEndTime(),
UserHandleHidden.myUserId());
if (events == null) return 0L;
UsageEvents.Event event = new UsageEvents.Event();
long lastTime = 0L;
while (events.hasNextEvent()) {
events.getNextEvent(event);
if (event.getPackageName().equals(packageName) && lastTime < event.getTimeStamp()) {
lastTime = event.getTimeStamp();
}
} catch (RemoteException ignore) {
}
return 0L;
return lastTime;
}

@NonNull
Expand Down

0 comments on commit e6e325a

Please sign in to comment.