1/*
2 * Copyright (C) 2006-2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.am;
18
19import android.app.AppGlobals;
20import android.content.ComponentName;
21import android.content.Context;
22import android.content.pm.IPackageManager;
23import android.content.pm.PackageInfo;
24import android.content.pm.PackageManager;
25import android.os.Binder;
26import android.os.IBinder;
27import android.os.FileUtils;
28import android.os.Parcel;
29import android.os.Process;
30import android.os.RemoteException;
31import android.os.ServiceManager;
32import android.os.SystemClock;
33import android.util.ArrayMap;
34import android.util.AtomicFile;
35import android.util.Slog;
36import android.util.Xml;
37
38import com.android.internal.app.IUsageStats;
39import com.android.internal.content.PackageMonitor;
40import com.android.internal.os.PkgUsageStats;
41import com.android.internal.util.FastXmlSerializer;
42
43import org.xmlpull.v1.XmlPullParser;
44import org.xmlpull.v1.XmlPullParserException;
45import org.xmlpull.v1.XmlSerializer;
46
47import java.io.File;
48import java.io.FileDescriptor;
49import java.io.FileInputStream;
50import java.io.FileNotFoundException;
51import java.io.FileOutputStream;
52import java.io.IOException;
53import java.io.PrintWriter;
54import java.util.ArrayList;
55import java.util.Calendar;
56import java.util.Collections;
57import java.util.HashMap;
58import java.util.HashSet;
59import java.util.List;
60import java.util.Map;
61import java.util.Set;
62import java.util.TimeZone;
63import java.util.concurrent.atomic.AtomicBoolean;
64import java.util.concurrent.atomic.AtomicInteger;
65import java.util.concurrent.atomic.AtomicLong;
66
67/**
68 * This service collects the statistics associated with usage
69 * of various components, like when a particular package is launched or
70 * paused and aggregates events like number of time a component is launched
71 * total duration of a component launch.
72 */
73public final class UsageStatsService extends IUsageStats.Stub {
74    public static final String SERVICE_NAME = "usagestats";
75    private static final boolean localLOGV = false;
76    private static final boolean REPORT_UNEXPECTED = false;
77    private static final String TAG = "UsageStats";
78
79    // Current on-disk Parcel version
80    private static final int VERSION = 1008;
81
82    private static final int CHECKIN_VERSION = 4;
83
84    private static final String FILE_PREFIX = "usage-";
85
86    private static final String FILE_HISTORY = FILE_PREFIX + "history.xml";
87
88    private static final int FILE_WRITE_INTERVAL = 30*60*1000; //ms
89
90    private static final int MAX_NUM_FILES = 5;
91
92    private static final int NUM_LAUNCH_TIME_BINS = 10;
93    private static final int[] LAUNCH_TIME_BINS = {
94        250, 500, 750, 1000, 1500, 2000, 3000, 4000, 5000
95    };
96
97    static IUsageStats sService;
98    private Context mContext;
99    // structure used to maintain statistics since the last checkin.
100    final private ArrayMap<String, PkgUsageStatsExtended> mStats;
101
102    // Maintains the last time any component was resumed, for all time.
103    final private ArrayMap<String, ArrayMap<String, Long>> mLastResumeTimes;
104
105    // To remove last-resume time stats when a pacakge is removed.
106    private PackageMonitor mPackageMonitor;
107
108    // Lock to update package stats. Methods suffixed by SLOCK should invoked with
109    // this lock held
110    final Object mStatsLock;
111    // Lock to write to file. Methods suffixed by FLOCK should invoked with
112    // this lock held.
113    final Object mFileLock;
114    // Order of locks is mFileLock followed by mStatsLock to avoid deadlocks
115    private String mLastResumedPkg;
116    private String mLastResumedComp;
117    private boolean mIsResumed;
118    private File mFile;
119    private AtomicFile mHistoryFile;
120    private String mFileLeaf;
121    private File mDir;
122
123    private Calendar mCal; // guarded by itself
124
125    private final AtomicInteger mLastWriteDay = new AtomicInteger(-1);
126    private final AtomicLong mLastWriteElapsedTime = new AtomicLong(0);
127    private final AtomicBoolean mUnforcedDiskWriteRunning = new AtomicBoolean(false);
128
129    static class TimeStats {
130        int count;
131        int[] times = new int[NUM_LAUNCH_TIME_BINS];
132
133        TimeStats() {
134        }
135
136        void incCount() {
137            count++;
138        }
139
140        void add(int val) {
141            final int[] bins = LAUNCH_TIME_BINS;
142            for (int i=0; i<NUM_LAUNCH_TIME_BINS-1; i++) {
143                if (val < bins[i]) {
144                    times[i]++;
145                    return;
146                }
147            }
148            times[NUM_LAUNCH_TIME_BINS-1]++;
149        }
150
151        TimeStats(Parcel in) {
152            count = in.readInt();
153            final int[] localTimes = times;
154            for (int i=0; i<NUM_LAUNCH_TIME_BINS; i++) {
155                localTimes[i] = in.readInt();
156            }
157        }
158
159        void writeToParcel(Parcel out) {
160            out.writeInt(count);
161            final int[] localTimes = times;
162            for (int i=0; i<NUM_LAUNCH_TIME_BINS; i++) {
163                out.writeInt(localTimes[i]);
164            }
165        }
166    }
167
168    private class PkgUsageStatsExtended {
169        final ArrayMap<String, TimeStats> mLaunchTimes
170                = new ArrayMap<String, TimeStats>();
171        final ArrayMap<String, TimeStats> mFullyDrawnTimes
172                = new ArrayMap<String, TimeStats>();
173        int mLaunchCount;
174        long mUsageTime;
175        long mPausedTime;
176        long mResumedTime;
177
178        PkgUsageStatsExtended() {
179            mLaunchCount = 0;
180            mUsageTime = 0;
181        }
182
183        PkgUsageStatsExtended(Parcel in) {
184            mLaunchCount = in.readInt();
185            mUsageTime = in.readLong();
186            if (localLOGV) Slog.v(TAG, "Launch count: " + mLaunchCount
187                    + ", Usage time:" + mUsageTime);
188
189            final int numLaunchTimeStats = in.readInt();
190            if (localLOGV) Slog.v(TAG, "Reading launch times: " + numLaunchTimeStats);
191            mLaunchTimes.ensureCapacity(numLaunchTimeStats);
192            for (int i=0; i<numLaunchTimeStats; i++) {
193                String comp = in.readString();
194                if (localLOGV) Slog.v(TAG, "Component: " + comp);
195                TimeStats times = new TimeStats(in);
196                mLaunchTimes.put(comp, times);
197            }
198
199            final int numFullyDrawnTimeStats = in.readInt();
200            if (localLOGV) Slog.v(TAG, "Reading fully drawn times: " + numFullyDrawnTimeStats);
201            mFullyDrawnTimes.ensureCapacity(numFullyDrawnTimeStats);
202            for (int i=0; i<numFullyDrawnTimeStats; i++) {
203                String comp = in.readString();
204                if (localLOGV) Slog.v(TAG, "Component: " + comp);
205                TimeStats times = new TimeStats(in);
206                mFullyDrawnTimes.put(comp, times);
207            }
208        }
209
210        void updateResume(String comp, boolean launched) {
211            if (launched) {
212                mLaunchCount ++;
213            }
214            mResumedTime = SystemClock.elapsedRealtime();
215        }
216
217        void updatePause() {
218            mPausedTime =  SystemClock.elapsedRealtime();
219            mUsageTime += (mPausedTime - mResumedTime);
220        }
221
222        void addLaunchCount(String comp) {
223            TimeStats times = mLaunchTimes.get(comp);
224            if (times == null) {
225                times = new TimeStats();
226                mLaunchTimes.put(comp, times);
227            }
228            times.incCount();
229        }
230
231        void addLaunchTime(String comp, int millis) {
232            TimeStats times = mLaunchTimes.get(comp);
233            if (times == null) {
234                times = new TimeStats();
235                mLaunchTimes.put(comp, times);
236            }
237            times.add(millis);
238        }
239
240        void addFullyDrawnTime(String comp, int millis) {
241            TimeStats times = mFullyDrawnTimes.get(comp);
242            if (times == null) {
243                times = new TimeStats();
244                mFullyDrawnTimes.put(comp, times);
245            }
246            times.add(millis);
247        }
248
249        void writeToParcel(Parcel out) {
250            out.writeInt(mLaunchCount);
251            out.writeLong(mUsageTime);
252            final int numLaunchTimeStats = mLaunchTimes.size();
253            out.writeInt(numLaunchTimeStats);
254            for (int i=0; i<numLaunchTimeStats; i++) {
255                out.writeString(mLaunchTimes.keyAt(i));
256                mLaunchTimes.valueAt(i).writeToParcel(out);
257            }
258            final int numFullyDrawnTimeStats = mFullyDrawnTimes.size();
259            out.writeInt(numFullyDrawnTimeStats);
260            for (int i=0; i<numFullyDrawnTimeStats; i++) {
261                out.writeString(mFullyDrawnTimes.keyAt(i));
262                mFullyDrawnTimes.valueAt(i).writeToParcel(out);
263            }
264        }
265
266        void clear() {
267            mLaunchTimes.clear();
268            mFullyDrawnTimes.clear();
269            mLaunchCount = 0;
270            mUsageTime = 0;
271        }
272    }
273
274    UsageStatsService(String dir) {
275        mStats = new ArrayMap<String, PkgUsageStatsExtended>();
276        mLastResumeTimes = new ArrayMap<String, ArrayMap<String, Long>>();
277        mStatsLock = new Object();
278        mFileLock = new Object();
279        mDir = new File(dir);
280        mCal = Calendar.getInstance(TimeZone.getTimeZone("GMT+0"));
281
282        mDir.mkdir();
283
284        // Remove any old usage files from previous versions.
285        File parentDir = mDir.getParentFile();
286        String fList[] = parentDir.list();
287        if (fList != null) {
288            String prefix = mDir.getName() + ".";
289            int i = fList.length;
290            while (i > 0) {
291                i--;
292                if (fList[i].startsWith(prefix)) {
293                    Slog.i(TAG, "Deleting old usage file: " + fList[i]);
294                    (new File(parentDir, fList[i])).delete();
295                }
296            }
297        }
298
299        // Update current stats which are binned by date
300        mFileLeaf = getCurrentDateStr(FILE_PREFIX);
301        mFile = new File(mDir, mFileLeaf);
302        mHistoryFile = new AtomicFile(new File(mDir, FILE_HISTORY));
303        readStatsFromFile();
304        readHistoryStatsFromFile();
305        mLastWriteElapsedTime.set(SystemClock.elapsedRealtime());
306        // mCal was set by getCurrentDateStr(), want to use that same time.
307        mLastWriteDay.set(mCal.get(Calendar.DAY_OF_YEAR));
308    }
309
310    /*
311     * Utility method to convert date into string.
312     */
313    private String getCurrentDateStr(String prefix) {
314        StringBuilder sb = new StringBuilder();
315        synchronized (mCal) {
316            mCal.setTimeInMillis(System.currentTimeMillis());
317            if (prefix != null) {
318                sb.append(prefix);
319            }
320            sb.append(mCal.get(Calendar.YEAR));
321            int mm = mCal.get(Calendar.MONTH) - Calendar.JANUARY +1;
322            if (mm < 10) {
323                sb.append("0");
324            }
325            sb.append(mm);
326            int dd = mCal.get(Calendar.DAY_OF_MONTH);
327            if (dd < 10) {
328                sb.append("0");
329            }
330            sb.append(dd);
331        }
332        return sb.toString();
333    }
334
335    private Parcel getParcelForFile(File file) throws IOException {
336        FileInputStream stream = new FileInputStream(file);
337        byte[] raw = readFully(stream);
338        Parcel in = Parcel.obtain();
339        in.unmarshall(raw, 0, raw.length);
340        in.setDataPosition(0);
341        stream.close();
342        return in;
343    }
344
345    private void readStatsFromFile() {
346        File newFile = mFile;
347        synchronized (mFileLock) {
348            try {
349                if (newFile.exists()) {
350                    readStatsFLOCK(newFile);
351                } else {
352                    // Check for file limit before creating a new file
353                    checkFileLimitFLOCK();
354                    newFile.createNewFile();
355                }
356            } catch (IOException e) {
357                Slog.w(TAG,"Error : " + e + " reading data from file:" + newFile);
358            }
359        }
360    }
361
362    private void readStatsFLOCK(File file) throws IOException {
363        Parcel in = getParcelForFile(file);
364        int vers = in.readInt();
365        if (vers != VERSION) {
366            Slog.w(TAG, "Usage stats version changed; dropping");
367            return;
368        }
369        int N = in.readInt();
370        while (N > 0) {
371            N--;
372            String pkgName = in.readString();
373            if (pkgName == null) {
374                break;
375            }
376            if (localLOGV) Slog.v(TAG, "Reading package #" + N + ": " + pkgName);
377            PkgUsageStatsExtended pus = new PkgUsageStatsExtended(in);
378            synchronized (mStatsLock) {
379                mStats.put(pkgName, pus);
380            }
381        }
382    }
383
384    private void readHistoryStatsFromFile() {
385        synchronized (mFileLock) {
386            if (mHistoryFile.getBaseFile().exists()) {
387                readHistoryStatsFLOCK(mHistoryFile);
388            }
389        }
390    }
391
392    private void readHistoryStatsFLOCK(AtomicFile file) {
393        FileInputStream fis = null;
394        try {
395            fis = mHistoryFile.openRead();
396            XmlPullParser parser = Xml.newPullParser();
397            parser.setInput(fis, null);
398            int eventType = parser.getEventType();
399            while (eventType != XmlPullParser.START_TAG) {
400                eventType = parser.next();
401            }
402            String tagName = parser.getName();
403            if ("usage-history".equals(tagName)) {
404                String pkg = null;
405                do {
406                    eventType = parser.next();
407                    if (eventType == XmlPullParser.START_TAG) {
408                        tagName = parser.getName();
409                        int depth = parser.getDepth();
410                        if ("pkg".equals(tagName) && depth == 2) {
411                            pkg = parser.getAttributeValue(null, "name");
412                        } else if ("comp".equals(tagName) && depth == 3 && pkg != null) {
413                            String comp = parser.getAttributeValue(null, "name");
414                            String lastResumeTimeStr = parser.getAttributeValue(null, "lrt");
415                            if (comp != null && lastResumeTimeStr != null) {
416                                try {
417                                    long lastResumeTime = Long.parseLong(lastResumeTimeStr);
418                                    synchronized (mStatsLock) {
419                                        ArrayMap<String, Long> lrt = mLastResumeTimes.get(pkg);
420                                        if (lrt == null) {
421                                            lrt = new ArrayMap<String, Long>();
422                                            mLastResumeTimes.put(pkg, lrt);
423                                        }
424                                        lrt.put(comp, lastResumeTime);
425                                    }
426                                } catch (NumberFormatException e) {
427                                }
428                            }
429                        }
430                    } else if (eventType == XmlPullParser.END_TAG) {
431                        if ("pkg".equals(parser.getName())) {
432                            pkg = null;
433                        }
434                    }
435                } while (eventType != XmlPullParser.END_DOCUMENT);
436            }
437        } catch (XmlPullParserException e) {
438            Slog.w(TAG,"Error reading history stats: " + e);
439        } catch (IOException e) {
440            Slog.w(TAG,"Error reading history stats: " + e);
441        } finally {
442            if (fis != null) {
443                try {
444                    fis.close();
445                } catch (IOException e) {
446                }
447            }
448        }
449    }
450
451    private ArrayList<String> getUsageStatsFileListFLOCK() {
452        // Check if there are too many files in the system and delete older files
453        String fList[] = mDir.list();
454        if (fList == null) {
455            return null;
456        }
457        ArrayList<String> fileList = new ArrayList<String>();
458        for (String file : fList) {
459            if (!file.startsWith(FILE_PREFIX)) {
460                continue;
461            }
462            if (file.endsWith(".bak")) {
463                (new File(mDir, file)).delete();
464                continue;
465            }
466            fileList.add(file);
467        }
468        return fileList;
469    }
470
471    private void checkFileLimitFLOCK() {
472        // Get all usage stats output files
473        ArrayList<String> fileList = getUsageStatsFileListFLOCK();
474        if (fileList == null) {
475            // Strange but we dont have to delete any thing
476            return;
477        }
478        int count = fileList.size();
479        if (count <= MAX_NUM_FILES) {
480            return;
481        }
482        // Sort files
483        Collections.sort(fileList);
484        count -= MAX_NUM_FILES;
485        // Delete older files
486        for (int i = 0; i < count; i++) {
487            String fileName = fileList.get(i);
488            File file = new File(mDir, fileName);
489            Slog.i(TAG, "Deleting usage file : " + fileName);
490            file.delete();
491        }
492    }
493
494    /**
495     * Conditionally start up a disk write if it's been awhile, or the
496     * day has rolled over.
497     *
498     * This is called indirectly from user-facing actions (when
499     * 'force' is false) so it tries to be quick, without writing to
500     * disk directly or acquiring heavy locks.
501     *
502     * @params force  do an unconditional, synchronous stats flush
503     *                to disk on the current thread.
504     * @params forceWriteHistoryStats Force writing of historical stats.
505     */
506    private void writeStatsToFile(final boolean force, final boolean forceWriteHistoryStats) {
507        int curDay;
508        synchronized (mCal) {
509            mCal.setTimeInMillis(System.currentTimeMillis());
510            curDay = mCal.get(Calendar.DAY_OF_YEAR);
511        }
512        final boolean dayChanged = curDay != mLastWriteDay.get();
513
514        // Determine if the day changed...  note that this will be wrong
515        // if the year has changed but we are in the same day of year...
516        // we can probably live with this.
517        final long currElapsedTime = SystemClock.elapsedRealtime();
518
519        // Fast common path, without taking the often-contentious
520        // mFileLock.
521        if (!force) {
522            if (!dayChanged &&
523                (currElapsedTime - mLastWriteElapsedTime.get()) < FILE_WRITE_INTERVAL) {
524                // wait till the next update
525                return;
526            }
527            if (mUnforcedDiskWriteRunning.compareAndSet(false, true)) {
528                new Thread("UsageStatsService_DiskWriter") {
529                    public void run() {
530                        try {
531                            if (localLOGV) Slog.d(TAG, "Disk writer thread starting.");
532                            writeStatsToFile(true, false);
533                        } finally {
534                            mUnforcedDiskWriteRunning.set(false);
535                            if (localLOGV) Slog.d(TAG, "Disk writer thread ending.");
536                        }
537                    }
538                }.start();
539            }
540            return;
541        }
542
543        synchronized (mFileLock) {
544            // Get the most recent file
545            mFileLeaf = getCurrentDateStr(FILE_PREFIX);
546            // Copy current file to back up
547            File backupFile = null;
548            if (mFile != null && mFile.exists()) {
549                backupFile = new File(mFile.getPath() + ".bak");
550                if (!backupFile.exists()) {
551                    if (!mFile.renameTo(backupFile)) {
552                        Slog.w(TAG, "Failed to persist new stats");
553                        return;
554                    }
555                } else {
556                    mFile.delete();
557                }
558            }
559
560            try {
561                // Write mStats to file
562                writeStatsFLOCK(mFile);
563                mLastWriteElapsedTime.set(currElapsedTime);
564                if (dayChanged) {
565                    mLastWriteDay.set(curDay);
566                    // clear stats
567                    synchronized (mStats) {
568                        mStats.clear();
569                    }
570                    mFile = new File(mDir, mFileLeaf);
571                    checkFileLimitFLOCK();
572                }
573
574                if (dayChanged || forceWriteHistoryStats) {
575                    // Write history stats daily, or when forced (due to shutdown).
576                    writeHistoryStatsFLOCK(mHistoryFile);
577                }
578
579                // Delete the backup file
580                if (backupFile != null) {
581                    backupFile.delete();
582                }
583            } catch (IOException e) {
584                Slog.w(TAG, "Failed writing stats to file:" + mFile);
585                if (backupFile != null) {
586                    mFile.delete();
587                    backupFile.renameTo(mFile);
588                }
589            }
590        }
591        if (localLOGV) Slog.d(TAG, "Dumped usage stats.");
592    }
593
594    private void writeStatsFLOCK(File file) throws IOException {
595        FileOutputStream stream = new FileOutputStream(file);
596        try {
597            Parcel out = Parcel.obtain();
598            writeStatsToParcelFLOCK(out);
599            stream.write(out.marshall());
600            out.recycle();
601            stream.flush();
602        } finally {
603            FileUtils.sync(stream);
604            stream.close();
605        }
606    }
607
608    private void writeStatsToParcelFLOCK(Parcel out) {
609        synchronized (mStatsLock) {
610            out.writeInt(VERSION);
611            Set<String> keys = mStats.keySet();
612            out.writeInt(keys.size());
613            for (String key : keys) {
614                PkgUsageStatsExtended pus = mStats.get(key);
615                out.writeString(key);
616                pus.writeToParcel(out);
617            }
618        }
619    }
620
621    /** Filter out stats for any packages which aren't present anymore. */
622    private void filterHistoryStats() {
623        synchronized (mStatsLock) {
624            IPackageManager pm = AppGlobals.getPackageManager();
625            for (int i=0; i<mLastResumeTimes.size(); i++) {
626                String pkg = mLastResumeTimes.keyAt(i);
627                try {
628                    if (pm.getPackageUid(pkg, 0) < 0) {
629                        mLastResumeTimes.removeAt(i);
630                        i--;
631                    }
632                } catch (RemoteException e) {
633                }
634            }
635        }
636    }
637
638    private void writeHistoryStatsFLOCK(AtomicFile historyFile) {
639        FileOutputStream fos = null;
640        try {
641            fos = historyFile.startWrite();
642            XmlSerializer out = new FastXmlSerializer();
643            out.setOutput(fos, "utf-8");
644            out.startDocument(null, true);
645            out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
646            out.startTag(null, "usage-history");
647            synchronized (mStatsLock) {
648                for (int i=0; i<mLastResumeTimes.size(); i++) {
649                    out.startTag(null, "pkg");
650                    out.attribute(null, "name", mLastResumeTimes.keyAt(i));
651                    ArrayMap<String, Long> comp = mLastResumeTimes.valueAt(i);
652                    for (int j=0; j<comp.size(); j++) {
653                        out.startTag(null, "comp");
654                        out.attribute(null, "name", comp.keyAt(j));
655                        out.attribute(null, "lrt", comp.valueAt(j).toString());
656                        out.endTag(null, "comp");
657                    }
658                    out.endTag(null, "pkg");
659                }
660            }
661            out.endTag(null, "usage-history");
662            out.endDocument();
663
664            historyFile.finishWrite(fos);
665        } catch (IOException e) {
666            Slog.w(TAG,"Error writing history stats" + e);
667            if (fos != null) {
668                historyFile.failWrite(fos);
669            }
670        }
671    }
672
673    public void publish(Context context) {
674        mContext = context;
675        ServiceManager.addService(SERVICE_NAME, asBinder());
676    }
677
678    /**
679     * Start watching packages to remove stats when a package is uninstalled.
680     * May only be called when the package manager is ready.
681     */
682    public void monitorPackages() {
683        mPackageMonitor = new PackageMonitor() {
684            @Override
685            public void onPackageRemovedAllUsers(String packageName, int uid) {
686                synchronized (mStatsLock) {
687                    mLastResumeTimes.remove(packageName);
688                }
689            }
690        };
691        mPackageMonitor.register(mContext, null, true);
692        filterHistoryStats();
693    }
694
695    public void shutdown() {
696        if (mPackageMonitor != null) {
697            mPackageMonitor.unregister();
698        }
699        Slog.i(TAG, "Writing usage stats before shutdown...");
700        writeStatsToFile(true, true);
701    }
702
703    public static IUsageStats getService() {
704        if (sService != null) {
705            return sService;
706        }
707        IBinder b = ServiceManager.getService(SERVICE_NAME);
708        sService = asInterface(b);
709        return sService;
710    }
711
712    public void noteResumeComponent(ComponentName componentName) {
713        enforceCallingPermission();
714        String pkgName;
715        synchronized (mStatsLock) {
716            if ((componentName == null) ||
717                    ((pkgName = componentName.getPackageName()) == null)) {
718                return;
719            }
720
721            final boolean samePackage = pkgName.equals(mLastResumedPkg);
722            if (mIsResumed) {
723                if (mLastResumedPkg != null) {
724                    // We last resumed some other package...  just pause it now
725                    // to recover.
726                    if (REPORT_UNEXPECTED) Slog.i(TAG, "Unexpected resume of " + pkgName
727                            + " while already resumed in " + mLastResumedPkg);
728                    PkgUsageStatsExtended pus = mStats.get(mLastResumedPkg);
729                    if (pus != null) {
730                        pus.updatePause();
731                    }
732                }
733            }
734
735            final boolean sameComp = samePackage
736                    && componentName.getClassName().equals(mLastResumedComp);
737
738            mIsResumed = true;
739            mLastResumedPkg = pkgName;
740            mLastResumedComp = componentName.getClassName();
741
742            if (localLOGV) Slog.i(TAG, "started component:" + pkgName);
743            PkgUsageStatsExtended pus = mStats.get(pkgName);
744            if (pus == null) {
745                pus = new PkgUsageStatsExtended();
746                mStats.put(pkgName, pus);
747            }
748            pus.updateResume(mLastResumedComp, !samePackage);
749            if (!sameComp) {
750                pus.addLaunchCount(mLastResumedComp);
751            }
752
753            ArrayMap<String, Long> componentResumeTimes = mLastResumeTimes.get(pkgName);
754            if (componentResumeTimes == null) {
755                componentResumeTimes = new ArrayMap<String, Long>();
756                mLastResumeTimes.put(pkgName, componentResumeTimes);
757            }
758            componentResumeTimes.put(mLastResumedComp, System.currentTimeMillis());
759        }
760    }
761
762    public void notePauseComponent(ComponentName componentName) {
763        enforceCallingPermission();
764
765        synchronized (mStatsLock) {
766            String pkgName;
767            if ((componentName == null) ||
768                    ((pkgName = componentName.getPackageName()) == null)) {
769                return;
770            }
771            if (!mIsResumed) {
772                if (REPORT_UNEXPECTED) Slog.i(TAG, "Something wrong here, didn't expect "
773                        + pkgName + " to be paused");
774                return;
775            }
776            mIsResumed = false;
777
778            if (localLOGV) Slog.i(TAG, "paused component:"+pkgName);
779
780            PkgUsageStatsExtended pus = mStats.get(pkgName);
781            if (pus == null) {
782                // Weird some error here
783                Slog.i(TAG, "No package stats for pkg:"+pkgName);
784                return;
785            }
786            pus.updatePause();
787        }
788
789        // Persist current data to file if needed.
790        writeStatsToFile(false, false);
791    }
792
793    public void noteLaunchTime(ComponentName componentName, int millis) {
794        enforceCallingPermission();
795        String pkgName;
796        if ((componentName == null) ||
797                ((pkgName = componentName.getPackageName()) == null)) {
798            return;
799        }
800
801        // Persist current data to file if needed.
802        writeStatsToFile(false, false);
803
804        synchronized (mStatsLock) {
805            PkgUsageStatsExtended pus = mStats.get(pkgName);
806            if (pus != null) {
807                pus.addLaunchTime(componentName.getClassName(), millis);
808            }
809        }
810    }
811
812    public void noteFullyDrawnTime(ComponentName componentName, int millis) {
813        enforceCallingPermission();
814        String pkgName;
815        if ((componentName == null) ||
816                ((pkgName = componentName.getPackageName()) == null)) {
817            return;
818        }
819
820        // Persist current data to file if needed.
821        writeStatsToFile(false, false);
822
823        synchronized (mStatsLock) {
824            PkgUsageStatsExtended pus = mStats.get(pkgName);
825            if (pus != null) {
826                pus.addFullyDrawnTime(componentName.getClassName(), millis);
827            }
828        }
829    }
830
831    public void enforceCallingPermission() {
832        if (Binder.getCallingPid() == Process.myPid()) {
833            return;
834        }
835        mContext.enforcePermission(android.Manifest.permission.UPDATE_DEVICE_STATS,
836                Binder.getCallingPid(), Binder.getCallingUid(), null);
837    }
838
839    public PkgUsageStats getPkgUsageStats(ComponentName componentName) {
840        mContext.enforceCallingOrSelfPermission(
841                android.Manifest.permission.PACKAGE_USAGE_STATS, null);
842        String pkgName;
843        if ((componentName == null) ||
844                ((pkgName = componentName.getPackageName()) == null)) {
845            return null;
846        }
847        synchronized (mStatsLock) {
848            PkgUsageStatsExtended pus = mStats.get(pkgName);
849            Map<String, Long> lastResumeTimes = mLastResumeTimes.get(pkgName);
850            if (pus == null && lastResumeTimes == null) {
851                return null;
852            }
853            int launchCount = pus != null ? pus.mLaunchCount : 0;
854            long usageTime = pus != null ? pus.mUsageTime : 0;
855            return new PkgUsageStats(pkgName, launchCount, usageTime, lastResumeTimes);
856        }
857    }
858
859    public PkgUsageStats[] getAllPkgUsageStats() {
860        mContext.enforceCallingOrSelfPermission(
861                android.Manifest.permission.PACKAGE_USAGE_STATS, null);
862        synchronized (mStatsLock) {
863            int size = mLastResumeTimes.size();
864            if (size <= 0) {
865                return null;
866            }
867            PkgUsageStats retArr[] = new PkgUsageStats[size];
868            for (int i=0; i<size; i++) {
869                String pkg = mLastResumeTimes.keyAt(i);
870                long usageTime = 0;
871                int launchCount = 0;
872
873                PkgUsageStatsExtended pus = mStats.get(pkg);
874                if (pus != null) {
875                    usageTime = pus.mUsageTime;
876                    launchCount = pus.mLaunchCount;
877                }
878                retArr[i] = new PkgUsageStats(pkg, launchCount, usageTime,
879                        mLastResumeTimes.valueAt(i));
880            }
881            return retArr;
882        }
883    }
884
885    static byte[] readFully(FileInputStream stream) throws java.io.IOException {
886        int pos = 0;
887        int avail = stream.available();
888        byte[] data = new byte[avail];
889        while (true) {
890            int amt = stream.read(data, pos, data.length-pos);
891            if (amt <= 0) {
892                return data;
893            }
894            pos += amt;
895            avail = stream.available();
896            if (avail > data.length-pos) {
897                byte[] newData = new byte[pos+avail];
898                System.arraycopy(data, 0, newData, 0, pos);
899                data = newData;
900            }
901        }
902    }
903
904    private void collectDumpInfoFLOCK(PrintWriter pw, boolean isCompactOutput,
905            boolean deleteAfterPrint, HashSet<String> packages) {
906        List<String> fileList = getUsageStatsFileListFLOCK();
907        if (fileList == null) {
908            return;
909        }
910        Collections.sort(fileList);
911        for (String file : fileList) {
912            if (deleteAfterPrint && file.equalsIgnoreCase(mFileLeaf)) {
913                // In this mode we don't print the current day's stats, since
914                // they are incomplete.
915                continue;
916            }
917            File dFile = new File(mDir, file);
918            String dateStr = file.substring(FILE_PREFIX.length());
919            if (dateStr.length() > 0 && (dateStr.charAt(0) <= '0' || dateStr.charAt(0) >= '9')) {
920                // If the remainder does not start with a number, it is not a date,
921                // so we should ignore it for purposes here.
922                continue;
923            }
924            try {
925                Parcel in = getParcelForFile(dFile);
926                collectDumpInfoFromParcelFLOCK(in, pw, dateStr, isCompactOutput,
927                        packages);
928                if (deleteAfterPrint) {
929                    // Delete old file after collecting info only for checkin requests
930                    dFile.delete();
931                }
932            } catch (FileNotFoundException e) {
933                Slog.w(TAG, "Failed with "+e+" when collecting dump info from file : " + file);
934                return;
935            } catch (IOException e) {
936                Slog.w(TAG, "Failed with "+e+" when collecting dump info from file : "+file);
937            }
938        }
939    }
940
941    private void collectDumpInfoFromParcelFLOCK(Parcel in, PrintWriter pw,
942            String date, boolean isCompactOutput, HashSet<String> packages) {
943        StringBuilder sb = new StringBuilder(512);
944        if (isCompactOutput) {
945            sb.append("D:");
946            sb.append(CHECKIN_VERSION);
947            sb.append(',');
948        } else {
949            sb.append("Date: ");
950        }
951
952        sb.append(date);
953
954        int vers = in.readInt();
955        if (vers != VERSION) {
956            sb.append(" (old data version)");
957            pw.println(sb.toString());
958            return;
959        }
960
961        pw.println(sb.toString());
962        int N = in.readInt();
963
964        while (N > 0) {
965            N--;
966            String pkgName = in.readString();
967            if (pkgName == null) {
968                break;
969            }
970            sb.setLength(0);
971            PkgUsageStatsExtended pus = new PkgUsageStatsExtended(in);
972            if (packages != null && !packages.contains(pkgName)) {
973                // This package has not been requested -- don't print
974                // anything for it.
975            } else if (isCompactOutput) {
976                sb.append("P:");
977                sb.append(pkgName);
978                sb.append(',');
979                sb.append(pus.mLaunchCount);
980                sb.append(',');
981                sb.append(pus.mUsageTime);
982                sb.append('\n');
983                final int NLT = pus.mLaunchTimes.size();
984                for (int i=0; i<NLT; i++) {
985                    sb.append("A:");
986                    String activity = pus.mLaunchTimes.keyAt(i);
987                    sb.append(activity);
988                    TimeStats times = pus.mLaunchTimes.valueAt(i);
989                    sb.append(',');
990                    sb.append(times.count);
991                    for (int j=0; j<NUM_LAUNCH_TIME_BINS; j++) {
992                        sb.append(",");
993                        sb.append(times.times[j]);
994                    }
995                    sb.append('\n');
996                }
997                final int NFDT = pus.mFullyDrawnTimes.size();
998                for (int i=0; i<NFDT; i++) {
999                    sb.append("A:");
1000                    String activity = pus.mFullyDrawnTimes.keyAt(i);
1001                    sb.append(activity);
1002                    TimeStats times = pus.mFullyDrawnTimes.valueAt(i);
1003                    for (int j=0; j<NUM_LAUNCH_TIME_BINS; j++) {
1004                        sb.append(",");
1005                        sb.append(times.times[j]);
1006                    }
1007                    sb.append('\n');
1008                }
1009
1010            } else {
1011                sb.append("  ");
1012                sb.append(pkgName);
1013                sb.append(": ");
1014                sb.append(pus.mLaunchCount);
1015                sb.append(" times, ");
1016                sb.append(pus.mUsageTime);
1017                sb.append(" ms");
1018                sb.append('\n');
1019                final int NLT = pus.mLaunchTimes.size();
1020                for (int i=0; i<NLT; i++) {
1021                    sb.append("    ");
1022                    sb.append(pus.mLaunchTimes.keyAt(i));
1023                    TimeStats times = pus.mLaunchTimes.valueAt(i);
1024                    sb.append(": ");
1025                    sb.append(times.count);
1026                    sb.append(" starts");
1027                    int lastBin = 0;
1028                    for (int j=0; j<NUM_LAUNCH_TIME_BINS-1; j++) {
1029                        if (times.times[j] != 0) {
1030                            sb.append(", ");
1031                            sb.append(lastBin);
1032                            sb.append('-');
1033                            sb.append(LAUNCH_TIME_BINS[j]);
1034                            sb.append("ms=");
1035                            sb.append(times.times[j]);
1036                        }
1037                        lastBin = LAUNCH_TIME_BINS[j];
1038                    }
1039                    if (times.times[NUM_LAUNCH_TIME_BINS-1] != 0) {
1040                        sb.append(", ");
1041                        sb.append(">=");
1042                        sb.append(lastBin);
1043                        sb.append("ms=");
1044                        sb.append(times.times[NUM_LAUNCH_TIME_BINS-1]);
1045                    }
1046                    sb.append('\n');
1047                }
1048                final int NFDT = pus.mFullyDrawnTimes.size();
1049                for (int i=0; i<NFDT; i++) {
1050                    sb.append("    ");
1051                    sb.append(pus.mFullyDrawnTimes.keyAt(i));
1052                    TimeStats times = pus.mFullyDrawnTimes.valueAt(i);
1053                    sb.append(": fully drawn ");
1054                    boolean needComma = false;
1055                    int lastBin = 0;
1056                    for (int j=0; j<NUM_LAUNCH_TIME_BINS-1; j++) {
1057                        if (times.times[j] != 0) {
1058                            if (needComma) {
1059                                sb.append(", ");
1060                            } else {
1061                                needComma = true;
1062                            }
1063                            sb.append(lastBin);
1064                            sb.append('-');
1065                            sb.append(LAUNCH_TIME_BINS[j]);
1066                            sb.append("ms=");
1067                            sb.append(times.times[j]);
1068                        }
1069                        lastBin = LAUNCH_TIME_BINS[j];
1070                    }
1071                    if (times.times[NUM_LAUNCH_TIME_BINS-1] != 0) {
1072                        if (needComma) {
1073                            sb.append(", ");
1074                        }
1075                        sb.append(">=");
1076                        sb.append(lastBin);
1077                        sb.append("ms=");
1078                        sb.append(times.times[NUM_LAUNCH_TIME_BINS-1]);
1079                    }
1080                    sb.append('\n');
1081                }
1082            }
1083
1084            pw.write(sb.toString());
1085        }
1086    }
1087
1088    /**
1089     * Searches array of arguments for the specified string
1090     * @param args array of argument strings
1091     * @param value value to search for
1092     * @return true if the value is contained in the array
1093     */
1094    private static boolean scanArgs(String[] args, String value) {
1095        if (args != null) {
1096            for (String arg : args) {
1097                if (value.equals(arg)) {
1098                    return true;
1099                }
1100            }
1101        }
1102        return false;
1103    }
1104
1105    /**
1106     * Searches array of arguments for the specified string's data
1107     * @param args array of argument strings
1108     * @param value value to search for
1109     * @return the string of data after the arg, or null if there is none
1110     */
1111    private static String scanArgsData(String[] args, String value) {
1112        if (args != null) {
1113            final int N = args.length;
1114            for (int i=0; i<N; i++) {
1115                if (value.equals(args[i])) {
1116                    i++;
1117                    return i < N ? args[i] : null;
1118                }
1119            }
1120        }
1121        return null;
1122    }
1123
1124    @Override
1125    /*
1126     * The data persisted to file is parsed and the stats are computed.
1127     */
1128    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
1129        if (mContext.checkCallingPermission(android.Manifest.permission.DUMP)
1130                != PackageManager.PERMISSION_GRANTED) {
1131            pw.println("Permission Denial: can't dump UsageStats from from pid="
1132                    + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
1133                    + " without permission " + android.Manifest.permission.DUMP);
1134            return;
1135        }
1136
1137        final boolean isCheckinRequest = scanArgs(args, "--checkin");
1138        final boolean isCompactOutput = isCheckinRequest || scanArgs(args, "-c");
1139        final boolean deleteAfterPrint = isCheckinRequest || scanArgs(args, "-d");
1140        final String rawPackages = scanArgsData(args, "--packages");
1141
1142        // Make sure the current stats are written to the file.  This
1143        // doesn't need to be done if we are deleting files after printing,
1144        // since it that case we won't print the current stats.
1145        if (!deleteAfterPrint) {
1146            writeStatsToFile(true, false);
1147        }
1148
1149        HashSet<String> packages = null;
1150        if (rawPackages != null) {
1151            if (!"*".equals(rawPackages)) {
1152                // A * is a wildcard to show all packages.
1153                String[] names = rawPackages.split(",");
1154                for (String n : names) {
1155                    if (packages == null) {
1156                        packages = new HashSet<String>();
1157                    }
1158                    packages.add(n);
1159                }
1160            }
1161        } else if (isCheckinRequest) {
1162            // If checkin doesn't specify any packages, then we simply won't
1163            // show anything.
1164            Slog.w(TAG, "Checkin without packages");
1165            return;
1166        }
1167
1168        synchronized (mFileLock) {
1169            collectDumpInfoFLOCK(pw, isCompactOutput, deleteAfterPrint, packages);
1170        }
1171    }
1172
1173}
1174