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