diff --git a/core/java/android/app/usage/UsageEvents.java b/core/java/android/app/usage/UsageEvents.java
index 84c68552c40a6..5dbca12fa3ddf 100644
--- a/core/java/android/app/usage/UsageEvents.java
+++ b/core/java/android/app/usage/UsageEvents.java
@@ -275,11 +275,25 @@ public final class UsageEvents implements Parcelable {
*/
public static final int DEVICE_STARTUP = 27;
+ /**
+ * An event type denoting that a user has been unlocked for the first time. This event
+ * mainly indicates when the user's credential encrypted storage was first accessible.
+ * @hide
+ */
+ public static final int USER_UNLOCKED = 28;
+
+ /**
+ * An event type denoting that a user has been stopped. This typically happens when the
+ * system is being turned off or when users are being switched.
+ * @hide
+ */
+ public static final int USER_STOPPED = 29;
+
/**
* Keep in sync with the greatest event type value.
* @hide
*/
- public static final int MAX_EVENT_TYPE = 27;
+ public static final int MAX_EVENT_TYPE = 29;
/** @hide */
public static final int FLAG_IS_PACKAGE_INSTANT_APP = 1 << 0;
diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java
index 749a011153cdc..656f474f72cee 100644
--- a/core/java/android/app/usage/UsageStatsManager.java
+++ b/core/java/android/app/usage/UsageStatsManager.java
@@ -31,6 +31,7 @@ import android.content.pm.ParceledListSlice;
import android.os.Build;
import android.os.RemoteException;
import android.os.UserHandle;
+import android.os.UserManager;
import android.util.ArrayMap;
import java.lang.annotation.Retention;
@@ -290,6 +291,9 @@ public final class UsageStatsManager {
*
*
* The caller must have {@link android.Manifest.permission#PACKAGE_USAGE_STATS}
+ * Note: Starting from {@link android.os.Build.VERSION_CODES#R Android R}, if the user's
+ * device is not in an unlocked state (as defined by {@link UserManager#isUserUnlocked()}),
+ * then {@code null} will be returned.
*
* @param intervalType The time interval by which the stats are aggregated.
* @param beginTime The inclusive beginning of the range of stats to include in the results.
@@ -324,6 +328,9 @@ public final class UsageStatsManager {
* the specified interval. The results are ordered as in
* {@link #queryUsageStats(int, long, long)}.
* The caller must have {@link android.Manifest.permission#PACKAGE_USAGE_STATS}
+ * Note: Starting from {@link android.os.Build.VERSION_CODES#R Android R}, if the user's
+ * device is not in an unlocked state (as defined by {@link UserManager#isUserUnlocked()}),
+ * then {@code null} will be returned.
*
* @param intervalType The time interval by which the stats are aggregated.
* @param beginTime The inclusive beginning of the range of stats to include in the results.
@@ -362,6 +369,9 @@ public final class UsageStatsManager {
*
*
* The caller must have {@link android.Manifest.permission#PACKAGE_USAGE_STATS}
+ * Note: Starting from {@link android.os.Build.VERSION_CODES#R Android R}, if the user's
+ * device is not in an unlocked state (as defined by {@link UserManager#isUserUnlocked()}),
+ * then {@code null} will be returned.
*
* @param intervalType The time interval by which the stats are aggregated.
* @param beginTime The inclusive beginning of the range of stats to include in the results.
@@ -395,6 +405,9 @@ public final class UsageStatsManager {
* Query for events in the given time range. Events are only kept by the system for a few
* days.
* The caller must have {@link android.Manifest.permission#PACKAGE_USAGE_STATS}
+ * Note: Starting from {@link android.os.Build.VERSION_CODES#R Android R}, if the user's
+ * device is not in an unlocked state (as defined by {@link UserManager#isUserUnlocked()}),
+ * then {@code null} will be returned.
*
* @param beginTime The inclusive beginning of the range of events to include in the results.
* Defined in terms of "Unix time", see
@@ -418,6 +431,9 @@ public final class UsageStatsManager {
/**
* Like {@link #queryEvents(long, long)}, but only returns events for the calling package.
+ * Note: Starting from {@link android.os.Build.VERSION_CODES#R Android R}, if the user's
+ * device is not in an unlocked state (as defined by {@link UserManager#isUserUnlocked()}),
+ * then {@code null} will be returned.
*
* @param beginTime The inclusive beginning of the range of events to include in the results.
* Defined in terms of "Unix time", see
diff --git a/core/java/android/app/usage/UsageStatsManagerInternal.java b/core/java/android/app/usage/UsageStatsManagerInternal.java
index bbec6b386d60a..b3260c4c5ccec 100644
--- a/core/java/android/app/usage/UsageStatsManagerInternal.java
+++ b/core/java/android/app/usage/UsageStatsManagerInternal.java
@@ -21,6 +21,7 @@ import android.app.usage.UsageStatsManager.StandbyBuckets;
import android.content.ComponentName;
import android.content.res.Configuration;
import android.os.UserHandle;
+import android.os.UserManager;
import java.util.List;
import java.util.Set;
@@ -33,7 +34,10 @@ import java.util.Set;
public abstract class UsageStatsManagerInternal {
/**
- * Reports an event to the UsageStatsManager.
+ * Reports an event to the UsageStatsManager.
+ * Note: Starting from {@link android.os.Build.VERSION_CODES#R Android R}, if the user's
+ * device is not in an unlocked state (as defined by {@link UserManager#isUserUnlocked()}),
+ * then this event will be added to a queue and processed once the device is unlocked.
*
* @param component The component for which this event occurred.
* @param userId The user id to which the component belongs to.
@@ -48,7 +52,10 @@ public abstract class UsageStatsManagerInternal {
int instanceId, ComponentName taskRoot);
/**
- * Reports an event to the UsageStatsManager.
+ * Reports an event to the UsageStatsManager.
+ * Note: Starting from {@link android.os.Build.VERSION_CODES#R Android R}, if the user's
+ * device is not in an unlocked state (as defined by {@link UserManager#isUserUnlocked()}),
+ * then this event will be added to a queue and processed once the device is unlocked.
*
* @param packageName The package for which this event occurred.
* @param userId The user id to which the component belongs to.
@@ -58,14 +65,20 @@ public abstract class UsageStatsManagerInternal {
public abstract void reportEvent(String packageName, @UserIdInt int userId, int eventType);
/**
- * Reports a configuration change to the UsageStatsManager.
+ * Reports a configuration change to the UsageStatsManager.
+ * Note: Starting from {@link android.os.Build.VERSION_CODES#R Android R}, if the user's
+ * device is not in an unlocked state (as defined by {@link UserManager#isUserUnlocked()}),
+ * then this event will be added to a queue and processed once the device is unlocked.
*
* @param config The new device configuration.
*/
public abstract void reportConfigurationChange(Configuration config, @UserIdInt int userId);
/**
- * Reports that an application has posted an interruptive notification.
+ * Reports that an application has posted an interruptive notification.
+ * Note: Starting from {@link android.os.Build.VERSION_CODES#R Android R}, if the user's
+ * device is not in an unlocked state (as defined by {@link UserManager#isUserUnlocked()}),
+ * then this event will be added to a queue and processed once the device is unlocked.
*
* @param packageName The package name of the app that posted the notification
* @param channelId The ID of the NotificationChannel to which the notification was posted
@@ -75,7 +88,10 @@ public abstract class UsageStatsManagerInternal {
@UserIdInt int userId);
/**
- * Reports that an action equivalent to a ShortcutInfo is taken by the user.
+ * Reports that an action equivalent to a ShortcutInfo is taken by the user.
+ * Note: Starting from {@link android.os.Build.VERSION_CODES#R Android R}, if the user's
+ * device is not in an unlocked state (as defined by {@link UserManager#isUserUnlocked()}),
+ * then this event will be added to a queue and processed once the device is unlocked.
*
* @param packageName The package name of the shortcut publisher
* @param shortcutId The ID of the shortcut in question
diff --git a/core/proto/android/server/usagestatsservice.proto b/core/proto/android/server/usagestatsservice.proto
index f26eefad24e18..75f265ea6a881 100644
--- a/core/proto/android/server/usagestatsservice.proto
+++ b/core/proto/android/server/usagestatsservice.proto
@@ -114,4 +114,6 @@ message IntervalStatsProto {
repeated UsageStats packages = 20;
repeated Configuration configurations = 21;
repeated Event event_log = 22;
+
+ repeated Event pending_events = 23; // TODO: move to usagestatsservice_v2.proto
}
diff --git a/core/tests/coretests/src/android/app/usage/UsageStatsTest.java b/core/tests/coretests/src/android/app/usage/UsageStatsTest.java
index 1633e1ac50a08..6ec3dc923efdc 100644
--- a/core/tests/coretests/src/android/app/usage/UsageStatsTest.java
+++ b/core/tests/coretests/src/android/app/usage/UsageStatsTest.java
@@ -34,7 +34,10 @@ import static org.junit.Assert.fail;
import android.app.usage.UsageEvents.Event;
import android.os.Parcel;
+import android.os.UserHandle;
+import android.support.test.uiautomator.UiDevice;
+import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -120,6 +123,15 @@ public class UsageStatsTest {
assertEquals(left.getTotalTimeInForeground(), 11);
}
+ @Test
+ public void testDataIsInCe() throws Exception {
+ final int userId = UserHandle.myUserId();
+ final String expectedPath = "/data/system_ce/" + userId + "/usagestats";
+ final String actualPath = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ .executeShellCommand("dumpsys usagestats stats-directory " + userId).trim();
+ assertEquals(expectedPath, actualPath);
+ }
+
@Test
public void testParcelable() {
left.mPackageName = "com.test";
diff --git a/services/usage/java/com/android/server/usage/UsageStatsProto.java b/services/usage/java/com/android/server/usage/UsageStatsProto.java
index 3e88d93cc303b..6d3f416065a37 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsProto.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsProto.java
@@ -606,4 +606,37 @@ final class UsageStatsProto {
proto.flush();
}
+
+ // TODO: move to UsageStatsProtoV2
+ static void readPendingEvents(InputStream in, List events)
+ throws IOException {
+ final ProtoInputStream proto = new ProtoInputStream(in);
+ final List stringPool = new ArrayList<>();
+ final IntervalStats tmpStatsObj = new IntervalStats();
+ while (true) {
+ switch (proto.nextField()) {
+ case (int) IntervalStatsProto.PENDING_EVENTS:
+ loadEvent(proto, IntervalStatsProto.PENDING_EVENTS, tmpStatsObj, stringPool);
+ break;
+ case ProtoInputStream.NO_MORE_FIELDS:
+ final int eventCount = tmpStatsObj.events.size();
+ for (int i = 0; i < eventCount; i++) {
+ events.add(tmpStatsObj.events.get(i));
+ }
+ return;
+ }
+ }
+ }
+
+ // TODO: move to UsageStatsProtoV2
+ static void writePendingEvents(OutputStream out, List events)
+ throws IOException {
+ final ProtoOutputStream proto = new ProtoOutputStream(out);
+ final IntervalStats tmpStatsObj = new IntervalStats();
+ final int eventCount = events.size();
+ for (int i = 0; i < eventCount; i++) {
+ writeEvent(proto, IntervalStatsProto.PENDING_EVENTS, tmpStatsObj, events.get(i));
+ }
+ proto.flush();
+ }
}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 2bdeddf6fe224..fc464a1f4393f 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -18,14 +18,18 @@ package com.android.server.usage;
import static android.app.usage.UsageEvents.Event.CHOOSER_ACTION;
import static android.app.usage.UsageEvents.Event.CONFIGURATION_CHANGE;
+import static android.app.usage.UsageEvents.Event.DEVICE_EVENT_PACKAGE_NAME;
import static android.app.usage.UsageEvents.Event.DEVICE_SHUTDOWN;
import static android.app.usage.UsageEvents.Event.FLUSH_TO_DISK;
import static android.app.usage.UsageEvents.Event.NOTIFICATION_INTERRUPTION;
import static android.app.usage.UsageEvents.Event.SHORTCUT_INVOCATION;
+import static android.app.usage.UsageEvents.Event.USER_STOPPED;
+import static android.app.usage.UsageEvents.Event.USER_UNLOCKED;
import static android.app.usage.UsageStatsManager.USAGE_SOURCE_CURRENT_ACTIVITY;
import static android.app.usage.UsageStatsManager.USAGE_SOURCE_TASK_ROOT_ACTIVITY;
import android.Manifest;
+import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.app.IUidObserver;
@@ -70,8 +74,10 @@ import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.util.ArraySet;
+import android.util.AtomicFile;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import com.android.internal.content.PackageMonitor;
@@ -81,11 +87,20 @@ import com.android.internal.util.IndentingPrintWriter;
import com.android.server.LocalServices;
import com.android.server.SystemService;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
import java.io.File;
import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
import java.util.Arrays;
+import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -111,6 +126,12 @@ public class UsageStatsService extends SystemService implements
private static final boolean ENABLE_KERNEL_UPDATES = true;
private static final File KERNEL_COUNTER_FILE = new File("/proc/uid_procstat/set");
+ private static final File USAGE_STATS_LEGACY_DIR = new File(
+ Environment.getDataSystemDirectory(), "usagestats");
+ // For migration purposes, indicates whether to keep the legacy usage stats directory or not
+ // STOPSHIP: b/138323140 this should be false on launch
+ private static final boolean KEEP_LEGACY_DIR = true;
+
private static final char TOKEN_DELIMITER = '/';
// Handler message types.
@@ -119,6 +140,7 @@ public class UsageStatsService extends SystemService implements
static final int MSG_REMOVE_USER = 2;
static final int MSG_UID_STATE_CHANGED = 3;
static final int MSG_REPORT_EVENT_TO_ALL_USERID = 4;
+ static final int MSG_UNLOCKED_USER = 5;
private final Object mLock = new Object();
Handler mHandler;
@@ -132,8 +154,8 @@ public class UsageStatsService extends SystemService implements
DevicePolicyManagerInternal mDpmInternal;
private final SparseArray mUserState = new SparseArray<>();
+ private final SparseBooleanArray mUserUnlockedStates = new SparseBooleanArray();
private final SparseIntArray mUidToKernelCounter = new SparseIntArray();
- private File mUsageStatsDir;
long mRealTimeSnapshot;
long mSystemTimeSnapshot;
int mUsageSource;
@@ -144,6 +166,8 @@ public class UsageStatsService extends SystemService implements
/** Manages app time limit observers */
AppTimeLimitController mAppTimeLimit;
+ // A map maintaining a queue of events to be reported per user.
+ private final SparseArray> mReportedEvents = new SparseArray<>();
final SparseArray> mUsageReporters = new SparseArray();
final SparseArray mVisibleActivities = new SparseArray();
@@ -165,7 +189,7 @@ public class UsageStatsService extends SystemService implements
SystemClock.elapsedRealtime());
event.mBucketAndReason = (bucket << 16) | (reason & 0xFFFF);
event.mPackage = packageName;
- mHandler.obtainMessage(MSG_REPORT_EVENT, userId, 0, event).sendToTarget();
+ reportEventOrAddToQueue(userId, event);
}
@Override
@@ -223,30 +247,17 @@ public class UsageStatsService extends SystemService implements
}, mHandler.getLooper());
mAppStandby.addListener(mStandbyChangeListener);
- File systemDataDir = new File(Environment.getDataDirectory(), "system");
- mUsageStatsDir = new File(systemDataDir, "usagestats");
- mUsageStatsDir.mkdirs();
- if (!mUsageStatsDir.exists()) {
- throw new IllegalStateException("Usage stats directory does not exist: "
- + mUsageStatsDir.getAbsolutePath());
- }
IntentFilter filter = new IntentFilter(Intent.ACTION_USER_REMOVED);
filter.addAction(Intent.ACTION_USER_STARTED);
getContext().registerReceiverAsUser(new UserActionsReceiver(), UserHandle.ALL, filter,
null, mHandler);
- synchronized (mLock) {
- cleanUpRemovedUsersLocked();
- }
-
mRealTimeSnapshot = SystemClock.elapsedRealtime();
mSystemTimeSnapshot = System.currentTimeMillis();
publishLocalService(UsageStatsManagerInternal.class, new LocalService());
publishBinderService(Context.USAGE_STATS_SERVICE, new BinderService());
- // Make sure we initialize the data, in case job scheduler needs it early.
- getUserDataAndInitializeIfNeededLocked(UserHandle.USER_SYSTEM, mSystemTimeSnapshot);
}
@Override
@@ -275,6 +286,85 @@ public class UsageStatsService extends SystemService implements
}
}
+ @Override
+ public void onStartUser(UserInfo userInfo) {
+ // Create an entry in the user state map to indicate that the user has been started but
+ // not necessarily unlocked. This will ensure that reported events are flushed to disk
+ // event if the user is never unlocked (following the logic in #flushToDiskLocked)
+ mUserState.put(userInfo.id, null);
+ super.onStartUser(userInfo);
+ }
+
+ @Override
+ public void onUnlockUser(@NonNull UserInfo userInfo) {
+ mHandler.obtainMessage(MSG_UNLOCKED_USER, userInfo.id, 0).sendToTarget();
+ super.onUnlockUser(userInfo);
+ }
+
+ @Override
+ public void onStopUser(@NonNull UserInfo userInfo) {
+ synchronized (mLock) {
+ // User was started but never unlocked so no need to report a user stopped event
+ if (!mUserUnlockedStates.get(userInfo.id)) {
+ persistPendingEventsLocked(userInfo.id);
+ super.onStopUser(userInfo);
+ return;
+ }
+
+ // Report a user stopped event before persisting all stats to disk via the user service
+ final Event event = new Event(USER_STOPPED, SystemClock.elapsedRealtime());
+ event.mPackage = Event.DEVICE_EVENT_PACKAGE_NAME;
+ reportEvent(event, userInfo.id);
+ final UserUsageStatsService userService = mUserState.get(userInfo.id);
+ if (userService != null) {
+ userService.userStopped();
+ }
+ mUserUnlockedStates.put(userInfo.id, false);
+ mUserState.put(userInfo.id, null); // release the service (mainly for GC)
+ }
+ super.onStopUser(userInfo);
+ }
+
+ private void onUserUnlocked(int userId) {
+ synchronized (mLock) {
+ // Create a user unlocked event to report
+ final Event unlockEvent = new Event(USER_UNLOCKED, SystemClock.elapsedRealtime());
+ unlockEvent.mPackage = Event.DEVICE_EVENT_PACKAGE_NAME;
+
+ migrateStatsToSystemCeIfNeededLocked(userId);
+
+ // Read pending reported events from disk and merge them with those stored in memory
+ final LinkedList pendingEvents = new LinkedList<>();
+ loadPendingEventsLocked(userId, pendingEvents);
+ final LinkedList eventsInMem = mReportedEvents.get(userId);
+ if (eventsInMem != null) {
+ pendingEvents.addAll(eventsInMem);
+ }
+ boolean needToFlush = !pendingEvents.isEmpty();
+
+ mUserUnlockedStates.put(userId, true);
+ final UserUsageStatsService userService = getUserDataAndInitializeIfNeededLocked(
+ userId, System.currentTimeMillis());
+ userService.userUnlocked(checkAndGetTimeLocked());
+ // Process all the pending reported events
+ while (pendingEvents.peek() != null) {
+ reportEvent(pendingEvents.poll(), userId);
+ }
+ reportEvent(unlockEvent, userId);
+
+ // Remove all the stats stored in memory and in system DE.
+ mReportedEvents.remove(userId);
+ deleteRecursively(new File(Environment.getDataSystemDeDirectory(userId), "usagestats"));
+ // Force a flush to disk for the current user to ensure important events are persisted.
+ // Note: there is a very very small chance that the system crashes between deleting
+ // the stats above from DE and persisting them to CE here in which case we will lose
+ // those events that were in memory and deleted from DE. (b/139836090)
+ if (needToFlush) {
+ userService.persistActiveStats();
+ }
+ }
+ }
+
private DevicePolicyManagerInternal getDpmInternal() {
if (mDpmInternal == null) {
mDpmInternal = LocalServices.getService(DevicePolicyManagerInternal.class);
@@ -350,33 +440,6 @@ public class UsageStatsService extends SystemService implements
return !mPackageManagerInternal.canAccessInstantApps(callingUid, userId);
}
- private void cleanUpRemovedUsersLocked() {
- final List users = mUserManager.getUsers(true);
- if (users == null || users.size() == 0) {
- throw new IllegalStateException("There can't be no users");
- }
-
- ArraySet toDelete = new ArraySet<>();
- String[] fileNames = mUsageStatsDir.list();
- if (fileNames == null) {
- // No users to delete.
- return;
- }
-
- toDelete.addAll(Arrays.asList(fileNames));
-
- final int userCount = users.size();
- for (int i = 0; i < userCount; i++) {
- final UserInfo userInfo = users.get(i);
- toDelete.remove(Integer.toString(userInfo.id));
- }
-
- final int deleteCount = toDelete.size();
- for (int i = 0; i < deleteCount; i++) {
- deleteRecursively(new File(mUsageStatsDir, toDelete.valueAt(i)));
- }
- }
-
private static void deleteRecursively(File f) {
File[] files = f.listFiles();
if (files != null) {
@@ -385,7 +448,7 @@ public class UsageStatsService extends SystemService implements
}
}
- if (!f.delete()) {
+ if (f.exists() && !f.delete()) {
Slog.e(TAG, "Failed to delete " + f);
}
}
@@ -394,14 +457,117 @@ public class UsageStatsService extends SystemService implements
long currentTimeMillis) {
UserUsageStatsService service = mUserState.get(userId);
if (service == null) {
- service = new UserUsageStatsService(getContext(), userId,
- new File(mUsageStatsDir, Integer.toString(userId)), this);
- service.init(currentTimeMillis);
+ final File usageStatsDir = new File(Environment.getDataSystemCeDirectory(userId),
+ "usagestats");
+ service = new UserUsageStatsService(getContext(), userId, usageStatsDir, this);
+ if (mUserUnlockedStates.get(userId)) {
+ service.init(currentTimeMillis);
+ }
mUserState.put(userId, service);
}
return service;
}
+ private void migrateStatsToSystemCeIfNeededLocked(int userId) {
+ final File usageStatsDir = new File(Environment.getDataSystemCeDirectory(userId),
+ "usagestats");
+ if (!usageStatsDir.mkdirs() && !usageStatsDir.exists()) {
+ throw new IllegalStateException("Usage stats directory does not exist: "
+ + usageStatsDir.getAbsolutePath());
+ }
+ // Check if the migrated status file exists - if not, migrate usage stats.
+ final File migrated = new File(usageStatsDir, "migrated");
+ if (migrated.exists()) {
+ try (BufferedReader reader = new BufferedReader(new FileReader(migrated))) {
+ final int previousVersion = Integer.parseInt(reader.readLine());
+ // UsageStatsDatabase.BACKUP_VERSION was 4 when usage stats were migrated to CE.
+ if (previousVersion >= 4) {
+ deleteLegacyDir(userId);
+ return;
+ }
+ // If migration logic needs to be changed in a future version, do it here.
+ } catch (NumberFormatException | IOException e) {
+ Slog.e(TAG, "Failed to read migration status file, possibly corrupted.");
+ deleteRecursively(usageStatsDir);
+ if (usageStatsDir.exists()) {
+ Slog.e(TAG, "Unable to delete usage stats CE directory.");
+ throw new RuntimeException(e);
+ } else {
+ // Make the directory again since previous migration was not complete
+ if (!usageStatsDir.mkdirs() && !usageStatsDir.exists()) {
+ throw new IllegalStateException("Usage stats directory does not exist: "
+ + usageStatsDir.getAbsolutePath());
+ }
+ }
+ }
+ }
+
+ Slog.i(TAG, "Starting migration to system CE for user " + userId);
+ final File legacyUserDir = new File(USAGE_STATS_LEGACY_DIR, Integer.toString(userId));
+ if (legacyUserDir.exists()) {
+ copyRecursively(usageStatsDir, legacyUserDir);
+ }
+ // Create a status file to indicate that the migration to CE has been completed.
+ try (BufferedWriter writer = new BufferedWriter(new FileWriter(migrated))) {
+ writer.write(Integer.toString(UsageStatsDatabase.BACKUP_VERSION));
+ writer.write("\n");
+ writer.flush();
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to write migrated status file");
+ throw new RuntimeException(e);
+ }
+ Slog.i(TAG, "Finished migration to system CE for user " + userId);
+
+ // Migration was successful - delete the legacy directory
+ deleteLegacyDir(userId);
+ }
+
+ private static void copyRecursively(final File parent, File f) {
+ final File[] files = f.listFiles();
+ if (files == null) {
+ try {
+ Files.copy(f.toPath(), new File(parent, f.getName()).toPath(),
+ StandardCopyOption.REPLACE_EXISTING);
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to move usage stats file : " + f.toString());
+ throw new RuntimeException(e);
+ }
+ return;
+ }
+
+ for (int i = files.length - 1; i >= 0; i--) {
+ File newParent = parent;
+ if (files[i].isDirectory()) {
+ newParent = new File(parent, files[i].getName());
+ final boolean mkdirSuccess = newParent.mkdirs();
+ if (!mkdirSuccess && !newParent.exists()) {
+ throw new IllegalStateException(
+ "Failed to create usage stats directory during migration: "
+ + newParent.getAbsolutePath());
+ }
+ }
+ copyRecursively(newParent, files[i]);
+ }
+ }
+
+ private void deleteLegacyDir(int userId) {
+ final File legacyUserDir = new File(USAGE_STATS_LEGACY_DIR, Integer.toString(userId));
+ if (!KEEP_LEGACY_DIR) {
+ deleteRecursively(legacyUserDir);
+ if (legacyUserDir.exists()) {
+ Slog.w(TAG, "Error occurred while attempting to delete legacy usage stats "
+ + "dir for user " + userId);
+ }
+ // If all users have been migrated, delete the parent legacy usage stats directory
+ if (USAGE_STATS_LEGACY_DIR.list() != null
+ && USAGE_STATS_LEGACY_DIR.list().length == 0) {
+ if (!USAGE_STATS_LEGACY_DIR.delete()) {
+ Slog.w(TAG, "Error occurred while attempting to delete legacy usage stats dir");
+ }
+ }
+ }
+ }
+
/**
* This should be the only way to get the time from the system.
*/
@@ -463,11 +629,95 @@ public class UsageStatsService extends SystemService implements
mHandler.sendEmptyMessage(MSG_FLUSH_TO_DISK);
}
+ private void loadPendingEventsLocked(int userId, LinkedList pendingEvents) {
+ final File usageStatsDeDir = new File(Environment.getDataSystemDeDirectory(userId),
+ "usagestats");
+ final File[] pendingEventsFiles = usageStatsDeDir.listFiles();
+ if (pendingEventsFiles == null || pendingEventsFiles.length == 0) {
+ return;
+ }
+ Arrays.sort(pendingEventsFiles);
+
+ for (int i = 0; i < pendingEventsFiles.length; i++) {
+ final AtomicFile af = new AtomicFile(pendingEventsFiles[i]);
+ try {
+ try (FileInputStream in = af.openRead()) {
+ UsageStatsProto.readPendingEvents(in, pendingEvents);
+ }
+ } catch (IOException e) {
+ // Even if one file read fails, exit here to keep all events in order on disk -
+ // they will be read and processed the next time user is unlocked.
+ Slog.e(TAG, "Could not read " + pendingEventsFiles[i] + " for user " + userId);
+ pendingEvents.clear();
+ return;
+ }
+ }
+ }
+
+ private void persistPendingEventsLocked(int userId) {
+ final LinkedList pendingEvents = mReportedEvents.get(userId);
+ if (pendingEvents == null || pendingEvents.isEmpty()) {
+ return;
+ }
+
+ final File usageStatsDeDir = new File(Environment.getDataSystemDeDirectory(userId),
+ "usagestats");
+ if (!usageStatsDeDir.mkdirs() && !usageStatsDeDir.exists()) {
+ throw new IllegalStateException("Usage stats DE directory does not exist: "
+ + usageStatsDeDir.getAbsolutePath());
+ }
+ final File pendingEventsFile = new File(usageStatsDeDir,
+ "pendingevents_" + System.currentTimeMillis());
+ final AtomicFile af = new AtomicFile(pendingEventsFile);
+ FileOutputStream fos = null;
+ try {
+ fos = af.startWrite();
+ UsageStatsProto.writePendingEvents(fos, pendingEvents);
+ af.finishWrite(fos);
+ fos = null;
+ pendingEvents.clear();
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to write " + pendingEventsFile.getAbsolutePath()
+ + " for user " + userId);
+ } finally {
+ af.failWrite(fos); // when fos is null (successful write), this will no-op
+ }
+ }
+
+ private void reportEventOrAddToQueue(int userId, Event event) {
+ synchronized (mLock) {
+ if (mUserUnlockedStates.get(userId)) {
+ mHandler.obtainMessage(MSG_REPORT_EVENT, userId, 0, event).sendToTarget();
+ return;
+ }
+
+ final LinkedList events = mReportedEvents.get(userId, new LinkedList<>());
+ events.add(event);
+ if (mReportedEvents.get(userId) == null) {
+ mReportedEvents.put(userId, events);
+ }
+ if (events.size() == 1) {
+ // Every time a file is persisted to disk, mReportedEvents is cleared for this user
+ // so trigger a flush to disk every time the first event has been added.
+ mHandler.sendEmptyMessageDelayed(MSG_FLUSH_TO_DISK, FLUSH_INTERVAL);
+ }
+ }
+ }
+
/**
* Called by the Binder stub.
*/
void reportEvent(Event event, int userId) {
synchronized (mLock) {
+ // This should never be called directly when the user is locked
+ if (!mUserUnlockedStates.get(userId)) {
+ Slog.wtf(TAG, "Failed to report event for locked user " + userId
+ + " (" + event.mPackage + "/" + event.mClass
+ + " eventType:" + event.mEventType
+ + " instanceId:" + event.mInstanceId + ")");
+ return;
+ }
+
final long timeNow = checkAndGetTimeLocked();
final long elapsedRealtime = SystemClock.elapsedRealtime();
convertToSystemTimeLocked(event);
@@ -581,7 +831,7 @@ public class UsageStatsService extends SystemService implements
final int userCount = mUserState.size();
for (int i = 0; i < userCount; i++) {
Event copy = new Event(event);
- reportEvent(copy, mUserState.keyAt(i));
+ reportEventOrAddToQueue(mUserState.keyAt(i), copy);
}
}
}
@@ -597,6 +847,7 @@ public class UsageStatsService extends SystemService implements
// The FLUSH_TO_DISK event is an internal event, it will not show up in IntervalStats'
// EventList.
Event event = new Event(FLUSH_TO_DISK, SystemClock.elapsedRealtime());
+ event.mPackage = DEVICE_EVENT_PACKAGE_NAME;
reportEventToAllUserId(event);
flushToDiskLocked();
}
@@ -611,7 +862,6 @@ public class UsageStatsService extends SystemService implements
mUserState.remove(userId);
mAppStandby.onUserRemoved(userId);
mAppTimeLimit.onUserRemoved(userId);
- cleanUpRemovedUsersLocked();
}
}
@@ -621,6 +871,11 @@ public class UsageStatsService extends SystemService implements
List queryUsageStats(int userId, int bucketType, long beginTime, long endTime,
boolean obfuscateInstantApps) {
synchronized (mLock) {
+ if (!mUserUnlockedStates.get(userId)) {
+ Slog.w(TAG, "Failed to query usage stats for locked user " + userId);
+ return null;
+ }
+
final long timeNow = checkAndGetTimeLocked();
if (!validRange(timeNow, beginTime, endTime)) {
return null;
@@ -643,7 +898,6 @@ public class UsageStatsService extends SystemService implements
}
}
}
-
return list;
}
}
@@ -654,6 +908,11 @@ public class UsageStatsService extends SystemService implements
List queryConfigurationStats(int userId, int bucketType, long beginTime,
long endTime) {
synchronized (mLock) {
+ if (!mUserUnlockedStates.get(userId)) {
+ Slog.w(TAG, "Failed to query configuration stats for locked user " + userId);
+ return null;
+ }
+
final long timeNow = checkAndGetTimeLocked();
if (!validRange(timeNow, beginTime, endTime)) {
return null;
@@ -671,6 +930,11 @@ public class UsageStatsService extends SystemService implements
List queryEventStats(int userId, int bucketType, long beginTime,
long endTime) {
synchronized (mLock) {
+ if (!mUserUnlockedStates.get(userId)) {
+ Slog.w(TAG, "Failed to query event stats for locked user " + userId);
+ return null;
+ }
+
final long timeNow = checkAndGetTimeLocked();
if (!validRange(timeNow, beginTime, endTime)) {
return null;
@@ -688,6 +952,11 @@ public class UsageStatsService extends SystemService implements
UsageEvents queryEvents(int userId, long beginTime, long endTime,
boolean shouldObfuscateInstantApps) {
synchronized (mLock) {
+ if (!mUserUnlockedStates.get(userId)) {
+ Slog.w(TAG, "Failed to query events for locked user " + userId);
+ return null;
+ }
+
final long timeNow = checkAndGetTimeLocked();
if (!validRange(timeNow, beginTime, endTime)) {
return null;
@@ -705,6 +974,11 @@ public class UsageStatsService extends SystemService implements
UsageEvents queryEventsForPackage(int userId, long beginTime, long endTime,
String packageName, boolean includeTaskRoot) {
synchronized (mLock) {
+ if (!mUserUnlockedStates.get(userId)) {
+ Slog.w(TAG, "Failed to query package events for locked user " + userId);
+ return null;
+ }
+
final long timeNow = checkAndGetTimeLocked();
if (!validRange(timeNow, beginTime, endTime)) {
return null;
@@ -731,9 +1005,14 @@ public class UsageStatsService extends SystemService implements
private void flushToDiskLocked() {
final int userCount = mUserState.size();
for (int i = 0; i < userCount; i++) {
- UserUsageStatsService service = mUserState.valueAt(i);
+ final int userId = mUserState.keyAt(i);
+ if (!mUserUnlockedStates.get(userId)) {
+ persistPendingEventsLocked(userId);
+ continue;
+ }
+ UserUsageStatsService service = mUserState.get(userId);
service.persistActiveStats();
- mAppStandby.flushToDisk(mUserState.keyAt(i));
+ mAppStandby.flushToDisk(userId);
}
mAppStandby.flushDurationsToDisk();
@@ -833,6 +1112,18 @@ public class UsageStatsService extends SystemService implements
} else if ("appstandby".equals(arg)) {
mAppStandby.dumpState(args, pw);
return;
+ } else if ("stats-directory".equals(arg)) {
+ final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
+ final int userId;
+ try {
+ userId = Integer.valueOf(args[i + 1]);
+ } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
+ ipw.println("invalid user specified.");
+ return;
+ }
+ ipw.println(new File(Environment.getDataSystemCeDirectory(userId),
+ "usagestats").getAbsolutePath());
+ return;
} else if (arg != null && !arg.startsWith("-")) {
// Anything else that doesn't start with '-' is a pkg to filter
pkg = arg;
@@ -887,7 +1178,9 @@ public class UsageStatsService extends SystemService implements
case MSG_FLUSH_TO_DISK:
flushToDisk();
break;
-
+ case MSG_UNLOCKED_USER:
+ onUserUnlocked(msg.arg1);
+ break;
case MSG_REMOVE_USER:
onUserRemoved(msg.arg1);
break;
@@ -1368,7 +1661,7 @@ public class UsageStatsService extends SystemService implements
event.mAction = action;
event.mContentType = contentType;
event.mContentAnnotations = annotations;
- mHandler.obtainMessage(MSG_REPORT_EVENT, userId, 0, event).sendToTarget();
+ reportEventOrAddToQueue(userId, event);
}
@Override
@@ -1640,7 +1933,7 @@ public class UsageStatsService extends SystemService implements
event.mTaskRootPackage = taskRoot.getPackageName();
event.mTaskRootClass = taskRoot.getClassName();
}
- mHandler.obtainMessage(MSG_REPORT_EVENT, userId, 0, event).sendToTarget();
+ reportEventOrAddToQueue(userId, event);
}
@Override
@@ -1652,7 +1945,7 @@ public class UsageStatsService extends SystemService implements
Event event = new Event(eventType, SystemClock.elapsedRealtime());
event.mPackage = packageName;
- mHandler.obtainMessage(MSG_REPORT_EVENT, userId, 0, event).sendToTarget();
+ reportEventOrAddToQueue(userId, event);
}
@Override
@@ -1665,7 +1958,7 @@ public class UsageStatsService extends SystemService implements
Event event = new Event(CONFIGURATION_CHANGE, SystemClock.elapsedRealtime());
event.mPackage = "android";
event.mConfiguration = new Configuration(config);
- mHandler.obtainMessage(MSG_REPORT_EVENT, userId, 0, event).sendToTarget();
+ reportEventOrAddToQueue(userId, event);
}
@Override
@@ -1679,7 +1972,7 @@ public class UsageStatsService extends SystemService implements
Event event = new Event(NOTIFICATION_INTERRUPTION, SystemClock.elapsedRealtime());
event.mPackage = packageName.intern();
event.mNotificationChannelId = channelId.intern();
- mHandler.obtainMessage(MSG_REPORT_EVENT, userId, 0, event).sendToTarget();
+ reportEventOrAddToQueue(userId, event);
}
@Override
@@ -1692,7 +1985,7 @@ public class UsageStatsService extends SystemService implements
Event event = new Event(SHORTCUT_INVOCATION, SystemClock.elapsedRealtime());
event.mPackage = packageName.intern();
event.mShortcutId = shortcutId.intern();
- mHandler.obtainMessage(MSG_REPORT_EVENT, userId, 0, event).sendToTarget();
+ reportEventOrAddToQueue(userId, event);
}
@Override
@@ -1749,8 +2042,13 @@ public class UsageStatsService extends SystemService implements
@Override
public byte[] getBackupPayload(int user, String key) {
- // Check to ensure that only user 0's data is b/r for now
synchronized (mLock) {
+ if (!mUserUnlockedStates.get(user)) {
+ Slog.w(TAG, "Failed to get backup payload for locked user " + user);
+ return null;
+ }
+
+ // Check to ensure that only user 0's data is b/r for now
if (user == UserHandle.USER_SYSTEM) {
final UserUsageStatsService userStats =
getUserDataAndInitializeIfNeededLocked(user, checkAndGetTimeLocked());
@@ -1764,6 +2062,11 @@ public class UsageStatsService extends SystemService implements
@Override
public void applyRestoredPayload(int user, String key, byte[] payload) {
synchronized (mLock) {
+ if (!mUserUnlockedStates.get(user)) {
+ Slog.w(TAG, "Failed to apply restored payload for locked user " + user);
+ return;
+ }
+
if (user == UserHandle.USER_SYSTEM) {
final UserUsageStatsService userStats =
getUserDataAndInitializeIfNeededLocked(user, checkAndGetTimeLocked());
diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
index 11c0e4abdb357..6fbd88227f9b2 100644
--- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
@@ -156,6 +156,15 @@ class UserUsageStatsService {
}
}
+ void userUnlocked(long currentTimeMillis) {
+ init(currentTimeMillis);
+ }
+
+ void userStopped() {
+ // Flush events to disk immediately to guarantee persistence.
+ persistActiveStats();
+ }
+
void onTimeChanged(long oldTime, long newTime) {
persistActiveStats();
mDatabase.onTimeChanged(newTime - oldTime);