diff options
author | Adam Lesinski <adamlesinski@google.com> | 2014-07-16 19:09:13 -0700 |
---|---|---|
committer | Adam Lesinski <adamlesinski@google.com> | 2014-07-18 15:24:20 -0700 |
commit | 0debc9aff4c0cbc28e083a948081d91b0f171319 (patch) | |
tree | 3ac4d7a9927cdd2741f65393d4e6855508ab3c26 | |
parent | d3de42cae84fadfa1befd082a2cf1bf72f9ad82a (diff) | |
download | frameworks_base-0debc9aff4c0cbc28e083a948081d91b0f171319.zip frameworks_base-0debc9aff4c0cbc28e083a948081d91b0f171319.tar.gz frameworks_base-0debc9aff4c0cbc28e083a948081d91b0f171319.tar.bz2 |
First iteration of a public UsageStats API
UsageStats API that allows apps to get a list of packages that have been
recently used, along with basic stats like how long they have been in
the foreground and the most recent time they were running.
Bug: 15165667
Change-Id: I2a2d1ff69bd0b5703ac3d9de1780df42ad90d439
26 files changed, 1596 insertions, 1770 deletions
@@ -93,6 +93,7 @@ LOCAL_SRC_FILES += \ core/java/android/app/backup/IFullBackupRestoreObserver.aidl \ core/java/android/app/backup/IRestoreObserver.aidl \ core/java/android/app/backup/IRestoreSession.aidl \ + core/java/android/app/usage/IUsageStatsManager.aidl \ core/java/android/bluetooth/IBluetooth.aidl \ core/java/android/bluetooth/IBluetoothA2dp.aidl \ core/java/android/bluetooth/IBluetoothA2dpSink.aidl \ @@ -251,7 +252,6 @@ LOCAL_SRC_FILES += \ core/java/com/android/internal/app/IAppOpsService.aidl \ core/java/com/android/internal/app/IBatteryStats.aidl \ core/java/com/android/internal/app/IProcessStats.aidl \ - core/java/com/android/internal/app/IUsageStats.aidl \ core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl \ core/java/com/android/internal/app/IVoiceInteractor.aidl \ core/java/com/android/internal/app/IVoiceInteractorCallback.aidl \ diff --git a/api/current.txt b/api/current.txt index e7a3ef1..f7e45ea 100644 --- a/api/current.txt +++ b/api/current.txt @@ -5602,6 +5602,52 @@ package android.app.job { } +package android.app.usage { + + public final class PackageUsageStats implements android.os.Parcelable { + method public int describeContents(); + method public long getLastTimeUsed(); + method public java.lang.String getPackageName(); + method public long getTotalTimeSpent(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public final class UsageStats implements android.os.Parcelable { + ctor public UsageStats(android.app.usage.UsageStats); + method public int describeContents(); + method public long getFirstTimeStamp(); + method public long getLastTimeStamp(); + method public android.app.usage.PackageUsageStats getPackage(int); + method public android.app.usage.PackageUsageStats getPackage(java.lang.String); + method public int getPackageCount(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public static class UsageStats.Event implements android.os.Parcelable { + ctor public UsageStats.Event(); + method public int describeContents(); + method public 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); + } + +} + package android.appwidget { public class AppWidgetHost { diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index d2540f1..d43db78 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -19,7 +19,6 @@ package android.app; import android.os.BatteryStats; import android.os.IBinder; import android.os.ParcelFileDescriptor; -import com.android.internal.app.IUsageStats; import com.android.internal.app.ProcessStats; import com.android.internal.os.TransferPipe; import com.android.internal.util.FastPrintWriter; @@ -2226,7 +2225,7 @@ public class ActivityManager { * * @hide */ - public Map<String, Integer> getAllPackageLaunchCounts() { + /*public Map<String, Integer> getAllPackageLaunchCounts() { try { IUsageStats usageStatsService = IUsageStats.Stub.asInterface( ServiceManager.getService("usagestats")); @@ -2250,7 +2249,7 @@ public class ActivityManager { Log.w(TAG, "Could not query launch counts", e); return new HashMap<String, Integer>(); } - } + }*/ /** @hide */ public static int checkComponentPermission(String permission, int uid, @@ -2352,24 +2351,6 @@ public class ActivityManager { } /** - * Returns the usage statistics of each installed package. - * - * @hide - */ - public UsageStats.PackageStats[] getAllPackageUsageStats() { - try { - IUsageStats usageStatsService = IUsageStats.Stub.asInterface( - ServiceManager.getService("usagestats")); - if (usageStatsService != null) { - return usageStatsService.getAllPkgUsageStats(ActivityThread.currentPackageName()); - } - } catch (RemoteException e) { - Log.w(TAG, "Could not query usage stats", e); - } - return new UsageStats.PackageStats[0]; - } - - /** * @param userid the user's id. Zero indicates the default user * @hide */ diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 2fd3443..8e3323b 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -16,6 +16,8 @@ package android.app; +import android.app.usage.IUsageStatsManager; +import android.app.usage.UsageStatsManager; import android.os.Build; import android.service.persistentdata.IPersistentDataBlockService; @@ -729,7 +731,8 @@ class ContextImpl extends Context { IBinder iBinder = ServiceManager.getService(TV_INPUT_SERVICE); ITvInputManager service = ITvInputManager.Stub.asInterface(iBinder); return new TvInputManager(service, UserHandle.myUserId()); - }}); + } + }); registerService(TV_PARENTAL_CONTROL_SERVICE, new ServiceFetcher() { public Object getService(ContextImpl ctx) { @@ -743,9 +746,12 @@ class ContextImpl extends Context { }); registerService(USAGE_STATS_SERVICE, new ServiceFetcher() { - public Object createService(ContextImpl ctx) { - return new UsageStatsManager(ctx.getOuterContext()); - }}); + public Object createService(ContextImpl ctx) { + IBinder iBinder = ServiceManager.getService(USAGE_STATS_SERVICE); + IUsageStatsManager service = IUsageStatsManager.Stub.asInterface(iBinder); + return new UsageStatsManager(ctx.getOuterContext(), service); + } + }); registerService(JOB_SCHEDULER_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { diff --git a/core/java/android/app/UsageStats.java b/core/java/android/app/UsageStats.java deleted file mode 100644 index 0aeba59..0000000 --- a/core/java/android/app/UsageStats.java +++ /dev/null @@ -1,406 +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; - -import android.content.res.Configuration; -import android.os.Parcel; -import android.os.Parcelable; -import android.os.SystemClock; -import android.util.ArrayMap; - -import java.util.Map; - -/** - * Snapshot of current usage stats data. - * @hide - */ -public class UsageStats implements Parcelable { - /** @hide */ - public final ArrayMap<String, PackageStats> mPackages = new ArrayMap<String, PackageStats>(); - /** @hide */ - public final ArrayMap<Configuration, ConfigurationStats> mConfigurations - = new ArrayMap<Configuration, ConfigurationStats>(); - - public static class PackageStats implements Parcelable { - private final String mPackageName; - private int mLaunchCount; - private long mUsageTime; - private long mResumedTime; - - /** @hide */ - public final ArrayMap<String, Long> componentResumeTimes; - - public static final Parcelable.Creator<PackageStats> CREATOR - = new Parcelable.Creator<PackageStats>() { - public PackageStats createFromParcel(Parcel in) { - return new PackageStats(in); - } - - public PackageStats[] newArray(int size) { - return new PackageStats[size]; - } - }; - - public String toString() { - return "PackageStats{" - + Integer.toHexString(System.identityHashCode(this)) - + " " + mPackageName + "}"; - } - - /** @hide */ - public PackageStats(String pkgName) { - mPackageName = pkgName; - componentResumeTimes = new ArrayMap<String, Long>(); - } - - /** @hide */ - public PackageStats(String pkgName, int count, long time, Map<String, Long> lastResumeTimes) { - mPackageName = pkgName; - mLaunchCount = count; - mUsageTime = time; - componentResumeTimes = new ArrayMap<String, Long>(); - componentResumeTimes.putAll(lastResumeTimes); - } - - /** @hide */ - public PackageStats(Parcel source) { - mPackageName = source.readString(); - mLaunchCount = source.readInt(); - mUsageTime = source.readLong(); - final int N = source.readInt(); - componentResumeTimes = new ArrayMap<String, Long>(N); - for (int i = 0; i < N; i++) { - String component = source.readString(); - long lastResumeTime = source.readLong(); - componentResumeTimes.put(component, lastResumeTime); - } - } - - /** @hide */ - public PackageStats(PackageStats pStats) { - mPackageName = pStats.mPackageName; - mLaunchCount = pStats.mLaunchCount; - mUsageTime = pStats.mUsageTime; - componentResumeTimes = new ArrayMap<String, Long>(pStats.componentResumeTimes); - } - - /** @hide */ - public void resume(boolean launched) { - if (launched) { - mLaunchCount++; - } - mResumedTime = SystemClock.elapsedRealtime(); - } - - /** @hide */ - public void pause() { - if (mResumedTime > 0) { - mUsageTime += SystemClock.elapsedRealtime() - mResumedTime; - } - mResumedTime = 0; - } - - public final String getPackageName() { - return mPackageName; - } - - public final long getUsageTime(long elapsedRealtime) { - return mUsageTime + (mResumedTime > 0 ? (elapsedRealtime- mResumedTime) : 0); - } - - public final int getLaunchCount() { - return mLaunchCount; - } - - /** @hide */ - public boolean clearUsageTimes() { - mLaunchCount = 0; - mUsageTime = 0; - return mResumedTime <= 0 && componentResumeTimes.isEmpty(); - } - - public final int describeContents() { - return 0; - } - - public final void writeToParcel(Parcel dest, int parcelableFlags) { - writeToParcel(dest, parcelableFlags, 0); - } - - final void writeToParcel(Parcel dest, int parcelableFlags, long elapsedRealtime) { - dest.writeString(mPackageName); - dest.writeInt(mLaunchCount); - dest.writeLong(elapsedRealtime > 0 ? getUsageTime(elapsedRealtime) : mUsageTime); - dest.writeInt(componentResumeTimes.size()); - for (Map.Entry<String, Long> ent : componentResumeTimes.entrySet()) { - dest.writeString(ent.getKey()); - dest.writeLong(ent.getValue()); - } - } - - /** @hide */ - public void writeExtendedToParcel(Parcel dest, int parcelableFlags) { - } - } - - public static class ConfigurationStats implements Parcelable { - private final Configuration mConfiguration; - private long mLastUsedTime; - private int mUsageCount; - private long mUsageTime; - private long mStartedTime; - - public static final Parcelable.Creator<ConfigurationStats> CREATOR - = new Parcelable.Creator<ConfigurationStats>() { - public ConfigurationStats createFromParcel(Parcel in) { - return new ConfigurationStats(in); - } - - public ConfigurationStats[] newArray(int size) { - return new ConfigurationStats[size]; - } - }; - - public String toString() { - return "ConfigurationStats{" - + Integer.toHexString(System.identityHashCode(this)) - + " " + mConfiguration + "}"; - } - - /** @hide */ - public ConfigurationStats(Configuration config) { - mConfiguration = config; - } - - /** @hide */ - public ConfigurationStats(Parcel source) { - mConfiguration = Configuration.CREATOR.createFromParcel(source); - mLastUsedTime = source.readLong(); - mUsageCount = source.readInt(); - mUsageTime = source.readLong(); - } - - /** @hide */ - public ConfigurationStats(ConfigurationStats pStats) { - mConfiguration = pStats.mConfiguration; - mLastUsedTime = pStats.mLastUsedTime; - mUsageCount = pStats.mUsageCount; - mUsageTime = pStats.mUsageTime; - } - - public final Configuration getConfiguration() { - return mConfiguration; - } - - public final long getLastUsedTime() { - return mLastUsedTime; - } - - public final long getUsageTime(long elapsedRealtime) { - return mUsageTime + (mStartedTime > 0 ? (elapsedRealtime- mStartedTime) : 0); - } - - public final int getUsageCount() { - return mUsageCount; - } - - /** @hide */ - public void start() { - mLastUsedTime = System.currentTimeMillis(); - mUsageCount++; - mStartedTime = SystemClock.elapsedRealtime(); - } - - /** @hide */ - public void stop() { - if (mStartedTime > 0) { - mUsageTime += SystemClock.elapsedRealtime() - mStartedTime; - } - mStartedTime = 0; - } - - /** @hide */ - public boolean clearUsageTimes() { - mUsageCount = 0; - mUsageTime = 0; - return mLastUsedTime == 0 && mStartedTime <= 0; - } - - public final int describeContents() { - return 0; - } - - public final void writeToParcel(Parcel dest, int parcelableFlags) { - writeToParcel(dest, parcelableFlags, 0); - } - - final void writeToParcel(Parcel dest, int parcelableFlags, long elapsedRealtime) { - mConfiguration.writeToParcel(dest, parcelableFlags); - dest.writeLong(mLastUsedTime); - dest.writeInt(mUsageCount); - dest.writeLong(elapsedRealtime > 0 ? getUsageTime(elapsedRealtime) : mUsageTime); - } - - /** @hide */ - public void writeExtendedToParcel(Parcel dest, int parcelableFlags) { - } - } - - /** @hide */ - public UsageStats() { - } - - /** @hide */ - public UsageStats(Parcel source, boolean extended) { - int N = source.readInt(); - for (int i=0; i<N; i++) { - PackageStats pkg = extended ? onNewPackageStats(source) : new PackageStats(source); - mPackages.put(pkg.getPackageName(), pkg); - } - N = source.readInt(); - for (int i=0; i<N; i++) { - ConfigurationStats config = extended ? onNewConfigurationStats(source) - : new ConfigurationStats(source); - mConfigurations.put(config.getConfiguration(), config); - } - } - - public int getPackageStatsCount() { - return mPackages.size(); - } - - public PackageStats getPackageStatsAt(int index) { - return mPackages.valueAt(index); - } - - public PackageStats getPackageStats(String pkgName) { - return mPackages.get(pkgName); - } - - /** @hide */ - public PackageStats getOrCreatePackageStats(String pkgName) { - PackageStats ps = mPackages.get(pkgName); - if (ps == null) { - ps = onNewPackageStats(pkgName); - mPackages.put(pkgName, ps); - } - return ps; - } - - public int getConfigurationStatsCount() { - return mConfigurations.size(); - } - - public ConfigurationStats getConfigurationStatsAt(int index) { - return mConfigurations.valueAt(index); - } - - public ConfigurationStats getConfigurationStats(Configuration config) { - return mConfigurations.get(config); - } - - /** @hide */ - public ConfigurationStats getOrCreateConfigurationStats(Configuration config) { - ConfigurationStats cs = mConfigurations.get(config); - if (cs == null) { - cs = onNewConfigurationStats(config); - mConfigurations.put(config, cs); - } - return cs; - } - - /** @hide */ - public void clearUsageTimes() { - for (int i=mPackages.size()-1; i>=0; i--) { - if (mPackages.valueAt(i).clearUsageTimes()) { - mPackages.removeAt(i); - } - } - for (int i=mConfigurations.size()-1; i>=0; i--) { - if (mConfigurations.valueAt(i).clearUsageTimes()) { - mConfigurations.removeAt(i); - } - } - } - - /** @hide */ - public PackageStats onNewPackageStats(String pkgName) { - return new PackageStats(pkgName); - } - - /** @hide */ - public PackageStats onNewPackageStats(Parcel source) { - return new PackageStats(source); - } - - /** @hide */ - public ConfigurationStats onNewConfigurationStats(Configuration config) { - return new ConfigurationStats(config); - } - - /** @hide */ - public ConfigurationStats onNewConfigurationStats(Parcel source) { - return new ConfigurationStats(source); - } - - public int describeContents() { - return 0; - } - - public void writeToParcel(Parcel dest, int parcelableFlags) { - writeToParcelInner(dest, parcelableFlags, false); - } - - /** @hide */ - public void writeExtendedToParcel(Parcel dest, int parcelableFlags) { - writeToParcelInner(dest, parcelableFlags, true); - } - - private void writeToParcelInner(Parcel dest, int parcelableFlags, boolean extended) { - final long elapsedRealtime = SystemClock.elapsedRealtime(); - - int N = mPackages.size(); - dest.writeInt(N); - for (int i=0; i<N; i++) { - PackageStats ps = mPackages.valueAt(i); - ps.writeToParcel(dest, parcelableFlags, elapsedRealtime); - if (extended) { - ps.writeExtendedToParcel(dest, parcelableFlags); - } - } - N = mConfigurations.size(); - dest.writeInt(N); - for (int i=0; i<N; i++) { - ConfigurationStats cs = mConfigurations.valueAt(i); - cs.writeToParcel(dest, parcelableFlags, elapsedRealtime); - if (extended) { - cs.writeExtendedToParcel(dest, parcelableFlags); - } - } - } - - public static final Parcelable.Creator<UsageStats> CREATOR - = new Parcelable.Creator<UsageStats>() { - public UsageStats createFromParcel(Parcel in) { - return new UsageStats(in, false); - } - - public UsageStats[] newArray(int size) { - return new UsageStats[size]; - } - }; -} diff --git a/core/java/android/app/UsageStatsManager.java b/core/java/android/app/UsageStatsManager.java deleted file mode 100644 index fbf9c3b..0000000 --- a/core/java/android/app/UsageStatsManager.java +++ /dev/null @@ -1,51 +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; - -import android.content.Context; -import android.os.ParcelableParcel; -import android.os.RemoteException; -import android.os.ServiceManager; -import com.android.internal.app.IUsageStats; - -/** - * Access to usage stats data. - * @hide - */ -public class UsageStatsManager { - final Context mContext; - final IUsageStats mService; - - /** @hide */ - public UsageStatsManager(Context context) { - mContext = context; - mService = IUsageStats.Stub.asInterface(ServiceManager.getService( - Context.USAGE_STATS_SERVICE)); - } - - public UsageStats getCurrentStats() { - try { - ParcelableParcel in = mService.getCurrentStats(mContext.getOpPackageName()); - if (in != null) { - return new UsageStats(in.getParcel(), false); - } - } catch (RemoteException e) { - // About to die. - } - return new UsageStats(); - } -} diff --git a/core/java/android/app/usage/IUsageStatsManager.aidl b/core/java/android/app/usage/IUsageStatsManager.aidl new file mode 100644 index 0000000..0924210 --- /dev/null +++ b/core/java/android/app/usage/IUsageStatsManager.aidl @@ -0,0 +1,30 @@ +/** + * 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.app.usage.UsageStats; +import android.content.ComponentName; + +/** + * System private API for talking with the UsageStatsManagerService. + * + * {@hide} + */ +interface IUsageStatsManager { + UsageStats[] getStatsSince(int bucketType, long time, String callingPackage); + UsageStats.Event[] getEventsSince(long time, String callingPackage); +} diff --git a/core/java/android/app/usage/PackageUsageStats.aidl b/core/java/android/app/usage/PackageUsageStats.aidl new file mode 100644 index 0000000..5293098 --- /dev/null +++ b/core/java/android/app/usage/PackageUsageStats.aidl @@ -0,0 +1,19 @@ +/** + * 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; + +parcelable PackageUsageStats; diff --git a/core/java/android/app/usage/PackageUsageStats.java b/core/java/android/app/usage/PackageUsageStats.java new file mode 100644 index 0000000..ba4fa21 --- /dev/null +++ b/core/java/android/app/usage/PackageUsageStats.java @@ -0,0 +1,95 @@ +/** + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package android.app.usage; + +import android.os.Parcel; +import android.os.Parcelable; + +public final class PackageUsageStats implements Parcelable { + + /** + * {@hide} + */ + public String mPackageName; + + /** + * {@hide} + */ + public long mTotalTimeSpent; + + /** + * {@hide} + */ + public long mLastTimeUsed; + + /** + * {@hide} + */ + public int mLastEvent; + + PackageUsageStats() { + } + + PackageUsageStats(PackageUsageStats stats) { + mPackageName = stats.mPackageName; + mTotalTimeSpent = stats.mTotalTimeSpent; + mLastTimeUsed = stats.mLastTimeUsed; + mLastEvent = stats.mLastEvent; + } + + public long getTotalTimeSpent() { + return mTotalTimeSpent; + } + + public long getLastTimeUsed() { + return mLastTimeUsed; + } + + public String getPackageName() { + return mPackageName; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mPackageName); + dest.writeLong(mTotalTimeSpent); + dest.writeLong(mLastTimeUsed); + dest.writeInt(mLastEvent); + } + + public static final Creator<PackageUsageStats> CREATOR = new Creator<PackageUsageStats>() { + @Override + public PackageUsageStats createFromParcel(Parcel in) { + PackageUsageStats stats = new PackageUsageStats(); + stats.mPackageName = in.readString(); + stats.mTotalTimeSpent = in.readLong(); + stats.mLastTimeUsed = in.readLong(); + stats.mLastEvent = in.readInt(); + return stats; + } + + @Override + public PackageUsageStats[] newArray(int size) { + return new PackageUsageStats[size]; + } + }; +} diff --git a/core/java/android/app/usage/TimeSparseArray.java b/core/java/android/app/usage/TimeSparseArray.java new file mode 100644 index 0000000..5a72d02 --- /dev/null +++ b/core/java/android/app/usage/TimeSparseArray.java @@ -0,0 +1,71 @@ +/** + * 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.util.LongSparseArray; + +/** + * An array that indexes by a long timestamp, representing milliseconds since the epoch. + * + * {@hide} + */ +public class TimeSparseArray<E> extends LongSparseArray<E> { + public TimeSparseArray() { + super(); + } + + public TimeSparseArray(int initialCapacity) { + super(initialCapacity); + } + + /** + * Finds the index of the first element whose timestamp is greater 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 closestIndexAfter(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 mid = -1; + long key = -1; + while (lo <= hi) { + mid = (lo + hi) >>> 1; + key = keyAt(mid); + + if (time > key) { + lo = mid + 1; + } else if (time < key) { + hi = mid - 1; + } else { + return mid; + } + } + + if (time < key) { + return mid; + } else if (time > key && lo < size) { + return lo; + } else { + return -1; + } + } +} diff --git a/core/java/android/app/UsageStats.aidl b/core/java/android/app/usage/UsageStats.aidl index 7dee70a..60dbd1c 100644 --- a/core/java/android/app/UsageStats.aidl +++ b/core/java/android/app/usage/UsageStats.aidl @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.app; +package android.app.usage; parcelable UsageStats; -parcelable UsageStats.PackageStats; +parcelable UsageStats.Event; diff --git a/core/java/android/app/usage/UsageStats.java b/core/java/android/app/usage/UsageStats.java new file mode 100644 index 0000000..35ff665 --- /dev/null +++ b/core/java/android/app/usage/UsageStats.java @@ -0,0 +1,240 @@ +/** + * 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; +import android.util.ArrayMap; + +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]; + + /** + * {@hide} + */ + public long mBeginTimeStamp; + + /** + * {@hide} + */ + public long mEndTimeStamp; + + /** + * {@hide} + */ + public long mLastTimeSaved; + + private ArrayMap<String, PackageUsageStats> mPackageStats = new ArrayMap<>(); + + /** + * Can be null + * {@hide} + */ + public TimeSparseArray<Event> mEvents; + + /** + * {@hide} + */ + public static UsageStats create(long beginTimeStamp, long endTimeStamp) { + UsageStats stats = new UsageStats(); + stats.mBeginTimeStamp = beginTimeStamp; + stats.mEndTimeStamp = endTimeStamp; + return stats; + } + + /** + * {@hide} + */ + public UsageStats() { + } + + public UsageStats(UsageStats stats) { + mBeginTimeStamp = stats.mBeginTimeStamp; + mEndTimeStamp = stats.mEndTimeStamp; + mLastTimeSaved = stats.mLastTimeSaved; + + final int pkgCount = stats.mPackageStats.size(); + 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)); + } + } + } + + public long getFirstTimeStamp() { + return mBeginTimeStamp; + } + + 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); + } + + /** + * {@hide} + */ + public PackageUsageStats getOrCreatePackageUsageStats(String packageName) { + PackageUsageStats pkgStats = mPackageStats.get(packageName); + if (pkgStats == null) { + pkgStats = new PackageUsageStats(); + pkgStats.mPackageName = packageName; + mPackageStats.put(packageName, pkgStats); + } + return pkgStats; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + 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); + } + } + + public static final Creator<UsageStats> CREATOR = new Creator<UsageStats>() { + @Override + public UsageStats createFromParcel(Parcel in) { + UsageStats stats = new UsageStats(); + 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); + } + } + + return stats; + } + + @Override + public UsageStats[] newArray(int size) { + return new UsageStats[size]; + } + }; +} diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java new file mode 100644 index 0000000..fe02637 --- /dev/null +++ b/core/java/android/app/usage/UsageStatsManager.java @@ -0,0 +1,124 @@ +/** + * 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.Context; +import android.os.RemoteException; + +public final class UsageStatsManager { + /** + * {@hide} + */ + public static final int DAILY_BUCKET = 0; + + /** + * {@hide} + */ + public static final int WEEKLY_BUCKET = 1; + + /** + * {@hide} + */ + public static final int MONTHLY_BUCKET = 2; + + /** + * {@hide} + */ + public static final int YEARLY_BUCKET = 3; + + /** + * {@hide} + */ + public static final int BUCKET_COUNT = 4; + + private final Context mContext; + private final IUsageStatsManager mService; + + /** + * {@hide} + */ + public UsageStatsManager(Context context, IUsageStatsManager service) { + mContext = context; + mService = service; + } + + public UsageStats[] getDailyStatsSince(long time) { + try { + return mService.getStatsSince(DAILY_BUCKET, time, mContext.getOpPackageName()); + } catch (RemoteException e) { + return null; + } + } + + public UsageStats[] getWeeklyStatsSince(long time) { + 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; + } + + for (UsageStats stat : stats) { + lastTime = stat.mEndTimeStamp; + + if (aggregatedStats == null) { + aggregatedStats = new UsageStats(); + aggregatedStats.mBeginTimeStamp = stat.mBeginTimeStamp; + } + + aggregatedStats.mEndTimeStamp = stat.mEndTimeStamp; + + final int pkgCount = stat.getPackageCount(); + for (int i = 0; i < pkgCount; i++) { + final PackageUsageStats pkgStats = stat.getPackage(i); + final PackageUsageStats aggPkgStats = + aggregatedStats.getOrCreatePackageUsageStats(pkgStats.mPackageName); + aggPkgStats.mTotalTimeSpent += pkgStats.mTotalTimeSpent; + aggPkgStats.mLastTimeUsed = pkgStats.mLastTimeUsed; + aggPkgStats.mLastEvent = pkgStats.mLastEvent; + } + } + } + return aggregatedStats; + } +} diff --git a/core/java/android/app/usage/UsageStatsManagerInternal.java b/core/java/android/app/usage/UsageStatsManagerInternal.java new file mode 100644 index 0000000..d6345f3 --- /dev/null +++ b/core/java/android/app/usage/UsageStatsManagerInternal.java @@ -0,0 +1,42 @@ +/** + * 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; + +/** + * UsageStatsManager local system service interface. + * + * {@hide} Only for use within the system server. + */ +public abstract class UsageStatsManagerInternal { + + /** + * Reports an event to the UsageStatsManager. + * + * @param component The component for which this event ocurred. + * @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} + */ + public abstract void reportEvent(ComponentName component, long timeStamp, int eventType); + + /** + * Prepares the UsageStatsService for shutdown. + */ + public abstract void prepareShutdown(); +} diff --git a/core/java/android/util/AtomicFile.java b/core/java/android/util/AtomicFile.java index 4fca570..a6466fc 100644 --- a/core/java/android/util/AtomicFile.java +++ b/core/java/android/util/AtomicFile.java @@ -200,6 +200,20 @@ public class AtomicFile { } /** + * Gets the last modified time of the atomic file. + * {@hide} + * + * @return last modified time in milliseconds since epoch. + * @throws IOException + */ + public long getLastModifiedTime() throws IOException { + if (mBackupName.exists()) { + return mBackupName.lastModified(); + } + return mBaseName.lastModified(); + } + + /** * A convenience for {@link #openRead()} that also reads all of the * file contents into a byte array which is returned. */ diff --git a/core/java/com/android/internal/app/IUsageStats.aidl b/core/java/com/android/internal/app/IUsageStats.aidl deleted file mode 100644 index 7e7f0e1..0000000 --- a/core/java/com/android/internal/app/IUsageStats.aidl +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2008 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.internal.app; - -import android.app.UsageStats; -import android.content.ComponentName; -import android.content.res.Configuration; -import android.os.ParcelableParcel; - -interface IUsageStats { - void noteResumeComponent(in ComponentName componentName); - void notePauseComponent(in ComponentName componentName); - void noteLaunchTime(in ComponentName componentName, int millis); - void noteStartConfig(in Configuration config); - UsageStats.PackageStats getPkgUsageStats(String callingPkg, in ComponentName componentName); - UsageStats.PackageStats[] getAllPkgUsageStats(String callingPkg); - ParcelableParcel getCurrentStats(String callingPkg); -} diff --git a/services/Android.mk b/services/Android.mk index b4de903..3c94f43 100644 --- a/services/Android.mk +++ b/services/Android.mk @@ -26,6 +26,7 @@ services := \ devicepolicy \ print \ restrictions \ + usage \ usb \ voiceinteraction diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 31c1c6c..cfbadb4 100755 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -36,6 +36,8 @@ 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.UsageStatsManagerInternal; import android.appwidget.AppWidgetManager; import android.graphics.Rect; import android.os.BatteryStats; @@ -810,7 +812,7 @@ public final class ActivityManagerService extends ActivityManagerNative /** * Information about component usage */ - final UsageStatsService mUsageStatsService; + UsageStatsManagerInternal mUsageStatsService; /** * Information about and control over application operations @@ -2026,6 +2028,10 @@ public final class ActivityManagerService extends ActivityManagerNative mStackSupervisor.setWindowManager(wm); } + public void setUsageStatsManager(UsageStatsManagerInternal usageStatsManager) { + mUsageStatsService = usageStatsManager; + } + public void startObservingNativeCrashes() { final NativeCrashListener ncl = new NativeCrashListener(this); ncl.start(); @@ -2175,7 +2181,6 @@ public final class ActivityManagerService extends ActivityManagerNative mProcessStats = new ProcessStatsService(this, new File(systemDir, "procstats")); - mUsageStatsService = new UsageStatsService(new File(systemDir, "usagestats").toString()); mAppOpsService = new AppOpsService(new File(systemDir, "appops.xml"), mHandler); mGrantFile = new AtomicFile(new File(systemDir, "urigrants.xml")); @@ -2247,7 +2252,6 @@ public final class ActivityManagerService extends ActivityManagerNative mProcessCpuThread.start(); mBatteryStatsService.publish(mContext); - mUsageStatsService.publish(mContext); mAppOpsService.publish(mContext); Slog.d("AppOps", "AppOpsService published"); LocalServices.addService(ActivityManagerInternal.class, new LocalService()); @@ -3073,12 +3077,18 @@ public final class ActivityManagerService extends ActivityManagerNative if (DEBUG_SWITCH) Slog.d(TAG, "updateUsageStats: comp=" + component + "res=" + resumed); final BatteryStatsImpl stats = mBatteryStatsService.getActiveStatistics(); if (resumed) { - mUsageStatsService.noteResumeComponent(component.realActivity); + if (mUsageStatsService != null) { + mUsageStatsService.reportEvent(component.realActivity, System.currentTimeMillis(), + UsageStats.Event.MOVE_TO_FOREGROUND); + } synchronized (stats) { stats.noteActivityResumedLocked(component.app.uid); } } else { - mUsageStatsService.notePauseComponent(component.realActivity); + if (mUsageStatsService != null) { + mUsageStatsService.reportEvent(component.realActivity, System.currentTimeMillis(), + UsageStats.Event.MOVE_TO_BACKGROUND); + } synchronized (stats) { stats.noteActivityPausedLocked(component.app.uid); } @@ -8804,7 +8814,7 @@ public final class ActivityManagerService extends ActivityManagerNative mCoreSettingsObserver = new CoreSettingsObserver(this); - mUsageStatsService.monitorPackages(); + //mUsageStatsService.monitorPackages(); } /** @@ -9026,7 +9036,9 @@ public final class ActivityManagerService extends ActivityManagerNative } mAppOpsService.shutdown(); - mUsageStatsService.shutdown(); + if (mUsageStatsService != null) { + mUsageStatsService.prepareShutdown(); + } mBatteryStatsService.shutdown(); synchronized (this) { mProcessStats.shutdownLocked(); @@ -10086,7 +10098,6 @@ public final class ActivityManagerService extends ActivityManagerNative } mAppOpsService.systemReady(); - mUsageStatsService.systemReady(); mSystemReady = true; } @@ -14986,7 +14997,7 @@ public final class ActivityManagerService extends ActivityManagerNative newConfig.seq = mConfigurationSeq; mConfiguration = newConfig; Slog.i(TAG, "Config changes=" + Integer.toHexString(changes) + " " + newConfig); - mUsageStatsService.noteStartConfig(newConfig); + //mUsageStatsService.noteStartConfig(newConfig); final Configuration configCopy = new Configuration(mConfiguration); diff --git a/services/core/java/com/android/server/am/ActivityRecord.java b/services/core/java/com/android/server/am/ActivityRecord.java index 6c47922..85f49ed 100755 --- a/services/core/java/com/android/server/am/ActivityRecord.java +++ b/services/core/java/com/android/server/am/ActivityRecord.java @@ -854,7 +854,7 @@ final class ActivityRecord { Log.i(ActivityManagerService.TAG, sb.toString()); } if (totalTime > 0) { - service.mUsageStatsService.noteFullyDrawnTime(realActivity, (int) totalTime); + //service.mUsageStatsService.noteFullyDrawnTime(realActivity, (int) totalTime); } fullyDrawnStartTime = 0; stack.mFullyDrawnStartTime = 0; @@ -886,7 +886,7 @@ final class ActivityRecord { } mStackSupervisor.reportActivityLaunchedLocked(false, this, thisTime, totalTime); if (totalTime > 0) { - service.mUsageStatsService.noteLaunchTime(realActivity, (int)totalTime); + //service.mUsageStatsService.noteLaunchTime(realActivity, (int)totalTime); } displayStartTime = 0; stack.mLaunchStartTime = 0; diff --git a/services/core/java/com/android/server/am/UsageStatsService.java b/services/core/java/com/android/server/am/UsageStatsService.java deleted file mode 100644 index 4a5a554..0000000 --- a/services/core/java/com/android/server/am/UsageStatsService.java +++ /dev/null @@ -1,1242 +0,0 @@ -/* - * Copyright (C) 2006-2007 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.am; - -import android.app.AppGlobals; -import android.app.AppOpsManager; -import android.app.UsageStats; -import android.content.ComponentName; -import android.content.Context; -import android.content.pm.IPackageManager; -import android.content.pm.PackageManager; -import android.content.res.Configuration; -import android.os.Binder; -import android.os.IBinder; -import android.os.FileUtils; -import android.os.Parcel; -import android.os.ParcelableParcel; -import android.os.Process; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.SystemClock; -import android.text.format.DateFormat; -import android.util.ArrayMap; -import android.util.AtomicFile; -import android.util.Slog; -import android.util.TimeUtils; -import android.util.Xml; - -import com.android.internal.app.IUsageStats; -import com.android.internal.content.PackageMonitor; -import com.android.internal.util.FastXmlSerializer; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlSerializer; - -import java.io.File; -import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.TimeZone; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; - -/** - * This service collects the statistics associated with usage - * of various components, like when a particular package is launched or - * paused and aggregates events like number of time a component is launched - * total duration of a component launch. - */ -public final class UsageStatsService extends IUsageStats.Stub { - public static final String SERVICE_NAME = "usagestats"; - private static final boolean localLOGV = false; - private static final boolean REPORT_UNEXPECTED = false; - private static final String TAG = "UsageStats"; - - // Current on-disk Parcel version - private static final int VERSION = 1010; - - private static final int CHECKIN_VERSION = 4; - - private static final String FILE_PREFIX = "usage-"; - - private static final String FILE_HISTORY = FILE_PREFIX + "history.xml"; - - private static final int FILE_WRITE_INTERVAL = (localLOGV) ? 0 : 30*60*1000; // 30m in ms - - private static final int MAX_NUM_FILES = 5; - - private static final int NUM_LAUNCH_TIME_BINS = 10; - private static final int[] LAUNCH_TIME_BINS = { - 250, 500, 750, 1000, 1500, 2000, 3000, 4000, 5000 - }; - - static IUsageStats sService; - private Context mContext; - private AppOpsManager mAppOps; - - // structure used to maintain statistics since the last checkin. - private LocalUsageStats mStats = new LocalUsageStats(); - - // To remove last-resume time stats when a pacakge is removed. - private PackageMonitor mPackageMonitor; - - // Lock to update package stats. Methods suffixed by SLOCK should invoked with - // this lock held - final Object mStatsLock = new Object(); - // Lock to write to file. Methods suffixed by FLOCK should invoked with - // this lock held. - final Object mFileLock = new Object(); - // Order of locks is mFileLock followed by mStatsLock to avoid deadlocks - private String mLastResumedPkg; - private String mLastResumedComp; - private boolean mIsResumed; - private ConfigUsageStatsExtended mCurrentConfigStats; - private File mFile; - private AtomicFile mHistoryFile; - private String mFileLeaf; - private File mDir; - - private final Calendar mCal // guarded by itself - = Calendar.getInstance(TimeZone.getTimeZone("GMT+0")); - - private final AtomicInteger mLastWriteDay = new AtomicInteger(-1); - private final AtomicLong mLastWriteElapsedTime = new AtomicLong(0); - private final AtomicBoolean mUnforcedDiskWriteRunning = new AtomicBoolean(false); - - static class LocalUsageStats extends UsageStats { - public LocalUsageStats() { - } - public LocalUsageStats(Parcel in, boolean extended) { - super(in, extended); - } - @Override - public PackageStats onNewPackageStats(String pkgName) { - return new PkgUsageStatsExtended(pkgName); - } - @Override - public PackageStats onNewPackageStats(Parcel in) { - return new PkgUsageStatsExtended(in); - } - @Override - public ConfigurationStats onNewConfigurationStats(Configuration config) { - return new ConfigUsageStatsExtended(config); - } - @Override - public ConfigurationStats onNewConfigurationStats(Parcel source) { - return new ConfigUsageStatsExtended(source); - } - } - - static class TimeStats { - int mCount; - final int[] mTimes = new int[NUM_LAUNCH_TIME_BINS]; - - TimeStats() { - } - - void incCount() { - mCount++; - } - - void add(int val) { - final int[] bins = LAUNCH_TIME_BINS; - for (int i=0; i<NUM_LAUNCH_TIME_BINS-1; i++) { - if (val < bins[i]) { - mTimes[i]++; - return; - } - } - mTimes[NUM_LAUNCH_TIME_BINS-1]++; - } - - TimeStats(Parcel in) { - mCount = in.readInt(); - final int[] localTimes = mTimes; - for (int i=0; i<NUM_LAUNCH_TIME_BINS; i++) { - localTimes[i] = in.readInt(); - } - } - - void writeToParcel(Parcel out) { - out.writeInt(mCount); - final int[] localTimes = mTimes; - for (int i=0; i<NUM_LAUNCH_TIME_BINS; i++) { - out.writeInt(localTimes[i]); - } - } - } - - static class PkgUsageStatsExtended extends UsageStats.PackageStats { - final ArrayMap<String, TimeStats> mLaunchTimes - = new ArrayMap<String, TimeStats>(); - final ArrayMap<String, TimeStats> mFullyDrawnTimes - = new ArrayMap<String, TimeStats>(); - - PkgUsageStatsExtended(String pkgName) { - super(pkgName); - } - - PkgUsageStatsExtended(Parcel in) { - super(in); - final int numLaunchTimeStats = in.readInt(); - if (localLOGV) Slog.v(TAG, "Reading launch times: " + numLaunchTimeStats); - mLaunchTimes.ensureCapacity(numLaunchTimeStats); - for (int i=0; i<numLaunchTimeStats; i++) { - String comp = in.readString(); - if (localLOGV) Slog.v(TAG, "Component: " + comp); - TimeStats times = new TimeStats(in); - mLaunchTimes.put(comp, times); - } - - final int numFullyDrawnTimeStats = in.readInt(); - if (localLOGV) Slog.v(TAG, "Reading fully drawn times: " + numFullyDrawnTimeStats); - mFullyDrawnTimes.ensureCapacity(numFullyDrawnTimeStats); - for (int i=0; i<numFullyDrawnTimeStats; i++) { - String comp = in.readString(); - if (localLOGV) Slog.v(TAG, "Component: " + comp); - TimeStats times = new TimeStats(in); - mFullyDrawnTimes.put(comp, times); - } - } - - void addLaunchCount(String comp) { - TimeStats times = mLaunchTimes.get(comp); - if (times == null) { - times = new TimeStats(); - mLaunchTimes.put(comp, times); - } - times.incCount(); - } - - void addLaunchTime(String comp, int millis) { - TimeStats times = mLaunchTimes.get(comp); - if (times == null) { - times = new TimeStats(); - mLaunchTimes.put(comp, times); - } - times.add(millis); - } - - void addFullyDrawnTime(String comp, int millis) { - TimeStats times = mFullyDrawnTimes.get(comp); - if (times == null) { - times = new TimeStats(); - mFullyDrawnTimes.put(comp, times); - } - times.add(millis); - } - - public void writeExtendedToParcel(Parcel out, int parcelableFlags) { - final int numLaunchTimeStats = mLaunchTimes.size(); - out.writeInt(numLaunchTimeStats); - for (int i=0; i<numLaunchTimeStats; i++) { - out.writeString(mLaunchTimes.keyAt(i)); - mLaunchTimes.valueAt(i).writeToParcel(out); - } - final int numFullyDrawnTimeStats = mFullyDrawnTimes.size(); - out.writeInt(numFullyDrawnTimeStats); - for (int i=0; i<numFullyDrawnTimeStats; i++) { - out.writeString(mFullyDrawnTimes.keyAt(i)); - mFullyDrawnTimes.valueAt(i).writeToParcel(out); - } - } - - @Override - public boolean clearUsageTimes() { - mLaunchTimes.clear(); - mFullyDrawnTimes.clear(); - return super.clearUsageTimes(); - } - } - - static class ConfigUsageStatsExtended extends UsageStats.ConfigurationStats { - ConfigUsageStatsExtended(Configuration config) { - super(config); - } - - ConfigUsageStatsExtended(Parcel in) { - super(in); - } - } - - UsageStatsService(String dir) { - if (localLOGV) Slog.v(TAG, "UsageStatsService: " + dir); - mDir = new File(dir); - mDir.mkdir(); - - // Remove any old /data/system/usagestats.* files from previous versions. - File parentDir = mDir.getParentFile(); - String files[] = parentDir.list(); - if (files != null) { - String prefix = mDir.getName() + "."; - for (String file : files) { - if (file.startsWith(prefix)) { - Slog.i(TAG, "Deleting old usage file: " + file); - (new File(parentDir, file)).delete(); - } - } - } - - // Update current stats which are binned by date - mFileLeaf = getCurrentDateStr(FILE_PREFIX); - mFile = new File(mDir, mFileLeaf); - mHistoryFile = new AtomicFile(new File(mDir, FILE_HISTORY)); - readStatsFromFile(); - readHistoryStatsFromFile(); - mLastWriteElapsedTime.set(SystemClock.elapsedRealtime()); - // mCal was set by getCurrentDateStr(), want to use that same time. - mLastWriteDay.set(mCal.get(Calendar.DAY_OF_YEAR)); - } - - /* - * Utility method to convert date into string. - */ - private String getCurrentDateStr(String prefix) { - StringBuilder sb = new StringBuilder(); - if (prefix != null) { - sb.append(prefix); - } - synchronized (mCal) { - mCal.setTimeInMillis(System.currentTimeMillis()); - sb.append(mCal.get(Calendar.YEAR)); - int mm = mCal.get(Calendar.MONTH) - Calendar.JANUARY +1; - if (mm < 10) { - sb.append("0"); - } - sb.append(mm); - int dd = mCal.get(Calendar.DAY_OF_MONTH); - if (dd < 10) { - sb.append("0"); - } - sb.append(dd); - } - return sb.toString(); - } - - private Parcel getParcelForFile(File file) throws IOException { - FileInputStream stream = new FileInputStream(file); - try { - byte[] raw = readFully(stream); - Parcel in = Parcel.obtain(); - in.unmarshall(raw, 0, raw.length); - in.setDataPosition(0); - return in; - } finally { - stream.close(); - } - } - - private void readStatsFromFile() { - File newFile = mFile; - synchronized (mFileLock) { - try { - if (newFile.exists()) { - readStatsFLOCK(newFile); - } else { - // Check for file limit before creating a new file - checkFileLimitFLOCK(); - newFile.createNewFile(); - } - } catch (IOException e) { - Slog.w(TAG,"Error : " + e + " reading data from file:" + newFile); - } - } - } - - private void readStatsFLOCK(File file) throws IOException { - Parcel in = getParcelForFile(file); - int vers = in.readInt(); - if (vers != VERSION) { // vers will be 0 if the parcel file was empty - Slog.w(TAG, "Usage stats version of " + file + " changed from " + vers + " to " - + VERSION + "; dropping"); - return; - } - LocalUsageStats stats = new LocalUsageStats(in, true); - synchronized (mStatsLock) { - mStats = stats; - } - } - - private void readHistoryStatsFromFile() { - synchronized (mFileLock) { - if (mHistoryFile.getBaseFile().exists()) { - readHistoryStatsFLOCK(); - } - } - } - - private void readHistoryStatsFLOCK() { - FileInputStream fis = null; - try { - fis = mHistoryFile.openRead(); - XmlPullParser parser = Xml.newPullParser(); - parser.setInput(fis, null); - int eventType = parser.getEventType(); - while (eventType != XmlPullParser.START_TAG && - eventType != XmlPullParser.END_DOCUMENT) { - eventType = parser.next(); - } - if (eventType == XmlPullParser.END_DOCUMENT) { - return; - } - - String tagName = parser.getName(); - if ("usage-history".equals(tagName)) { - String pkg = null; - do { - eventType = parser.next(); - if (eventType == XmlPullParser.START_TAG) { - tagName = parser.getName(); - int depth = parser.getDepth(); - if ("pkg".equals(tagName) && depth == 2) { - pkg = parser.getAttributeValue(null, "name"); - } else if ("comp".equals(tagName) && depth == 3 && pkg != null) { - String comp = parser.getAttributeValue(null, "name"); - String lastResumeTimeStr = parser.getAttributeValue(null, "lrt"); - if (comp != null && lastResumeTimeStr != null) { - try { - long lastResumeTime = Long.parseLong(lastResumeTimeStr); - synchronized (mStatsLock) { - PkgUsageStatsExtended pus = (PkgUsageStatsExtended) - mStats.getOrCreatePackageStats(pkg); - pus.componentResumeTimes.put(comp, lastResumeTime); - } - } catch (NumberFormatException e) { - } - } - } - } else if (eventType == XmlPullParser.END_TAG) { - if ("pkg".equals(parser.getName())) { - pkg = null; - } - } - } while (eventType != XmlPullParser.END_DOCUMENT); - } - } catch (XmlPullParserException e) { - Slog.w(TAG,"Error reading history stats: " + e); - } catch (IOException e) { - Slog.w(TAG,"Error reading history stats: " + e); - } finally { - if (fis != null) { - try { - fis.close(); - } catch (IOException e) { - } - } - } - } - - private ArrayList<String> getUsageStatsFileListFLOCK() { - // Check if there are too many files in the system and delete older files - String fList[] = mDir.list(); - if (fList == null) { - return null; - } - ArrayList<String> fileList = new ArrayList<String>(); - for (String file : fList) { - if (!file.startsWith(FILE_PREFIX)) { - continue; - } - if (file.endsWith(".bak")) { - (new File(mDir, file)).delete(); - continue; - } - fileList.add(file); - } - return fileList; - } - - private void checkFileLimitFLOCK() { - // Get all usage stats output files - ArrayList<String> fileList = getUsageStatsFileListFLOCK(); - if (fileList == null) { - // Empty /data/system/usagestats/ so we don't have anything to delete - return; - } - int count = fileList.size(); - if (count <= MAX_NUM_FILES) { - return; - } - // Sort files - Collections.sort(fileList); - count -= MAX_NUM_FILES; - // Delete older files - for (int i = 0; i < count; i++) { - String fileName = fileList.get(i); - File file = new File(mDir, fileName); - Slog.i(TAG, "Deleting usage file : " + fileName); - file.delete(); - } - } - - /** - * Conditionally start up a disk write if it's been awhile, or the - * day has rolled over. - * - * This is called indirectly from user-facing actions (when - * 'force' is false) so it tries to be quick, without writing to - * disk directly or acquiring heavy locks. - * - * @params force do an unconditional, synchronous stats flush - * to disk on the current thread. - * @params forceWriteHistoryStats Force writing of historical stats. - */ - private void writeStatsToFile(final boolean force, final boolean forceWriteHistoryStats) { - int curDay; - synchronized (mCal) { - mCal.setTimeInMillis(System.currentTimeMillis()); - curDay = mCal.get(Calendar.DAY_OF_YEAR); - } - final boolean dayChanged = curDay != mLastWriteDay.get(); - - // Determine if the day changed... note that this will be wrong - // if the year has changed but we are in the same day of year... - // we can probably live with this. - final long currElapsedTime = SystemClock.elapsedRealtime(); - - // Fast common path, without taking the often-contentious - // mFileLock. - if (!force) { - if (!dayChanged && - (currElapsedTime - mLastWriteElapsedTime.get()) < FILE_WRITE_INTERVAL) { - // wait till the next update - return; - } - if (mUnforcedDiskWriteRunning.compareAndSet(false, true)) { - new Thread("UsageStatsService_DiskWriter") { - public void run() { - try { - if (localLOGV) Slog.d(TAG, "Disk writer thread starting."); - writeStatsToFile(true, false); - } finally { - mUnforcedDiskWriteRunning.set(false); - if (localLOGV) Slog.d(TAG, "Disk writer thread ending."); - } - } - }.start(); - } - return; - } - - Parcel out = Parcel.obtain(); - synchronized (mStatsLock) { - out.writeInt(VERSION); - mStats.writeExtendedToParcel(out, 0); - if (dayChanged) { - mStats.clearUsageTimes(); - } - } - - synchronized (mFileLock) { - // Get the most recent file - mFileLeaf = getCurrentDateStr(FILE_PREFIX); - // Copy current file to back up - File backupFile = null; - if (mFile != null && mFile.exists()) { - backupFile = new File(mFile.getPath() + ".bak"); - if (!backupFile.exists()) { - if (!mFile.renameTo(backupFile)) { - Slog.w(TAG, "Failed to persist new stats"); - out.recycle(); - return; - } - } else { - mFile.delete(); - } - } - - try { - // Write mStats to file - writeStatsFLOCK(mFile, out); - mLastWriteElapsedTime.set(currElapsedTime); - if (dayChanged) { - mLastWriteDay.set(curDay); - mFile = new File(mDir, mFileLeaf); - checkFileLimitFLOCK(); - } - - if (dayChanged || forceWriteHistoryStats) { - // Write history stats daily or when forced (due to shutdown) or when debugging. - writeHistoryStatsFLOCK(); - } - - // Delete the backup file - if (backupFile != null) { - backupFile.delete(); - } - } catch (IOException e) { - Slog.w(TAG, "Failed writing stats to file:" + mFile); - if (backupFile != null) { - mFile.delete(); - backupFile.renameTo(mFile); - } - } - out.recycle(); - } - if (localLOGV) Slog.d(TAG, "Dumped usage stats."); - } - - private void writeStatsFLOCK(File file, Parcel parcel) throws IOException { - FileOutputStream stream = new FileOutputStream(file); - try { - stream.write(parcel.marshall()); - stream.flush(); - } finally { - FileUtils.sync(stream); - stream.close(); - } - } - - /** Filter out stats for any packages which aren't present anymore. */ - private void filterHistoryStats() { - synchronized (mStatsLock) { - IPackageManager pm = AppGlobals.getPackageManager(); - for (int i=mStats.mPackages.size()-1; i>=0; i--) { - try { - if (pm.getPackageUid(mStats.mPackages.valueAt(i).getPackageName(), 0) < 0) { - mStats.mPackages.removeAt(i); - } - } catch (RemoteException e) { - } - } - } - } - - private void writeHistoryStatsFLOCK() { - FileOutputStream fos = null; - try { - fos = mHistoryFile.startWrite(); - XmlSerializer out = new FastXmlSerializer(); - out.setOutput(fos, "utf-8"); - out.startDocument(null, true); - out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); - out.startTag(null, "usage-history"); - synchronized (mStatsLock) { - int NP = mStats.mPackages.size(); - for (int i=0; i<NP; i++) { - UsageStats.PackageStats ps = mStats.mPackages.valueAt(i); - out.startTag(null, "pkg"); - out.attribute(null, "name", ps.getPackageName()); - ArrayMap<String, Long> comp = ps.componentResumeTimes; - for (int j=0; j<comp.size(); j++) { - out.startTag(null, "comp"); - out.attribute(null, "name", comp.keyAt(j)); - out.attribute(null, "lrt", comp.valueAt(j).toString()); - out.endTag(null, "comp"); - } - out.endTag(null, "pkg"); - } - } - out.endTag(null, "usage-history"); - out.endDocument(); - - mHistoryFile.finishWrite(fos); - } catch (IOException e) { - Slog.w(TAG,"Error writing history stats" + e); - if (fos != null) { - mHistoryFile.failWrite(fos); - } - } - } - - public void publish(Context context) { - mContext = context; - ServiceManager.addService(SERVICE_NAME, asBinder()); - } - - public void systemReady() { - mAppOps = (AppOpsManager)mContext.getSystemService(Context.APP_OPS_SERVICE); - } - - /** - * Start watching packages to remove stats when a package is uninstalled. - * May only be called when the package manager is ready. - */ - public void monitorPackages() { - mPackageMonitor = new PackageMonitor() { - @Override - public void onPackageRemovedAllUsers(String packageName, int uid) { - synchronized (mStatsLock) { - mStats.mPackages.remove(packageName); - } - } - }; - mPackageMonitor.register(mContext, null, true); - filterHistoryStats(); - } - - public void shutdown() { - if (mPackageMonitor != null) { - mPackageMonitor.unregister(); - } - Slog.i(TAG, "Writing usage stats before shutdown..."); - writeStatsToFile(true, true); - } - - public static IUsageStats getService() { - if (sService != null) { - return sService; - } - IBinder b = ServiceManager.getService(SERVICE_NAME); - sService = asInterface(b); - return sService; - } - - @Override - public void noteResumeComponent(ComponentName componentName) { - enforceCallingPermission(); - String pkgName; - synchronized (mStatsLock) { - if ((componentName == null) || - ((pkgName = componentName.getPackageName()) == null)) { - return; - } - - final boolean samePackage = pkgName.equals(mLastResumedPkg); - if (mIsResumed) { - if (mLastResumedPkg != null) { - // We last resumed some other package... just pause it now - // to recover. - if (REPORT_UNEXPECTED) Slog.i(TAG, "Unexpected resume of " + pkgName - + " while already resumed in " + mLastResumedPkg); - PkgUsageStatsExtended pus = (PkgUsageStatsExtended)mStats.getPackageStats( - mLastResumedPkg); - if (pus != null) { - pus.pause(); - } - } - } - - final boolean sameComp = samePackage - && componentName.getClassName().equals(mLastResumedComp); - - mIsResumed = true; - mLastResumedPkg = pkgName; - mLastResumedComp = componentName.getClassName(); - - if (localLOGV) Slog.i(TAG, "started component:" + pkgName); - PkgUsageStatsExtended pus = (PkgUsageStatsExtended) - mStats.getOrCreatePackageStats(pkgName); - pus.resume(!samePackage); - if (!sameComp) { - pus.addLaunchCount(mLastResumedComp); - } - pus.componentResumeTimes.put(mLastResumedComp, System.currentTimeMillis()); - } - } - - @Override - public void notePauseComponent(ComponentName componentName) { - enforceCallingPermission(); - - synchronized (mStatsLock) { - String pkgName; - if ((componentName == null) || - ((pkgName = componentName.getPackageName()) == null)) { - return; - } - if (!mIsResumed) { - if (REPORT_UNEXPECTED) Slog.i(TAG, "Something wrong here, didn't expect " - + pkgName + " to be paused"); - return; - } - mIsResumed = false; - - if (localLOGV) Slog.i(TAG, "paused component:"+pkgName); - - PkgUsageStatsExtended pus = (PkgUsageStatsExtended)mStats.getPackageStats(pkgName); - if (pus == null) { - // Weird some error here - Slog.i(TAG, "No package stats for pkg:"+pkgName); - return; - } - pus.pause(); - } - - // Persist current data to file if needed. - writeStatsToFile(false, false); - } - - @Override - public void noteLaunchTime(ComponentName componentName, int millis) { - enforceCallingPermission(); - String pkgName; - if ((componentName == null) || - ((pkgName = componentName.getPackageName()) == null)) { - return; - } - - // Persist current data to file if needed. - writeStatsToFile(false, false); - - synchronized (mStatsLock) { - PkgUsageStatsExtended pus = (PkgUsageStatsExtended)mStats.getPackageStats(pkgName); - if (pus != null) { - pus.addLaunchTime(componentName.getClassName(), millis); - } - } - } - - public void noteFullyDrawnTime(ComponentName componentName, int millis) { - enforceCallingPermission(); - String pkgName; - if ((componentName == null) || - ((pkgName = componentName.getPackageName()) == null)) { - return; - } - - // Persist current data to file if needed. - writeStatsToFile(false, false); - - synchronized (mStatsLock) { - PkgUsageStatsExtended pus = (PkgUsageStatsExtended)mStats.getPackageStats(pkgName); - if (pus != null) { - pus.addFullyDrawnTime(componentName.getClassName(), millis); - } - } - } - - public void noteStartConfig(Configuration config) { - enforceCallingPermission(); - synchronized (mStatsLock) { - config = new Configuration(config); - ConfigUsageStatsExtended cus = (ConfigUsageStatsExtended) - mStats.getOrCreateConfigurationStats(config); - if (cus != mCurrentConfigStats) { - if (mCurrentConfigStats != null) { - mCurrentConfigStats.stop(); - } - cus.start(); - mCurrentConfigStats = cus; - } - } - } - - public void enforceCallingPermission() { - if (Binder.getCallingPid() == Process.myPid()) { - return; - } - mContext.enforcePermission(android.Manifest.permission.UPDATE_DEVICE_STATS, - Binder.getCallingPid(), Binder.getCallingUid(), null); - } - - @Override - public UsageStats.PackageStats getPkgUsageStats(String callingPkg, - ComponentName componentName) { - checkCallerPermission(callingPkg, "getPkgUsageStats"); - String pkgName; - if ((componentName == null) || - ((pkgName = componentName.getPackageName()) == null)) { - return null; - } - synchronized (mStatsLock) { - PkgUsageStatsExtended pus = (PkgUsageStatsExtended)mStats.getPackageStats(pkgName); - if (pus == null) { - return null; - } - return new UsageStats.PackageStats(pus); - } - } - - @Override - public UsageStats.PackageStats[] getAllPkgUsageStats(String callingPkg) { - checkCallerPermission(callingPkg, "getAllPkgUsageStats"); - synchronized (mStatsLock) { - int NP = mStats.mPackages.size(); - if (NP <= 0) { - return null; - } - UsageStats.PackageStats retArr[] = new UsageStats.PackageStats[NP]; - for (int p=0; p<NP; p++) { - UsageStats.PackageStats ps = mStats.mPackages.valueAt(p); - retArr[p] = new UsageStats.PackageStats(ps); - } - return retArr; - } - } - - @Override - public ParcelableParcel getCurrentStats(String callingPkg) { - checkCallerPermission(callingPkg, "getCurrentStats"); - synchronized (mStatsLock) { - ParcelableParcel out = new ParcelableParcel(null); - mStats.writeToParcel(out.getParcel(), 0); - return out; - } - } - - private void checkCallerPermission(String callingPkg, String callingOp) { - // Because the permission for this is system-only, its use with - // app ops is a little different: the op is disabled by default, - // and enabling it allows apps to get access even if they don't - // hold the permission. - int mode = mAppOps.noteOpNoThrow(AppOpsManager.OP_GET_USAGE_STATS, Binder.getCallingUid(), - callingPkg); - if (mode == AppOpsManager.MODE_ALLOWED) { - return; - } else if (mode != AppOpsManager.MODE_IGNORED) { - if (mContext.checkCallingOrSelfPermission( - android.Manifest.permission.PACKAGE_USAGE_STATS) - == PackageManager.PERMISSION_GRANTED) { - return; - } - } - - String msg = "Package " + callingPkg + " not allowed to call " + callingOp; - throw new SecurityException(msg); - } - - static byte[] readFully(FileInputStream stream) throws IOException { - int pos = 0; - int avail = stream.available(); - byte[] data = new byte[avail]; - while (true) { - int amt = stream.read(data, pos, data.length-pos); - if (amt <= 0) { - return data; - } - pos += amt; - avail = stream.available(); - if (avail > data.length-pos) { - byte[] newData = new byte[pos+avail]; - System.arraycopy(data, 0, newData, 0, pos); - data = newData; - } - } - } - - private void collectDumpInfoFLOCK(PrintWriter pw, boolean isCompactOutput, - boolean deleteAfterPrint, HashSet<String> packages) { - List<String> fileList = getUsageStatsFileListFLOCK(); - if (fileList == null) { - return; - } - Collections.sort(fileList); - for (String file : fileList) { - if (deleteAfterPrint && file.equalsIgnoreCase(mFileLeaf)) { - // In this mode we don't print the current day's stats, since - // they are incomplete. - continue; - } - File dFile = new File(mDir, file); - String dateStr = file.substring(FILE_PREFIX.length()); - if (dateStr.length() > 0 && (dateStr.charAt(0) <= '0' || dateStr.charAt(0) >= '9')) { - // If the remainder does not start with a number, it is not a date, - // so we should ignore it for purposes here. - continue; - } - try { - Parcel in = getParcelForFile(dFile); - collectDumpInfoFromParcelFLOCK(in, pw, dateStr, isCompactOutput, - packages); - if (deleteAfterPrint) { - // Delete old file after collecting info only for checkin requests - dFile.delete(); - } - } catch (IOException e) { - Slog.w(TAG, "Failed with "+e+" when collecting dump info from file : "+file); - } - } - } - - private void collectDumpInfoFromParcelFLOCK(Parcel in, PrintWriter pw, - String date, boolean isCompactOutput, HashSet<String> packages) { - StringBuilder sb = new StringBuilder(512); - if (isCompactOutput) { - sb.append("D:"); - sb.append(CHECKIN_VERSION); - sb.append(','); - } else { - sb.append("Date: "); - } - - sb.append(date); - - int vers = in.readInt(); - if (vers != VERSION) { - sb.append(" (old data version)"); - pw.println(sb.toString()); - return; - } - - final LocalUsageStats stats = new LocalUsageStats(in, true); - final long time = SystemClock.elapsedRealtime(); - - pw.println(sb.toString()); - int NP = stats.mPackages.size(); - for (int p=0; p<NP; p++) { - PkgUsageStatsExtended pus = (PkgUsageStatsExtended)stats.mPackages.valueAt(p); - sb.setLength(0); - if (packages != null && !packages.contains(pus.getPackageName())) { - // This package has not been requested -- don't print - // anything for it. - } else if (isCompactOutput) { - sb.append("P:"); - sb.append(pus.getPackageName()); - sb.append(','); - sb.append(pus.getLaunchCount()); - sb.append(','); - sb.append(pus.getUsageTime(time)); - sb.append('\n'); - final int NLT = pus.mLaunchTimes.size(); - for (int i=0; i<NLT; i++) { - sb.append("L:"); - String activity = pus.mLaunchTimes.keyAt(i); - sb.append(activity); - TimeStats times = pus.mLaunchTimes.valueAt(i); - sb.append(','); - sb.append(times.mCount); - for (int j=0; j<NUM_LAUNCH_TIME_BINS; j++) { - sb.append(","); - sb.append(times.mTimes[j]); - } - sb.append('\n'); - } - final int NFDT = pus.mFullyDrawnTimes.size(); - for (int i=0; i<NFDT; i++) { - sb.append("D:"); - String activity = pus.mFullyDrawnTimes.keyAt(i); - sb.append(activity); - TimeStats times = pus.mFullyDrawnTimes.valueAt(i); - for (int j=0; j<NUM_LAUNCH_TIME_BINS; j++) { - sb.append(","); - sb.append(times.mTimes[j]); - } - sb.append('\n'); - } - final int NC = pus.componentResumeTimes.size(); - for (int c=0; c<NC; c++) { - pw.print("R:"); pw.print(pus.componentResumeTimes.keyAt(c)); pw.print(","); - pw.println(pus.componentResumeTimes.valueAt(c)); - } - - } else { - sb.append(" "); - sb.append(pus.getPackageName()); - if (pus.getLaunchCount() != 0 || pus.getUsageTime(time) != 0) { - sb.append(": "); - sb.append(pus.getLaunchCount()); - sb.append(" times, "); - TimeUtils.formatDuration(pus.getUsageTime(time), sb); - } else { - sb.append(":"); - } - sb.append('\n'); - final int NLT = pus.mLaunchTimes.size(); - for (int i=0; i<NLT; i++) { - sb.append(" "); - sb.append(pus.mLaunchTimes.keyAt(i)); - TimeStats times = pus.mLaunchTimes.valueAt(i); - sb.append(": "); - sb.append(times.mCount); - sb.append(" starts"); - int lastBin = 0; - for (int j=0; j<NUM_LAUNCH_TIME_BINS-1; j++) { - if (times.mTimes[j] != 0) { - sb.append(", "); - sb.append(lastBin); - sb.append('-'); - sb.append(LAUNCH_TIME_BINS[j]); - sb.append("ms="); - sb.append(times.mTimes[j]); - } - lastBin = LAUNCH_TIME_BINS[j]; - } - if (times.mTimes[NUM_LAUNCH_TIME_BINS-1] != 0) { - sb.append(", "); - sb.append(">="); - sb.append(lastBin); - sb.append("ms="); - sb.append(times.mTimes[NUM_LAUNCH_TIME_BINS-1]); - } - sb.append('\n'); - } - final int NFDT = pus.mFullyDrawnTimes.size(); - for (int i=0; i<NFDT; i++) { - sb.append(" "); - sb.append(pus.mFullyDrawnTimes.keyAt(i)); - TimeStats times = pus.mFullyDrawnTimes.valueAt(i); - sb.append(": fully drawn "); - boolean needComma = false; - int lastBin = 0; - for (int j=0; j<NUM_LAUNCH_TIME_BINS-1; j++) { - if (times.mTimes[j] != 0) { - if (needComma) { - sb.append(", "); - } else { - needComma = true; - } - sb.append(lastBin); - sb.append('-'); - sb.append(LAUNCH_TIME_BINS[j]); - sb.append("ms="); - sb.append(times.mTimes[j]); - } - lastBin = LAUNCH_TIME_BINS[j]; - } - if (times.mTimes[NUM_LAUNCH_TIME_BINS-1] != 0) { - if (needComma) { - sb.append(", "); - } - sb.append(">="); - sb.append(lastBin); - sb.append("ms="); - sb.append(times.mTimes[NUM_LAUNCH_TIME_BINS-1]); - } - sb.append('\n'); - } - final int NC = pus.componentResumeTimes.size(); - for (int c=0; c<NC; c++) { - sb.append(" "); - sb.append(pus.componentResumeTimes.keyAt(c)); - sb.append(" last resumed "); - sb.append(DateFormat.format("yyyy-MM-dd-HH-mm-ss", - pus.componentResumeTimes.valueAt(c)).toString()); - sb.append('\n'); - } - } - - pw.write(sb.toString()); - } - if (packages == null) { - int NC = stats.mConfigurations.size(); - for (int c=0; c<NC; c++) { - ConfigUsageStatsExtended cus - = (ConfigUsageStatsExtended)stats.mConfigurations.valueAt(c); - sb.setLength(0); - if (isCompactOutput) { - sb.append("C:"); sb.append(cus.getConfiguration().toString()); - sb.append(","); sb.append(cus.getUsageCount()); sb.append(","); - sb.append(cus.getUsageTime(time)); - } else { - sb.append(" "); - sb.append(cus.getConfiguration().toString()); - sb.append(":\n"); - if (cus.getUsageCount() != 0 || cus.getUsageTime(time) != 0) { - sb.append(" Used "); - sb.append(cus.getUsageCount()); - sb.append(" times, "); - TimeUtils.formatDuration(cus.getUsageTime(time), sb); - sb.append("\n"); - } - if (cus.getLastUsedTime() > 0) { - sb.append(" Last used: "); - sb.append(DateFormat.format("yyyy-MM-dd-HH-mm-ss", - cus.getLastUsedTime()).toString()); - sb.append("\n"); - } - } - pw.write(sb.toString()); - } - } - } - - /** - * Searches array of arguments for the specified string - * @param args array of argument strings - * @param value value to search for - * @return true if the value is contained in the array - */ - private static boolean scanArgs(String[] args, String value) { - if (args != null) { - for (String arg : args) { - if (value.equals(arg)) { - return true; - } - } - } - return false; - } - - /** - * Searches array of arguments for the specified string's data - * @param args array of argument strings - * @param value value to search for - * @return the string of data after the arg, or null if there is none - */ - private static String scanArgsData(String[] args, String value) { - if (args != null) { - final int N = args.length; - for (int i=0; i<N; i++) { - if (value.equals(args[i])) { - i++; - return i < N ? args[i] : null; - } - } - } - return null; - } - - /* - * The data persisted to file is parsed and the stats are computed. - */ - @Override - protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - if (mContext.checkCallingPermission(android.Manifest.permission.DUMP) - != PackageManager.PERMISSION_GRANTED) { - pw.println("Permission Denial: can't dump UsageStats from from pid=" - + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid() - + " without permission " + android.Manifest.permission.DUMP); - return; - } - - final boolean isCheckinRequest = scanArgs(args, "--checkin"); - final boolean isCompactOutput = isCheckinRequest || scanArgs(args, "-c"); - final boolean deleteAfterPrint = isCheckinRequest || scanArgs(args, "-d"); - final String rawPackages = scanArgsData(args, "--packages"); - - // Make sure the current stats are written to the file. This - // doesn't need to be done if we are deleting files after printing, - // since in that case we won't print the current stats. - if (!deleteAfterPrint) { - writeStatsToFile(true, false); - } - - HashSet<String> packages = null; - if (rawPackages != null) { - if (!"*".equals(rawPackages)) { - // A * is a wildcard to show all packages. - String[] names = rawPackages.split(","); - if (names.length != 0) { - packages = new HashSet<String>(); - } - for (String n : names) { - packages.add(n); - } - } - } else if (isCheckinRequest) { - // If checkin doesn't specify any packages, then we simply won't - // show anything. - Slog.w(TAG, "Checkin without packages"); - return; - } - - synchronized (mFileLock) { - collectDumpInfoFLOCK(pw, isCompactOutput, deleteAfterPrint, packages); - } - } -} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index de929cb..bf2edc8 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -20,6 +20,7 @@ import android.app.ActivityManagerNative; import android.app.ActivityThread; import android.app.IAlarmManager; import android.app.INotificationManager; +import android.app.usage.UsageStatsManagerInternal; import android.bluetooth.BluetoothAdapter; import android.content.ComponentName; import android.content.ContentResolver; @@ -91,6 +92,7 @@ import com.android.server.storage.DeviceStorageMonitorService; import com.android.server.trust.TrustManagerService; import com.android.server.tv.TvInputManagerService; import com.android.server.twilight.TwilightService; +import com.android.server.usage.UsageStatsService; import com.android.server.usb.UsbService; import com.android.server.wallpaper.WallpaperManagerService; import com.android.server.webkit.WebViewUpdateService; @@ -366,6 +368,11 @@ public final class SystemServer { // Tracks the battery level. Requires LightService. mSystemServiceManager.startService(BatteryService.class); + + // Tracks application usage stats. + mSystemServiceManager.startService(UsageStatsService.class); + mActivityManagerService.setUsageStatsManager( + LocalServices.getService(UsageStatsManagerInternal.class)); } /** diff --git a/services/usage/Android.mk b/services/usage/Android.mk new file mode 100644 index 0000000..d4b7fa8 --- /dev/null +++ b/services/usage/Android.mk @@ -0,0 +1,10 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := services.usage + +LOCAL_SRC_FILES += \ + $(call all-java-files-under,java) + +include $(BUILD_STATIC_JAVA_LIBRARY) diff --git a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java new file mode 100644 index 0000000..4e75f61 --- /dev/null +++ b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java @@ -0,0 +1,199 @@ +/** + * 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.UsageStats; +import android.app.usage.UsageStatsManager; +import android.util.AtomicFile; +import android.util.Slog; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Calendar; + +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 TimeSparseArray<AtomicFile>[] mSortedStatFiles; + private final Calendar mCal; + + public UsageStatsDatabase(File dir) { + mBucketDirs = new File[] { + new File(dir, "daily"), + new File(dir, "weekly"), + new File(dir, "monthly"), + new File(dir, "yearly"), + }; + mSortedStatFiles = new TimeSparseArray[mBucketDirs.length]; + mCal = Calendar.getInstance(); + } + + void init() { + synchronized (mLock) { + for (File f : mBucketDirs) { + f.mkdirs(); + if (!f.exists()) { + throw new IllegalStateException("Failed to create directory " + + f.getAbsolutePath()); + } + } + + final FilenameFilter backupFileFilter = new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return !name.endsWith(".bak"); + } + }; + + // 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); + if (files != null) { + if (DEBUG) { + Slog.d(TAG, "Found " + files.length + " stat files for bucket " + i); + } + + for (File f : files) { + mSortedStatFiles[i].put(Long.parseLong(f.getName()), new AtomicFile(f)); + } + } + } + } + } + + public UsageStats getLatestUsageStats(int bucketType) { + synchronized (mLock) { + if (bucketType < 0 || bucketType >= mBucketDirs.length) { + throw new IllegalArgumentException("Bad bucket type " + bucketType); + } + + final int fileCount = mSortedStatFiles[bucketType].size(); + if (fileCount == 0) { + return null; + } + + try { + final AtomicFile f = mSortedStatFiles[bucketType].valueAt(fileCount - 1); + UsageStats stats = UsageStatsXml.read(f); + stats.mLastTimeSaved = f.getLastModifiedTime(); + return stats; + } catch (IOException e) { + Slog.e(TAG, "Failed to read usage stats file", e); + } + } + return null; + } + + public UsageStats[] getUsageStats(int bucketType, long beginTime, int limit) { + synchronized (mLock) { + if (bucketType < 0 || bucketType >= mBucketDirs.length) { + throw new IllegalArgumentException("Bad bucket type " + bucketType); + } + + if (limit <= 0) { + return UsageStats.EMPTY_STATS; + } + + int startIndex = mSortedStatFiles[bucketType].closestIndexAfter(beginTime); + if (startIndex < 0) { + return UsageStats.EMPTY_STATS; + } + + 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); + + 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); + } + } + return stats.toArray(new UsageStats[stats.size()]); + } catch (IOException e) { + Slog.e(TAG, "Failed to read usage stats file", e); + return UsageStats.EMPTY_STATS; + } + } + } + + public void prune() { + synchronized (mLock) { + long timeNow = System.currentTimeMillis(); + + mCal.setTimeInMillis(timeNow); + mCal.add(Calendar.MONTH, -6); + pruneFilesOlderThan(mBucketDirs[UsageStatsManager.MONTHLY_BUCKET], + mCal.getTimeInMillis()); + + mCal.setTimeInMillis(timeNow); + mCal.add(Calendar.WEEK_OF_YEAR, -4); + pruneFilesOlderThan(mBucketDirs[UsageStatsManager.WEEKLY_BUCKET], + mCal.getTimeInMillis()); + + mCal.setTimeInMillis(timeNow); + mCal.add(Calendar.DAY_OF_YEAR, -7); + pruneFilesOlderThan(mBucketDirs[UsageStatsManager.DAILY_BUCKET], + mCal.getTimeInMillis()); + } + } + + private static void pruneFilesOlderThan(File dir, long expiryTime) { + File[] files = dir.listFiles(); + if (files != null) { + for (File f : files) { + long beginTime = Long.parseLong(f.getName()); + if (beginTime < expiryTime) { + new AtomicFile(f).delete(); + } + } + } + } + + public void putUsageStats(int bucketType, UsageStats stats) + throws IOException { + synchronized (mLock) { + if (bucketType < 0 || bucketType >= mBucketDirs.length) { + throw new IllegalArgumentException("Bad bucket type " + bucketType); + } + + AtomicFile f = mSortedStatFiles[bucketType].get(stats.mBeginTimeStamp); + if (f == null) { + f = new AtomicFile(new File(mBucketDirs[bucketType], + Long.toString(stats.mBeginTimeStamp))); + mSortedStatFiles[bucketType].append(stats.mBeginTimeStamp, f); + } + + UsageStatsXml.write(stats, f); + stats.mLastTimeSaved = f.getLastModifiedTime(); + } + } + +} diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java new file mode 100644 index 0000000..13cbf8a --- /dev/null +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -0,0 +1,437 @@ +/** + * 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.AppOpsManager; +import android.app.usage.IUsageStatsManager; +import android.app.usage.PackageUsageStats; +import android.app.usage.TimeSparseArray; +import android.app.usage.UsageStats; +import android.app.usage.UsageStatsManager; +import android.app.usage.UsageStatsManagerInternal; +import android.content.ComponentName; +import android.content.Context; +import android.os.Binder; +import android.os.Environment; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.ArraySet; +import android.util.Slog; +import com.android.internal.os.BackgroundThread; +import com.android.server.SystemService; + +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Calendar; + +public class UsageStatsService extends SystemService { + static final String TAG = "UsageStatsService"; + + 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 FLUSH_INTERVAL = DEBUG ? TEN_SECONDS : TWENTY_MINUTES; + private static final int USAGE_STAT_RESULT_LIMIT = 10; + private static final SimpleDateFormat sDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + // Handler message types. + static final int MSG_REPORT_EVENT = 0; + static final int MSG_FLUSH_TO_DISK = 1; + + final Object mLock = new Object(); + Handler mHandler; + AppOpsManager mAppOps; + + private UsageStatsDatabase mDatabase; + private UsageStats[] mCurrentStats = new UsageStats[UsageStatsManager.BUCKET_COUNT]; + private TimeSparseArray<UsageStats.Event> mCurrentEvents = new TimeSparseArray<>(); + private boolean mStatsChanged = false; + private Calendar mDailyExpiryDate; + + public UsageStatsService(Context context) { + super(context); + } + + @Override + public void onStart() { + mDailyExpiryDate = Calendar.getInstance(); + mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE); + mHandler = new H(BackgroundThread.get().getLooper()); + + File systemDataDir = new File(Environment.getDataDirectory(), "system"); + mDatabase = new UsageStatsDatabase(new File(systemDataDir, "usagestats")); + mDatabase.init(); + + synchronized (mLock) { + initLocked(); + } + + publishLocalService(UsageStatsManagerInternal.class, new LocalService()); + publishBinderService(Context.USAGE_STATS_SERVICE, new BinderService()); + } + + private void initLocked() { + int nullCount = 0; + for (int i = 0; i < mCurrentStats.length; i++) { + mCurrentStats[i] = mDatabase.getLatestUsageStats(i); + if (mCurrentStats[i] == null) { + nullCount++; + } + } + + if (nullCount > 0) { + if (nullCount != mCurrentStats.length) { + // This is weird, but we shouldn't fail if something like this + // happens. + Slog.w(TAG, "Some stats have no latest available"); + } else { + // This must be first boot. + } + + // By calling loadActiveStatsLocked, we will + // generate new stats for each bucket. + loadActiveStatsLocked(); + } else { + // Set up the expiry date to be one day from the latest daily stat. + // This may actually be today and we will rollover on the first event + // that is reported. + mDailyExpiryDate.setTimeInMillis( + mCurrentStats[UsageStatsManager.DAILY_BUCKET].mBeginTimeStamp); + mDailyExpiryDate.add(Calendar.DAY_OF_YEAR, 1); + UsageStatsUtils.truncateDateTo(UsageStatsManager.DAILY_BUCKET, mDailyExpiryDate); + Slog.i(TAG, "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 (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) { + updateStatsLocked(stat, pkgStats.mPackageName, stat.mLastTimeSaved, + UsageStats.Event.END_OF_DAY); + notifyStatsChangedLocked(); + } + } + } + } + + private void rolloverStatsLocked() { + final long startTime = System.currentTimeMillis(); + Slog.i(TAG, "Rolling over usage stats"); + + // 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 (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) { + continuePreviousDay.add(pkgStats.mPackageName); + updateStatsLocked(stat, pkgStats.mPackageName, + mDailyExpiryDate.getTimeInMillis() - 1, UsageStats.Event.END_OF_DAY); + mStatsChanged = true; + } + } + } + + persistActiveStatsLocked(); + mDatabase.prune(); + loadActiveStatsLocked(); + + final int continueCount = continuePreviousDay.size(); + for (int i = 0; i < continueCount; i++) { + String name = continuePreviousDay.valueAt(i); + for (UsageStats stat : mCurrentStats) { + updateStatsLocked(stat, name, + mCurrentStats[UsageStatsManager.DAILY_BUCKET].mBeginTimeStamp, + UsageStats.Event.CONTINUE_PREVIOUS_DAY); + mStatsChanged = true; + } + } + persistActiveStatsLocked(); + + final long totalTime = System.currentTimeMillis() - startTime; + Slog.i(TAG, "Rolling over usage stats complete. Took " + totalTime + " milliseconds"); + } + + private void notifyStatsChangedLocked() { + if (!mStatsChanged) { + mStatsChanged = true; + mHandler.sendEmptyMessageDelayed(MSG_FLUSH_TO_DISK, FLUSH_INTERVAL); + } + } + + /** + * Called by the Bunder stub + */ + void shutdown() { + synchronized (mLock) { + mHandler.removeMessages(MSG_REPORT_EVENT); + mHandler.removeMessages(MSG_FLUSH_TO_DISK); + persistActiveStatsLocked(); + } + } + + private static String eventToString(int eventType) { + switch (eventType) { + case UsageStats.Event.NONE: + return "NONE"; + case UsageStats.Event.MOVE_TO_BACKGROUND: + return "MOVE_TO_BACKGROUND"; + case UsageStats.Event.MOVE_TO_FOREGROUND: + return "MOVE_TO_FOREGROUND"; + case UsageStats.Event.END_OF_DAY: + return "END_OF_DAY"; + case UsageStats.Event.CONTINUE_PREVIOUS_DAY: + return "CONTINUE_PREVIOUS_DAY"; + default: + return "UNKNOWN"; + } + } + + /** + * Called by the Binder stub. + */ + void reportEvent(UsageStats.Event event) { + synchronized (mLock) { + if (DEBUG) { + Slog.d(TAG, "Got usage event for " + event.packageName + + "[" + event.timeStamp + "]: " + + eventToString(event.eventType)); + } + + if (event.timeStamp >= mDailyExpiryDate.getTimeInMillis()) { + // Need to rollover + rolloverStatsLocked(); + } + + mCurrentEvents.append(event.timeStamp, event); + + for (UsageStats stats : mCurrentStats) { + updateStatsLocked(stats, event.packageName, event.timeStamp, event.eventType); + } + notifyStatsChangedLocked(); + } + } + + /** + * Called by the Binder stub. + */ + UsageStats[] getUsageStats(int bucketType, long beginTime) { + if (bucketType < 0 || bucketType >= mCurrentStats.length) { + return UsageStats.EMPTY_STATS; + } + + final long timeNow = System.currentTimeMillis(); + if (beginTime > timeNow) { + return UsageStats.EMPTY_STATS; + } + + synchronized (mLock) { + if (beginTime >= mCurrentStats[bucketType].mEndTimeStamp) { + if (DEBUG) { + Slog.d(TAG, "Requesting stats after " + beginTime + " but latest is " + + mCurrentStats[bucketType].mEndTimeStamp); + } + // Nothing newer available. + return UsageStats.EMPTY_STATS; + } else if (beginTime >= mCurrentStats[bucketType].mBeginTimeStamp) { + if (DEBUG) { + Slog.d(TAG, "Returning in-memory stats"); + } + // 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. + persistActiveStatsLocked(); + } + } + + if (DEBUG) { + Slog.d(TAG, "SELECT * FROM " + bucketType + " WHERE beginTime >= " + + beginTime + " LIMIT " + USAGE_STAT_RESULT_LIMIT); + } + + UsageStats[] results = mDatabase.getUsageStats(bucketType, beginTime, + USAGE_STAT_RESULT_LIMIT); + + if (DEBUG) { + Slog.d(TAG, "Results: " + results.length); + } + return results; + } + + /** + * Called by the Binder stub. + */ + UsageStats.Event[] getEvents(long time) { + return UsageStats.Event.EMPTY_EVENTS; + } + + private void loadActiveStatsLocked() { + final long timeNow = System.currentTimeMillis(); + + Calendar tempCal = mDailyExpiryDate; + for (int i = 0; i < mCurrentStats.length; i++) { + tempCal.setTimeInMillis(timeNow); + UsageStatsUtils.truncateDateTo(i, tempCal); + + if (mCurrentStats[i] != null && + mCurrentStats[i].mBeginTimeStamp == 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]; + } else { + mCurrentStats[i] = UsageStats.create(tempCal.getTimeInMillis(), timeNow); + } + } + mStatsChanged = false; + mDailyExpiryDate.setTimeInMillis(timeNow); + mDailyExpiryDate.add(Calendar.DAY_OF_YEAR, 1); + UsageStatsUtils.truncateDateTo(UsageStatsManager.DAILY_BUCKET, mDailyExpiryDate); + Slog.i(TAG, "Rollover scheduled for " + sDateFormat.format(mDailyExpiryDate.getTime())); + } + + + private void persistActiveStatsLocked() { + if (mStatsChanged) { + Slog.i(TAG, "Flushing usage stats to disk"); + try { + for (int i = 0; i < mCurrentStats.length; i++) { + mDatabase.putUsageStats(i, mCurrentStats[i]); + } + mStatsChanged = false; + mHandler.removeMessages(MSG_FLUSH_TO_DISK); + } catch (IOException e) { + Slog.e(TAG, "Failed to persist active stats", e); + } + } + } + + private void updateStatsLocked(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; + } + + class H extends Handler { + public H(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_REPORT_EVENT: + reportEvent((UsageStats.Event) msg.obj); + break; + + case MSG_FLUSH_TO_DISK: + synchronized (mLock) { + persistActiveStatsLocked(); + } + break; + + default: + super.handleMessage(msg); + break; + } + } + } + + private class BinderService extends IUsageStatsManager.Stub { + + @Override + public UsageStats[] getStatsSince(int bucketType, long time, String callingPackage) { + if (mAppOps.checkOp(AppOpsManager.OP_GET_USAGE_STATS, Binder.getCallingUid(), + callingPackage) != AppOpsManager.MODE_ALLOWED) { + return UsageStats.EMPTY_STATS; + } + + long token = Binder.clearCallingIdentity(); + try { + return getUsageStats(bucketType, time); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public UsageStats.Event[] getEventsSince(long time, String callingPackage) { + if (mAppOps.checkOp(AppOpsManager.OP_GET_USAGE_STATS, Binder.getCallingUid(), + callingPackage) != AppOpsManager.MODE_ALLOWED) { + return UsageStats.Event.EMPTY_EVENTS; + } + + long token = Binder.clearCallingIdentity(); + try { + return getEvents(time); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } + + /** + * This local service implementation is primarily used by ActivityManagerService. + * ActivityManagerService will call these methods holding the 'am' lock, which means we + * shouldn't be doing any IO work or other long running tasks in these methods. + */ + private class LocalService extends UsageStatsManagerInternal { + + @Override + public void reportEvent(ComponentName component, long timeStamp, int eventType) { + UsageStats.Event event = new UsageStats.Event(component.getPackageName(), timeStamp, + eventType); + mHandler.obtainMessage(MSG_REPORT_EVENT, event).sendToTarget(); + } + + @Override + public void prepareShutdown() { + // This method *WILL* do IO work, but we must block until it is finished or else + // we might not shutdown cleanly. This is ok to do with the 'am' lock held, because + // we are shutting down. + shutdown(); + } + } +} diff --git a/services/usage/java/com/android/server/usage/UsageStatsUtils.java b/services/usage/java/com/android/server/usage/UsageStatsUtils.java new file mode 100644 index 0000000..887e016 --- /dev/null +++ b/services/usage/java/com/android/server/usage/UsageStatsUtils.java @@ -0,0 +1,63 @@ +/** + * 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.UsageStatsManager; + +import java.util.Calendar; + +/** + * A collection of utility methods used by the UsageStatsService and accompanying classes. + */ +final class UsageStatsUtils { + private 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, + * with the time set to 00:00:00. + * + * @param bucket The UsageStats bucket to truncate to. + * @param cal The date to truncate. + */ + public static void truncateDateTo(int bucket, Calendar cal) { + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + + switch (bucket) { + case UsageStatsManager.YEARLY_BUCKET: + cal.set(Calendar.DAY_OF_YEAR, 0); + break; + + case UsageStatsManager.MONTHLY_BUCKET: + cal.set(Calendar.DAY_OF_MONTH, 0); + break; + + case UsageStatsManager.WEEKLY_BUCKET: + cal.set(Calendar.DAY_OF_WEEK, 0); + break; + + case UsageStatsManager.DAILY_BUCKET: + break; + + default: + throw new UnsupportedOperationException("Can't truncate date to bucket " + bucket); + } + } +} diff --git a/services/usage/java/com/android/server/usage/UsageStatsXml.java b/services/usage/java/com/android/server/usage/UsageStatsXml.java new file mode 100644 index 0000000..78f89d0 --- /dev/null +++ b/services/usage/java/com/android/server/usage/UsageStatsXml.java @@ -0,0 +1,161 @@ +/** + * 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.UsageStats; +import android.util.AtomicFile; +import android.util.Slog; +import android.util.Xml; +import com.android.internal.util.FastXmlSerializer; +import com.android.internal.util.XmlUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +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; + + public static UsageStats read(AtomicFile file) throws IOException { + try { + FileInputStream in = file.openRead(); + try { + return read(in); + } finally { + try { + in.close(); + } catch (IOException e) { + // Empty + } + } + } catch (FileNotFoundException e) { + Slog.e(TAG, "UsageStats Xml", e); + throw e; + } + } + + 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 UsageStats read(InputStream in) throws IOException { + XmlPullParser parser = Xml.newPullParser(); + try { + parser.setInput(in, "utf-8"); + XmlUtils.beginDocument(parser, USAGESTATS_TAG); + String versionStr = parser.getAttributeValue(null, VERSION_ATTR); + try { + switch (Integer.parseInt(versionStr)) { + case 1: + return loadVersion1(parser); + default: + Slog.e(TAG, "Unrecognized version " + versionStr); + throw new IOException("Unrecognized version " + versionStr); + } + } catch (NumberFormatException e) { + Slog.e(TAG, "Bad version"); + throw new IOException(e); + } + } catch (XmlPullParserException e) { + Slog.e(TAG, "Failed to parse Xml", e); + throw new IOException(e); + } + } + + 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 { + 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. + + xml.endTag(null, USAGESTATS_TAG); + xml.endDocument(); + } +} |