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