summaryrefslogtreecommitdiffstats
path: root/services/usage
diff options
context:
space:
mode:
authorAdam Lesinski <adamlesinski@google.com>2014-07-21 15:25:30 -0700
committerAdam Lesinski <adamlesinski@google.com>2014-08-08 12:08:15 -0700
commit3516800b611a79339a3c188332d13a26e9086b09 (patch)
tree4c9f8791534cc81cd9562223d2929ee8ff554bfc /services/usage
parent5c09e8ad5ee8e67976066366527ee58792551953 (diff)
downloadframeworks_base-3516800b611a79339a3c188332d13a26e9086b09.zip
frameworks_base-3516800b611a79339a3c188332d13a26e9086b09.tar.gz
frameworks_base-3516800b611a79339a3c188332d13a26e9086b09.tar.bz2
Second iteration of the UsageStats API
Based on feedback from API council, updated the API. Also added support for querying the event log. Change-Id: Ibaa008b9e5bd145acdfe8e20c25c2ed2d96be123
Diffstat (limited to 'services/usage')
-rw-r--r--services/usage/java/com/android/server/usage/IntervalStats.java81
-rw-r--r--services/usage/java/com/android/server/usage/UsageStatsDatabase.java151
-rw-r--r--services/usage/java/com/android/server/usage/UsageStatsService.java67
-rw-r--r--services/usage/java/com/android/server/usage/UsageStatsUtils.java10
-rw-r--r--services/usage/java/com/android/server/usage/UsageStatsXml.java95
-rw-r--r--services/usage/java/com/android/server/usage/UsageStatsXmlV1.java183
-rw-r--r--services/usage/java/com/android/server/usage/UserUsageStatsService.java240
7 files changed, 605 insertions, 222 deletions
diff --git a/services/usage/java/com/android/server/usage/IntervalStats.java b/services/usage/java/com/android/server/usage/IntervalStats.java
new file mode 100644
index 0000000..43027ad
--- /dev/null
+++ b/services/usage/java/com/android/server/usage/IntervalStats.java
@@ -0,0 +1,81 @@
+/**
+ * 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.UsageEvents;
+import android.app.usage.UsageStats;
+import android.content.ComponentName;
+import android.util.ArrayMap;
+
+class IntervalStats {
+ public long beginTime;
+ public long endTime;
+ public long lastTimeSaved;
+ public final ArrayMap<String, UsageStats> stats = new ArrayMap<>();
+ public TimeSparseArray<UsageEvents.Event> events;
+
+ // Maps flattened string representations of component names to ComponentName.
+ // This helps save memory from using many duplicate ComponentNames and
+ // parse time when reading XML.
+ private final ArrayMap<String, ComponentName> mComponentNames = new ArrayMap<>();
+
+ UsageStats getOrCreateUsageStats(String packageName) {
+ UsageStats usageStats = stats.get(packageName);
+ if (usageStats == null) {
+ usageStats = new UsageStats();
+ usageStats.mPackageName = packageName;
+ usageStats.mBeginTimeStamp = beginTime;
+ usageStats.mEndTimeStamp = endTime;
+ stats.put(packageName, usageStats);
+ }
+ return usageStats;
+ }
+
+ void update(String packageName, long timeStamp, int eventType) {
+ UsageStats usageStats = getOrCreateUsageStats(packageName);
+
+ // TODO(adamlesinski): Ensure that we recover from incorrect event sequences
+ // like double MOVE_TO_BACKGROUND, etc.
+ if (eventType == UsageEvents.Event.MOVE_TO_BACKGROUND ||
+ eventType == UsageEvents.Event.END_OF_DAY) {
+ if (usageStats.mLastEvent == UsageEvents.Event.MOVE_TO_FOREGROUND ||
+ usageStats.mLastEvent == UsageEvents.Event.CONTINUE_PREVIOUS_DAY) {
+ usageStats.mTotalTimeInForeground += timeStamp - usageStats.mLastTimeUsed;
+ }
+ }
+ usageStats.mLastEvent = eventType;
+ usageStats.mLastTimeUsed = timeStamp;
+ usageStats.mEndTimeStamp = timeStamp;
+ endTime = timeStamp;
+ }
+
+ /**
+ * Return a ComponentName for the given string representation. This will use a cached
+ * copy of the ComponentName if possible, otherwise it will parse and add it to the
+ * internal cache.
+ */
+ ComponentName getCachedComponentName(String str) {
+ ComponentName name = mComponentNames.get(str);
+ if (name == null) {
+ name = ComponentName.unflattenFromString(str);
+ if (name != null) {
+ mComponentNames.put(str, name);
+ }
+ }
+ return name;
+ }
+}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
index 4e75f61..e6ce0fe 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
@@ -27,30 +27,37 @@ import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
+import java.util.List;
+/**
+ * Provides an interface to query for UsageStat data from an XML database.
+ */
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 File[] mIntervalDirs;
private final TimeSparseArray<AtomicFile>[] mSortedStatFiles;
private final Calendar mCal;
public UsageStatsDatabase(File dir) {
- mBucketDirs = new File[] {
+ mIntervalDirs = new File[] {
new File(dir, "daily"),
new File(dir, "weekly"),
new File(dir, "monthly"),
new File(dir, "yearly"),
};
- mSortedStatFiles = new TimeSparseArray[mBucketDirs.length];
+ mSortedStatFiles = new TimeSparseArray[mIntervalDirs.length];
mCal = Calendar.getInstance();
}
+ /**
+ * Initialize any directories required and index what stats are available.
+ */
void init() {
synchronized (mLock) {
- for (File f : mBucketDirs) {
+ for (File f : mIntervalDirs) {
f.mkdirs();
if (!f.exists()) {
throw new IllegalStateException("Failed to create directory "
@@ -68,10 +75,10 @@ class UsageStatsDatabase {
// 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);
+ File[] files = mIntervalDirs[i].listFiles(backupFileFilter);
if (files != null) {
if (DEBUG) {
- Slog.d(TAG, "Found " + files.length + " stat files for bucket " + i);
+ Slog.d(TAG, "Found " + files.length + " stat files for interval " + i);
}
for (File f : files) {
@@ -82,21 +89,24 @@ class UsageStatsDatabase {
}
}
- public UsageStats getLatestUsageStats(int bucketType) {
+ /**
+ * Get the latest stats that exist for this interval type.
+ */
+ public IntervalStats getLatestUsageStats(int intervalType) {
synchronized (mLock) {
- if (bucketType < 0 || bucketType >= mBucketDirs.length) {
- throw new IllegalArgumentException("Bad bucket type " + bucketType);
+ if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
+ throw new IllegalArgumentException("Bad interval type " + intervalType);
}
- final int fileCount = mSortedStatFiles[bucketType].size();
+ final int fileCount = mSortedStatFiles[intervalType].size();
if (fileCount == 0) {
return null;
}
try {
- final AtomicFile f = mSortedStatFiles[bucketType].valueAt(fileCount - 1);
- UsageStats stats = UsageStatsXml.read(f);
- stats.mLastTimeSaved = f.getLastModifiedTime();
+ final AtomicFile f = mSortedStatFiles[intervalType].valueAt(fileCount - 1);
+ IntervalStats stats = new IntervalStats();
+ UsageStatsXml.read(f, stats);
return stats;
} catch (IOException e) {
Slog.e(TAG, "Failed to read usage stats file", e);
@@ -105,62 +115,114 @@ class UsageStatsDatabase {
return null;
}
- public UsageStats[] getUsageStats(int bucketType, long beginTime, int limit) {
+ /**
+ * Get the time at which the latest stats begin for this interval type.
+ */
+ public long getLatestUsageStatsBeginTime(int intervalType) {
synchronized (mLock) {
- if (bucketType < 0 || bucketType >= mBucketDirs.length) {
- throw new IllegalArgumentException("Bad bucket type " + bucketType);
+ if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
+ throw new IllegalArgumentException("Bad interval type " + intervalType);
}
- if (limit <= 0) {
- return UsageStats.EMPTY_STATS;
+ final int statsFileCount = mSortedStatFiles[intervalType].size();
+ if (statsFileCount > 0) {
+ return mSortedStatFiles[intervalType].keyAt(statsFileCount - 1);
}
+ return -1;
+ }
+ }
- int startIndex = mSortedStatFiles[bucketType].closestIndexAfter(beginTime);
+ /**
+ * Find all {@link UsageStats} for the given range and interval type.
+ */
+ public List<UsageStats> queryUsageStats(int intervalType, long beginTime, long endTime) {
+ synchronized (mLock) {
+ if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
+ throw new IllegalArgumentException("Bad interval type " + intervalType);
+ }
+
+ if (endTime < beginTime) {
+ return null;
+ }
+
+ final int startIndex = mSortedStatFiles[intervalType].closestIndexOnOrBefore(beginTime);
if (startIndex < 0) {
- return UsageStats.EMPTY_STATS;
+ return null;
+ }
+
+ int endIndex = mSortedStatFiles[intervalType].closestIndexOnOrAfter(endTime);
+ if (endIndex < 0) {
+ endIndex = mSortedStatFiles[intervalType].size() - 1;
}
- 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);
+ IntervalStats stats = new IntervalStats();
+ ArrayList<UsageStats> results = new ArrayList<>();
+ for (int i = startIndex; i <= endIndex; i++) {
+ final AtomicFile f = mSortedStatFiles[intervalType].valueAt(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);
+ UsageStatsXml.read(f, stats);
+ if (beginTime < stats.endTime) {
+ results.addAll(stats.stats.values());
}
}
- return stats.toArray(new UsageStats[stats.size()]);
+ return results;
} catch (IOException e) {
Slog.e(TAG, "Failed to read usage stats file", e);
- return UsageStats.EMPTY_STATS;
+ return null;
}
}
}
+ /**
+ * Find the interval that best matches this range.
+ *
+ * TODO(adamlesinski): Use endTimeStamp in best fit calculation.
+ */
+ public int findBestFitBucket(long beginTimeStamp, long endTimeStamp) {
+ synchronized (mLock) {
+ int bestBucket = -1;
+ long smallestDiff = Long.MAX_VALUE;
+ for (int i = mSortedStatFiles.length - 1; i >= 0; i--) {
+ final int index = mSortedStatFiles[i].closestIndexOnOrBefore(beginTimeStamp);
+ int size = mSortedStatFiles[i].size();
+ if (index >= 0 && index < size) {
+ // We have some results here, check if they are better than our current match.
+ long diff = Math.abs(mSortedStatFiles[i].keyAt(index) - beginTimeStamp);
+ if (diff < smallestDiff) {
+ smallestDiff = diff;
+ bestBucket = i;
+ }
+ }
+ }
+ return bestBucket;
+ }
+ }
+
+ /**
+ * Remove any usage stat files that are too old.
+ */
public void prune() {
synchronized (mLock) {
long timeNow = System.currentTimeMillis();
mCal.setTimeInMillis(timeNow);
mCal.add(Calendar.MONTH, -6);
- pruneFilesOlderThan(mBucketDirs[UsageStatsManager.MONTHLY_BUCKET],
+ pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_MONTHLY],
mCal.getTimeInMillis());
mCal.setTimeInMillis(timeNow);
mCal.add(Calendar.WEEK_OF_YEAR, -4);
- pruneFilesOlderThan(mBucketDirs[UsageStatsManager.WEEKLY_BUCKET],
+ pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_WEEKLY],
mCal.getTimeInMillis());
mCal.setTimeInMillis(timeNow);
mCal.add(Calendar.DAY_OF_YEAR, -7);
- pruneFilesOlderThan(mBucketDirs[UsageStatsManager.DAILY_BUCKET],
+ pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_DAILY],
mCal.getTimeInMillis());
}
}
@@ -177,23 +239,24 @@ class UsageStatsDatabase {
}
}
- public void putUsageStats(int bucketType, UsageStats stats)
- throws IOException {
+ /**
+ * Update the stats in the database. They may not be written to disk immediately.
+ */
+ public void putUsageStats(int intervalType, IntervalStats stats) throws IOException {
synchronized (mLock) {
- if (bucketType < 0 || bucketType >= mBucketDirs.length) {
- throw new IllegalArgumentException("Bad bucket type " + bucketType);
+ if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
+ throw new IllegalArgumentException("Bad interval type " + intervalType);
}
- AtomicFile f = mSortedStatFiles[bucketType].get(stats.mBeginTimeStamp);
+ AtomicFile f = mSortedStatFiles[intervalType].get(stats.beginTime);
if (f == null) {
- f = new AtomicFile(new File(mBucketDirs[bucketType],
- Long.toString(stats.mBeginTimeStamp)));
- mSortedStatFiles[bucketType].append(stats.mBeginTimeStamp, f);
+ f = new AtomicFile(new File(mIntervalDirs[intervalType],
+ Long.toString(stats.beginTime)));
+ mSortedStatFiles[intervalType].put(stats.beginTime, f);
}
- UsageStatsXml.write(stats, f);
- stats.mLastTimeSaved = f.getLastModifiedTime();
+ UsageStatsXml.write(f, stats);
+ stats.lastTimeSaved = f.getLastModifiedTime();
}
}
-
}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 1c20d5d..c38391a 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -19,8 +19,8 @@ package com.android.server.usage;
import android.Manifest;
import android.app.AppOpsManager;
import android.app.usage.IUsageStatsManager;
+import android.app.usage.UsageEvents;
import android.app.usage.UsageStats;
-import android.app.usage.UsageStatsManager;
import android.app.usage.UsageStatsManagerInternal;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -28,6 +28,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
import android.content.pm.UserInfo;
import android.os.Binder;
import android.os.Environment;
@@ -47,6 +48,10 @@ import java.io.File;
import java.util.Arrays;
import java.util.List;
+/**
+ * A service that collects, aggregates, and persists application usage data.
+ * This data can be queried by apps that have been granted permission by AppOps.
+ */
public class UsageStatsService extends SystemService implements
UserUsageStatsService.StatsUpdatedListener {
static final String TAG = "UsageStatsService";
@@ -54,8 +59,9 @@ public class UsageStatsService extends SystemService implements
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 TWO_MINUTES = 2 * 60 * 1000;
private static final long FLUSH_INTERVAL = DEBUG ? TEN_SECONDS : TWENTY_MINUTES;
- static final int USAGE_STAT_RESULT_LIMIT = 10;
+ private static final long END_TIME_DELAY = DEBUG ? 0 : TWO_MINUTES;
// Handler message types.
static final int MSG_REPORT_EVENT = 0;
@@ -181,7 +187,7 @@ public class UsageStatsService extends SystemService implements
/**
* Called by the Binder stub.
*/
- void reportEvent(UsageStats.Event event, int userId) {
+ void reportEvent(UsageEvents.Event event, int userId) {
synchronized (mLock) {
final UserUsageStatsService service = getUserDataAndInitializeIfNeededLocked(userId);
service.reportEvent(event);
@@ -211,27 +217,37 @@ public class UsageStatsService extends SystemService implements
/**
* Called by the Binder stub.
*/
- UsageStats[] getUsageStats(int userId, int bucketType, long beginTime) {
- if (bucketType < 0 || bucketType >= UsageStatsManager.BUCKET_COUNT) {
- return UsageStats.EMPTY_STATS;
- }
-
+ List<UsageStats> queryUsageStats(int userId, int bucketType, long beginTime, long endTime) {
final long timeNow = System.currentTimeMillis();
if (beginTime > timeNow) {
- return UsageStats.EMPTY_STATS;
+ return null;
}
synchronized (mLock) {
UserUsageStatsService service = getUserDataAndInitializeIfNeededLocked(userId);
- return service.getUsageStats(bucketType, beginTime);
+ return service.queryUsageStats(bucketType, beginTime, endTime);
}
}
/**
* Called by the Binder stub.
*/
- UsageStats.Event[] getEvents(int userId, long time) {
- return UsageStats.Event.EMPTY_EVENTS;
+ UsageEvents queryEvents(int userId, long beginTime, long endTime) {
+ final long timeNow = System.currentTimeMillis();
+
+ // Adjust the endTime so that we don't query for the latest events.
+ // This is to prevent apps from making decision based on what app launched them,
+ // etc.
+ endTime = Math.min(endTime, timeNow - END_TIME_DELAY);
+
+ if (beginTime > endTime) {
+ return null;
+ }
+
+ synchronized (mLock) {
+ UserUsageStatsService service = getUserDataAndInitializeIfNeededLocked(userId);
+ return service.queryEvents(beginTime, endTime);
+ }
}
private void flushToDiskLocked() {
@@ -253,7 +269,7 @@ public class UsageStatsService extends SystemService implements
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_REPORT_EVENT:
- reportEvent((UsageStats.Event) msg.obj, msg.arg1);
+ reportEvent((UsageEvents.Event) msg.obj, msg.arg1);
break;
case MSG_FLUSH_TO_DISK:
@@ -286,30 +302,32 @@ public class UsageStatsService extends SystemService implements
}
@Override
- public UsageStats[] getStatsSince(int bucketType, long time, String callingPackage) {
+ public ParceledListSlice<UsageStats> queryUsageStats(int bucketType, long beginTime,
+ long endTime, String callingPackage) {
if (!hasPermission(callingPackage)) {
- return UsageStats.EMPTY_STATS;
+ return null;
}
final int userId = UserHandle.getCallingUserId();
final long token = Binder.clearCallingIdentity();
try {
- return getUsageStats(userId, bucketType, time);
+ return new ParceledListSlice<>(UsageStatsService.this.queryUsageStats(
+ userId, bucketType, beginTime, endTime));
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override
- public UsageStats.Event[] getEventsSince(long time, String callingPackage) {
+ public UsageEvents queryEvents(long beginTime, long endTime, String callingPackage) {
if (!hasPermission(callingPackage)) {
- return UsageStats.Event.EMPTY_EVENTS;
+ return null;
}
final int userId = UserHandle.getCallingUserId();
final long token = Binder.clearCallingIdentity();
try {
- return getEvents(userId, time);
+ return UsageStatsService.this.queryEvents(userId, beginTime, endTime);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -326,8 +344,15 @@ public class UsageStatsService extends SystemService implements
@Override
public void reportEvent(ComponentName component, int userId,
long timeStamp, int eventType) {
- UsageStats.Event event = new UsageStats.Event(component.getPackageName(), timeStamp,
- eventType);
+ if (component == null) {
+ Slog.w(TAG, "Event reported without a component name");
+ return;
+ }
+
+ UsageEvents.Event event = new UsageEvents.Event();
+ event.mComponent = component;
+ event.mTimeStamp = timeStamp;
+ event.mEventType = eventType;
mHandler.obtainMessage(MSG_REPORT_EVENT, userId, 0, event).sendToTarget();
}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsUtils.java b/services/usage/java/com/android/server/usage/UsageStatsUtils.java
index 887e016..dd5f3b9 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsUtils.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsUtils.java
@@ -28,7 +28,7 @@ final class 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,
+ * {@link UsageStatsManager#INTERVAL_YEARLY}, 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.
@@ -41,19 +41,19 @@ final class UsageStatsUtils {
cal.set(Calendar.MILLISECOND, 0);
switch (bucket) {
- case UsageStatsManager.YEARLY_BUCKET:
+ case UsageStatsManager.INTERVAL_YEARLY:
cal.set(Calendar.DAY_OF_YEAR, 0);
break;
- case UsageStatsManager.MONTHLY_BUCKET:
+ case UsageStatsManager.INTERVAL_MONTHLY:
cal.set(Calendar.DAY_OF_MONTH, 0);
break;
- case UsageStatsManager.WEEKLY_BUCKET:
+ case UsageStatsManager.INTERVAL_WEEKLY:
cal.set(Calendar.DAY_OF_WEEK, 0);
break;
- case UsageStatsManager.DAILY_BUCKET:
+ case UsageStatsManager.INTERVAL_DAILY:
break;
default:
diff --git a/services/usage/java/com/android/server/usage/UsageStatsXml.java b/services/usage/java/com/android/server/usage/UsageStatsXml.java
index 78f89d0..48881d0 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsXml.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsXml.java
@@ -16,8 +16,6 @@
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;
@@ -32,17 +30,19 @@ 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;
+ private static final String USAGESTATS_TAG = "usagestats";
+ private static final String VERSION_ATTR = "version";
- public static UsageStats read(AtomicFile file) throws IOException {
+ public static void read(AtomicFile file, IntervalStats statsOut) throws IOException {
try {
FileInputStream in = file.openRead();
try {
- return read(in);
+ read(in, statsOut);
+ statsOut.lastTimeSaved = file.getLastModifiedTime();
} finally {
try {
in.close();
@@ -56,17 +56,19 @@ public class UsageStatsXml {
}
}
- 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 void write(AtomicFile file, IntervalStats stats) throws IOException {
+ FileOutputStream fos = file.startWrite();
+ try {
+ write(fos, stats);
+ file.finishWrite(fos);
+ fos = null;
+ } finally {
+ // When fos is null (successful write), this will no-op
+ file.failWrite(fos);
+ }
+ }
- public static UsageStats read(InputStream in) throws IOException {
+ private static void read(InputStream in, IntervalStats statsOut) throws IOException {
XmlPullParser parser = Xml.newPullParser();
try {
parser.setInput(in, "utf-8");
@@ -75,7 +77,9 @@ public class UsageStatsXml {
try {
switch (Integer.parseInt(versionStr)) {
case 1:
- return loadVersion1(parser);
+ UsageStatsXmlV1.read(parser, statsOut);
+ break;
+
default:
Slog.e(TAG, "Unrecognized version " + versionStr);
throw new IOException("Unrecognized version " + versionStr);
@@ -90,70 +94,15 @@ public class UsageStatsXml {
}
}
- 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 {
+ private static void write(OutputStream out, IntervalStats stats) 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.
+ UsageStatsXmlV1.write(xml, stats);
xml.endTag(null, USAGESTATS_TAG);
xml.endDocument();
diff --git a/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java b/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java
new file mode 100644
index 0000000..916601b
--- /dev/null
+++ b/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java
@@ -0,0 +1,183 @@
+/**
+ * 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 com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.app.usage.TimeSparseArray;
+import android.app.usage.UsageEvents;
+import android.app.usage.UsageStats;
+import android.content.ComponentName;
+
+import java.io.IOException;
+import java.net.ProtocolException;
+
+/**
+ * UsageStats reader/writer for version 1 of the XML format.
+ */
+final class UsageStatsXmlV1 {
+ 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";
+ private static final String EVENT_LOG_TAG = "event-log";
+ private static final String TYPE_ATTR = "type";
+ private static final String TIME_ATTR = "time";
+
+ private static UsageStats readNextUsageStats(XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ XmlUtils.nextElement(parser);
+ }
+
+ if (parser.getEventType() != XmlPullParser.START_TAG ||
+ !parser.getName().equals(PACKAGE_TAG)) {
+ return null;
+ }
+
+ final String name = parser.getAttributeValue(null, NAME_ATTR);
+ if (name == null) {
+ throw new ProtocolException("no " + NAME_ATTR + " attribute present");
+ }
+
+ UsageStats stats = new UsageStats();
+ stats.mPackageName = name;
+ stats.mTotalTimeInForeground = XmlUtils.readLongAttribute(parser, TOTAL_TIME_ACTIVE_ATTR);
+ stats.mLastTimeUsed = XmlUtils.readLongAttribute(parser, LAST_TIME_ACTIVE_ATTR);
+ stats.mLastEvent = XmlUtils.readIntAttribute(parser, LAST_EVENT_ATTR);
+ XmlUtils.skipCurrentTag(parser);
+ return stats;
+ }
+
+ private static UsageEvents.Event readNextEvent(XmlPullParser parser, IntervalStats statsOut)
+ throws XmlPullParserException, IOException {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ XmlUtils.nextElement(parser);
+ }
+
+ if (parser.getEventType() != XmlPullParser.START_TAG ||
+ !parser.getName().equals(EVENT_LOG_TAG)) {
+ return null;
+ }
+
+ final String componentName = XmlUtils.readStringAttribute(parser, NAME_ATTR);
+ if (componentName == null) {
+ throw new ProtocolException("no " + NAME_ATTR + " attribute present");
+ }
+
+ ComponentName component = statsOut.getCachedComponentName(componentName);
+ if (component == null) {
+ throw new ProtocolException("ComponentName " + componentName + " is invalid");
+ }
+
+ UsageEvents.Event event = new UsageEvents.Event();
+ event.mComponent = component;
+ event.mEventType = XmlUtils.readIntAttribute(parser, TYPE_ATTR);
+ event.mTimeStamp = XmlUtils.readLongAttribute(parser, TIME_ATTR);
+ XmlUtils.skipCurrentTag(parser);
+ return event;
+ }
+
+ private static void writeUsageStats(FastXmlSerializer serializer, UsageStats stats)
+ throws IOException {
+ serializer.startTag(null, PACKAGE_TAG);
+ serializer.attribute(null, NAME_ATTR, stats.mPackageName);
+ serializer.attribute(null, TOTAL_TIME_ACTIVE_ATTR,
+ Long.toString(stats.mTotalTimeInForeground));
+ serializer.attribute(null, LAST_TIME_ACTIVE_ATTR, Long.toString(stats.mLastTimeUsed));
+ serializer.attribute(null, LAST_EVENT_ATTR, Integer.toString(stats.mLastEvent));
+ serializer.endTag(null, PACKAGE_TAG);
+ }
+
+ private static void writeEvent(FastXmlSerializer serializer, UsageEvents.Event event)
+ throws IOException {
+ serializer.startTag(null, EVENT_LOG_TAG);
+ serializer.attribute(null, NAME_ATTR, event.getComponent().flattenToString());
+ serializer.attribute(null, TYPE_ATTR, Integer.toString(event.getEventType()));
+ serializer.attribute(null, TIME_ATTR, Long.toString(event.getTimeStamp()));
+ serializer.endTag(null, EVENT_LOG_TAG);
+ }
+
+ /**
+ * Reads from the {@link XmlPullParser}, assuming that it is already on the
+ * <code><usagestats></code> tag.
+ *
+ * @param parser The parser from which to read events.
+ * @param statsOut The stats object to populate with the data from the XML file.
+ */
+ public static void read(XmlPullParser parser, IntervalStats statsOut)
+ throws XmlPullParserException, IOException {
+ statsOut.stats.clear();
+
+ if (statsOut.events != null) {
+ statsOut.events.clear();
+ }
+
+ statsOut.beginTime = XmlUtils.readLongAttribute(parser, BEGIN_TIME_ATTR);
+ statsOut.endTime = XmlUtils.readLongAttribute(parser, END_TIME_ATTR);
+ XmlUtils.nextElement(parser);
+
+ UsageStats pkgStats;
+ while ((pkgStats = readNextUsageStats(parser)) != null) {
+ pkgStats.mBeginTimeStamp = statsOut.beginTime;
+ pkgStats.mEndTimeStamp = statsOut.endTime;
+ statsOut.stats.put(pkgStats.mPackageName, pkgStats);
+ }
+
+ UsageEvents.Event event;
+ while ((event = readNextEvent(parser, statsOut)) != null) {
+ if (statsOut.events == null) {
+ statsOut.events = new TimeSparseArray<>();
+ }
+ statsOut.events.put(event.getTimeStamp(), event);
+ }
+ }
+
+ /**
+ * Writes the stats object to an XML file. The {@link FastXmlSerializer}
+ * has already written the <code><usagestats></code> tag, but attributes may still
+ * be added.
+ *
+ * @param serializer The serializer to which to write the stats data.
+ * @param stats The stats object to write to the XML file.
+ */
+ public static void write(FastXmlSerializer serializer, IntervalStats stats) throws IOException {
+ serializer.attribute(null, BEGIN_TIME_ATTR, Long.toString(stats.beginTime));
+ serializer.attribute(null, END_TIME_ATTR, Long.toString(stats.endTime));
+
+ final int statsCount = stats.stats.size();
+ for (int i = 0; i < statsCount; i++) {
+ writeUsageStats(serializer, stats.stats.valueAt(i));
+ }
+
+ if (stats.events != null) {
+ final int eventCount = stats.events.size();
+ for (int i = 0; i < eventCount; i++) {
+ writeEvent(serializer, stats.events.valueAt(i));
+ }
+ }
+ }
+
+ private UsageStatsXmlV1() {
+ }
+}
diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
index d124188..2dfd0f6 100644
--- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
@@ -1,15 +1,36 @@
+/**
+ * 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.TimeSparseArray;
+import android.app.usage.UsageEvents;
import android.app.usage.UsageStats;
import android.app.usage.UsageStatsManager;
+import android.content.ComponentName;
import android.util.ArraySet;
import android.util.Slog;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Calendar;
+import java.util.List;
/**
* A per-user UsageStatsService. All methods are meant to be called with the main lock held
@@ -21,7 +42,7 @@ class UserUsageStatsService {
private static final SimpleDateFormat sDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private final UsageStatsDatabase mDatabase;
- private final UsageStats[] mCurrentStats = new UsageStats[UsageStatsManager.BUCKET_COUNT];
+ private final IntervalStats[] mCurrentStats;
private boolean mStatsChanged = false;
private final Calendar mDailyExpiryDate;
private final StatsUpdatedListener mListener;
@@ -34,6 +55,7 @@ class UserUsageStatsService {
UserUsageStatsService(int userId, File usageStatsDir, StatsUpdatedListener listener) {
mDailyExpiryDate = Calendar.getInstance();
mDatabase = new UsageStatsDatabase(usageStatsDir);
+ mCurrentStats = new IntervalStats[UsageStatsManager.INTERVAL_COUNT];
mListener = listener;
mLogPrefix = "User[" + Integer.toString(userId) + "] ";
}
@@ -45,6 +67,8 @@ class UserUsageStatsService {
for (int i = 0; i < mCurrentStats.length; i++) {
mCurrentStats[i] = mDatabase.getLatestUsageStats(i);
if (mCurrentStats[i] == null) {
+ // Find out how many intervals we don't have data for.
+ // Ideally it should be all or none.
nullCount++;
}
}
@@ -66,85 +90,138 @@ class UserUsageStatsService {
// This may actually be today and we will rollover on the first event
// that is reported.
mDailyExpiryDate.setTimeInMillis(
- mCurrentStats[UsageStatsManager.DAILY_BUCKET].mBeginTimeStamp);
+ mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime);
mDailyExpiryDate.add(Calendar.DAY_OF_YEAR, 1);
- UsageStatsUtils.truncateDateTo(UsageStatsManager.DAILY_BUCKET, mDailyExpiryDate);
+ UsageStatsUtils.truncateDateTo(UsageStatsManager.INTERVAL_DAILY, mDailyExpiryDate);
Slog.i(TAG, mLogPrefix + "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 (IntervalStats stat : mCurrentStats) {
+ final int pkgCount = stat.stats.size();
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) {
- updateStats(stat, pkgStats.mPackageName, stat.mLastTimeSaved,
- UsageStats.Event.END_OF_DAY);
+ UsageStats pkgStats = stat.stats.valueAt(i);
+ if (pkgStats.mLastEvent == UsageEvents.Event.MOVE_TO_FOREGROUND ||
+ pkgStats.mLastEvent == UsageEvents.Event.CONTINUE_PREVIOUS_DAY) {
+ stat.update(pkgStats.mPackageName, stat.lastTimeSaved,
+ UsageEvents.Event.END_OF_DAY);
notifyStatsChanged();
}
}
}
}
- void reportEvent(UsageStats.Event event) {
+ void reportEvent(UsageEvents.Event event) {
if (DEBUG) {
- Slog.d(TAG, mLogPrefix + "Got usage event for " + event.packageName
- + "[" + event.timeStamp + "]: "
- + eventToString(event.eventType));
+ Slog.d(TAG, mLogPrefix + "Got usage event for " + event.getComponent().getPackageName()
+ + "[" + event.getTimeStamp() + "]: "
+ + eventToString(event.getEventType()));
}
- if (event.timeStamp >= mDailyExpiryDate.getTimeInMillis()) {
+ if (event.getTimeStamp() >= mDailyExpiryDate.getTimeInMillis()) {
// Need to rollover
rolloverStats();
}
- for (UsageStats stats : mCurrentStats) {
- updateStats(stats, event.packageName, event.timeStamp, event.eventType);
+ if (mCurrentStats[UsageStatsManager.INTERVAL_DAILY].events == null) {
+ mCurrentStats[UsageStatsManager.INTERVAL_DAILY].events = new TimeSparseArray<>();
+ }
+ mCurrentStats[UsageStatsManager.INTERVAL_DAILY].events.put(event.getTimeStamp(), event);
+
+ for (IntervalStats stats : mCurrentStats) {
+ stats.update(event.getComponent().getPackageName(), event.getTimeStamp(),
+ event.getEventType());
}
notifyStatsChanged();
}
- UsageStats[] getUsageStats(int bucketType, long beginTime) {
- if (beginTime >= mCurrentStats[bucketType].mEndTimeStamp) {
+ List<UsageStats> queryUsageStats(int bucketType, long beginTime, long endTime) {
+ if (bucketType == UsageStatsManager.INTERVAL_BEST) {
+ bucketType = mDatabase.findBestFitBucket(beginTime, endTime);
+ }
+
+ if (bucketType < 0 || bucketType >= mCurrentStats.length) {
+ if (DEBUG) {
+ Slog.d(TAG, mLogPrefix + "Bad bucketType used " + bucketType);
+ }
+ return null;
+ }
+
+ if (beginTime >= mCurrentStats[bucketType].endTime) {
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Requesting stats after " + beginTime + " but latest is "
- + mCurrentStats[bucketType].mEndTimeStamp);
+ + mCurrentStats[bucketType].endTime);
}
// Nothing newer available.
- return UsageStats.EMPTY_STATS;
+ return null;
- } else if (beginTime >= mCurrentStats[bucketType].mBeginTimeStamp) {
+ } else if (beginTime >= mCurrentStats[bucketType].beginTime) {
if (DEBUG) {
- Slog.d(TAG, mLogPrefix + "Returning in-memory stats");
+ Slog.d(TAG, mLogPrefix + "Returning in-memory stats for bucket " + bucketType);
}
// 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.
- persistActiveStats();
+ ArrayList<UsageStats> results = new ArrayList<>();
+ final int packageCount = mCurrentStats[bucketType].stats.size();
+ for (int i = 0; i < packageCount; i++) {
+ results.add(new UsageStats(mCurrentStats[bucketType].stats.valueAt(i)));
+ }
+ return results;
}
+ // Flush any changes that were made to disk before we do a disk query.
+ // If we're not grabbing the ongoing stats, no need to persist.
+ persistActiveStats();
+
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "SELECT * FROM " + bucketType + " WHERE beginTime >= "
- + beginTime + " LIMIT " + UsageStatsService.USAGE_STAT_RESULT_LIMIT);
+ + beginTime + " AND endTime < " + endTime);
}
- final UsageStats[] results = mDatabase.getUsageStats(bucketType, beginTime,
- UsageStatsService.USAGE_STAT_RESULT_LIMIT);
-
+ final List<UsageStats> results = mDatabase.queryUsageStats(bucketType, beginTime, endTime);
if (DEBUG) {
- Slog.d(TAG, mLogPrefix + "Results: " + results.length);
+ Slog.d(TAG, mLogPrefix + "Results: " + results.size());
}
return results;
}
+ UsageEvents queryEvents(long beginTime, long endTime) {
+ if (endTime > mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime) {
+ if (beginTime > mCurrentStats[UsageStatsManager.INTERVAL_DAILY].endTime) {
+ return null;
+ }
+
+ TimeSparseArray<UsageEvents.Event> events =
+ mCurrentStats[UsageStatsManager.INTERVAL_DAILY].events;
+ if (events == null) {
+ return null;
+ }
+
+ final int startIndex = events.closestIndexOnOrAfter(beginTime);
+ if (startIndex < 0) {
+ return null;
+ }
+
+ ArraySet<ComponentName> names = new ArraySet<>();
+ ArrayList<UsageEvents.Event> results = new ArrayList<>();
+ final int size = events.size();
+ for (int i = startIndex; i < size; i++) {
+ if (events.keyAt(i) >= endTime) {
+ break;
+ }
+ names.add(events.valueAt(i).getComponent());
+ results.add(events.valueAt(i));
+ }
+ ComponentName[] table = names.toArray(new ComponentName[names.size()]);
+ Arrays.sort(table);
+ return new UsageEvents(results, table);
+ }
+
+ // TODO(adamlesinski): Query the previous days.
+ return null;
+ }
+
void persistActiveStats() {
if (mStatsChanged) {
Slog.i(TAG, mLogPrefix + "Flushing usage stats to disk");
@@ -166,15 +243,15 @@ class UserUsageStatsService {
// 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 (IntervalStats stat : mCurrentStats) {
+ final int pkgCount = stat.stats.size();
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) {
+ UsageStats pkgStats = stat.stats.valueAt(i);
+ if (pkgStats.mLastEvent == UsageEvents.Event.MOVE_TO_FOREGROUND ||
+ pkgStats.mLastEvent == UsageEvents.Event.CONTINUE_PREVIOUS_DAY) {
continuePreviousDay.add(pkgStats.mPackageName);
- updateStats(stat, pkgStats.mPackageName,
- mDailyExpiryDate.getTimeInMillis() - 1, UsageStats.Event.END_OF_DAY);
+ stat.update(pkgStats.mPackageName,
+ mDailyExpiryDate.getTimeInMillis() - 1, UsageEvents.Event.END_OF_DAY);
mStatsChanged = true;
}
}
@@ -187,10 +264,9 @@ class UserUsageStatsService {
final int continueCount = continuePreviousDay.size();
for (int i = 0; i < continueCount; i++) {
String name = continuePreviousDay.valueAt(i);
- for (UsageStats stat : mCurrentStats) {
- updateStats(stat, name,
- mCurrentStats[UsageStatsManager.DAILY_BUCKET].mBeginTimeStamp,
- UsageStats.Event.CONTINUE_PREVIOUS_DAY);
+ for (IntervalStats stat : mCurrentStats) {
+ stat.update(name, mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime,
+ UsageEvents.Event.CONTINUE_PREVIOUS_DAY);
mStatsChanged = true;
}
}
@@ -212,61 +288,67 @@ class UserUsageStatsService {
final long timeNow = System.currentTimeMillis();
Calendar tempCal = mDailyExpiryDate;
- for (int i = 0; i < mCurrentStats.length; i++) {
+ for (int bucketType = 0; bucketType < mCurrentStats.length; bucketType++) {
tempCal.setTimeInMillis(timeNow);
- UsageStatsUtils.truncateDateTo(i, tempCal);
+ UsageStatsUtils.truncateDateTo(bucketType, tempCal);
- if (mCurrentStats[i] != null &&
- mCurrentStats[i].mBeginTimeStamp == tempCal.getTimeInMillis()) {
+ if (mCurrentStats[bucketType] != null &&
+ mCurrentStats[bucketType].beginTime == 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];
+ final long lastBeginTime = mDatabase.getLatestUsageStatsBeginTime(bucketType);
+ if (lastBeginTime >= tempCal.getTimeInMillis()) {
+ if (DEBUG) {
+ Slog.d(TAG, mLogPrefix + "Loading existing stats (" + lastBeginTime +
+ ") for bucket " + bucketType);
+ }
+ mCurrentStats[bucketType] = mDatabase.getLatestUsageStats(bucketType);
+ if (DEBUG) {
+ if (mCurrentStats[bucketType] != null) {
+ Slog.d(TAG, mLogPrefix + "Found " +
+ (mCurrentStats[bucketType].events == null ?
+ 0 : mCurrentStats[bucketType].events.size()) +
+ " events");
+ }
+ }
} else {
- mCurrentStats[i] = UsageStats.create(tempCal.getTimeInMillis(), timeNow);
+ mCurrentStats[bucketType] = null;
+ }
+
+ if (mCurrentStats[bucketType] == null) {
+ if (DEBUG) {
+ Slog.d(TAG, "Creating new stats (" + tempCal.getTimeInMillis() +
+ ") for bucket " + bucketType);
+
+ }
+ mCurrentStats[bucketType] = new IntervalStats();
+ mCurrentStats[bucketType].beginTime = tempCal.getTimeInMillis();
+ mCurrentStats[bucketType].endTime = timeNow;
}
}
mStatsChanged = false;
mDailyExpiryDate.setTimeInMillis(timeNow);
mDailyExpiryDate.add(Calendar.DAY_OF_YEAR, 1);
- UsageStatsUtils.truncateDateTo(UsageStatsManager.DAILY_BUCKET, mDailyExpiryDate);
+ UsageStatsUtils.truncateDateTo(UsageStatsManager.INTERVAL_DAILY, mDailyExpiryDate);
Slog.i(TAG, mLogPrefix + "Rollover scheduled for "
+ sDateFormat.format(mDailyExpiryDate.getTime()));
}
- private void updateStats(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;
- }
private static String eventToString(int eventType) {
switch (eventType) {
- case UsageStats.Event.NONE:
+ case UsageEvents.Event.NONE:
return "NONE";
- case UsageStats.Event.MOVE_TO_BACKGROUND:
+ case UsageEvents.Event.MOVE_TO_BACKGROUND:
return "MOVE_TO_BACKGROUND";
- case UsageStats.Event.MOVE_TO_FOREGROUND:
+ case UsageEvents.Event.MOVE_TO_FOREGROUND:
return "MOVE_TO_FOREGROUND";
- case UsageStats.Event.END_OF_DAY:
+ case UsageEvents.Event.END_OF_DAY:
return "END_OF_DAY";
- case UsageStats.Event.CONTINUE_PREVIOUS_DAY:
+ case UsageEvents.Event.CONTINUE_PREVIOUS_DAY:
return "CONTINUE_PREVIOUS_DAY";
default:
return "UNKNOWN";