From 3a3152025063143ea2b3110f226f15294a13cb36 Mon Sep 17 00:00:00 2001
From: Varun Shah
Date: Wed, 24 Jul 2019 16:33:42 -0700
Subject: [PATCH] Move usage stats data to credential encrypted storage.
Move all of the usage stats data from the regular system storage to
the system CE (credential encrypted) storage.
All events being reported while the user is locked will be stored
in memory and will be processed via the handler after the user is
unlocked. All of these events reported will regularly be persisted to
system DE storage to ensure important events are not lost.
Any query requests will be ignored while the user is locked.
Two new usage events have been added: one to indicate the user has been
unlocked and the other to indicate the user has been stopped.
Also updated documentation for APIs in UsageStatsManager to reflect the
new behavior in R when the user is locked.
Bug: 135484470
Test: atest android.app.usage.UsageStatsTest#testDataIsInCe
Test: atest android.app.usage.cts.UsageStatsTest#testUserUnlockedEventExists
Test: atest android.app.usage.cts.UsageStatsTest
Test: atest UsageStatsDatabaseTest
Change-Id: I18da1a9354b6686472972c5f2304e4c19e28307f
---
core/java/android/app/usage/UsageEvents.java | 16 +-
.../android/app/usage/UsageStatsManager.java | 16 +
.../app/usage/UsageStatsManagerInternal.java | 26 +-
.../android/server/usagestatsservice.proto | 2 +
.../src/android/app/usage/UsageStatsTest.java | 12 +
.../android/server/usage/UsageStatsProto.java | 33 ++
.../server/usage/UsageStatsService.java | 421 +++++++++++++++---
.../server/usage/UserUsageStatsService.java | 9 +
8 files changed, 470 insertions(+), 65 deletions(-)
diff --git a/core/java/android/app/usage/UsageEvents.java b/core/java/android/app/usage/UsageEvents.java
index b564c3121d2e3..767771b94b10a 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);