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