UserUsageStatsService.java revision da4a3775735ffc0fa510031f75bc459b764b259d
1/**
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations
14 * under the License.
15 */
16
17package com.android.server.usage;
18
19import android.app.usage.ConfigurationStats;
20import android.app.usage.TimeSparseArray;
21import android.app.usage.UsageEvents;
22import android.app.usage.UsageEvents.Event;
23import android.app.usage.UsageStats;
24import android.app.usage.UsageStatsManager;
25import android.content.pm.PackageInfo;
26import android.content.pm.PackageManager;
27import android.content.res.Configuration;
28import android.os.SystemClock;
29import android.content.Context;
30import android.text.format.DateUtils;
31import android.util.ArrayMap;
32import android.util.ArraySet;
33import android.util.Slog;
34
35import com.android.internal.util.IndentingPrintWriter;
36import com.android.server.usage.UsageStatsDatabase.StatCombiner;
37
38import java.io.File;
39import java.io.IOException;
40import java.text.SimpleDateFormat;
41import java.util.ArrayList;
42import java.util.Arrays;
43import java.util.List;
44
45/**
46 * A per-user UsageStatsService. All methods are meant to be called with the main lock held
47 * in UsageStatsService.
48 */
49class UserUsageStatsService {
50    private static final String TAG = "UsageStatsService";
51    private static final boolean DEBUG = UsageStatsService.DEBUG;
52    private static final SimpleDateFormat sDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
53    private static final int sDateFormatFlags =
54            DateUtils.FORMAT_SHOW_DATE
55            | DateUtils.FORMAT_SHOW_TIME
56            | DateUtils.FORMAT_SHOW_YEAR
57            | DateUtils.FORMAT_NUMERIC_DATE;
58
59    private final Context mContext;
60    private final UsageStatsDatabase mDatabase;
61    private final IntervalStats[] mCurrentStats;
62    private IntervalStats mAppIdleRollingWindow;
63    private boolean mStatsChanged = false;
64    private final UnixCalendar mDailyExpiryDate;
65    private final StatsUpdatedListener mListener;
66    private final String mLogPrefix;
67    private final int mUserId;
68
69    interface StatsUpdatedListener {
70        void onStatsUpdated();
71    }
72
73    UserUsageStatsService(Context context, int userId, File usageStatsDir,
74            StatsUpdatedListener listener) {
75        mContext = context;
76        mDailyExpiryDate = new UnixCalendar(0);
77        mDatabase = new UsageStatsDatabase(usageStatsDir);
78        mCurrentStats = new IntervalStats[UsageStatsManager.INTERVAL_COUNT];
79        mListener = listener;
80        mLogPrefix = "User[" + Integer.toString(userId) + "] ";
81        mUserId = userId;
82    }
83
84    void init(final long currentTimeMillis, final long deviceUsageTime) {
85        mDatabase.init(currentTimeMillis);
86
87        int nullCount = 0;
88        for (int i = 0; i < mCurrentStats.length; i++) {
89            mCurrentStats[i] = mDatabase.getLatestUsageStats(i);
90            if (mCurrentStats[i] == null) {
91                // Find out how many intervals we don't have data for.
92                // Ideally it should be all or none.
93                nullCount++;
94            }
95        }
96
97        if (nullCount > 0) {
98            if (nullCount != mCurrentStats.length) {
99                // This is weird, but we shouldn't fail if something like this
100                // happens.
101                Slog.w(TAG, mLogPrefix + "Some stats have no latest available");
102            } else {
103                // This must be first boot.
104            }
105
106            // By calling loadActiveStats, we will
107            // generate new stats for each bucket.
108            loadActiveStats(currentTimeMillis,/*force=*/ false, /*resetBeginIdleTime=*/ false);
109        } else {
110            // Set up the expiry date to be one day from the latest daily stat.
111            // This may actually be today and we will rollover on the first event
112            // that is reported.
113            mDailyExpiryDate.setTimeInMillis(
114                    mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime);
115            mDailyExpiryDate.addDays(1);
116            mDailyExpiryDate.truncateToDay();
117            Slog.i(TAG, mLogPrefix + "Rollover scheduled @ " +
118                    sDateFormat.format(mDailyExpiryDate.getTimeInMillis()) +
119                    "(" + mDailyExpiryDate.getTimeInMillis() + ")");
120        }
121
122        // Now close off any events that were open at the time this was saved.
123        for (IntervalStats stat : mCurrentStats) {
124            final int pkgCount = stat.packageStats.size();
125            for (int i = 0; i < pkgCount; i++) {
126                UsageStats pkgStats = stat.packageStats.valueAt(i);
127                if (pkgStats.mLastEvent == UsageEvents.Event.MOVE_TO_FOREGROUND ||
128                        pkgStats.mLastEvent == UsageEvents.Event.CONTINUE_PREVIOUS_DAY) {
129                    stat.update(pkgStats.mPackageName, stat.lastTimeSaved,
130                            UsageEvents.Event.END_OF_DAY);
131                    notifyStatsChanged();
132                }
133            }
134
135            stat.updateConfigurationStats(null, stat.lastTimeSaved);
136        }
137
138        if (mDatabase.isNewUpdate()) {
139            initializeDefaultsForApps(currentTimeMillis, deviceUsageTime,
140                    mDatabase.isFirstUpdate());
141        }
142
143        refreshAppIdleRollingWindow(currentTimeMillis);
144    }
145
146    /**
147     * If any of the apps don't have a last-used entry, add one now.
148     * @param currentTimeMillis the current time
149     * @param firstUpdate if it is the first update, touch all installed apps, otherwise only
150     *        touch the system apps
151     */
152    private void initializeDefaultsForApps(long currentTimeMillis, long deviceUsageTime,
153            boolean firstUpdate) {
154        PackageManager pm = mContext.getPackageManager();
155        List<PackageInfo> packages = pm.getInstalledPackages(0, mUserId);
156        final int packageCount = packages.size();
157        for (int i = 0; i < packageCount; i++) {
158            final PackageInfo pi = packages.get(i);
159            String packageName = pi.packageName;
160            if (pi.applicationInfo != null && (firstUpdate || pi.applicationInfo.isSystemApp())
161                    && getBeginIdleTime(packageName) == -1) {
162                for (IntervalStats stats : mCurrentStats) {
163                    stats.update(packageName, currentTimeMillis, Event.SYSTEM_INTERACTION);
164                    stats.updateBeginIdleTime(packageName, deviceUsageTime);
165                    mStatsChanged = true;
166                }
167            }
168        }
169        // Persist the new OTA-related access stats.
170        persistActiveStats();
171    }
172
173    void onTimeChanged(long oldTime, long newTime, boolean resetBeginIdleTime) {
174        persistActiveStats();
175        mDatabase.onTimeChanged(newTime - oldTime);
176        loadActiveStats(newTime, /* force= */ true, resetBeginIdleTime);
177        refreshAppIdleRollingWindow(newTime);
178    }
179
180    void reportEvent(UsageEvents.Event event, long deviceUsageTime) {
181        if (DEBUG) {
182            Slog.d(TAG, mLogPrefix + "Got usage event for " + event.mPackage
183                    + "[" + event.mTimeStamp + "]: "
184                    + eventToString(event.mEventType));
185        }
186
187        if (event.mTimeStamp >= mDailyExpiryDate.getTimeInMillis()) {
188            // Need to rollover
189            rolloverStats(event.mTimeStamp);
190        }
191
192        final IntervalStats currentDailyStats = mCurrentStats[UsageStatsManager.INTERVAL_DAILY];
193
194        final Configuration newFullConfig = event.mConfiguration;
195        if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE &&
196                currentDailyStats.activeConfiguration != null) {
197            // Make the event configuration a delta.
198            event.mConfiguration = Configuration.generateDelta(
199                    currentDailyStats.activeConfiguration, newFullConfig);
200        }
201
202        // Add the event to the daily list.
203        if (currentDailyStats.events == null) {
204            currentDailyStats.events = new TimeSparseArray<>();
205        }
206        if (event.mEventType != UsageEvents.Event.SYSTEM_INTERACTION) {
207            currentDailyStats.events.put(event.mTimeStamp, event);
208        }
209
210        for (IntervalStats stats : mCurrentStats) {
211            if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE) {
212                stats.updateConfigurationStats(newFullConfig, event.mTimeStamp);
213            } else {
214                stats.update(event.mPackage, event.mTimeStamp, event.mEventType);
215                stats.updateBeginIdleTime(event.mPackage, deviceUsageTime);
216            }
217        }
218
219        if (event.mEventType != Event.CONFIGURATION_CHANGE) {
220            mAppIdleRollingWindow.update(event.mPackage, event.mTimeStamp, event.mEventType);
221            mAppIdleRollingWindow.updateBeginIdleTime(event.mPackage, deviceUsageTime);
222        }
223
224        notifyStatsChanged();
225    }
226
227    /**
228     * Sets the beginIdleTime for each of the intervals.
229     * @param beginIdleTime
230     */
231    void setBeginIdleTime(String packageName, long beginIdleTime) {
232        for (IntervalStats stats : mCurrentStats) {
233            stats.updateBeginIdleTime(packageName, beginIdleTime);
234        }
235        mAppIdleRollingWindow.updateBeginIdleTime(packageName, beginIdleTime);
236        notifyStatsChanged();
237    }
238
239    void setSystemLastUsedTime(String packageName, long lastUsedTime) {
240        for (IntervalStats stats : mCurrentStats) {
241            stats.updateSystemLastUsedTime(packageName, lastUsedTime);
242        }
243        mAppIdleRollingWindow.updateSystemLastUsedTime(packageName, lastUsedTime);
244        notifyStatsChanged();
245    }
246
247    private static final StatCombiner<UsageStats> sUsageStatsCombiner =
248            new StatCombiner<UsageStats>() {
249                @Override
250                public void combine(IntervalStats stats, boolean mutable,
251                        List<UsageStats> accResult) {
252                    if (!mutable) {
253                        accResult.addAll(stats.packageStats.values());
254                        return;
255                    }
256
257                    final int statCount = stats.packageStats.size();
258                    for (int i = 0; i < statCount; i++) {
259                        accResult.add(new UsageStats(stats.packageStats.valueAt(i)));
260                    }
261                }
262            };
263
264    private static final StatCombiner<ConfigurationStats> sConfigStatsCombiner =
265            new StatCombiner<ConfigurationStats>() {
266                @Override
267                public void combine(IntervalStats stats, boolean mutable,
268                        List<ConfigurationStats> accResult) {
269                    if (!mutable) {
270                        accResult.addAll(stats.configurations.values());
271                        return;
272                    }
273
274                    final int configCount = stats.configurations.size();
275                    for (int i = 0; i < configCount; i++) {
276                        accResult.add(new ConfigurationStats(stats.configurations.valueAt(i)));
277                    }
278                }
279            };
280
281    /**
282     * Generic query method that selects the appropriate IntervalStats for the specified time range
283     * and bucket, then calls the {@link com.android.server.usage.UsageStatsDatabase.StatCombiner}
284     * provided to select the stats to use from the IntervalStats object.
285     */
286    private <T> List<T> queryStats(int intervalType, final long beginTime, final long endTime,
287            StatCombiner<T> combiner) {
288        if (intervalType == UsageStatsManager.INTERVAL_BEST) {
289            intervalType = mDatabase.findBestFitBucket(beginTime, endTime);
290            if (intervalType < 0) {
291                // Nothing saved to disk yet, so every stat is just as equal (no rollover has
292                // occurred.
293                intervalType = UsageStatsManager.INTERVAL_DAILY;
294            }
295        }
296
297        if (intervalType < 0 || intervalType >= mCurrentStats.length) {
298            if (DEBUG) {
299                Slog.d(TAG, mLogPrefix + "Bad intervalType used " + intervalType);
300            }
301            return null;
302        }
303
304        final IntervalStats currentStats = mCurrentStats[intervalType];
305
306        if (DEBUG) {
307            Slog.d(TAG, mLogPrefix + "SELECT * FROM " + intervalType + " WHERE beginTime >= "
308                    + beginTime + " AND endTime < " + endTime);
309        }
310
311        if (beginTime >= currentStats.endTime) {
312            if (DEBUG) {
313                Slog.d(TAG, mLogPrefix + "Requesting stats after " + beginTime + " but latest is "
314                        + currentStats.endTime);
315            }
316            // Nothing newer available.
317            return null;
318        }
319
320        // Truncate the endTime to just before the in-memory stats. Then, we'll append the
321        // in-memory stats to the results (if necessary) so as to avoid writing to disk too
322        // often.
323        final long truncatedEndTime = Math.min(currentStats.beginTime, endTime);
324
325        // Get the stats from disk.
326        List<T> results = mDatabase.queryUsageStats(intervalType, beginTime,
327                truncatedEndTime, combiner);
328        if (DEBUG) {
329            Slog.d(TAG, "Got " + (results != null ? results.size() : 0) + " results from disk");
330            Slog.d(TAG, "Current stats beginTime=" + currentStats.beginTime +
331                    " endTime=" + currentStats.endTime);
332        }
333
334        // Now check if the in-memory stats match the range and add them if they do.
335        if (beginTime < currentStats.endTime && endTime > currentStats.beginTime) {
336            if (DEBUG) {
337                Slog.d(TAG, mLogPrefix + "Returning in-memory stats");
338            }
339
340            if (results == null) {
341                results = new ArrayList<>();
342            }
343            combiner.combine(currentStats, true, results);
344        }
345
346        if (DEBUG) {
347            Slog.d(TAG, mLogPrefix + "Results: " + (results != null ? results.size() : 0));
348        }
349        return results;
350    }
351
352    List<UsageStats> queryUsageStats(int bucketType, long beginTime, long endTime) {
353        return queryStats(bucketType, beginTime, endTime, sUsageStatsCombiner);
354    }
355
356    List<ConfigurationStats> queryConfigurationStats(int bucketType, long beginTime, long endTime) {
357        return queryStats(bucketType, beginTime, endTime, sConfigStatsCombiner);
358    }
359
360    UsageEvents queryEvents(final long beginTime, final long endTime) {
361        final ArraySet<String> names = new ArraySet<>();
362        List<UsageEvents.Event> results = queryStats(UsageStatsManager.INTERVAL_DAILY,
363                beginTime, endTime, new StatCombiner<UsageEvents.Event>() {
364                    @Override
365                    public void combine(IntervalStats stats, boolean mutable,
366                            List<UsageEvents.Event> accumulatedResult) {
367                        if (stats.events == null) {
368                            return;
369                        }
370
371                        final int startIndex = stats.events.closestIndexOnOrAfter(beginTime);
372                        if (startIndex < 0) {
373                            return;
374                        }
375
376                        final int size = stats.events.size();
377                        for (int i = startIndex; i < size; i++) {
378                            if (stats.events.keyAt(i) >= endTime) {
379                                return;
380                            }
381
382                            final UsageEvents.Event event = stats.events.valueAt(i);
383                            names.add(event.mPackage);
384                            if (event.mClass != null) {
385                                names.add(event.mClass);
386                            }
387                            accumulatedResult.add(event);
388                        }
389                    }
390                });
391
392        if (results == null || results.isEmpty()) {
393            return null;
394        }
395
396        String[] table = names.toArray(new String[names.size()]);
397        Arrays.sort(table);
398        return new UsageEvents(results, table);
399    }
400
401    long getBeginIdleTime(String packageName) {
402        UsageStats packageUsage;
403        if ((packageUsage = mAppIdleRollingWindow.packageStats.get(packageName)) == null) {
404            return -1;
405        } else {
406            return packageUsage.getBeginIdleTime();
407        }
408    }
409
410    long getSystemLastUsedTime(String packageName) {
411        UsageStats packageUsage;
412        if ((packageUsage = mAppIdleRollingWindow.packageStats.get(packageName)) == null) {
413            return -1;
414        } else {
415            return packageUsage.getLastTimeSystemUsed();
416        }
417    }
418
419    void persistActiveStats() {
420        if (mStatsChanged) {
421            Slog.i(TAG, mLogPrefix + "Flushing usage stats to disk");
422            try {
423                for (int i = 0; i < mCurrentStats.length; i++) {
424                    mDatabase.putUsageStats(i, mCurrentStats[i]);
425                }
426                mStatsChanged = false;
427            } catch (IOException e) {
428                Slog.e(TAG, mLogPrefix + "Failed to persist active stats", e);
429            }
430        }
431    }
432
433    private void rolloverStats(final long currentTimeMillis) {
434        final long startTime = SystemClock.elapsedRealtime();
435        Slog.i(TAG, mLogPrefix + "Rolling over usage stats");
436
437        // Finish any ongoing events with an END_OF_DAY event. Make a note of which components
438        // need a new CONTINUE_PREVIOUS_DAY entry.
439        final Configuration previousConfig =
440                mCurrentStats[UsageStatsManager.INTERVAL_DAILY].activeConfiguration;
441        ArraySet<String> continuePreviousDay = new ArraySet<>();
442        for (IntervalStats stat : mCurrentStats) {
443            final int pkgCount = stat.packageStats.size();
444            for (int i = 0; i < pkgCount; i++) {
445                UsageStats pkgStats = stat.packageStats.valueAt(i);
446                if (pkgStats.mLastEvent == UsageEvents.Event.MOVE_TO_FOREGROUND ||
447                        pkgStats.mLastEvent == UsageEvents.Event.CONTINUE_PREVIOUS_DAY) {
448                    continuePreviousDay.add(pkgStats.mPackageName);
449                    stat.update(pkgStats.mPackageName, mDailyExpiryDate.getTimeInMillis() - 1,
450                            UsageEvents.Event.END_OF_DAY);
451                    notifyStatsChanged();
452                }
453            }
454
455            stat.updateConfigurationStats(null, mDailyExpiryDate.getTimeInMillis() - 1);
456        }
457
458        persistActiveStats();
459        mDatabase.prune(currentTimeMillis);
460        loadActiveStats(currentTimeMillis, /*force=*/ false, /*resetBeginIdleTime=*/ false);
461
462        final int continueCount = continuePreviousDay.size();
463        for (int i = 0; i < continueCount; i++) {
464            String name = continuePreviousDay.valueAt(i);
465            final long beginTime = mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime;
466            for (IntervalStats stat : mCurrentStats) {
467                stat.update(name, beginTime, UsageEvents.Event.CONTINUE_PREVIOUS_DAY);
468                stat.updateConfigurationStats(previousConfig, beginTime);
469                notifyStatsChanged();
470            }
471        }
472        persistActiveStats();
473
474        refreshAppIdleRollingWindow(currentTimeMillis);
475
476        final long totalTime = SystemClock.elapsedRealtime() - startTime;
477        Slog.i(TAG, mLogPrefix + "Rolling over usage stats complete. Took " + totalTime
478                + " milliseconds");
479    }
480
481    private void notifyStatsChanged() {
482        if (!mStatsChanged) {
483            mStatsChanged = true;
484            mListener.onStatsUpdated();
485        }
486    }
487
488    /**
489     * @param force To force all in-memory stats to be reloaded.
490     */
491    private void loadActiveStats(final long currentTimeMillis, boolean force,
492            boolean resetBeginIdleTime) {
493        final UnixCalendar tempCal = mDailyExpiryDate;
494        for (int intervalType = 0; intervalType < mCurrentStats.length; intervalType++) {
495            tempCal.setTimeInMillis(currentTimeMillis);
496            UnixCalendar.truncateTo(tempCal, intervalType);
497
498            if (!force && mCurrentStats[intervalType] != null &&
499                    mCurrentStats[intervalType].beginTime == tempCal.getTimeInMillis()) {
500                // These are the same, no need to load them (in memory stats are always newer
501                // than persisted stats).
502                continue;
503            }
504
505            final long lastBeginTime = mDatabase.getLatestUsageStatsBeginTime(intervalType);
506            if (lastBeginTime >= tempCal.getTimeInMillis()) {
507                if (DEBUG) {
508                    Slog.d(TAG, mLogPrefix + "Loading existing stats @ " +
509                            sDateFormat.format(lastBeginTime) + "(" + lastBeginTime +
510                            ") for interval " + intervalType);
511                }
512                mCurrentStats[intervalType] = mDatabase.getLatestUsageStats(intervalType);
513            } else {
514                mCurrentStats[intervalType] = null;
515            }
516
517            if (mCurrentStats[intervalType] == null) {
518                if (DEBUG) {
519                    Slog.d(TAG, "Creating new stats @ " +
520                            sDateFormat.format(tempCal.getTimeInMillis()) + "(" +
521                            tempCal.getTimeInMillis() + ") for interval " + intervalType);
522
523                }
524                mCurrentStats[intervalType] = new IntervalStats();
525                mCurrentStats[intervalType].beginTime = tempCal.getTimeInMillis();
526                mCurrentStats[intervalType].endTime = currentTimeMillis;
527            }
528
529            if (resetBeginIdleTime) {
530                for (UsageStats usageStats : mCurrentStats[intervalType].packageStats.values()) {
531                    usageStats.mBeginIdleTime = 0;
532                }
533            }
534        }
535
536        mStatsChanged = false;
537        mDailyExpiryDate.setTimeInMillis(currentTimeMillis);
538        mDailyExpiryDate.addDays(1);
539        mDailyExpiryDate.truncateToDay();
540        Slog.i(TAG, mLogPrefix + "Rollover scheduled @ " +
541                sDateFormat.format(mDailyExpiryDate.getTimeInMillis()) + "(" +
542                tempCal.getTimeInMillis() + ")");
543    }
544
545    private static void mergePackageStats(IntervalStats dst, IntervalStats src) {
546        dst.endTime = Math.max(dst.endTime, src.endTime);
547
548        final int srcPackageCount = src.packageStats.size();
549        for (int i = 0; i < srcPackageCount; i++) {
550            final String packageName = src.packageStats.keyAt(i);
551            final UsageStats srcStats = src.packageStats.valueAt(i);
552            final UsageStats dstStats = dst.packageStats.get(packageName);
553            if (dstStats == null) {
554                dst.packageStats.put(packageName, new UsageStats(srcStats));
555            } else {
556                dstStats.add(src.packageStats.valueAt(i));
557            }
558        }
559    }
560
561    /**
562     * Merges all the stats into the first element of the resulting list.
563     */
564    private static final StatCombiner<IntervalStats> sPackageStatsMerger =
565            new StatCombiner<IntervalStats>() {
566        @Override
567        public void combine(IntervalStats stats, boolean mutable,
568                            List<IntervalStats> accumulatedResult) {
569            IntervalStats accum;
570            if (accumulatedResult.isEmpty()) {
571                accum = new IntervalStats();
572                accum.beginTime = stats.beginTime;
573                accumulatedResult.add(accum);
574            } else {
575                accum = accumulatedResult.get(0);
576            }
577
578            mergePackageStats(accum, stats);
579        }
580    };
581
582    /**
583     * App idle operates on a rolling window of time. When we roll over time, we end up with a
584     * period of time where in-memory stats are empty and we don't hit the disk for older stats
585     * for performance reasons. Suddenly all apps will become idle.
586     *
587     * Instead, at times we do a deep query to find all the apps that have run in the past few
588     * days and keep the cached data up to date.
589     *
590     * @param currentTimeMillis
591     */
592    void refreshAppIdleRollingWindow(long currentTimeMillis) {
593        // Start the rolling window for AppIdle requests.
594        List<IntervalStats> stats = mDatabase.queryUsageStats(UsageStatsManager.INTERVAL_DAILY,
595                currentTimeMillis - (1000 * 60 * 60 * 24 * 2), currentTimeMillis,
596                sPackageStatsMerger);
597
598        if (stats == null || stats.isEmpty()) {
599            mAppIdleRollingWindow = new IntervalStats();
600            mergePackageStats(mAppIdleRollingWindow,
601                    mCurrentStats[UsageStatsManager.INTERVAL_YEARLY]);
602        } else {
603            mAppIdleRollingWindow = stats.get(0);
604        }
605    }
606
607    //
608    // -- DUMP related methods --
609    //
610
611    void checkin(final IndentingPrintWriter pw, final long screenOnTime) {
612        mDatabase.checkinDailyFiles(new UsageStatsDatabase.CheckinAction() {
613            @Override
614            public boolean checkin(IntervalStats stats) {
615                printIntervalStats(pw, stats, screenOnTime, false);
616                return true;
617            }
618        });
619    }
620
621    void dump(IndentingPrintWriter pw, final long screenOnTime) {
622        // This is not a check-in, only dump in-memory stats.
623        for (int interval = 0; interval < mCurrentStats.length; interval++) {
624            pw.print("In-memory ");
625            pw.print(intervalToString(interval));
626            pw.println(" stats");
627            printIntervalStats(pw, mCurrentStats[interval], screenOnTime, true);
628        }
629
630        pw.println("AppIdleRollingWindow cache");
631        printIntervalStats(pw, mAppIdleRollingWindow, screenOnTime, true);
632    }
633
634    private String formatDateTime(long dateTime, boolean pretty) {
635        if (pretty) {
636            return "\"" + DateUtils.formatDateTime(mContext, dateTime, sDateFormatFlags) + "\"";
637        }
638        return Long.toString(dateTime);
639    }
640
641    private String formatElapsedTime(long elapsedTime, boolean pretty) {
642        if (pretty) {
643            return "\"" + DateUtils.formatElapsedTime(elapsedTime / 1000) + "\"";
644        }
645        return Long.toString(elapsedTime);
646    }
647
648    void printIntervalStats(IndentingPrintWriter pw, IntervalStats stats, long screenOnTime,
649            boolean prettyDates) {
650        if (prettyDates) {
651            pw.printPair("timeRange", "\"" + DateUtils.formatDateRange(mContext,
652                    stats.beginTime, stats.endTime, sDateFormatFlags) + "\"");
653        } else {
654            pw.printPair("beginTime", stats.beginTime);
655            pw.printPair("endTime", stats.endTime);
656        }
657        pw.println();
658        pw.increaseIndent();
659        pw.println("packages");
660        pw.increaseIndent();
661        final ArrayMap<String, UsageStats> pkgStats = stats.packageStats;
662        final int pkgCount = pkgStats.size();
663        for (int i = 0; i < pkgCount; i++) {
664            final UsageStats usageStats = pkgStats.valueAt(i);
665            pw.printPair("package", usageStats.mPackageName);
666            pw.printPair("totalTime",
667                    formatElapsedTime(usageStats.mTotalTimeInForeground, prettyDates));
668            pw.printPair("lastTime", formatDateTime(usageStats.mLastTimeUsed, prettyDates));
669            pw.printPair("lastTimeSystem",
670                    formatDateTime(usageStats.mLastTimeSystemUsed, prettyDates));
671            pw.printPair("inactiveTime",
672                    formatElapsedTime(screenOnTime - usageStats.mBeginIdleTime, prettyDates));
673            pw.println();
674        }
675        pw.decreaseIndent();
676
677        pw.println("configurations");
678        pw.increaseIndent();
679        final ArrayMap<Configuration, ConfigurationStats> configStats = stats.configurations;
680        final int configCount = configStats.size();
681        for (int i = 0; i < configCount; i++) {
682            final ConfigurationStats config = configStats.valueAt(i);
683            pw.printPair("config", Configuration.resourceQualifierString(config.mConfiguration));
684            pw.printPair("totalTime", formatElapsedTime(config.mTotalTimeActive, prettyDates));
685            pw.printPair("lastTime", formatDateTime(config.mLastTimeActive, prettyDates));
686            pw.printPair("count", config.mActivationCount);
687            pw.println();
688        }
689        pw.decreaseIndent();
690
691        pw.println("events");
692        pw.increaseIndent();
693        final TimeSparseArray<UsageEvents.Event> events = stats.events;
694        final int eventCount = events != null ? events.size() : 0;
695        for (int i = 0; i < eventCount; i++) {
696            final UsageEvents.Event event = events.valueAt(i);
697            pw.printPair("time", formatDateTime(event.mTimeStamp, prettyDates));
698            pw.printPair("type", eventToString(event.mEventType));
699            pw.printPair("package", event.mPackage);
700            if (event.mClass != null) {
701                pw.printPair("class", event.mClass);
702            }
703            if (event.mConfiguration != null) {
704                pw.printPair("config", Configuration.resourceQualifierString(event.mConfiguration));
705            }
706            pw.println();
707        }
708        pw.decreaseIndent();
709        pw.decreaseIndent();
710    }
711
712    private static String intervalToString(int interval) {
713        switch (interval) {
714            case UsageStatsManager.INTERVAL_DAILY:
715                return "daily";
716            case UsageStatsManager.INTERVAL_WEEKLY:
717                return "weekly";
718            case UsageStatsManager.INTERVAL_MONTHLY:
719                return "monthly";
720            case UsageStatsManager.INTERVAL_YEARLY:
721                return "yearly";
722            default:
723                return "?";
724        }
725    }
726
727    private static String eventToString(int eventType) {
728        switch (eventType) {
729            case UsageEvents.Event.NONE:
730                return "NONE";
731            case UsageEvents.Event.MOVE_TO_BACKGROUND:
732                return "MOVE_TO_BACKGROUND";
733            case UsageEvents.Event.MOVE_TO_FOREGROUND:
734                return "MOVE_TO_FOREGROUND";
735            case UsageEvents.Event.END_OF_DAY:
736                return "END_OF_DAY";
737            case UsageEvents.Event.CONTINUE_PREVIOUS_DAY:
738                return "CONTINUE_PREVIOUS_DAY";
739            case UsageEvents.Event.CONFIGURATION_CHANGE:
740                return "CONFIGURATION_CHANGE";
741            case UsageEvents.Event.SYSTEM_INTERACTION:
742                return "SYSTEM_INTERACTION";
743            case UsageEvents.Event.USER_INTERACTION:
744                return "USER_INTERACTION";
745            default:
746                return "UNKNOWN";
747        }
748    }
749}
750