diff options
Diffstat (limited to 'services/core/java/com/android/server/am/UsageStatsService.java')
-rw-r--r-- | services/core/java/com/android/server/am/UsageStatsService.java | 1173 |
1 files changed, 1173 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/am/UsageStatsService.java b/services/core/java/com/android/server/am/UsageStatsService.java new file mode 100644 index 0000000..e96d8b1 --- /dev/null +++ b/services/core/java/com/android/server/am/UsageStatsService.java @@ -0,0 +1,1173 @@ +/* + * 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.content.ComponentName; +import android.content.Context; +import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Binder; +import android.os.IBinder; +import android.os.FileUtils; +import android.os.Parcel; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.util.ArrayMap; +import android.util.AtomicFile; +import android.util.Slog; +import android.util.Xml; + +import com.android.internal.app.IUsageStats; +import com.android.internal.content.PackageMonitor; +import com.android.internal.os.PkgUsageStats; +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.FileNotFoundException; +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.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +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 = 1008; + + 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 = 30*60*1000; //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; + // structure used to maintain statistics since the last checkin. + final private ArrayMap<String, PkgUsageStatsExtended> mStats; + + // Maintains the last time any component was resumed, for all time. + final private ArrayMap<String, ArrayMap<String, Long>> mLastResumeTimes; + + // 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; + // Lock to write to file. Methods suffixed by FLOCK should invoked with + // this lock held. + final Object mFileLock; + // Order of locks is mFileLock followed by mStatsLock to avoid deadlocks + private String mLastResumedPkg; + private String mLastResumedComp; + private boolean mIsResumed; + private File mFile; + private AtomicFile mHistoryFile; + private String mFileLeaf; + private File mDir; + + private Calendar mCal; // guarded by itself + + private final AtomicInteger mLastWriteDay = new AtomicInteger(-1); + private final AtomicLong mLastWriteElapsedTime = new AtomicLong(0); + private final AtomicBoolean mUnforcedDiskWriteRunning = new AtomicBoolean(false); + + static class TimeStats { + int count; + int[] times = new int[NUM_LAUNCH_TIME_BINS]; + + TimeStats() { + } + + void incCount() { + count++; + } + + 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]) { + times[i]++; + return; + } + } + times[NUM_LAUNCH_TIME_BINS-1]++; + } + + TimeStats(Parcel in) { + count = in.readInt(); + final int[] localTimes = times; + for (int i=0; i<NUM_LAUNCH_TIME_BINS; i++) { + localTimes[i] = in.readInt(); + } + } + + void writeToParcel(Parcel out) { + out.writeInt(count); + final int[] localTimes = times; + for (int i=0; i<NUM_LAUNCH_TIME_BINS; i++) { + out.writeInt(localTimes[i]); + } + } + } + + private class PkgUsageStatsExtended { + final ArrayMap<String, TimeStats> mLaunchTimes + = new ArrayMap<String, TimeStats>(); + final ArrayMap<String, TimeStats> mFullyDrawnTimes + = new ArrayMap<String, TimeStats>(); + int mLaunchCount; + long mUsageTime; + long mPausedTime; + long mResumedTime; + + PkgUsageStatsExtended() { + mLaunchCount = 0; + mUsageTime = 0; + } + + PkgUsageStatsExtended(Parcel in) { + mLaunchCount = in.readInt(); + mUsageTime = in.readLong(); + if (localLOGV) Slog.v(TAG, "Launch count: " + mLaunchCount + + ", Usage time:" + mUsageTime); + + 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 updateResume(String comp, boolean launched) { + if (launched) { + mLaunchCount ++; + } + mResumedTime = SystemClock.elapsedRealtime(); + } + + void updatePause() { + mPausedTime = SystemClock.elapsedRealtime(); + mUsageTime += (mPausedTime - mResumedTime); + } + + 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); + } + + void writeToParcel(Parcel out) { + out.writeInt(mLaunchCount); + out.writeLong(mUsageTime); + 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); + } + } + + void clear() { + mLaunchTimes.clear(); + mFullyDrawnTimes.clear(); + mLaunchCount = 0; + mUsageTime = 0; + } + } + + UsageStatsService(String dir) { + mStats = new ArrayMap<String, PkgUsageStatsExtended>(); + mLastResumeTimes = new ArrayMap<String, ArrayMap<String, Long>>(); + mStatsLock = new Object(); + mFileLock = new Object(); + mDir = new File(dir); + mCal = Calendar.getInstance(TimeZone.getTimeZone("GMT+0")); + + mDir.mkdir(); + + // Remove any old usage files from previous versions. + File parentDir = mDir.getParentFile(); + String fList[] = parentDir.list(); + if (fList != null) { + String prefix = mDir.getName() + "."; + int i = fList.length; + while (i > 0) { + i--; + if (fList[i].startsWith(prefix)) { + Slog.i(TAG, "Deleting old usage file: " + fList[i]); + (new File(parentDir, fList[i])).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(); + synchronized (mCal) { + mCal.setTimeInMillis(System.currentTimeMillis()); + if (prefix != null) { + sb.append(prefix); + } + 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); + byte[] raw = readFully(stream); + Parcel in = Parcel.obtain(); + in.unmarshall(raw, 0, raw.length); + in.setDataPosition(0); + stream.close(); + return in; + } + + 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) { + Slog.w(TAG, "Usage stats version changed; dropping"); + return; + } + int N = in.readInt(); + while (N > 0) { + N--; + String pkgName = in.readString(); + if (pkgName == null) { + break; + } + if (localLOGV) Slog.v(TAG, "Reading package #" + N + ": " + pkgName); + PkgUsageStatsExtended pus = new PkgUsageStatsExtended(in); + synchronized (mStatsLock) { + mStats.put(pkgName, pus); + } + } + } + + private void readHistoryStatsFromFile() { + synchronized (mFileLock) { + if (mHistoryFile.getBaseFile().exists()) { + readHistoryStatsFLOCK(mHistoryFile); + } + } + } + + private void readHistoryStatsFLOCK(AtomicFile file) { + 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 = parser.next(); + } + 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) { + ArrayMap<String, Long> lrt = mLastResumeTimes.get(pkg); + if (lrt == null) { + lrt = new ArrayMap<String, Long>(); + mLastResumeTimes.put(pkg, lrt); + } + lrt.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) { + // Strange but we dont have to delete any thing + 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; + } + + 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"); + return; + } + } else { + mFile.delete(); + } + } + + try { + // Write mStats to file + writeStatsFLOCK(mFile); + mLastWriteElapsedTime.set(currElapsedTime); + if (dayChanged) { + mLastWriteDay.set(curDay); + // clear stats + synchronized (mStats) { + mStats.clear(); + } + mFile = new File(mDir, mFileLeaf); + checkFileLimitFLOCK(); + } + + if (dayChanged || forceWriteHistoryStats) { + // Write history stats daily, or when forced (due to shutdown). + writeHistoryStatsFLOCK(mHistoryFile); + } + + // 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); + } + } + } + if (localLOGV) Slog.d(TAG, "Dumped usage stats."); + } + + private void writeStatsFLOCK(File file) throws IOException { + FileOutputStream stream = new FileOutputStream(file); + try { + Parcel out = Parcel.obtain(); + writeStatsToParcelFLOCK(out); + stream.write(out.marshall()); + out.recycle(); + stream.flush(); + } finally { + FileUtils.sync(stream); + stream.close(); + } + } + + private void writeStatsToParcelFLOCK(Parcel out) { + synchronized (mStatsLock) { + out.writeInt(VERSION); + Set<String> keys = mStats.keySet(); + out.writeInt(keys.size()); + for (String key : keys) { + PkgUsageStatsExtended pus = mStats.get(key); + out.writeString(key); + pus.writeToParcel(out); + } + } + } + + /** Filter out stats for any packages which aren't present anymore. */ + private void filterHistoryStats() { + synchronized (mStatsLock) { + IPackageManager pm = AppGlobals.getPackageManager(); + for (int i=0; i<mLastResumeTimes.size(); i++) { + String pkg = mLastResumeTimes.keyAt(i); + try { + if (pm.getPackageUid(pkg, 0) < 0) { + mLastResumeTimes.removeAt(i); + i--; + } + } catch (RemoteException e) { + } + } + } + } + + private void writeHistoryStatsFLOCK(AtomicFile historyFile) { + FileOutputStream fos = null; + try { + fos = historyFile.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) { + for (int i=0; i<mLastResumeTimes.size(); i++) { + out.startTag(null, "pkg"); + out.attribute(null, "name", mLastResumeTimes.keyAt(i)); + ArrayMap<String, Long> comp = mLastResumeTimes.valueAt(i); + 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(); + + historyFile.finishWrite(fos); + } catch (IOException e) { + Slog.w(TAG,"Error writing history stats" + e); + if (fos != null) { + historyFile.failWrite(fos); + } + } + } + + public void publish(Context context) { + mContext = context; + ServiceManager.addService(SERVICE_NAME, asBinder()); + } + + /** + * 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) { + mLastResumeTimes.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; + } + + 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 = mStats.get(mLastResumedPkg); + if (pus != null) { + pus.updatePause(); + } + } + } + + 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 = mStats.get(pkgName); + if (pus == null) { + pus = new PkgUsageStatsExtended(); + mStats.put(pkgName, pus); + } + pus.updateResume(mLastResumedComp, !samePackage); + if (!sameComp) { + pus.addLaunchCount(mLastResumedComp); + } + + ArrayMap<String, Long> componentResumeTimes = mLastResumeTimes.get(pkgName); + if (componentResumeTimes == null) { + componentResumeTimes = new ArrayMap<String, Long>(); + mLastResumeTimes.put(pkgName, componentResumeTimes); + } + componentResumeTimes.put(mLastResumedComp, System.currentTimeMillis()); + } + } + + 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 = mStats.get(pkgName); + if (pus == null) { + // Weird some error here + Slog.i(TAG, "No package stats for pkg:"+pkgName); + return; + } + pus.updatePause(); + } + + // Persist current data to file if needed. + writeStatsToFile(false, false); + } + + 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 = mStats.get(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 = mStats.get(pkgName); + if (pus != null) { + pus.addFullyDrawnTime(componentName.getClassName(), millis); + } + } + } + + public void enforceCallingPermission() { + if (Binder.getCallingPid() == Process.myPid()) { + return; + } + mContext.enforcePermission(android.Manifest.permission.UPDATE_DEVICE_STATS, + Binder.getCallingPid(), Binder.getCallingUid(), null); + } + + public PkgUsageStats getPkgUsageStats(ComponentName componentName) { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.PACKAGE_USAGE_STATS, null); + String pkgName; + if ((componentName == null) || + ((pkgName = componentName.getPackageName()) == null)) { + return null; + } + synchronized (mStatsLock) { + PkgUsageStatsExtended pus = mStats.get(pkgName); + Map<String, Long> lastResumeTimes = mLastResumeTimes.get(pkgName); + if (pus == null && lastResumeTimes == null) { + return null; + } + int launchCount = pus != null ? pus.mLaunchCount : 0; + long usageTime = pus != null ? pus.mUsageTime : 0; + return new PkgUsageStats(pkgName, launchCount, usageTime, lastResumeTimes); + } + } + + public PkgUsageStats[] getAllPkgUsageStats() { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.PACKAGE_USAGE_STATS, null); + synchronized (mStatsLock) { + int size = mLastResumeTimes.size(); + if (size <= 0) { + return null; + } + PkgUsageStats retArr[] = new PkgUsageStats[size]; + for (int i=0; i<size; i++) { + String pkg = mLastResumeTimes.keyAt(i); + long usageTime = 0; + int launchCount = 0; + + PkgUsageStatsExtended pus = mStats.get(pkg); + if (pus != null) { + usageTime = pus.mUsageTime; + launchCount = pus.mLaunchCount; + } + retArr[i] = new PkgUsageStats(pkg, launchCount, usageTime, + mLastResumeTimes.valueAt(i)); + } + return retArr; + } + } + + static byte[] readFully(FileInputStream stream) throws java.io.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 (FileNotFoundException e) { + Slog.w(TAG, "Failed with "+e+" when collecting dump info from file : " + file); + return; + } 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; + } + + pw.println(sb.toString()); + int N = in.readInt(); + + while (N > 0) { + N--; + String pkgName = in.readString(); + if (pkgName == null) { + break; + } + sb.setLength(0); + PkgUsageStatsExtended pus = new PkgUsageStatsExtended(in); + if (packages != null && !packages.contains(pkgName)) { + // This package has not been requested -- don't print + // anything for it. + } else if (isCompactOutput) { + sb.append("P:"); + sb.append(pkgName); + sb.append(','); + sb.append(pus.mLaunchCount); + sb.append(','); + sb.append(pus.mUsageTime); + sb.append('\n'); + final int NLT = pus.mLaunchTimes.size(); + for (int i=0; i<NLT; i++) { + sb.append("A:"); + String activity = pus.mLaunchTimes.keyAt(i); + sb.append(activity); + TimeStats times = pus.mLaunchTimes.valueAt(i); + sb.append(','); + sb.append(times.count); + for (int j=0; j<NUM_LAUNCH_TIME_BINS; j++) { + sb.append(","); + sb.append(times.times[j]); + } + sb.append('\n'); + } + final int NFDT = pus.mFullyDrawnTimes.size(); + for (int i=0; i<NFDT; i++) { + sb.append("A:"); + 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.times[j]); + } + sb.append('\n'); + } + + } else { + sb.append(" "); + sb.append(pkgName); + sb.append(": "); + sb.append(pus.mLaunchCount); + sb.append(" times, "); + sb.append(pus.mUsageTime); + sb.append(" ms"); + 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.count); + sb.append(" starts"); + int lastBin = 0; + for (int j=0; j<NUM_LAUNCH_TIME_BINS-1; j++) { + if (times.times[j] != 0) { + sb.append(", "); + sb.append(lastBin); + sb.append('-'); + sb.append(LAUNCH_TIME_BINS[j]); + sb.append("ms="); + sb.append(times.times[j]); + } + lastBin = LAUNCH_TIME_BINS[j]; + } + if (times.times[NUM_LAUNCH_TIME_BINS-1] != 0) { + sb.append(", "); + sb.append(">="); + sb.append(lastBin); + sb.append("ms="); + sb.append(times.times[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.times[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.times[j]); + } + lastBin = LAUNCH_TIME_BINS[j]; + } + if (times.times[NUM_LAUNCH_TIME_BINS-1] != 0) { + if (needComma) { + sb.append(", "); + } + sb.append(">="); + sb.append(lastBin); + sb.append("ms="); + sb.append(times.times[NUM_LAUNCH_TIME_BINS-1]); + } + 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; + } + + @Override + /* + * The data persisted to file is parsed and the stats are computed. + */ + 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 it 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(","); + for (String n : names) { + if (packages == null) { + packages = new HashSet<String>(); + } + 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); + } + } + +} |