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