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