am ed9be6fb: am 38daa3c5: Merge "Second iteration of the UsageStats API" into lmp-dev

* commit 'ed9be6fb4c2b575db01bd106df94585701f37341':
  Second iteration of the UsageStats API
This commit is contained in:
Adam Lesinski
2014-08-08 22:44:27 +00:00
committed by Android Git Automerger
22 changed files with 1343 additions and 589 deletions

View File

@@ -5691,46 +5691,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 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;
}
public static class UsageStats.Event implements android.os.Parcelable {
ctor public UsageStats.Event();
method public int describeContents();
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
}
}

View File

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

View File

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

View File

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

View File

@@ -16,5 +16,4 @@
package android.app.usage;
parcelable UsageStats;
parcelable UsageStats.Event;
parcelable UsageEvents;

View File

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

View File

@@ -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;
private ArrayMap<String, PackageUsageStats> mPackageStats = new ArrayMap<>();
/**
* Can be null
* {@hide}
*/
public TimeSparseArray<Event> mEvents;
public long mLastTimeUsed;
/**
* {@hide}
*/
public static UsageStats create(long beginTimeStamp, long endTimeStamp) {
UsageStats stats = new UsageStats();
stats.mBeginTimeStamp = beginTimeStamp;
stats.mEndTimeStamp = endTimeStamp;
return stats;
}
public long mTotalTimeInForeground;
/**
* {@hide}
*/
public int mLaunchCount;
/**
* {@hide}
*/
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));
}
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));
}
}
mLastTimeUsed = stats.mLastTimeUsed;
mTotalTimeInForeground = stats.mTotalTimeInForeground;
mLaunchCount = stats.mLaunchCount;
mLastEvent = stats.mLastEvent;
}
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);
}
public PackageUsageStats getPackage(String packageName) {
return mPackageStats.get(packageName);
/**
* Get the last time this package was used.
*/
public long getLastTimeUsed() {
return mLastTimeUsed;
}
/**
* {@hide}
* Get the total time this package spent in the foreground.
*/
public PackageUsageStats getOrCreatePackageUsageStats(String packageName) {
PackageUsageStats pkgStats = mPackageStats.get(packageName);
if (pkgStats == null) {
pkgStats = new PackageUsageStats();
pkgStats.mPackageName = packageName;
mPackageStats.put(packageName, pkgStats);
public long getTotalTimeInForeground() {
return mTotalTimeInForeground;
}
/**
* 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 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;
}

View File

@@ -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}
*/
public static final int DAILY_BUCKET = 0;
/**
* {@hide}
* An interval type that spans a day. See {@link #queryUsageStats(int, long, long)}.
*/
public static final int WEEKLY_BUCKET = 1;
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 MONTHLY_BUCKET = 2;
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 YEARLY_BUCKET = 3;
public static final int INTERVAL_MONTHLY = 2;
/**
* An interval type that spans a year. See {@link #queryUsageStats(int, long, long)}.
*/
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) {
/**
* 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(DAILY_BUCKET, time, mContext.getOpPackageName());
} catch (RemoteException e) {
return null;
}
}
public UsageStats[] getWeeklyStatsSince(long time) {
try {
return mService.getStatsSince(WEEKLY_BUCKET, time, mContext.getOpPackageName());
} catch (RemoteException e) {
return null;
}
}
public UsageStats[] getMonthlyStatsSince(long time) {
try {
return mService.getStatsSince(MONTHLY_BUCKET, time, mContext.getOpPackageName());
} catch (RemoteException e) {
return null;
}
}
public UsageStats[] getYearlyStatsSince(long time) {
try {
return mService.getStatsSince(YEARLY_BUCKET, time, mContext.getOpPackageName());
} catch (RemoteException e) {
return null;
}
}
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;
ParceledListSlice<UsageStats> slice = mService.queryUsageStats(intervalType, beginTime,
endTime, mContext.getOpPackageName());
if (slice != null) {
return slice.getList();
}
} catch (RemoteException e) {
// fallthrough and return null.
}
return Collections.EMPTY_LIST;
}
for (UsageStats stat : stats) {
lastTime = stat.mEndTimeStamp;
/**
* 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 {
UsageEvents iter = mService.queryEvents(beginTime, endTime,
mContext.getOpPackageName());
if (iter != null) {
return iter;
}
} catch (RemoteException e) {
// fallthrough and return null
}
return sEmptyResults;
}
if (aggregatedStats == null) {
aggregatedStats = new UsageStats();
aggregatedStats.mBeginTimeStamp = stat.mBeginTimeStamp;
}
/**
* 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;
}
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;

View File

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

View File

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

View File

@@ -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;
@@ -3160,7 +3160,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);
@@ -3169,7 +3169,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);

View File

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

View File

@@ -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;
}
}
/**
* 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);
}
int startIndex = mSortedStatFiles[bucketType].closestIndexAfter(beginTime);
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();
}
}
}

View File

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

View File

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

View File

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

View File

@@ -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() {
}
}

View File

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

View File

@@ -13,5 +13,7 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".UsageLogActivity" />
</application>
</manifest>

View File

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

View File

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

View File

@@ -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,10 +53,28 @@ 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();
@@ -61,24 +82,25 @@ public class UsageStatsActivity extends ListActivity {
}
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;
}
}