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 /services/usage | |
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
Diffstat (limited to 'services/usage')
5 files changed, 870 insertions, 0 deletions
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(); + } +} |