/* * 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.Parcel; import android.os.Process; import android.os.ServiceManager; import android.os.SystemClock; import android.util.Log; 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.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * 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 String TAG = "UsageStats"; static IUsageStats sService; private Context mContext; // structure used to maintain statistics since the last checkin. final private Map<String, PkgUsageStatsExtended> 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 mResumedPkg; private File mFile; //private File mBackupFile; private long mLastWriteRealTime; private int _FILE_WRITE_INTERVAL = 30*60*1000; //ms private static final String _PREFIX_DELIMIT="."; private String mFilePrefix; private Calendar mCal; private static final int _MAX_NUM_FILES = 10; private long mLastTime; private class PkgUsageStatsExtended { int mLaunchCount; long mUsageTime; long mPausedTime; long mResumedTime; PkgUsageStatsExtended() { mLaunchCount = 0; mUsageTime = 0; } void updateResume() { mLaunchCount ++; mResumedTime = SystemClock.elapsedRealtime(); } void updatePause() { mPausedTime = SystemClock.elapsedRealtime(); mUsageTime += (mPausedTime - mResumedTime); } void clear() { mLaunchCount = 0; mUsageTime = 0; } } UsageStatsService(String fileName) { mStats = new HashMap<String, PkgUsageStatsExtended>(); mStatsLock = new Object(); mFileLock = new Object(); mFilePrefix = fileName; mCal = Calendar.getInstance(); // Update current stats which are binned by date String uFileName = getCurrentDateStr(mFilePrefix); mFile = new File(uFileName); readStatsFromFile(); mLastWriteRealTime = SystemClock.elapsedRealtime(); mLastTime = new Date().getTime(); } /* * Utility method to convert date into string. */ private String getCurrentDateStr(String prefix) { mCal.setTime(new Date()); StringBuilder sb = new StringBuilder(); if (prefix != null) { sb.append(prefix); sb.append("."); } 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); sb.append(mCal.get(Calendar.YEAR)); 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) { Log.w(TAG,"Error : " + e + " reading data from file:" + newFile); } } } private void readStatsFLOCK(File file) throws IOException { Parcel in = getParcelForFile(file); while (in.dataAvail() > 0) { String pkgName = in.readString(); PkgUsageStatsExtended pus = new PkgUsageStatsExtended(); pus.mLaunchCount = in.readInt(); pus.mUsageTime = in.readLong(); synchronized (mStatsLock) { mStats.put(pkgName, pus); } } } private ArrayList<String> getUsageStatsFileListFLOCK() { File dir = getUsageFilesDir(); if (dir == null) { Log.w(TAG, "Couldnt find writable directory for usage stats file"); return null; } // Check if there are too many files in the system and delete older files String fList[] = dir.list(); if (fList == null) { return null; } File pre = new File(mFilePrefix); String filePrefix = pre.getName(); // file name followed by dot int prefixLen = filePrefix.length()+1; ArrayList<String> fileList = new ArrayList<String>(); for (String file : fList) { int index = file.indexOf(filePrefix); if (index == -1) { continue; } if (file.endsWith(".bak")) { continue; } fileList.add(file); } return fileList; } private File getUsageFilesDir() { if (mFilePrefix == null) { return null; } File pre = new File(mFilePrefix); return new File(pre.getParent()); } private void checkFileLimitFLOCK() { File dir = getUsageFilesDir(); if (dir == null) { Log.w(TAG, "Couldnt find writable directory for usage stats file"); return; } // 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(dir, fileName); Log.i(TAG, "Deleting file : "+fileName); file.delete(); } } private void writeStatsToFile() { synchronized (mFileLock) { long currTime = new Date().getTime(); boolean dayChanged = ((currTime - mLastTime) >= (24*60*60*1000)); long currRealTime = SystemClock.elapsedRealtime(); if (((currRealTime-mLastWriteRealTime) < _FILE_WRITE_INTERVAL) && (!dayChanged)) { // wait till the next update return; } // Get the most recent file String todayStr = getCurrentDateStr(mFilePrefix); // Copy current file to back up File backupFile = new File(mFile.getPath() + ".bak"); mFile.renameTo(backupFile); try { checkFileLimitFLOCK(); mFile.createNewFile(); // Write mStats to file writeStatsFLOCK(); mLastWriteRealTime = currRealTime; mLastTime = currTime; if (dayChanged) { // clear stats synchronized (mStats) { mStats.clear(); } mFile = new File(todayStr); } // Delete the backup file if (backupFile != null) { backupFile.delete(); } } catch (IOException e) { Log.w(TAG, "Failed writing stats to file:" + mFile); if (backupFile != null) { backupFile.renameTo(mFile); } } } } private void writeStatsFLOCK() throws IOException { FileOutputStream stream = new FileOutputStream(mFile); Parcel out = Parcel.obtain(); writeStatsToParcelFLOCK(out); stream.write(out.marshall()); out.recycle(); stream.flush(); stream.close(); } private void writeStatsToParcelFLOCK(Parcel out) { synchronized (mStatsLock) { Set<String> keys = mStats.keySet(); for (String key : keys) { PkgUsageStatsExtended pus = mStats.get(key); out.writeString(key); out.writeInt(pus.mLaunchCount); out.writeLong(pus.mUsageTime); } } } public void publish(Context context) { mContext = context; ServiceManager.addService(SERVICE_NAME, asBinder()); } 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; if ((componentName == null) || ((pkgName = componentName.getPackageName()) == null)) { return; } if ((mResumedPkg != null) && (mResumedPkg.equalsIgnoreCase(pkgName))) { // Moving across activities in same package. just return return; } if (localLOGV) Log.i(TAG, "started component:"+pkgName); synchronized (mStatsLock) { PkgUsageStatsExtended pus = mStats.get(pkgName); if (pus == null) { pus = new PkgUsageStatsExtended(); mStats.put(pkgName, pus); } pus.updateResume(); } mResumedPkg = pkgName; } public void notePauseComponent(ComponentName componentName) { enforceCallingPermission(); String pkgName; if ((componentName == null) || ((pkgName = componentName.getPackageName()) == null)) { return; } if ((mResumedPkg == null) || (!pkgName.equalsIgnoreCase(mResumedPkg))) { Log.w(TAG, "Something wrong here, Didn't expect "+pkgName+" to be paused"); return; } if (localLOGV) Log.i(TAG, "paused component:"+pkgName); synchronized (mStatsLock) { PkgUsageStatsExtended pus = mStats.get(pkgName); if (pus == null) { // Weird some error here Log.w(TAG, "No package stats for pkg:"+pkgName); return; } pus.updatePause(); } // Persist data to file writeStatsToFile(); } 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<String> 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, String[] args) { List<String> fileList = getUsageStatsFileListFLOCK(); if (fileList == null) { return; } final boolean isCheckinRequest = scanArgs(args, "-c"); Collections.sort(fileList); File usageFile = new File(mFilePrefix); String dirName = usageFile.getParent(); File dir = new File(dirName); String filePrefix = usageFile.getName(); // file name followed by dot int prefixLen = filePrefix.length()+1; String todayStr = getCurrentDateStr(null); for (String file : fileList) { File dFile = new File(dir, file); String dateStr = file.substring(prefixLen); try { Parcel in = getParcelForFile(dFile); collectDumpInfoFromParcelFLOCK(in, pw, dateStr, isCheckinRequest); if (isCheckinRequest && !todayStr.equalsIgnoreCase(dateStr)) { // Delete old file after collecting info only for checkin requests dFile.delete(); } } catch (FileNotFoundException e) { Log.w(TAG, "Failed with "+e+" when collecting dump info from file : " + file); return; } catch (IOException e) { Log.w(TAG, "Failed with "+e+" when collecting dump info from file : "+file); } } } private void collectDumpInfoFromParcelFLOCK(Parcel in, PrintWriter pw, String date, boolean isCheckinRequest) { StringBuilder sb = new StringBuilder(); sb.append("Date:"); sb.append(date); boolean first = true; while (in.dataAvail() > 0) { String pkgName = in.readString(); int launchCount = in.readInt(); long usageTime = in.readLong(); if (isCheckinRequest) { if (!first) { sb.append(","); } sb.append(pkgName); sb.append(","); sb.append(launchCount); sb.append(","); sb.append(usageTime); sb.append("ms"); } else { if (first) { sb.append("\n"); } sb.append("pkg="); sb.append(pkgName); sb.append(", launchCount="); sb.append(launchCount); sb.append(", usageTime="); sb.append(usageTime); sb.append(" ms\n"); } first = false; } 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; } @Override /* * The data persisted to file is parsed and the stats are computed. */ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { synchronized (mFileLock) { collectDumpInfoFLOCK(pw, args); } } }