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