/* * 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 mStats; // Maintains the last time any component was resumed, for all time. final private ArrayMap> 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 mLaunchTimes = new ArrayMap(); final ArrayMap mFullyDrawnTimes = new ArrayMap(); 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(); mLastResumeTimes = new ArrayMap>(); 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 != 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) { ArrayMap lrt = mLastResumeTimes.get(pkg); if (lrt == null) { lrt = new ArrayMap(); 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 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 fileList = new ArrayList(); 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 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 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 comp = mLastResumeTimes.valueAt(i); for (int j=0; j componentResumeTimes = mLastResumeTimes.get(pkgName); if (componentResumeTimes == null) { componentResumeTimes = new ArrayMap(); 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 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 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 packages) { List 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 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="); 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="); 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 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(); } 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); } } }