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