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