blob: e96d8b1ce91c8e59e29d5c19650f3eb820ada0eb [file] [log] [blame]
/*
* 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);
}
}
}