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