/* * 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 com.android.internal.app.IUsageStats; import android.content.ComponentName; import android.content.Context; import android.os.Binder; import android.os.IBinder; import com.android.internal.os.PkgUsageStats; import android.os.FileUtils; import android.os.Parcel; import android.os.Process; import android.os.ServiceManager; import android.os.SystemClock; import android.util.Slog; 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 = 1005; private static final int CHECKIN_VERSION = 4; private static final String FILE_PREFIX = "usage-"; 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 Map mStats; // 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 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 HashMap(); 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 N = in.readInt(); if (localLOGV) Slog.v(TAG, "Reading comps: " + N); for (int i=0; i 0) { for (Map.Entry ent : mLaunchTimes.entrySet()) { out.writeString(ent.getKey()); TimeStats times = ent.getValue(); times.writeToParcel(out); } } } void clear() { mLaunchTimes.clear(); mLaunchCount = 0; mUsageTime = 0; } } UsageStatsService(String dir) { mStats = new HashMap(); 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); readStatsFromFile(); 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 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. */ private void writeStatsToFile(final boolean force) { 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); } 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(); } // 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); Parcel out = null; try { out = Parcel.obtain(); writeStatsToParcelFLOCK(out); stream.write(out.marshall()); stream.flush(); } finally { if (out != null) { out.recycle(); } 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); } } } public void publish(Context context) { mContext = context; ServiceManager.addService(SERVICE_NAME, asBinder()); } public void shutdown() { Slog.i(TAG, "Writing usage stats before shutdown..."); writeStatsToFile(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(!samePackage); if (!sameComp) { pus.addLaunchCount(mLastResumedComp); } } } 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); } 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); synchronized (mStatsLock) { PkgUsageStatsExtended pus = mStats.get(pkgName); if (pus != null) { pus.addLaunchTime(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); if (pus == null) { return null; } return new PkgUsageStats(pkgName, pus.mLaunchCount, pus.mUsageTime); } } public PkgUsageStats[] getAllPkgUsageStats() { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.PACKAGE_USAGE_STATS, null); synchronized (mStatsLock) { Set keys = mStats.keySet(); int size = keys.size(); if (size <= 0) { return null; } PkgUsageStats retArr[] = new PkgUsageStats[size]; int i = 0; for (String key: keys) { PkgUsageStatsExtended pus = mStats.get(key); retArr[i] = new PkgUsageStats(key, pus.mLaunchCount, pus.mUsageTime); 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 packages) { List fileList = getUsageStatsFileListFLOCK(); if (fileList == null) { return; } Collections.sort(fileList); mFileLeaf = getCurrentDateStr(FILE_PREFIX); 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()); Parcel in = null; try { 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); } finally { if (in != null) { in.recycle(); } } } } 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 NC = pus.mLaunchTimes.size(); if (NC > 0) { for (Map.Entry ent : pus.mLaunchTimes.entrySet()) { sb.append("A:"); String activity = ent.getKey(); if (activity.startsWith(pkgName)) { sb.append('*'); sb.append(activity.substring( pkgName.length(), activity.length())); } else { sb.append(activity); } TimeStats times = ent.getValue(); sb.append(','); sb.append(times.count); for (int i=0; i 0) { for (Map.Entry ent : pus.mLaunchTimes.entrySet()) { sb.append(" "); sb.append(ent.getKey()); TimeStats times = ent.getValue(); sb.append(": "); sb.append(times.count); sb.append(" starts"); int lastBin = 0; 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); } } }