Merge "Move usage stats data to credential encrypted storage."

This commit is contained in:
TreeHugger Robot
2019-08-30 22:12:24 +00:00
committed by Android (Google) Code Review
8 changed files with 470 additions and 65 deletions

View File

@@ -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;

View File

@@ -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 {
* </p>
*
* <p> The caller must have {@link android.Manifest.permission#PACKAGE_USAGE_STATS} </p>
* <em>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.</em>
*
* @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)}.
* <p> The caller must have {@link android.Manifest.permission#PACKAGE_USAGE_STATS} </p>
* <em>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.</em>
*
* @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 {
* </ul>
*
* <p> The caller must have {@link android.Manifest.permission#PACKAGE_USAGE_STATS} </p>
* <em>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.</em>
*
* @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.
* <p> The caller must have {@link android.Manifest.permission#PACKAGE_USAGE_STATS} </p>
* <em>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.</em>
*
* @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.
* <em>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.</em>
*
* @param beginTime The inclusive beginning of the range of events to include in the results.
* Defined in terms of "Unix time", see

View File

@@ -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. <br/>
* <em>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.</em>
*
* @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. <br/>
* <em>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.</em>
*
* @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. <br/>
* <em>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.</em>
*
* @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. <br/>
* <em>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.</em>
*
* @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. <br/>
* <em>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.</em>
*
* @param packageName The package name of the shortcut publisher
* @param shortcutId The ID of the shortcut in question

View File

@@ -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
}

View File

@@ -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";

View File

@@ -606,4 +606,37 @@ final class UsageStatsProto {
proto.flush();
}
// TODO: move to UsageStatsProtoV2
static void readPendingEvents(InputStream in, List<UsageEvents.Event> events)
throws IOException {
final ProtoInputStream proto = new ProtoInputStream(in);
final List<String> 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<UsageEvents.Event> 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();
}
}

View File

@@ -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<UserUsageStatsService> 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<LinkedList<Event>> mReportedEvents = new SparseArray<>();
final SparseArray<ArraySet<String>> mUsageReporters = new SparseArray();
final SparseArray<ActivityData> 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<Event> pendingEvents = new LinkedList<>();
loadPendingEventsLocked(userId, pendingEvents);
final LinkedList<Event> 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<UserInfo> users = mUserManager.getUsers(true);
if (users == null || users.size() == 0) {
throw new IllegalStateException("There can't be no users");
}
ArraySet<String> 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<Event> 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<Event> 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<Event> 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<UsageStats> 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<ConfigurationStats> 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<EventStats> 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());

View File

@@ -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);