summaryrefslogtreecommitdiffstats
path: root/services/usage
diff options
context:
space:
mode:
authorAdam Lesinski <adamlesinski@google.com>2014-07-16 19:09:13 -0700
committerAdam Lesinski <adamlesinski@google.com>2014-07-18 15:24:20 -0700
commit0debc9aff4c0cbc28e083a948081d91b0f171319 (patch)
tree3ac4d7a9927cdd2741f65393d4e6855508ab3c26 /services/usage
parentd3de42cae84fadfa1befd082a2cf1bf72f9ad82a (diff)
downloadframeworks_base-0debc9aff4c0cbc28e083a948081d91b0f171319.zip
frameworks_base-0debc9aff4c0cbc28e083a948081d91b0f171319.tar.gz
frameworks_base-0debc9aff4c0cbc28e083a948081d91b0f171319.tar.bz2
First iteration of a public UsageStats API
UsageStats API that allows apps to get a list of packages that have been recently used, along with basic stats like how long they have been in the foreground and the most recent time they were running. Bug: 15165667 Change-Id: I2a2d1ff69bd0b5703ac3d9de1780df42ad90d439
Diffstat (limited to 'services/usage')
-rw-r--r--services/usage/Android.mk10
-rw-r--r--services/usage/java/com/android/server/usage/UsageStatsDatabase.java199
-rw-r--r--services/usage/java/com/android/server/usage/UsageStatsService.java437
-rw-r--r--services/usage/java/com/android/server/usage/UsageStatsUtils.java63
-rw-r--r--services/usage/java/com/android/server/usage/UsageStatsXml.java161
5 files changed, 870 insertions, 0 deletions
diff --git a/services/usage/Android.mk b/services/usage/Android.mk
new file mode 100644
index 0000000..d4b7fa8
--- /dev/null
+++ b/services/usage/Android.mk
@@ -0,0 +1,10 @@
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := services.usage
+
+LOCAL_SRC_FILES += \
+ $(call all-java-files-under,java)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
new file mode 100644
index 0000000..4e75f61
--- /dev/null
+++ b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
@@ -0,0 +1,199 @@
+/**
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.android.server.usage;
+
+import android.app.usage.TimeSparseArray;
+import android.app.usage.UsageStats;
+import android.app.usage.UsageStatsManager;
+import android.util.AtomicFile;
+import android.util.Slog;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Calendar;
+
+class UsageStatsDatabase {
+ private static final String TAG = "UsageStatsDatabase";
+ private static final boolean DEBUG = UsageStatsService.DEBUG;
+
+ private final Object mLock = new Object();
+ private final File[] mBucketDirs;
+ private final TimeSparseArray<AtomicFile>[] mSortedStatFiles;
+ private final Calendar mCal;
+
+ public UsageStatsDatabase(File dir) {
+ mBucketDirs = new File[] {
+ new File(dir, "daily"),
+ new File(dir, "weekly"),
+ new File(dir, "monthly"),
+ new File(dir, "yearly"),
+ };
+ mSortedStatFiles = new TimeSparseArray[mBucketDirs.length];
+ mCal = Calendar.getInstance();
+ }
+
+ void init() {
+ synchronized (mLock) {
+ for (File f : mBucketDirs) {
+ f.mkdirs();
+ if (!f.exists()) {
+ throw new IllegalStateException("Failed to create directory "
+ + f.getAbsolutePath());
+ }
+ }
+
+ final FilenameFilter backupFileFilter = new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ return !name.endsWith(".bak");
+ }
+ };
+
+ // Index the available usage stat files on disk.
+ for (int i = 0; i < mSortedStatFiles.length; i++) {
+ mSortedStatFiles[i] = new TimeSparseArray<>();
+ File[] files = mBucketDirs[i].listFiles(backupFileFilter);
+ if (files != null) {
+ if (DEBUG) {
+ Slog.d(TAG, "Found " + files.length + " stat files for bucket " + i);
+ }
+
+ for (File f : files) {
+ mSortedStatFiles[i].put(Long.parseLong(f.getName()), new AtomicFile(f));
+ }
+ }
+ }
+ }
+ }
+
+ public UsageStats getLatestUsageStats(int bucketType) {
+ synchronized (mLock) {
+ if (bucketType < 0 || bucketType >= mBucketDirs.length) {
+ throw new IllegalArgumentException("Bad bucket type " + bucketType);
+ }
+
+ final int fileCount = mSortedStatFiles[bucketType].size();
+ if (fileCount == 0) {
+ return null;
+ }
+
+ try {
+ final AtomicFile f = mSortedStatFiles[bucketType].valueAt(fileCount - 1);
+ UsageStats stats = UsageStatsXml.read(f);
+ stats.mLastTimeSaved = f.getLastModifiedTime();
+ return stats;
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to read usage stats file", e);
+ }
+ }
+ return null;
+ }
+
+ public UsageStats[] getUsageStats(int bucketType, long beginTime, int limit) {
+ synchronized (mLock) {
+ if (bucketType < 0 || bucketType >= mBucketDirs.length) {
+ throw new IllegalArgumentException("Bad bucket type " + bucketType);
+ }
+
+ if (limit <= 0) {
+ return UsageStats.EMPTY_STATS;
+ }
+
+ int startIndex = mSortedStatFiles[bucketType].closestIndexAfter(beginTime);
+ if (startIndex < 0) {
+ return UsageStats.EMPTY_STATS;
+ }
+
+ final int realLimit = Math.min(limit, mSortedStatFiles[bucketType].size() - startIndex);
+ try {
+ ArrayList<UsageStats> stats = new ArrayList<>(realLimit);
+ for (int i = 0; i < realLimit; i++) {
+ final AtomicFile f = mSortedStatFiles[bucketType].valueAt(startIndex + i);
+
+ if (DEBUG) {
+ Slog.d(TAG, "Reading stat file " + f.getBaseFile().getAbsolutePath());
+ }
+
+ UsageStats stat = UsageStatsXml.read(f);
+ if (beginTime < stat.mEndTimeStamp) {
+ stat.mLastTimeSaved = f.getLastModifiedTime();
+ stats.add(stat);
+ }
+ }
+ return stats.toArray(new UsageStats[stats.size()]);
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to read usage stats file", e);
+ return UsageStats.EMPTY_STATS;
+ }
+ }
+ }
+
+ public void prune() {
+ synchronized (mLock) {
+ long timeNow = System.currentTimeMillis();
+
+ mCal.setTimeInMillis(timeNow);
+ mCal.add(Calendar.MONTH, -6);
+ pruneFilesOlderThan(mBucketDirs[UsageStatsManager.MONTHLY_BUCKET],
+ mCal.getTimeInMillis());
+
+ mCal.setTimeInMillis(timeNow);
+ mCal.add(Calendar.WEEK_OF_YEAR, -4);
+ pruneFilesOlderThan(mBucketDirs[UsageStatsManager.WEEKLY_BUCKET],
+ mCal.getTimeInMillis());
+
+ mCal.setTimeInMillis(timeNow);
+ mCal.add(Calendar.DAY_OF_YEAR, -7);
+ pruneFilesOlderThan(mBucketDirs[UsageStatsManager.DAILY_BUCKET],
+ mCal.getTimeInMillis());
+ }
+ }
+
+ private static void pruneFilesOlderThan(File dir, long expiryTime) {
+ File[] files = dir.listFiles();
+ if (files != null) {
+ for (File f : files) {
+ long beginTime = Long.parseLong(f.getName());
+ if (beginTime < expiryTime) {
+ new AtomicFile(f).delete();
+ }
+ }
+ }
+ }
+
+ public void putUsageStats(int bucketType, UsageStats stats)
+ throws IOException {
+ synchronized (mLock) {
+ if (bucketType < 0 || bucketType >= mBucketDirs.length) {
+ throw new IllegalArgumentException("Bad bucket type " + bucketType);
+ }
+
+ AtomicFile f = mSortedStatFiles[bucketType].get(stats.mBeginTimeStamp);
+ if (f == null) {
+ f = new AtomicFile(new File(mBucketDirs[bucketType],
+ Long.toString(stats.mBeginTimeStamp)));
+ mSortedStatFiles[bucketType].append(stats.mBeginTimeStamp, f);
+ }
+
+ UsageStatsXml.write(stats, f);
+ stats.mLastTimeSaved = f.getLastModifiedTime();
+ }
+ }
+
+}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
new file mode 100644
index 0000000..13cbf8a
--- /dev/null
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -0,0 +1,437 @@
+/**
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.android.server.usage;
+
+import android.app.AppOpsManager;
+import android.app.usage.IUsageStatsManager;
+import android.app.usage.PackageUsageStats;
+import android.app.usage.TimeSparseArray;
+import android.app.usage.UsageStats;
+import android.app.usage.UsageStatsManager;
+import android.app.usage.UsageStatsManagerInternal;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Binder;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.ArraySet;
+import android.util.Slog;
+import com.android.internal.os.BackgroundThread;
+import com.android.server.SystemService;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+
+public class UsageStatsService extends SystemService {
+ static final String TAG = "UsageStatsService";
+
+ static final boolean DEBUG = false;
+ private static final long TEN_SECONDS = 10 * 1000;
+ private static final long TWENTY_MINUTES = 20 * 60 * 1000;
+ private static final long FLUSH_INTERVAL = DEBUG ? TEN_SECONDS : TWENTY_MINUTES;
+ private static final int USAGE_STAT_RESULT_LIMIT = 10;
+ private static final SimpleDateFormat sDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+ // Handler message types.
+ static final int MSG_REPORT_EVENT = 0;
+ static final int MSG_FLUSH_TO_DISK = 1;
+
+ final Object mLock = new Object();
+ Handler mHandler;
+ AppOpsManager mAppOps;
+
+ private UsageStatsDatabase mDatabase;
+ private UsageStats[] mCurrentStats = new UsageStats[UsageStatsManager.BUCKET_COUNT];
+ private TimeSparseArray<UsageStats.Event> mCurrentEvents = new TimeSparseArray<>();
+ private boolean mStatsChanged = false;
+ private Calendar mDailyExpiryDate;
+
+ public UsageStatsService(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onStart() {
+ mDailyExpiryDate = Calendar.getInstance();
+ mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE);
+ mHandler = new H(BackgroundThread.get().getLooper());
+
+ File systemDataDir = new File(Environment.getDataDirectory(), "system");
+ mDatabase = new UsageStatsDatabase(new File(systemDataDir, "usagestats"));
+ mDatabase.init();
+
+ synchronized (mLock) {
+ initLocked();
+ }
+
+ publishLocalService(UsageStatsManagerInternal.class, new LocalService());
+ publishBinderService(Context.USAGE_STATS_SERVICE, new BinderService());
+ }
+
+ private void initLocked() {
+ int nullCount = 0;
+ for (int i = 0; i < mCurrentStats.length; i++) {
+ mCurrentStats[i] = mDatabase.getLatestUsageStats(i);
+ if (mCurrentStats[i] == null) {
+ nullCount++;
+ }
+ }
+
+ if (nullCount > 0) {
+ if (nullCount != mCurrentStats.length) {
+ // This is weird, but we shouldn't fail if something like this
+ // happens.
+ Slog.w(TAG, "Some stats have no latest available");
+ } else {
+ // This must be first boot.
+ }
+
+ // By calling loadActiveStatsLocked, we will
+ // generate new stats for each bucket.
+ loadActiveStatsLocked();
+ } else {
+ // Set up the expiry date to be one day from the latest daily stat.
+ // This may actually be today and we will rollover on the first event
+ // that is reported.
+ mDailyExpiryDate.setTimeInMillis(
+ mCurrentStats[UsageStatsManager.DAILY_BUCKET].mBeginTimeStamp);
+ mDailyExpiryDate.add(Calendar.DAY_OF_YEAR, 1);
+ UsageStatsUtils.truncateDateTo(UsageStatsManager.DAILY_BUCKET, mDailyExpiryDate);
+ Slog.i(TAG, "Rollover scheduled for " + sDateFormat.format(mDailyExpiryDate.getTime()));
+ }
+
+ // Now close off any events that were open at the time this was saved.
+ for (UsageStats stat : mCurrentStats) {
+ final int pkgCount = stat.getPackageCount();
+ for (int i = 0; i < pkgCount; i++) {
+ PackageUsageStats pkgStats = stat.getPackage(i);
+ if (pkgStats.mLastEvent == UsageStats.Event.MOVE_TO_FOREGROUND ||
+ pkgStats.mLastEvent == UsageStats.Event.CONTINUE_PREVIOUS_DAY) {
+ updateStatsLocked(stat, pkgStats.mPackageName, stat.mLastTimeSaved,
+ UsageStats.Event.END_OF_DAY);
+ notifyStatsChangedLocked();
+ }
+ }
+ }
+ }
+
+ private void rolloverStatsLocked() {
+ final long startTime = System.currentTimeMillis();
+ Slog.i(TAG, "Rolling over usage stats");
+
+ // Finish any ongoing events with an END_OF_DAY event. Make a note of which components
+ // need a new CONTINUE_PREVIOUS_DAY entry.
+ ArraySet<String> continuePreviousDay = new ArraySet<>();
+ for (UsageStats stat : mCurrentStats) {
+ final int pkgCount = stat.getPackageCount();
+ for (int i = 0; i < pkgCount; i++) {
+ PackageUsageStats pkgStats = stat.getPackage(i);
+ if (pkgStats.mLastEvent == UsageStats.Event.MOVE_TO_FOREGROUND ||
+ pkgStats.mLastEvent == UsageStats.Event.CONTINUE_PREVIOUS_DAY) {
+ continuePreviousDay.add(pkgStats.mPackageName);
+ updateStatsLocked(stat, pkgStats.mPackageName,
+ mDailyExpiryDate.getTimeInMillis() - 1, UsageStats.Event.END_OF_DAY);
+ mStatsChanged = true;
+ }
+ }
+ }
+
+ persistActiveStatsLocked();
+ mDatabase.prune();
+ loadActiveStatsLocked();
+
+ final int continueCount = continuePreviousDay.size();
+ for (int i = 0; i < continueCount; i++) {
+ String name = continuePreviousDay.valueAt(i);
+ for (UsageStats stat : mCurrentStats) {
+ updateStatsLocked(stat, name,
+ mCurrentStats[UsageStatsManager.DAILY_BUCKET].mBeginTimeStamp,
+ UsageStats.Event.CONTINUE_PREVIOUS_DAY);
+ mStatsChanged = true;
+ }
+ }
+ persistActiveStatsLocked();
+
+ final long totalTime = System.currentTimeMillis() - startTime;
+ Slog.i(TAG, "Rolling over usage stats complete. Took " + totalTime + " milliseconds");
+ }
+
+ private void notifyStatsChangedLocked() {
+ if (!mStatsChanged) {
+ mStatsChanged = true;
+ mHandler.sendEmptyMessageDelayed(MSG_FLUSH_TO_DISK, FLUSH_INTERVAL);
+ }
+ }
+
+ /**
+ * Called by the Bunder stub
+ */
+ void shutdown() {
+ synchronized (mLock) {
+ mHandler.removeMessages(MSG_REPORT_EVENT);
+ mHandler.removeMessages(MSG_FLUSH_TO_DISK);
+ persistActiveStatsLocked();
+ }
+ }
+
+ private static String eventToString(int eventType) {
+ switch (eventType) {
+ case UsageStats.Event.NONE:
+ return "NONE";
+ case UsageStats.Event.MOVE_TO_BACKGROUND:
+ return "MOVE_TO_BACKGROUND";
+ case UsageStats.Event.MOVE_TO_FOREGROUND:
+ return "MOVE_TO_FOREGROUND";
+ case UsageStats.Event.END_OF_DAY:
+ return "END_OF_DAY";
+ case UsageStats.Event.CONTINUE_PREVIOUS_DAY:
+ return "CONTINUE_PREVIOUS_DAY";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ /**
+ * Called by the Binder stub.
+ */
+ void reportEvent(UsageStats.Event event) {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slog.d(TAG, "Got usage event for " + event.packageName
+ + "[" + event.timeStamp + "]: "
+ + eventToString(event.eventType));
+ }
+
+ if (event.timeStamp >= mDailyExpiryDate.getTimeInMillis()) {
+ // Need to rollover
+ rolloverStatsLocked();
+ }
+
+ mCurrentEvents.append(event.timeStamp, event);
+
+ for (UsageStats stats : mCurrentStats) {
+ updateStatsLocked(stats, event.packageName, event.timeStamp, event.eventType);
+ }
+ notifyStatsChangedLocked();
+ }
+ }
+
+ /**
+ * Called by the Binder stub.
+ */
+ UsageStats[] getUsageStats(int bucketType, long beginTime) {
+ if (bucketType < 0 || bucketType >= mCurrentStats.length) {
+ return UsageStats.EMPTY_STATS;
+ }
+
+ final long timeNow = System.currentTimeMillis();
+ if (beginTime > timeNow) {
+ return UsageStats.EMPTY_STATS;
+ }
+
+ synchronized (mLock) {
+ if (beginTime >= mCurrentStats[bucketType].mEndTimeStamp) {
+ if (DEBUG) {
+ Slog.d(TAG, "Requesting stats after " + beginTime + " but latest is "
+ + mCurrentStats[bucketType].mEndTimeStamp);
+ }
+ // Nothing newer available.
+ return UsageStats.EMPTY_STATS;
+ } else if (beginTime >= mCurrentStats[bucketType].mBeginTimeStamp) {
+ if (DEBUG) {
+ Slog.d(TAG, "Returning in-memory stats");
+ }
+ // Fast path for retrieving in-memory state.
+ // TODO(adamlesinski): This copy just to parcel the object is wasteful.
+ // It would be nice to parcel it here and send that back, but the Binder API
+ // would need to change.
+ return new UsageStats[] { new UsageStats(mCurrentStats[bucketType]) };
+ } else {
+ // Flush any changes that were made to disk before we do a disk query.
+ persistActiveStatsLocked();
+ }
+ }
+
+ if (DEBUG) {
+ Slog.d(TAG, "SELECT * FROM " + bucketType + " WHERE beginTime >= "
+ + beginTime + " LIMIT " + USAGE_STAT_RESULT_LIMIT);
+ }
+
+ UsageStats[] results = mDatabase.getUsageStats(bucketType, beginTime,
+ USAGE_STAT_RESULT_LIMIT);
+
+ if (DEBUG) {
+ Slog.d(TAG, "Results: " + results.length);
+ }
+ return results;
+ }
+
+ /**
+ * Called by the Binder stub.
+ */
+ UsageStats.Event[] getEvents(long time) {
+ return UsageStats.Event.EMPTY_EVENTS;
+ }
+
+ private void loadActiveStatsLocked() {
+ final long timeNow = System.currentTimeMillis();
+
+ Calendar tempCal = mDailyExpiryDate;
+ for (int i = 0; i < mCurrentStats.length; i++) {
+ tempCal.setTimeInMillis(timeNow);
+ UsageStatsUtils.truncateDateTo(i, tempCal);
+
+ if (mCurrentStats[i] != null &&
+ mCurrentStats[i].mBeginTimeStamp == tempCal.getTimeInMillis()) {
+ // These are the same, no need to load them (in memory stats are always newer
+ // than persisted stats).
+ continue;
+ }
+
+ UsageStats[] stats = mDatabase.getUsageStats(i, timeNow, 1);
+ if (stats != null && stats.length > 0) {
+ mCurrentStats[i] = stats[stats.length - 1];
+ } else {
+ mCurrentStats[i] = UsageStats.create(tempCal.getTimeInMillis(), timeNow);
+ }
+ }
+ mStatsChanged = false;
+ mDailyExpiryDate.setTimeInMillis(timeNow);
+ mDailyExpiryDate.add(Calendar.DAY_OF_YEAR, 1);
+ UsageStatsUtils.truncateDateTo(UsageStatsManager.DAILY_BUCKET, mDailyExpiryDate);
+ Slog.i(TAG, "Rollover scheduled for " + sDateFormat.format(mDailyExpiryDate.getTime()));
+ }
+
+
+ private void persistActiveStatsLocked() {
+ if (mStatsChanged) {
+ Slog.i(TAG, "Flushing usage stats to disk");
+ try {
+ for (int i = 0; i < mCurrentStats.length; i++) {
+ mDatabase.putUsageStats(i, mCurrentStats[i]);
+ }
+ mStatsChanged = false;
+ mHandler.removeMessages(MSG_FLUSH_TO_DISK);
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to persist active stats", e);
+ }
+ }
+ }
+
+ private void updateStatsLocked(UsageStats stats, String packageName, long timeStamp,
+ int eventType) {
+ PackageUsageStats pkgStats = stats.getOrCreatePackageUsageStats(packageName);
+
+ // TODO(adamlesinski): Ensure that we recover from incorrect event sequences
+ // like double MOVE_TO_BACKGROUND, etc.
+ if (eventType == UsageStats.Event.MOVE_TO_BACKGROUND ||
+ eventType == UsageStats.Event.END_OF_DAY) {
+ if (pkgStats.mLastEvent == UsageStats.Event.MOVE_TO_FOREGROUND ||
+ pkgStats.mLastEvent == UsageStats.Event.CONTINUE_PREVIOUS_DAY) {
+ pkgStats.mTotalTimeSpent += timeStamp - pkgStats.mLastTimeUsed;
+ }
+ }
+ pkgStats.mLastEvent = eventType;
+ pkgStats.mLastTimeUsed = timeStamp;
+ stats.mEndTimeStamp = timeStamp;
+ }
+
+ class H extends Handler {
+ public H(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_REPORT_EVENT:
+ reportEvent((UsageStats.Event) msg.obj);
+ break;
+
+ case MSG_FLUSH_TO_DISK:
+ synchronized (mLock) {
+ persistActiveStatsLocked();
+ }
+ break;
+
+ default:
+ super.handleMessage(msg);
+ break;
+ }
+ }
+ }
+
+ private class BinderService extends IUsageStatsManager.Stub {
+
+ @Override
+ public UsageStats[] getStatsSince(int bucketType, long time, String callingPackage) {
+ if (mAppOps.checkOp(AppOpsManager.OP_GET_USAGE_STATS, Binder.getCallingUid(),
+ callingPackage) != AppOpsManager.MODE_ALLOWED) {
+ return UsageStats.EMPTY_STATS;
+ }
+
+ long token = Binder.clearCallingIdentity();
+ try {
+ return getUsageStats(bucketType, time);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public UsageStats.Event[] getEventsSince(long time, String callingPackage) {
+ if (mAppOps.checkOp(AppOpsManager.OP_GET_USAGE_STATS, Binder.getCallingUid(),
+ callingPackage) != AppOpsManager.MODE_ALLOWED) {
+ return UsageStats.Event.EMPTY_EVENTS;
+ }
+
+ long token = Binder.clearCallingIdentity();
+ try {
+ return getEvents(time);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ }
+
+ /**
+ * This local service implementation is primarily used by ActivityManagerService.
+ * ActivityManagerService will call these methods holding the 'am' lock, which means we
+ * shouldn't be doing any IO work or other long running tasks in these methods.
+ */
+ private class LocalService extends UsageStatsManagerInternal {
+
+ @Override
+ public void reportEvent(ComponentName component, long timeStamp, int eventType) {
+ UsageStats.Event event = new UsageStats.Event(component.getPackageName(), timeStamp,
+ eventType);
+ mHandler.obtainMessage(MSG_REPORT_EVENT, event).sendToTarget();
+ }
+
+ @Override
+ public void prepareShutdown() {
+ // This method *WILL* do IO work, but we must block until it is finished or else
+ // we might not shutdown cleanly. This is ok to do with the 'am' lock held, because
+ // we are shutting down.
+ shutdown();
+ }
+ }
+}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsUtils.java b/services/usage/java/com/android/server/usage/UsageStatsUtils.java
new file mode 100644
index 0000000..887e016
--- /dev/null
+++ b/services/usage/java/com/android/server/usage/UsageStatsUtils.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.android.server.usage;
+
+import android.app.usage.UsageStatsManager;
+
+import java.util.Calendar;
+
+/**
+ * A collection of utility methods used by the UsageStatsService and accompanying classes.
+ */
+final class UsageStatsUtils {
+ private UsageStatsUtils() {}
+
+ /**
+ * Truncates the date to the given UsageStats bucket. For example, if the bucket is
+ * {@link UsageStatsManager#YEARLY_BUCKET}, the date is truncated to the 1st day of the year,
+ * with the time set to 00:00:00.
+ *
+ * @param bucket The UsageStats bucket to truncate to.
+ * @param cal The date to truncate.
+ */
+ public static void truncateDateTo(int bucket, Calendar cal) {
+ cal.set(Calendar.HOUR_OF_DAY, 0);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+
+ switch (bucket) {
+ case UsageStatsManager.YEARLY_BUCKET:
+ cal.set(Calendar.DAY_OF_YEAR, 0);
+ break;
+
+ case UsageStatsManager.MONTHLY_BUCKET:
+ cal.set(Calendar.DAY_OF_MONTH, 0);
+ break;
+
+ case UsageStatsManager.WEEKLY_BUCKET:
+ cal.set(Calendar.DAY_OF_WEEK, 0);
+ break;
+
+ case UsageStatsManager.DAILY_BUCKET:
+ break;
+
+ default:
+ throw new UnsupportedOperationException("Can't truncate date to bucket " + bucket);
+ }
+ }
+}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsXml.java b/services/usage/java/com/android/server/usage/UsageStatsXml.java
new file mode 100644
index 0000000..78f89d0
--- /dev/null
+++ b/services/usage/java/com/android/server/usage/UsageStatsXml.java
@@ -0,0 +1,161 @@
+/**
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.android.server.usage;
+
+import android.app.usage.PackageUsageStats;
+import android.app.usage.UsageStats;
+import android.util.AtomicFile;
+import android.util.Slog;
+import android.util.Xml;
+import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.XmlUtils;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.ProtocolException;
+
+public class UsageStatsXml {
+ private static final String TAG = "UsageStatsXml";
+ private static final int CURRENT_VERSION = 1;
+
+ public static UsageStats read(AtomicFile file) throws IOException {
+ try {
+ FileInputStream in = file.openRead();
+ try {
+ return read(in);
+ } finally {
+ try {
+ in.close();
+ } catch (IOException e) {
+ // Empty
+ }
+ }
+ } catch (FileNotFoundException e) {
+ Slog.e(TAG, "UsageStats Xml", e);
+ throw e;
+ }
+ }
+
+ private static final String USAGESTATS_TAG = "usagestats";
+ private static final String VERSION_ATTR = "version";
+ private static final String BEGIN_TIME_ATTR = "beginTime";
+ private static final String END_TIME_ATTR = "endTime";
+ private static final String PACKAGE_TAG = "package";
+ private static final String NAME_ATTR = "name";
+ private static final String TOTAL_TIME_ACTIVE_ATTR = "totalTimeActive";
+ private static final String LAST_TIME_ACTIVE_ATTR = "lastTimeActive";
+ private static final String LAST_EVENT_ATTR = "lastEvent";
+
+ public static UsageStats read(InputStream in) throws IOException {
+ XmlPullParser parser = Xml.newPullParser();
+ try {
+ parser.setInput(in, "utf-8");
+ XmlUtils.beginDocument(parser, USAGESTATS_TAG);
+ String versionStr = parser.getAttributeValue(null, VERSION_ATTR);
+ try {
+ switch (Integer.parseInt(versionStr)) {
+ case 1:
+ return loadVersion1(parser);
+ default:
+ Slog.e(TAG, "Unrecognized version " + versionStr);
+ throw new IOException("Unrecognized version " + versionStr);
+ }
+ } catch (NumberFormatException e) {
+ Slog.e(TAG, "Bad version");
+ throw new IOException(e);
+ }
+ } catch (XmlPullParserException e) {
+ Slog.e(TAG, "Failed to parse Xml", e);
+ throw new IOException(e);
+ }
+ }
+
+ private static UsageStats loadVersion1(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ long beginTime = XmlUtils.readLongAttribute(parser, BEGIN_TIME_ATTR);
+ long endTime = XmlUtils.readLongAttribute(parser, END_TIME_ATTR);
+ UsageStats stats = UsageStats.create(beginTime, endTime);
+
+ XmlUtils.nextElement(parser);
+
+ while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
+ if (parser.getName().equals(PACKAGE_TAG)) {
+ String name = parser.getAttributeValue(null, NAME_ATTR);
+ if (name == null) {
+ throw new ProtocolException("no " + NAME_ATTR + " attribute present");
+ }
+
+ PackageUsageStats pkgStats = stats.getOrCreatePackageUsageStats(name);
+ pkgStats.mTotalTimeSpent = XmlUtils.readLongAttribute(parser,
+ TOTAL_TIME_ACTIVE_ATTR);
+ pkgStats.mLastTimeUsed = XmlUtils.readLongAttribute(parser, LAST_TIME_ACTIVE_ATTR);
+ pkgStats.mLastEvent = XmlUtils.readIntAttribute(parser, LAST_EVENT_ATTR);
+ }
+
+ // TODO(adamlesinski): Read in events here if there are any.
+
+ XmlUtils.skipCurrentTag(parser);
+ }
+ return stats;
+ }
+
+ public static void write(UsageStats stats, AtomicFile file) throws IOException {
+ FileOutputStream fos = file.startWrite();
+ try {
+ write(stats, fos);
+ file.finishWrite(fos);
+ fos = null;
+ } finally {
+ // When fos is null (successful write), this will no-op
+ file.failWrite(fos);
+ }
+ }
+
+ public static void write(UsageStats stats, OutputStream out) throws IOException {
+ FastXmlSerializer xml = new FastXmlSerializer();
+ xml.setOutput(out, "utf-8");
+ xml.startDocument("utf-8", true);
+ xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+ xml.startTag(null, USAGESTATS_TAG);
+ xml.attribute(null, VERSION_ATTR, Integer.toString(CURRENT_VERSION));
+ xml.attribute(null, BEGIN_TIME_ATTR, Long.toString(stats.mBeginTimeStamp));
+ xml.attribute(null, END_TIME_ATTR, Long.toString(stats.mEndTimeStamp));
+
+ // Body of the stats
+ final int pkgCount = stats.getPackageCount();
+ for (int i = 0; i < pkgCount; i++) {
+ final PackageUsageStats pkgStats = stats.getPackage(i);
+ xml.startTag(null, PACKAGE_TAG);
+ xml.attribute(null, NAME_ATTR, pkgStats.mPackageName);
+ xml.attribute(null, TOTAL_TIME_ACTIVE_ATTR, Long.toString(pkgStats.mTotalTimeSpent));
+ xml.attribute(null, LAST_TIME_ACTIVE_ATTR, Long.toString(pkgStats.mLastTimeUsed));
+ xml.attribute(null, LAST_EVENT_ATTR, Integer.toString(pkgStats.mLastEvent));
+ xml.endTag(null, PACKAGE_TAG);
+ }
+
+ // TODO(adamlesinski): Write out events here if there are any.
+
+ xml.endTag(null, USAGESTATS_TAG);
+ xml.endDocument();
+ }
+}