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:
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,5 +16,4 @@
|
||||
|
||||
package android.app.usage;
|
||||
|
||||
parcelable UsageStats;
|
||||
parcelable UsageStats.Event;
|
||||
parcelable UsageEvents;
|
||||
283
core/java/android/app/usage/UsageEvents.java
Normal file
283
core/java/android/app/usage/UsageEvents.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -13,5 +13,7 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".UsageLogActivity" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
5
tests/UsageStatsTest/res/menu/main.xml
Normal file
5
tests/UsageStatsTest/res/menu/main.xml
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user