New APIs let you indicate what user(s) to monitor, and tell you what user is changing when receiving a callback. Fix package manager to only deliver package brpadcasts to the running users. (This isn't really a change in behavior, since the activity manager would not deliver to stopped users anyway). Make sure all broadcasts that package monitor receives also include user information for it to use. Update wallpaper service to (hopefully) now Really Correctly monitor package changes per user. Change-Id: Idd952dd274abcaeab452277d9160d1ae62919aa0
1083 lines
39 KiB
Java
1083 lines
39 KiB
Java
/*
|
|
* 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.content.ComponentName;
|
|
import android.content.Context;
|
|
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.ServiceManager;
|
|
import android.os.SystemClock;
|
|
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 = 1007;
|
|
|
|
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 Map<String, PkgUsageStatsExtended> mStats;
|
|
|
|
// Maintains the last time any component was resumed, for all time.
|
|
final private Map<String, Map<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 HashMap<String, TimeStats> mLaunchTimes
|
|
= new HashMap<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 numTimeStats = in.readInt();
|
|
if (localLOGV) Slog.v(TAG, "Reading comps: " + numTimeStats);
|
|
for (int i=0; i<numTimeStats; i++) {
|
|
String comp = in.readString();
|
|
if (localLOGV) Slog.v(TAG, "Component: " + comp);
|
|
TimeStats times = new TimeStats(in);
|
|
mLaunchTimes.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 writeToParcel(Parcel out) {
|
|
out.writeInt(mLaunchCount);
|
|
out.writeLong(mUsageTime);
|
|
final int numTimeStats = mLaunchTimes.size();
|
|
out.writeInt(numTimeStats);
|
|
if (numTimeStats > 0) {
|
|
for (Map.Entry<String, TimeStats> 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<String, PkgUsageStatsExtended>();
|
|
mLastResumeTimes = new HashMap<String, Map<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) {
|
|
Map<String, Long> lrt = mLastResumeTimes.get(pkg);
|
|
if (lrt == null) {
|
|
lrt = new HashMap<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) {
|
|
// Copy and clear the last resume times map, then copy back stats
|
|
// for all installed packages.
|
|
Map<String, Map<String, Long>> tmpLastResumeTimes =
|
|
new HashMap<String, Map<String, Long>>(mLastResumeTimes);
|
|
mLastResumeTimes.clear();
|
|
for (PackageInfo info : mContext.getPackageManager().getInstalledPackages(0)) {
|
|
if (tmpLastResumeTimes.containsKey(info.packageName)) {
|
|
mLastResumeTimes.put(info.packageName, tmpLastResumeTimes.get(info.packageName));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 (Map.Entry<String, Map<String, Long>> pkgEntry : mLastResumeTimes.entrySet()) {
|
|
out.startTag(null, "pkg");
|
|
out.attribute(null, "name", pkgEntry.getKey());
|
|
for (Map.Entry<String, Long> compEntry : pkgEntry.getValue().entrySet()) {
|
|
out.startTag(null, "comp");
|
|
out.attribute(null, "name", compEntry.getKey());
|
|
out.attribute(null, "lrt", compEntry.getValue().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);
|
|
}
|
|
|
|
Map<String, Long> componentResumeTimes = mLastResumeTimes.get(pkgName);
|
|
if (componentResumeTimes == null) {
|
|
componentResumeTimes = new HashMap<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 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];
|
|
int i = 0;
|
|
for (Map.Entry<String, Map<String, Long>> entry : mLastResumeTimes.entrySet()) {
|
|
String pkg = entry.getKey();
|
|
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, entry.getValue());
|
|
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());
|
|
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 NC = pus.mLaunchTimes.size();
|
|
if (NC > 0) {
|
|
for (Map.Entry<String, TimeStats> 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<NUM_LAUNCH_TIME_BINS; i++) {
|
|
sb.append(",");
|
|
sb.append(times.times[i]);
|
|
}
|
|
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 NC = pus.mLaunchTimes.size();
|
|
if (NC > 0) {
|
|
for (Map.Entry<String, TimeStats> 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<NUM_LAUNCH_TIME_BINS-1; i++) {
|
|
if (times.times[i] != 0) {
|
|
sb.append(", ");
|
|
sb.append(lastBin);
|
|
sb.append('-');
|
|
sb.append(LAUNCH_TIME_BINS[i]);
|
|
sb.append("ms=");
|
|
sb.append(times.times[i]);
|
|
}
|
|
lastBin = LAUNCH_TIME_BINS[i];
|
|
}
|
|
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');
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
}
|