summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/current.txt51
-rw-r--r--core/java/android/app/usage/IUsageStatsManager.aidl9
-rw-r--r--core/java/android/app/usage/PackageUsageStats.java95
-rw-r--r--core/java/android/app/usage/TimeSparseArray.java26
-rw-r--r--core/java/android/app/usage/UsageEvents.aidl (renamed from core/java/android/app/usage/UsageStats.aidl)3
-rw-r--r--core/java/android/app/usage/UsageEvents.java283
-rw-r--r--core/java/android/app/usage/UsageStats.java200
-rw-r--r--core/java/android/app/usage/UsageStatsManager.java189
-rw-r--r--core/java/android/app/usage/UsageStatsManagerInternal.java2
-rw-r--r--core/java/com/android/internal/app/ResolverActivity.java13
-rwxr-xr-xservices/core/java/com/android/server/am/ActivityManagerService.java6
-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
-rw-r--r--tests/UsageStatsTest/AndroidManifest.xml2
-rw-r--r--tests/UsageStatsTest/res/menu/main.xml5
-rw-r--r--tests/UsageStatsTest/src/com/android/tests/usagestats/UsageLogActivity.java135
-rw-r--r--tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java52
22 files changed, 1326 insertions, 572 deletions
diff --git a/api/current.txt b/api/current.txt
index 5f23a36..b6ebd30 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -5677,46 +5677,47 @@ package android.app.job {
package android.app.usage {
- public final class PackageUsageStats implements android.os.Parcelable {
+ public final class UsageEvents implements android.os.Parcelable {
method public int describeContents();
- method public long getLastTimeUsed();
- method public java.lang.String getPackageName();
- method public long getTotalTimeSpent();
+ method public boolean getNextEvent(android.app.usage.UsageEvents.Event);
+ method public boolean hasNextEvent();
+ method public void resetToStart();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator CREATOR;
}
+ public static final class UsageEvents.Event {
+ ctor public UsageEvents.Event();
+ method public android.content.ComponentName getComponent();
+ method public int getEventType();
+ method public long getTimeStamp();
+ field public static final int MOVE_TO_BACKGROUND = 2; // 0x2
+ field public static final int MOVE_TO_FOREGROUND = 1; // 0x1
+ field public static final int NONE = 0; // 0x0
+ }
+
public final class UsageStats implements android.os.Parcelable {
ctor public UsageStats(android.app.usage.UsageStats);
+ method public void add(android.app.usage.UsageStats);
method public int describeContents();
method public long getFirstTimeStamp();
method public long getLastTimeStamp();
- method public android.app.usage.PackageUsageStats getPackage(int);
- method public android.app.usage.PackageUsageStats getPackage(java.lang.String);
- method public int getPackageCount();
- method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator CREATOR;
- }
-
- public static class UsageStats.Event implements android.os.Parcelable {
- ctor public UsageStats.Event();
- method public int describeContents();
+ method public long getLastTimeUsed();
+ method public java.lang.String getPackageName();
+ method public long getTotalTimeInForeground();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator CREATOR;
- field public static final int MOVE_TO_BACKGROUND = 2; // 0x2
- field public static final int MOVE_TO_FOREGROUND = 1; // 0x1
- field public static final int NONE = 0; // 0x0
- field public int eventType;
- field public java.lang.String packageName;
- field public long timeStamp;
}
public final class UsageStatsManager {
- method public android.app.usage.UsageStats[] getDailyStatsSince(long);
- method public android.app.usage.UsageStats[] getMonthlyStatsSince(long);
- method public android.app.usage.UsageStats getRecentStatsSince(long);
- method public android.app.usage.UsageStats[] getWeeklyStatsSince(long);
- method public android.app.usage.UsageStats[] getYearlyStatsSince(long);
+ method public android.util.ArrayMap<java.lang.String, android.app.usage.UsageStats> queryAndAggregateUsageStats(long, long);
+ method public android.app.usage.UsageEvents queryEvents(long, long);
+ method public java.util.List<android.app.usage.UsageStats> queryUsageStats(int, long, long);
+ field public static final int INTERVAL_BEST = 4; // 0x4
+ field public static final int INTERVAL_DAILY = 0; // 0x0
+ field public static final int INTERVAL_MONTHLY = 2; // 0x2
+ field public static final int INTERVAL_WEEKLY = 1; // 0x1
+ field public static final int INTERVAL_YEARLY = 3; // 0x3
}
}
diff --git a/core/java/android/app/usage/IUsageStatsManager.aidl b/core/java/android/app/usage/IUsageStatsManager.aidl
index 0924210..3b09888 100644
--- a/core/java/android/app/usage/IUsageStatsManager.aidl
+++ b/core/java/android/app/usage/IUsageStatsManager.aidl
@@ -16,8 +16,8 @@
package android.app.usage;
-import android.app.usage.UsageStats;
-import android.content.ComponentName;
+import android.app.usage.UsageEvents;
+import android.content.pm.ParceledListSlice;
/**
* System private API for talking with the UsageStatsManagerService.
@@ -25,6 +25,7 @@ import android.content.ComponentName;
* {@hide}
*/
interface IUsageStatsManager {
- UsageStats[] getStatsSince(int bucketType, long time, String callingPackage);
- UsageStats.Event[] getEventsSince(long time, String callingPackage);
+ ParceledListSlice queryUsageStats(int bucketType, long beginTime, long endTime,
+ String callingPackage);
+ UsageEvents queryEvents(long beginTime, long endTime, String callingPackage);
}
diff --git a/core/java/android/app/usage/PackageUsageStats.java b/core/java/android/app/usage/PackageUsageStats.java
deleted file mode 100644
index ba4fa21..0000000
--- a/core/java/android/app/usage/PackageUsageStats.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/**
- * 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 android.app.usage;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-
-public final class PackageUsageStats implements Parcelable {
-
- /**
- * {@hide}
- */
- public String mPackageName;
-
- /**
- * {@hide}
- */
- public long mTotalTimeSpent;
-
- /**
- * {@hide}
- */
- public long mLastTimeUsed;
-
- /**
- * {@hide}
- */
- public int mLastEvent;
-
- PackageUsageStats() {
- }
-
- PackageUsageStats(PackageUsageStats stats) {
- mPackageName = stats.mPackageName;
- mTotalTimeSpent = stats.mTotalTimeSpent;
- mLastTimeUsed = stats.mLastTimeUsed;
- mLastEvent = stats.mLastEvent;
- }
-
- public long getTotalTimeSpent() {
- return mTotalTimeSpent;
- }
-
- public long getLastTimeUsed() {
- return mLastTimeUsed;
- }
-
- public String getPackageName() {
- return mPackageName;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeString(mPackageName);
- dest.writeLong(mTotalTimeSpent);
- dest.writeLong(mLastTimeUsed);
- dest.writeInt(mLastEvent);
- }
-
- public static final Creator<PackageUsageStats> CREATOR = new Creator<PackageUsageStats>() {
- @Override
- public PackageUsageStats createFromParcel(Parcel in) {
- PackageUsageStats stats = new PackageUsageStats();
- stats.mPackageName = in.readString();
- stats.mTotalTimeSpent = in.readLong();
- stats.mLastTimeUsed = in.readLong();
- stats.mLastEvent = in.readInt();
- return stats;
- }
-
- @Override
- public PackageUsageStats[] newArray(int size) {
- return new PackageUsageStats[size];
- }
- };
-}
diff --git a/core/java/android/app/usage/TimeSparseArray.java b/core/java/android/app/usage/TimeSparseArray.java
index 5a72d02..7974fa7 100644
--- a/core/java/android/app/usage/TimeSparseArray.java
+++ b/core/java/android/app/usage/TimeSparseArray.java
@@ -39,16 +39,16 @@ public class TimeSparseArray<E> extends LongSparseArray<E> {
* @param time The timestamp for which to search the array.
* @return The index of the matched element, or -1 if no such match exists.
*/
- public int closestIndexAfter(long time) {
+ public int closestIndexOnOrAfter(long time) {
// This is essentially a binary search, except that if no match is found
// the closest index is returned.
final int size = size();
int lo = 0;
- int hi = size;
+ int hi = size - 1;
int mid = -1;
long key = -1;
while (lo <= hi) {
- mid = (lo + hi) >>> 1;
+ mid = lo + ((hi - lo) / 2);
key = keyAt(mid);
if (time > key) {
@@ -68,4 +68,24 @@ public class TimeSparseArray<E> extends LongSparseArray<E> {
return -1;
}
}
+
+ /**
+ * Finds the index of the first element whose timestamp is less than or equal to
+ * the given time.
+ *
+ * @param time The timestamp for which to search the array.
+ * @return The index of the matched element, or -1 if no such match exists.
+ */
+ public int closestIndexOnOrBefore(long time) {
+ final int index = closestIndexOnOrAfter(time);
+ if (index < 0) {
+ // Everything is larger, so we use the last element, or -1 if the list is empty.
+ return size() - 1;
+ }
+
+ if (keyAt(index) == time) {
+ return index;
+ }
+ return index - 1;
+ }
}
diff --git a/core/java/android/app/usage/UsageStats.aidl b/core/java/android/app/usage/UsageEvents.aidl
index 60dbd1c..f1bceba 100644
--- a/core/java/android/app/usage/UsageStats.aidl
+++ b/core/java/android/app/usage/UsageEvents.aidl
@@ -16,5 +16,4 @@
package android.app.usage;
-parcelable UsageStats;
-parcelable UsageStats.Event;
+parcelable UsageEvents;
diff --git a/core/java/android/app/usage/UsageEvents.java b/core/java/android/app/usage/UsageEvents.java
new file mode 100644
index 0000000..d1ebc5f
--- /dev/null
+++ b/core/java/android/app/usage/UsageEvents.java
@@ -0,0 +1,283 @@
+/**
+ * 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 android.app.usage;
+
+import android.content.ComponentName;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A result returned from {@link android.app.usage.UsageStatsManager#queryEvents(long, long)}
+ * from which to read {@link android.app.usage.UsageEvents.Event} objects.
+ */
+public final class UsageEvents implements Parcelable {
+
+ /**
+ * An event representing a state change for a component.
+ */
+ public static final class Event {
+
+ /**
+ * No event type.
+ */
+ public static final int NONE = 0;
+
+ /**
+ * An event type denoting that a component moved to the foreground.
+ */
+ public static final int MOVE_TO_FOREGROUND = 1;
+
+ /**
+ * An event type denoting that a component moved to the background.
+ */
+ public static final int MOVE_TO_BACKGROUND = 2;
+
+ /**
+ * An event type denoting that a component was in the foreground when the stats
+ * rolled-over. This is effectively treated as a {@link #MOVE_TO_BACKGROUND}.
+ * {@hide}
+ */
+ public static final int END_OF_DAY = 3;
+
+ /**
+ * An event type denoting that a component was in the foreground the previous day.
+ * This is effectively treated as a {@link #MOVE_TO_FOREGROUND}.
+ * {@hide}
+ */
+ public static final int CONTINUE_PREVIOUS_DAY = 4;
+
+ /**
+ * {@hide}
+ */
+ public ComponentName mComponent;
+
+ /**
+ * {@hide}
+ */
+ public long mTimeStamp;
+
+ /**
+ * {@hide}
+ */
+ public int mEventType;
+
+ /**
+ * The component this event represents.
+ */
+ public ComponentName getComponent() {
+ return mComponent;
+ }
+
+ /**
+ * The time at which this event occurred.
+ */
+ public long getTimeStamp() {
+ return mTimeStamp;
+ }
+
+ /**
+ * The event type.
+ *
+ * See {@link #MOVE_TO_BACKGROUND}
+ * See {@link #MOVE_TO_FOREGROUND}
+ */
+ public int getEventType() {
+ return mEventType;
+ }
+ }
+
+ // Only used when creating the resulting events. Not used for reading/unparceling.
+ private List<Event> mEventsToWrite = null;
+
+ // Only used for reading/unparceling events.
+ private Parcel mParcel = null;
+ private final int mEventCount;
+
+ private int mIndex = 0;
+
+ /*
+ * In order to save space, since ComponentNames will be duplicated everywhere,
+ * we use a map and index into it.
+ */
+ private ComponentName[] mComponentNameTable;
+
+ /**
+ * Construct the iterator from a parcel.
+ * {@hide}
+ */
+ public UsageEvents(Parcel in) {
+ mEventCount = in.readInt();
+ mIndex = in.readInt();
+ if (mEventCount > 0) {
+ mComponentNameTable = in.createTypedArray(ComponentName.CREATOR);
+
+ final int listByteLength = in.readInt();
+ final int positionInParcel = in.readInt();
+ mParcel = Parcel.obtain();
+ mParcel.setDataPosition(0);
+ mParcel.appendFrom(in, in.dataPosition(), listByteLength);
+ mParcel.setDataSize(mParcel.dataPosition());
+ mParcel.setDataPosition(positionInParcel);
+ }
+ }
+
+ /**
+ * Create an empty iterator.
+ * {@hide}
+ */
+ UsageEvents() {
+ mEventCount = 0;
+ }
+
+ /**
+ * Construct the iterator in preparation for writing it to a parcel.
+ * {@hide}
+ */
+ public UsageEvents(List<Event> events, ComponentName[] nameTable) {
+ mComponentNameTable = nameTable;
+ mEventCount = events.size();
+ mEventsToWrite = events;
+ }
+
+ /**
+ * Returns whether or not there are more events to read using
+ * {@link #getNextEvent(android.app.usage.UsageEvents.Event)}.
+ *
+ * @return true if there are more events, false otherwise.
+ */
+ public boolean hasNextEvent() {
+ return mIndex < mEventCount;
+ }
+
+ /**
+ * Retrieve the next {@link android.app.usage.UsageEvents.Event} from the collection and put the
+ * resulting data into {@code eventOut}.
+ *
+ * @param eventOut The {@link android.app.usage.UsageEvents.Event} object that will receive the
+ * next event data.
+ * @return true if an event was available, false if there are no more events.
+ */
+ public boolean getNextEvent(Event eventOut) {
+ if (mIndex >= mEventCount) {
+ return false;
+ }
+
+ final int index = mParcel.readInt();
+ eventOut.mComponent = mComponentNameTable[index];
+ eventOut.mEventType = mParcel.readInt();
+ eventOut.mTimeStamp = mParcel.readLong();
+ mIndex++;
+
+ if (mIndex >= mEventCount) {
+ mParcel.recycle();
+ mParcel = null;
+ }
+ return true;
+ }
+
+ /**
+ * Resets the collection so that it can be iterated over from the beginning.
+ */
+ public void resetToStart() {
+ mIndex = 0;
+ if (mParcel != null) {
+ mParcel.setDataPosition(0);
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mEventCount);
+ dest.writeInt(mIndex);
+ if (mEventCount > 0) {
+ dest.writeTypedArray(mComponentNameTable, flags);
+
+ if (mEventsToWrite != null) {
+ // Write out the events
+ Parcel p = Parcel.obtain();
+ try {
+ p.setDataPosition(0);
+ for (int i = 0; i < mEventCount; i++) {
+ final Event event = mEventsToWrite.get(i);
+
+ int index = Arrays.binarySearch(mComponentNameTable, event.getComponent());
+ if (index < 0) {
+ throw new IllegalStateException(event.getComponent().toShortString() +
+ " is not in the component name table");
+ }
+ p.writeInt(index);
+ p.writeInt(event.getEventType());
+ p.writeLong(event.getTimeStamp());
+ }
+ final int listByteLength = p.dataPosition();
+
+ // Write the total length of the data.
+ dest.writeInt(listByteLength);
+
+ // Write our current position into the data.
+ dest.writeInt(0);
+
+ // Write the data.
+ dest.appendFrom(p, 0, listByteLength);
+ } finally {
+ p.recycle();
+ }
+
+ } else if (mParcel != null) {
+ // Write the total length of the data.
+ dest.writeInt(mParcel.dataSize());
+
+ // Write out current position into the data.
+ dest.writeInt(mParcel.dataPosition());
+
+ // Write the data.
+ dest.appendFrom(mParcel, 0, mParcel.dataSize());
+ } else {
+ throw new IllegalStateException(
+ "Either mParcel or mEventsToWrite must not be null");
+ }
+ }
+ }
+
+ public static final Creator<UsageEvents> CREATOR = new Creator<UsageEvents>() {
+ @Override
+ public UsageEvents createFromParcel(Parcel source) {
+ return new UsageEvents(source);
+ }
+
+ @Override
+ public UsageEvents[] newArray(int size) {
+ return new UsageEvents[size];
+ }
+ };
+
+ @Override
+ protected void finalize() throws Throwable {
+ super.finalize();
+ if (mParcel != null) {
+ mParcel.recycle();
+ mParcel = null;
+ }
+ }
+}
diff --git a/core/java/android/app/usage/UsageStats.java b/core/java/android/app/usage/UsageStats.java
index 57d2011..e47a802 100644
--- a/core/java/android/app/usage/UsageStats.java
+++ b/core/java/android/app/usage/UsageStats.java
@@ -18,76 +18,17 @@ package android.app.usage;
import android.os.Parcel;
import android.os.Parcelable;
-import android.util.ArrayMap;
+/**
+ * Contains usage statistics for an app package for a specific
+ * time range.
+ */
public final class UsageStats implements Parcelable {
- public static class Event implements Parcelable {
- /**
- * {@hide}
- */
- public static final Event[] EMPTY_EVENTS = new Event[0];
-
- public static final int NONE = 0;
- public static final int MOVE_TO_FOREGROUND = 1;
- public static final int MOVE_TO_BACKGROUND = 2;
-
- /**
- * {@hide}
- */
- public static final int END_OF_DAY = 3;
-
- /**
- * {@hide}
- */
- public static final int CONTINUE_PREVIOUS_DAY = 4;
-
- public Event() {}
-
- /**
- * {@hide}
- */
- public Event(String packageName, long timeStamp, int eventType) {
- this.packageName = packageName;
- this.timeStamp = timeStamp;
- this.eventType = eventType;
- }
-
- public String packageName;
- public long timeStamp;
- public int eventType;
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeLong(timeStamp);
- dest.writeInt(eventType);
- dest.writeString(packageName);
- }
-
- public static final Creator<Event> CREATOR = new Creator<Event>() {
- @Override
- public Event createFromParcel(Parcel source) {
- final long time = source.readLong();
- final int type = source.readInt();
- final String name = source.readString();
- return new Event(name, time, type);
- }
-
- @Override
- public Event[] newArray(int size) {
- return new Event[size];
- }
- };
- }
/**
* {@hide}
*/
- public static final UsageStats[] EMPTY_STATS = new UsageStats[0];
+ public String mPackageName;
/**
* {@hide}
@@ -102,25 +43,22 @@ public final class UsageStats implements Parcelable {
/**
* {@hide}
*/
- public long mLastTimeSaved;
+ public long mLastTimeUsed;
- private ArrayMap<String, PackageUsageStats> mPackageStats = new ArrayMap<>();
+ /**
+ * {@hide}
+ */
+ public long mTotalTimeInForeground;
/**
- * Can be null
* {@hide}
*/
- public TimeSparseArray<Event> mEvents;
+ public int mLaunchCount;
/**
* {@hide}
*/
- public static UsageStats create(long beginTimeStamp, long endTimeStamp) {
- UsageStats stats = new UsageStats();
- stats.mBeginTimeStamp = beginTimeStamp;
- stats.mEndTimeStamp = endTimeStamp;
- return stats;
- }
+ public int mLastEvent;
/**
* {@hide}
@@ -129,57 +67,68 @@ public final class UsageStats implements Parcelable {
}
public UsageStats(UsageStats stats) {
+ mPackageName = stats.mPackageName;
mBeginTimeStamp = stats.mBeginTimeStamp;
mEndTimeStamp = stats.mEndTimeStamp;
- mLastTimeSaved = stats.mLastTimeSaved;
-
- final int pkgCount = stats.mPackageStats.size();
- mPackageStats.ensureCapacity(pkgCount);
- for (int i = 0; i < pkgCount; i++) {
- PackageUsageStats pkgStats = stats.mPackageStats.valueAt(i);
- mPackageStats.append(stats.mPackageStats.keyAt(i), new PackageUsageStats(pkgStats));
- }
+ mLastTimeUsed = stats.mLastTimeUsed;
+ mTotalTimeInForeground = stats.mTotalTimeInForeground;
+ mLaunchCount = stats.mLaunchCount;
+ mLastEvent = stats.mLastEvent;
+ }
- final int eventCount = stats.mEvents == null ? 0 : stats.mEvents.size();
- if (eventCount > 0) {
- mEvents = new TimeSparseArray<>();
- for (int i = 0; i < eventCount; i++) {
- mEvents.append(stats.mEvents.keyAt(i), stats.mEvents.valueAt(i));
- }
- }
+ public String getPackageName() {
+ return mPackageName;
}
+ /**
+ * Get the beginning of the time range this {@link android.app.usage.UsageStats} represents.
+ */
public long getFirstTimeStamp() {
return mBeginTimeStamp;
}
+ /**
+ * Get the end of the time range this {@link android.app.usage.UsageStats} represents.
+ */
public long getLastTimeStamp() {
return mEndTimeStamp;
}
- public int getPackageCount() {
- return mPackageStats.size();
- }
-
- public PackageUsageStats getPackage(int index) {
- return mPackageStats.valueAt(index);
+ /**
+ * Get the last time this package was used.
+ */
+ public long getLastTimeUsed() {
+ return mLastTimeUsed;
}
- public PackageUsageStats getPackage(String packageName) {
- return mPackageStats.get(packageName);
+ /**
+ * Get the total time this package spent in the foreground.
+ */
+ public long getTotalTimeInForeground() {
+ return mTotalTimeInForeground;
}
/**
- * {@hide}
+ * Add the statistics from the right {@link UsageStats} to the left. The package name for
+ * both {@link UsageStats} objects must be the same.
+ * @param right The {@link UsageStats} object to merge into this one.
+ * @throws java.lang.IllegalArgumentException if the package names of the two
+ * {@link UsageStats} objects are different.
*/
- public PackageUsageStats getOrCreatePackageUsageStats(String packageName) {
- PackageUsageStats pkgStats = mPackageStats.get(packageName);
- if (pkgStats == null) {
- pkgStats = new PackageUsageStats();
- pkgStats.mPackageName = packageName;
- mPackageStats.put(packageName, pkgStats);
+ public void add(UsageStats right) {
+ if (!mPackageName.equals(right.mPackageName)) {
+ throw new IllegalArgumentException("Can't merge UsageStats for package '" +
+ mPackageName + "' with UsageStats for package '" + right.mPackageName + "'.");
}
- return pkgStats;
+
+ if (right.mEndTimeStamp > mEndTimeStamp) {
+ mLastEvent = right.mLastEvent;
+ mEndTimeStamp = right.mEndTimeStamp;
+ mLastTimeUsed = right.mLastTimeUsed;
+ }
+ mBeginTimeStamp = Math.min(mBeginTimeStamp, right.mBeginTimeStamp);
+ mTotalTimeInForeground += right.mTotalTimeInForeground;
+ mLaunchCount += right.mLaunchCount;
}
@Override
@@ -189,47 +138,26 @@ public final class UsageStats implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mPackageName);
dest.writeLong(mBeginTimeStamp);
dest.writeLong(mEndTimeStamp);
- dest.writeLong(mLastTimeSaved);
-
- int size = mPackageStats.size();
- dest.writeInt(size);
- for (int i = 0; i < size; i++) {
- mPackageStats.valueAt(i).writeToParcel(dest, flags);
- }
-
- size = mEvents == null ? 0 : mEvents.size();
- dest.writeInt(size);
- for (int i = 0; i < size; i++) {
- mEvents.valueAt(i).writeToParcel(dest, flags);
- }
+ dest.writeLong(mLastTimeUsed);
+ dest.writeLong(mTotalTimeInForeground);
+ dest.writeInt(mLaunchCount);
+ dest.writeInt(mLastEvent);
}
public static final Creator<UsageStats> CREATOR = new Creator<UsageStats>() {
@Override
public UsageStats createFromParcel(Parcel in) {
UsageStats stats = new UsageStats();
+ stats.mPackageName = in.readString();
stats.mBeginTimeStamp = in.readLong();
stats.mEndTimeStamp = in.readLong();
- stats.mLastTimeSaved = in.readLong();
-
- int size = in.readInt();
- stats.mPackageStats.ensureCapacity(size);
- for (int i = 0; i < size; i++) {
- final PackageUsageStats pkgStats = PackageUsageStats.CREATOR.createFromParcel(in);
- stats.mPackageStats.put(pkgStats.mPackageName, pkgStats);
- }
-
- size = in.readInt();
- if (size > 0) {
- stats.mEvents = new TimeSparseArray<>(size);
- for (int i = 0; i < size; i++) {
- final Event event = Event.CREATOR.createFromParcel(in);
- stats.mEvents.put(event.timeStamp, event);
- }
- }
-
+ stats.mLastTimeUsed = in.readLong();
+ stats.mTotalTimeInForeground = in.readLong();
+ stats.mLaunchCount = in.readInt();
+ stats.mLastEvent = in.readInt();
return stats;
}
diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java
index fe02637..f9b8928 100644
--- a/core/java/android/app/usage/UsageStatsManager.java
+++ b/core/java/android/app/usage/UsageStatsManager.java
@@ -17,33 +17,73 @@
package android.app.usage;
import android.content.Context;
+import android.content.pm.ParceledListSlice;
import android.os.RemoteException;
+import android.util.ArrayMap;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Provides access to device usage history and statistics. Usage data is aggregated into
+ * time intervals: days, weeks, months, and years.
+ * <p />
+ * When requesting usage data since a particular time, the request might look something like this:
+ * <pre>
+ * PAST REQUEST_TIME TODAY FUTURE
+ * ————————————————————————————||———————————————————————————¦-----------------------|
+ * YEAR || ¦ |
+ * ————————————————————————————||———————————————————————————¦-----------------------|
+ * MONTH | || MONTH ¦ |
+ * ——————————————————|—————————||———————————————————————————¦-----------------------|
+ * | WEEK | WEEK|| | WEEK | WE¦EK | WEEK |
+ * ————————————————————————————||———————————————————|———————¦-----------------------|
+ * || |DAY|DAY|DAY|DAY¦DAY|DAY|DAY|DAY|DAY|DAY|
+ * ————————————————————————————||———————————————————————————¦-----------------------|
+ * </pre>
+ * A request for data in the middle of a time interval will include that interval.
+ * <p/>
+ * <b>NOTE:</b> This API requires the permission android.permission.PACKAGE_USAGE_STATS, which
+ * is a system-level permission and will not be granted to third-party apps. However, declaring
+ * the permission implies intention to use the API and the user of the device can grant permission
+ * through the Settings application.
+ */
public final class UsageStatsManager {
+
/**
- * {@hide}
+ * An interval type that spans a day. See {@link #queryUsageStats(int, long, long)}.
*/
- public static final int DAILY_BUCKET = 0;
+ public static final int INTERVAL_DAILY = 0;
/**
- * {@hide}
+ * An interval type that spans a week. See {@link #queryUsageStats(int, long, long)}.
*/
- public static final int WEEKLY_BUCKET = 1;
+ public static final int INTERVAL_WEEKLY = 1;
/**
- * {@hide}
+ * An interval type that spans a month. See {@link #queryUsageStats(int, long, long)}.
*/
- public static final int MONTHLY_BUCKET = 2;
+ public static final int INTERVAL_MONTHLY = 2;
/**
- * {@hide}
+ * An interval type that spans a year. See {@link #queryUsageStats(int, long, long)}.
*/
- public static final int YEARLY_BUCKET = 3;
+ public static final int INTERVAL_YEARLY = 3;
/**
+ * An interval type that will use the best fit interval for the given time range.
+ * See {@link #queryUsageStats(int, long, long)}.
+ */
+ public static final int INTERVAL_BEST = 4;
+
+ /**
+ * The number of available intervals. Does not include {@link #INTERVAL_BEST}, since it
+ * is a pseudo interval (it actually selects a real interval).
* {@hide}
*/
- public static final int BUCKET_COUNT = 4;
+ public static final int INTERVAL_COUNT = 4;
+
+ private static final UsageEvents sEmptyResults = new UsageEvents();
private final Context mContext;
private final IUsageStatsManager mService;
@@ -56,67 +96,100 @@ public final class UsageStatsManager {
mService = service;
}
- public UsageStats[] getDailyStatsSince(long time) {
- try {
- return mService.getStatsSince(DAILY_BUCKET, time, mContext.getOpPackageName());
- } catch (RemoteException e) {
- return null;
- }
- }
-
- public UsageStats[] getWeeklyStatsSince(long time) {
+ /**
+ * Gets application usage stats for the given time range, aggregated by the specified interval.
+ * <p>The returned list will contain a {@link UsageStats} object for each package that
+ * has data for an interval that is a subset of the time range given. To illustrate:</p>
+ * <pre>
+ * intervalType = INTERVAL_YEARLY
+ * beginTime = 2013
+ * endTime = 2015 (exclusive)
+ *
+ * Results:
+ * 2013 - com.example.alpha
+ * 2013 - com.example.beta
+ * 2014 - com.example.alpha
+ * 2014 - com.example.beta
+ * 2014 - com.example.charlie
+ * </pre>
+ *
+ * @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.
+ * @param endTime The exclusive end of the range of stats to include in the results.
+ * @return A list of {@link UsageStats} or null if none are available.
+ *
+ * @see #INTERVAL_DAILY
+ * @see #INTERVAL_WEEKLY
+ * @see #INTERVAL_MONTHLY
+ * @see #INTERVAL_YEARLY
+ * @see #INTERVAL_BEST
+ */
+ @SuppressWarnings("unchecked")
+ public List<UsageStats> queryUsageStats(int intervalType, long beginTime, long endTime) {
try {
- return mService.getStatsSince(WEEKLY_BUCKET, time, mContext.getOpPackageName());
+ ParceledListSlice<UsageStats> slice = mService.queryUsageStats(intervalType, beginTime,
+ endTime, mContext.getOpPackageName());
+ if (slice != null) {
+ return slice.getList();
+ }
} catch (RemoteException e) {
- return null;
+ // fallthrough and return null.
}
+ return Collections.EMPTY_LIST;
}
- public UsageStats[] getMonthlyStatsSince(long time) {
+ /**
+ * Query for events in the given time range. Events are only kept by the system for a few
+ * days.
+ * <p />
+ * <b>NOTE:</b> The last few minutes of the event log will be truncated to prevent abuse
+ * by applications.
+ *
+ * @param beginTime The inclusive beginning of the range of events to include in the results.
+ * @param endTime The exclusive end of the range of events to include in the results.
+ * @return A {@link UsageEvents}.
+ */
+ @SuppressWarnings("unchecked")
+ public UsageEvents queryEvents(long beginTime, long endTime) {
try {
- return mService.getStatsSince(MONTHLY_BUCKET, time, mContext.getOpPackageName());
+ UsageEvents iter = mService.queryEvents(beginTime, endTime,
+ mContext.getOpPackageName());
+ if (iter != null) {
+ return iter;
+ }
} catch (RemoteException e) {
- return null;
+ // fallthrough and return null
}
+ return sEmptyResults;
}
- public UsageStats[] getYearlyStatsSince(long time) {
- try {
- return mService.getStatsSince(YEARLY_BUCKET, time, mContext.getOpPackageName());
- } catch (RemoteException e) {
- return null;
+ /**
+ * A convenience method that queries for all stats in the given range (using the best interval
+ * for that range), merges the resulting data, and keys it by package name.
+ * See {@link #queryUsageStats(int, long, long)}.
+ *
+ * @param beginTime The inclusive beginning of the range of stats to include in the results.
+ * @param endTime The exclusive end of the range of stats to include in the results.
+ * @return An {@link android.util.ArrayMap} keyed by package name or null if no stats are
+ * available.
+ */
+ public ArrayMap<String, UsageStats> queryAndAggregateUsageStats(long beginTime, long endTime) {
+ List<UsageStats> stats = queryUsageStats(INTERVAL_BEST, beginTime, endTime);
+ if (stats.isEmpty()) {
+ @SuppressWarnings("unchecked")
+ ArrayMap<String, UsageStats> emptyStats = ArrayMap.EMPTY;
+ return emptyStats;
}
- }
-
- public UsageStats getRecentStatsSince(long time) {
- UsageStats aggregatedStats = null;
- long lastTime = time;
- UsageStats[] stats;
- while (true) {
- stats = getDailyStatsSince(lastTime);
- if (stats == null || stats.length == 0) {
- break;
- }
- for (UsageStats stat : stats) {
- lastTime = stat.mEndTimeStamp;
-
- if (aggregatedStats == null) {
- aggregatedStats = new UsageStats();
- aggregatedStats.mBeginTimeStamp = stat.mBeginTimeStamp;
- }
-
- aggregatedStats.mEndTimeStamp = stat.mEndTimeStamp;
-
- final int pkgCount = stat.getPackageCount();
- for (int i = 0; i < pkgCount; i++) {
- final PackageUsageStats pkgStats = stat.getPackage(i);
- final PackageUsageStats aggPkgStats =
- aggregatedStats.getOrCreatePackageUsageStats(pkgStats.mPackageName);
- aggPkgStats.mTotalTimeSpent += pkgStats.mTotalTimeSpent;
- aggPkgStats.mLastTimeUsed = pkgStats.mLastTimeUsed;
- aggPkgStats.mLastEvent = pkgStats.mLastEvent;
- }
+ ArrayMap<String, UsageStats> aggregatedStats = new ArrayMap<>();
+ final int statCount = stats.size();
+ for (int i = 0; i < statCount; i++) {
+ UsageStats newStat = stats.get(i);
+ UsageStats existingStat = aggregatedStats.get(newStat.getPackageName());
+ if (existingStat == null) {
+ aggregatedStats.put(newStat.mPackageName, newStat);
+ } else {
+ existingStat.add(newStat);
}
}
return aggregatedStats;
diff --git a/core/java/android/app/usage/UsageStatsManagerInternal.java b/core/java/android/app/usage/UsageStatsManagerInternal.java
index 0d41be2..119d705 100644
--- a/core/java/android/app/usage/UsageStatsManagerInternal.java
+++ b/core/java/android/app/usage/UsageStatsManagerInternal.java
@@ -32,7 +32,7 @@ public abstract class UsageStatsManagerInternal {
* @param userId The user id to which the component belongs to.
* @param timeStamp The time at which this event ocurred.
* @param eventType The event that occured. Valid values can be found at
- * {@link android.app.usage.UsageStats.Event}
+ * {@link UsageEvents}
*/
public abstract void reportEvent(ComponentName component, int userId,
long timeStamp, int eventType);
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index f0e7215..f156a08 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -17,10 +17,10 @@
package com.android.internal.app;
import android.app.Activity;
-import android.app.usage.PackageUsageStats;
import android.app.usage.UsageStats;
import android.app.usage.UsageStatsManager;
import android.os.AsyncTask;
+import android.util.ArrayMap;
import android.widget.AbsListView;
import android.widget.GridView;
import com.android.internal.R;
@@ -95,7 +95,7 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic
private boolean mResolvingHome = false;
private UsageStatsManager mUsm;
- private UsageStats mStats;
+ private ArrayMap<String, UsageStats> mStats;
private static final long USAGE_STATS_PERIOD = 1000 * 60 * 60 * 24 * 14;
private boolean mRegistered;
@@ -205,7 +205,7 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic
mUsm = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE);
final long sinceTime = System.currentTimeMillis() - USAGE_STATS_PERIOD;
- mStats = mUsm.getRecentStatsSince(sinceTime);
+ mStats = mUsm.queryAndAggregateUsageStats(sinceTime, System.currentTimeMillis());
Log.d(TAG, "sinceTime=" + sinceTime);
mMaxColumns = getResources().getInteger(R.integer.config_maxResolverActivityColumns);
@@ -1018,9 +1018,6 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic
if (lhs.targetUserId != UserHandle.USER_CURRENT) {
return 1;
}
- if (lhs.targetUserId != UserHandle.USER_CURRENT) {
- return -1;
- }
if (mStats != null) {
final long timeDiff =
@@ -1042,9 +1039,9 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic
private long getPackageTimeSpent(String packageName) {
if (mStats != null) {
- final PackageUsageStats stats = mStats.getPackage(packageName);
+ final UsageStats stats = mStats.get(packageName);
if (stats != null) {
- return stats.getTotalTimeSpent();
+ return stats.getTotalTimeInForeground();
}
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 275185a..7bd2e4f 100755
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -37,7 +37,7 @@ import android.app.IActivityContainer;
import android.app.IActivityContainerCallback;
import android.app.IAppTask;
import android.app.admin.DevicePolicyManager;
-import android.app.usage.UsageStats;
+import android.app.usage.UsageEvents;
import android.app.usage.UsageStatsManagerInternal;
import android.appwidget.AppWidgetManager;
import android.graphics.Rect;
@@ -3161,7 +3161,7 @@ public final class ActivityManagerService extends ActivityManagerNative
if (mUsageStatsService != null) {
mUsageStatsService.reportEvent(component.realActivity, component.userId,
System.currentTimeMillis(),
- UsageStats.Event.MOVE_TO_FOREGROUND);
+ UsageEvents.Event.MOVE_TO_FOREGROUND);
}
synchronized (stats) {
stats.noteActivityResumedLocked(component.app.uid);
@@ -3170,7 +3170,7 @@ public final class ActivityManagerService extends ActivityManagerNative
if (mUsageStatsService != null) {
mUsageStatsService.reportEvent(component.realActivity, component.userId,
System.currentTimeMillis(),
- UsageStats.Event.MOVE_TO_BACKGROUND);
+ UsageEvents.Event.MOVE_TO_BACKGROUND);
}
synchronized (stats) {
stats.noteActivityPausedLocked(component.app.uid);
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";
diff --git a/tests/UsageStatsTest/AndroidManifest.xml b/tests/UsageStatsTest/AndroidManifest.xml
index fac5810..589674a 100644
--- a/tests/UsageStatsTest/AndroidManifest.xml
+++ b/tests/UsageStatsTest/AndroidManifest.xml
@@ -13,5 +13,7 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
+
+ <activity android:name=".UsageLogActivity" />
</application>
</manifest>
diff --git a/tests/UsageStatsTest/res/menu/main.xml b/tests/UsageStatsTest/res/menu/main.xml
new file mode 100644
index 0000000..e781058
--- /dev/null
+++ b/tests/UsageStatsTest/res/menu/main.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@+id/log"
+ android:title="View Log"/>
+</menu>
diff --git a/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageLogActivity.java b/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageLogActivity.java
new file mode 100644
index 0000000..5d8fc9e
--- /dev/null
+++ b/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageLogActivity.java
@@ -0,0 +1,135 @@
+/**
+ * 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.tests.usagestats;
+
+import android.app.ListActivity;
+import android.app.usage.UsageEvents;
+import android.app.usage.UsageStatsManager;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+
+public class UsageLogActivity extends ListActivity implements Runnable {
+ private static final long USAGE_STATS_PERIOD = 1000 * 60 * 60 * 24 * 14;
+
+ private UsageStatsManager mUsageStatsManager;
+ private Adapter mAdapter;
+ private Handler mHandler = new Handler();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mUsageStatsManager = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE);
+ mAdapter = new Adapter();
+ setListAdapter(mAdapter);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ run();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mHandler.removeCallbacks(this);
+ }
+
+ @Override
+ public void run() {
+ long now = System.currentTimeMillis();
+ long beginTime = now - USAGE_STATS_PERIOD;
+ UsageEvents events = mUsageStatsManager.queryEvents(beginTime, now);
+ mAdapter.update(events);
+ mHandler.postDelayed(this, 1000 * 5);
+ }
+
+ private class Adapter extends BaseAdapter {
+
+ private final ArrayList<UsageEvents.Event> mEvents = new ArrayList<>();
+
+ public void update(UsageEvents results) {
+ mEvents.clear();
+ while (results.hasNextEvent()) {
+ UsageEvents.Event event = new UsageEvents.Event();
+ results.getNextEvent(event);
+ mEvents.add(event);
+ }
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ return mEvents.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mEvents.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final ViewHolder holder;
+ if (convertView == null) {
+ convertView = LayoutInflater.from(UsageLogActivity.this)
+ .inflate(R.layout.row_item, parent, false);
+ holder = new ViewHolder();
+ holder.packageName = (TextView) convertView.findViewById(android.R.id.text1);
+ holder.state = (TextView) convertView.findViewById(android.R.id.text2);
+ convertView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ holder.packageName.setText(mEvents.get(position).getComponent().toShortString());
+ String state;
+ switch (mEvents.get(position).getEventType()) {
+ case UsageEvents.Event.MOVE_TO_FOREGROUND:
+ state = "Foreground";
+ break;
+
+ case UsageEvents.Event.MOVE_TO_BACKGROUND:
+ state = "Background";
+ break;
+
+ default:
+ state = "Unknown: " + mEvents.get(position).getEventType();
+ break;
+ }
+ holder.state.setText(state);
+ return convertView;
+ }
+ }
+
+ static class ViewHolder {
+ public TextView packageName;
+ public TextView state;
+ }
+}
diff --git a/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java b/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java
index 73143c5..b6591bd 100644
--- a/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java
+++ b/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java
@@ -17,31 +17,34 @@
package com.android.tests.usagestats;
import android.app.ListActivity;
-import android.app.usage.PackageUsageStats;
import android.app.usage.UsageStats;
import android.app.usage.UsageStatsManager;
import android.content.Context;
+import android.content.Intent;
import android.os.Bundle;
import android.text.format.DateUtils;
+import android.util.ArrayMap;
import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import java.util.ArrayList;
-import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
public class UsageStatsActivity extends ListActivity {
-
+ private static final long USAGE_STATS_PERIOD = 1000 * 60 * 60 * 24 * 14;
private UsageStatsManager mUsageStatsManager;
private Adapter mAdapter;
- private Comparator<PackageUsageStats> mComparator = new Comparator<PackageUsageStats>() {
+ private Comparator<UsageStats> mComparator = new Comparator<UsageStats>() {
@Override
- public int compare(PackageUsageStats o1, PackageUsageStats o2) {
- return Long.compare(o2.getTotalTimeSpent(), o1.getTotalTimeSpent());
+ public int compare(UsageStats o1, UsageStats o2) {
+ return Long.compare(o2.getTotalTimeInForeground(), o1.getTotalTimeInForeground());
}
};
@@ -50,35 +53,54 @@ public class UsageStatsActivity extends ListActivity {
super.onCreate(savedInstanceState);
mUsageStatsManager = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE);
mAdapter = new Adapter();
- updateAdapter();
setListAdapter(mAdapter);
}
@Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.main, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.log:
+ startActivity(new Intent(this, UsageLogActivity.class));
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
protected void onResume() {
super.onResume();
updateAdapter();
}
private void updateAdapter() {
- Calendar cal = Calendar.getInstance();
- cal.add(Calendar.DAY_OF_YEAR, -14);
- UsageStats stats = mUsageStatsManager.getRecentStatsSince(cal.getTimeInMillis());
+ long now = System.currentTimeMillis();
+ long beginTime = now - USAGE_STATS_PERIOD;
+ ArrayMap<String, UsageStats> stats = mUsageStatsManager.queryAndAggregateUsageStats(
+ beginTime, now);
mAdapter.update(stats);
}
private class Adapter extends BaseAdapter {
- private ArrayList<PackageUsageStats> mStats = new ArrayList<>();
+ private ArrayList<UsageStats> mStats = new ArrayList<>();
- public void update(UsageStats stats) {
+ public void update(ArrayMap<String, UsageStats> stats) {
mStats.clear();
if (stats == null) {
return;
}
- final int packageCount = stats.getPackageCount();
+ final int packageCount = stats.size();
for (int i = 0; i < packageCount; i++) {
- mStats.add(stats.getPackage(i));
+ mStats.add(stats.valueAt(i));
}
Collections.sort(mStats, mComparator);
@@ -116,7 +138,7 @@ public class UsageStatsActivity extends ListActivity {
holder.packageName.setText(mStats.get(position).getPackageName());
holder.usageTime.setText(DateUtils.formatDuration(
- mStats.get(position).getTotalTimeSpent()));
+ mStats.get(position).getTotalTimeInForeground()));
return convertView;
}
}