diff options
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; } } |