UserUsageStatsService.java revision 37a46b48dcb7e34ee3669cfb2ed78af08bfca3c7
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.util.ArraySet;
26import android.util.Slog;
27
28import com.android.server.usage.UsageStatsDatabase.StatCombiner;
29
30import java.io.File;
31import java.io.IOException;
32import java.text.SimpleDateFormat;
33import java.util.ArrayList;
34import java.util.Arrays;
35import java.util.List;
36
37/**
38 * A per-user UsageStatsService. All methods are meant to be called with the main lock held
39 * in UsageStatsService.
40 */
41class UserUsageStatsService {
42    private static final String TAG = "UsageStatsService";
43    private static final boolean DEBUG = UsageStatsService.DEBUG;
44    private static final SimpleDateFormat sDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
45
46    private final UsageStatsDatabase mDatabase;
47    private final IntervalStats[] mCurrentStats;
48    private boolean mStatsChanged = false;
49    private final UnixCalendar mDailyExpiryDate;
50    private final StatsUpdatedListener mListener;
51    private final String mLogPrefix;
52
53    interface StatsUpdatedListener {
54        void onStatsUpdated();
55    }
56
57    UserUsageStatsService(int userId, File usageStatsDir, StatsUpdatedListener listener) {
58        mDailyExpiryDate = new UnixCalendar(0);
59        mDatabase = new UsageStatsDatabase(usageStatsDir);
60        mCurrentStats = new IntervalStats[UsageStatsManager.INTERVAL_COUNT];
61        mListener = listener;
62        mLogPrefix = "User[" + Integer.toString(userId) + "] ";
63    }
64
65    void init() {
66        mDatabase.init();
67
68        final long timeNow = System.currentTimeMillis();
69        int nullCount = 0;
70        for (int i = 0; i < mCurrentStats.length; i++) {
71            mCurrentStats[i] = mDatabase.getLatestUsageStats(i);
72            if (mCurrentStats[i] == null) {
73                // Find out how many intervals we don't have data for.
74                // Ideally it should be all or none.
75                nullCount++;
76            } else if (mCurrentStats[i].beginTime > timeNow) {
77                Slog.e(TAG, mLogPrefix + "Interval " + i + " has stat in the future " +
78                        mCurrentStats[i].beginTime);
79                mCurrentStats[i] = null;
80                nullCount++;
81            }
82        }
83
84        if (nullCount > 0) {
85            if (nullCount != mCurrentStats.length) {
86                // This is weird, but we shouldn't fail if something like this
87                // happens.
88                Slog.w(TAG, mLogPrefix + "Some stats have no latest available");
89            } else {
90                // This must be first boot.
91            }
92
93            // By calling loadActiveStats, we will
94            // generate new stats for each bucket.
95            loadActiveStats();
96        } else {
97            // Set up the expiry date to be one day from the latest daily stat.
98            // This may actually be today and we will rollover on the first event
99            // that is reported.
100            mDailyExpiryDate.setTimeInMillis(
101                    mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime);
102            mDailyExpiryDate.addDays(1);
103            mDailyExpiryDate.truncateToDay();
104            Slog.i(TAG, mLogPrefix + "Rollover scheduled @ " +
105                    sDateFormat.format(mDailyExpiryDate.getTimeInMillis()) +
106                    "(" + mDailyExpiryDate.getTimeInMillis() + ")");
107        }
108
109        // Now close off any events that were open at the time this was saved.
110        for (IntervalStats stat : mCurrentStats) {
111            final int pkgCount = stat.packageStats.size();
112            for (int i = 0; i < pkgCount; i++) {
113                UsageStats pkgStats = stat.packageStats.valueAt(i);
114                if (pkgStats.mLastEvent == UsageEvents.Event.MOVE_TO_FOREGROUND ||
115                        pkgStats.mLastEvent == UsageEvents.Event.CONTINUE_PREVIOUS_DAY) {
116                    stat.update(pkgStats.mPackageName, stat.lastTimeSaved,
117                            UsageEvents.Event.END_OF_DAY);
118                    notifyStatsChanged();
119                }
120            }
121
122            stat.updateConfigurationStats(null, stat.lastTimeSaved);
123        }
124    }
125
126    void reportEvent(UsageEvents.Event event) {
127        if (DEBUG) {
128            Slog.d(TAG, mLogPrefix + "Got usage event for " + event.mPackage
129                    + "[" + event.mTimeStamp + "]: "
130                    + eventToString(event.mEventType));
131        }
132
133        if (event.mTimeStamp >= mDailyExpiryDate.getTimeInMillis()) {
134            // Need to rollover
135            rolloverStats();
136        }
137
138        final IntervalStats currentDailyStats = mCurrentStats[UsageStatsManager.INTERVAL_DAILY];
139
140        final Configuration newFullConfig = event.mConfiguration;
141        if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE &&
142                currentDailyStats.activeConfiguration != null) {
143            // Make the event configuration a delta.
144            event.mConfiguration = Configuration.generateDelta(
145                    currentDailyStats.activeConfiguration, newFullConfig);
146        }
147
148        // Add the event to the daily list.
149        if (currentDailyStats.events == null) {
150            currentDailyStats.events = new TimeSparseArray<>();
151        }
152        currentDailyStats.events.put(event.mTimeStamp, event);
153
154        for (IntervalStats stats : mCurrentStats) {
155            if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE) {
156                stats.updateConfigurationStats(newFullConfig, event.mTimeStamp);
157            } else {
158                stats.update(event.mPackage, event.mTimeStamp, event.mEventType);
159            }
160        }
161
162        notifyStatsChanged();
163    }
164
165    private static final StatCombiner<UsageStats> sUsageStatsCombiner =
166            new StatCombiner<UsageStats>() {
167                @Override
168                public void combine(IntervalStats stats, boolean mutable,
169                        List<UsageStats> accResult) {
170                    if (!mutable) {
171                        accResult.addAll(stats.packageStats.values());
172                        return;
173                    }
174
175                    final int statCount = stats.packageStats.size();
176                    for (int i = 0; i < statCount; i++) {
177                        accResult.add(new UsageStats(stats.packageStats.valueAt(i)));
178                    }
179                }
180            };
181
182    private static final StatCombiner<ConfigurationStats> sConfigStatsCombiner =
183            new StatCombiner<ConfigurationStats>() {
184                @Override
185                public void combine(IntervalStats stats, boolean mutable,
186                        List<ConfigurationStats> accResult) {
187                    if (!mutable) {
188                        accResult.addAll(stats.configurations.values());
189                        return;
190                    }
191
192                    final int configCount = stats.configurations.size();
193                    for (int i = 0; i < configCount; i++) {
194                        accResult.add(new ConfigurationStats(stats.configurations.valueAt(i)));
195                    }
196                }
197            };
198
199    /**
200     * Generic query method that selects the appropriate IntervalStats for the specified time range
201     * and bucket, then calls the {@link com.android.server.usage.UsageStatsDatabase.StatCombiner}
202     * provided to select the stats to use from the IntervalStats object.
203     */
204    private <T> List<T> queryStats(int intervalType, final long beginTime, final long endTime,
205            StatCombiner<T> combiner) {
206        if (intervalType == UsageStatsManager.INTERVAL_BEST) {
207            intervalType = mDatabase.findBestFitBucket(beginTime, endTime);
208            if (intervalType < 0) {
209                // Nothing saved to disk yet, so every stat is just as equal (no rollover has
210                // occurred.
211                intervalType = UsageStatsManager.INTERVAL_DAILY;
212            }
213        }
214
215        if (intervalType < 0 || intervalType >= mCurrentStats.length) {
216            if (DEBUG) {
217                Slog.d(TAG, mLogPrefix + "Bad intervalType used " + intervalType);
218            }
219            return null;
220        }
221
222        final IntervalStats currentStats = mCurrentStats[intervalType];
223
224        if (DEBUG) {
225            Slog.d(TAG, mLogPrefix + "SELECT * FROM " + intervalType + " WHERE beginTime >= "
226                    + beginTime + " AND endTime < " + endTime);
227        }
228
229        if (beginTime >= currentStats.endTime) {
230            if (DEBUG) {
231                Slog.d(TAG, mLogPrefix + "Requesting stats after " + beginTime + " but latest is "
232                        + currentStats.endTime);
233            }
234            // Nothing newer available.
235            return null;
236        }
237
238        // Truncate the endTime to just before the in-memory stats. Then, we'll append the
239        // in-memory stats to the results (if necessary) so as to avoid writing to disk too
240        // often.
241        final long truncatedEndTime = Math.min(currentStats.beginTime, endTime);
242
243        // Get the stats from disk.
244        List<T> results = mDatabase.queryUsageStats(intervalType, beginTime,
245                truncatedEndTime, combiner);
246        if (DEBUG) {
247            Slog.d(TAG, "Got " + (results != null ? results.size() : 0) + " results from disk");
248            Slog.d(TAG, "Current stats beginTime=" + currentStats.beginTime +
249                    " endTime=" + currentStats.endTime);
250        }
251
252        // Now check if the in-memory stats match the range and add them if they do.
253        if (beginTime < currentStats.endTime && endTime > currentStats.beginTime) {
254            if (DEBUG) {
255                Slog.d(TAG, mLogPrefix + "Returning in-memory stats");
256            }
257
258            if (results == null) {
259                results = new ArrayList<>();
260            }
261            combiner.combine(currentStats, true, results);
262        }
263
264        if (DEBUG) {
265            Slog.d(TAG, mLogPrefix + "Results: " + (results != null ? results.size() : 0));
266        }
267        return results;
268    }
269
270    List<UsageStats> queryUsageStats(int bucketType, long beginTime, long endTime) {
271        return queryStats(bucketType, beginTime, endTime, sUsageStatsCombiner);
272    }
273
274    List<ConfigurationStats> queryConfigurationStats(int bucketType, long beginTime, long endTime) {
275        return queryStats(bucketType, beginTime, endTime, sConfigStatsCombiner);
276    }
277
278    UsageEvents queryEvents(final long beginTime, final long endTime) {
279        final ArraySet<String> names = new ArraySet<>();
280        List<UsageEvents.Event> results = queryStats(UsageStatsManager.INTERVAL_DAILY,
281                beginTime, endTime, new StatCombiner<UsageEvents.Event>() {
282                    @Override
283                    public void combine(IntervalStats stats, boolean mutable,
284                            List<UsageEvents.Event> accumulatedResult) {
285                        if (stats.events == null) {
286                            return;
287                        }
288
289                        final int startIndex = stats.events.closestIndexOnOrAfter(beginTime);
290                        if (startIndex < 0) {
291                            return;
292                        }
293
294                        final int size = stats.events.size();
295                        for (int i = startIndex; i < size; i++) {
296                            if (stats.events.keyAt(i) >= endTime) {
297                                return;
298                            }
299
300                            final UsageEvents.Event event = stats.events.valueAt(i);
301                            names.add(event.mPackage);
302                            if (event.mClass != null) {
303                                names.add(event.mClass);
304                            }
305                            accumulatedResult.add(event);
306                        }
307                    }
308                });
309
310        if (results == null || results.isEmpty()) {
311            return null;
312        }
313
314        String[] table = names.toArray(new String[names.size()]);
315        Arrays.sort(table);
316        return new UsageEvents(results, table);
317    }
318
319    void persistActiveStats() {
320        if (mStatsChanged) {
321            Slog.i(TAG, mLogPrefix + "Flushing usage stats to disk");
322            try {
323                for (int i = 0; i < mCurrentStats.length; i++) {
324                    mDatabase.putUsageStats(i, mCurrentStats[i]);
325                }
326                mStatsChanged = false;
327            } catch (IOException e) {
328                Slog.e(TAG, mLogPrefix + "Failed to persist active stats", e);
329            }
330        }
331    }
332
333    private void rolloverStats() {
334        final long startTime = System.currentTimeMillis();
335        Slog.i(TAG, mLogPrefix + "Rolling over usage stats");
336
337        // Finish any ongoing events with an END_OF_DAY event. Make a note of which components
338        // need a new CONTINUE_PREVIOUS_DAY entry.
339        final Configuration previousConfig =
340                mCurrentStats[UsageStatsManager.INTERVAL_DAILY].activeConfiguration;
341        ArraySet<String> continuePreviousDay = new ArraySet<>();
342        for (IntervalStats stat : mCurrentStats) {
343            final int pkgCount = stat.packageStats.size();
344            for (int i = 0; i < pkgCount; i++) {
345                UsageStats pkgStats = stat.packageStats.valueAt(i);
346                if (pkgStats.mLastEvent == UsageEvents.Event.MOVE_TO_FOREGROUND ||
347                        pkgStats.mLastEvent == UsageEvents.Event.CONTINUE_PREVIOUS_DAY) {
348                    continuePreviousDay.add(pkgStats.mPackageName);
349                    stat.update(pkgStats.mPackageName, mDailyExpiryDate.getTimeInMillis() - 1,
350                            UsageEvents.Event.END_OF_DAY);
351                    mStatsChanged = true;
352                }
353            }
354
355            stat.updateConfigurationStats(null, mDailyExpiryDate.getTimeInMillis() - 1);
356        }
357
358        persistActiveStats();
359        mDatabase.prune();
360        loadActiveStats();
361
362        final int continueCount = continuePreviousDay.size();
363        for (int i = 0; i < continueCount; i++) {
364            String name = continuePreviousDay.valueAt(i);
365            final long beginTime = mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime;
366            for (IntervalStats stat : mCurrentStats) {
367                stat.update(name, beginTime, UsageEvents.Event.CONTINUE_PREVIOUS_DAY);
368                stat.updateConfigurationStats(previousConfig, beginTime);
369                mStatsChanged = true;
370            }
371        }
372        persistActiveStats();
373
374        final long totalTime = System.currentTimeMillis() - startTime;
375        Slog.i(TAG, mLogPrefix + "Rolling over usage stats complete. Took " + totalTime
376                + " milliseconds");
377    }
378
379    private void notifyStatsChanged() {
380        if (!mStatsChanged) {
381            mStatsChanged = true;
382            mListener.onStatsUpdated();
383        }
384    }
385
386    private void loadActiveStats() {
387        final long timeNow = System.currentTimeMillis();
388
389        final UnixCalendar tempCal = mDailyExpiryDate;
390        for (int intervalType = 0; intervalType < mCurrentStats.length; intervalType++) {
391            tempCal.setTimeInMillis(timeNow);
392            UnixCalendar.truncateTo(tempCal, intervalType);
393
394            if (mCurrentStats[intervalType] != null &&
395                    mCurrentStats[intervalType].beginTime == tempCal.getTimeInMillis()) {
396                // These are the same, no need to load them (in memory stats are always newer
397                // than persisted stats).
398                continue;
399            }
400
401            final long lastBeginTime = mDatabase.getLatestUsageStatsBeginTime(intervalType);
402            if (lastBeginTime > timeNow) {
403                Slog.e(TAG, mLogPrefix + "Latest usage stats for interval " +
404                        intervalType + " begins in the future");
405                mCurrentStats[intervalType] = null;
406            } else if (lastBeginTime >= tempCal.getTimeInMillis()) {
407                if (DEBUG) {
408                    Slog.d(TAG, mLogPrefix + "Loading existing stats @ " +
409                            sDateFormat.format(lastBeginTime) + "(" + lastBeginTime +
410                            ") for interval " + intervalType);
411                }
412                mCurrentStats[intervalType] = mDatabase.getLatestUsageStats(intervalType);
413            } else {
414                mCurrentStats[intervalType] = null;
415            }
416
417            if (mCurrentStats[intervalType] == null) {
418                if (DEBUG) {
419                    Slog.d(TAG, "Creating new stats @ " +
420                            sDateFormat.format(tempCal.getTimeInMillis()) + "(" +
421                            tempCal.getTimeInMillis() + ") for interval " + intervalType);
422
423                }
424                mCurrentStats[intervalType] = new IntervalStats();
425                mCurrentStats[intervalType].beginTime = tempCal.getTimeInMillis();
426                mCurrentStats[intervalType].endTime = timeNow;
427            }
428        }
429        mStatsChanged = false;
430        mDailyExpiryDate.setTimeInMillis(timeNow);
431        mDailyExpiryDate.addDays(1);
432        mDailyExpiryDate.truncateToDay();
433        Slog.i(TAG, mLogPrefix + "Rollover scheduled @ " +
434                sDateFormat.format(mDailyExpiryDate.getTimeInMillis()) + "(" +
435                tempCal.getTimeInMillis() + ")");
436    }
437
438
439    private static String eventToString(int eventType) {
440        switch (eventType) {
441            case UsageEvents.Event.NONE:
442                return "NONE";
443            case UsageEvents.Event.MOVE_TO_BACKGROUND:
444                return "MOVE_TO_BACKGROUND";
445            case UsageEvents.Event.MOVE_TO_FOREGROUND:
446                return "MOVE_TO_FOREGROUND";
447            case UsageEvents.Event.END_OF_DAY:
448                return "END_OF_DAY";
449            case UsageEvents.Event.CONTINUE_PREVIOUS_DAY:
450                return "CONTINUE_PREVIOUS_DAY";
451            case UsageEvents.Event.CONFIGURATION_CHANGE:
452                return "CONFIGURATION_CHANGE";
453            default:
454                return "UNKNOWN";
455        }
456    }
457}
458