UserUsageStatsService.java revision 9d9607527f5bbf49c96565b63b90e36276b0dda7
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.TimeSparseArray;
20import android.app.usage.UsageEvents;
21import android.app.usage.UsageStats;
22import android.app.usage.UsageStatsManager;
23import android.util.ArraySet;
24import android.util.Slog;
25
26import java.io.File;
27import java.io.IOException;
28import java.text.SimpleDateFormat;
29import java.util.ArrayList;
30import java.util.Arrays;
31import java.util.Calendar;
32import java.util.List;
33
34/**
35 * A per-user UsageStatsService. All methods are meant to be called with the main lock held
36 * in UsageStatsService.
37 */
38class UserUsageStatsService {
39    private static final String TAG = "UsageStatsService";
40    private static final boolean DEBUG = UsageStatsService.DEBUG;
41    private static final SimpleDateFormat sDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
42
43    private final UsageStatsDatabase mDatabase;
44    private final IntervalStats[] mCurrentStats;
45    private boolean mStatsChanged = false;
46    private final Calendar mDailyExpiryDate;
47    private final StatsUpdatedListener mListener;
48    private final String mLogPrefix;
49
50    interface StatsUpdatedListener {
51        void onStatsUpdated();
52    }
53
54    UserUsageStatsService(int userId, File usageStatsDir, StatsUpdatedListener listener) {
55        mDailyExpiryDate = Calendar.getInstance();
56        mDatabase = new UsageStatsDatabase(usageStatsDir);
57        mCurrentStats = new IntervalStats[UsageStatsManager.INTERVAL_COUNT];
58        mListener = listener;
59        mLogPrefix = "User[" + Integer.toString(userId) + "] ";
60    }
61
62    void init() {
63        mDatabase.init();
64
65        int nullCount = 0;
66        for (int i = 0; i < mCurrentStats.length; i++) {
67            mCurrentStats[i] = mDatabase.getLatestUsageStats(i);
68            if (mCurrentStats[i] == null) {
69                // Find out how many intervals we don't have data for.
70                // Ideally it should be all or none.
71                nullCount++;
72            }
73        }
74
75        if (nullCount > 0) {
76            if (nullCount != mCurrentStats.length) {
77                // This is weird, but we shouldn't fail if something like this
78                // happens.
79                Slog.w(TAG, mLogPrefix + "Some stats have no latest available");
80            } else {
81                // This must be first boot.
82            }
83
84            // By calling loadActiveStats, we will
85            // generate new stats for each bucket.
86            loadActiveStats();
87        } else {
88            // Set up the expiry date to be one day from the latest daily stat.
89            // This may actually be today and we will rollover on the first event
90            // that is reported.
91            mDailyExpiryDate.setTimeInMillis(
92                    mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime);
93            mDailyExpiryDate.add(Calendar.DAY_OF_YEAR, 1);
94            UsageStatsUtils.truncateDateTo(UsageStatsManager.INTERVAL_DAILY, mDailyExpiryDate);
95            Slog.i(TAG, mLogPrefix + "Rollover scheduled for "
96                    + sDateFormat.format(mDailyExpiryDate.getTime()));
97        }
98
99        // Now close off any events that were open at the time this was saved.
100        for (IntervalStats stat : mCurrentStats) {
101            final int pkgCount = stat.stats.size();
102            for (int i = 0; i < pkgCount; i++) {
103                UsageStats pkgStats = stat.stats.valueAt(i);
104                if (pkgStats.mLastEvent == UsageEvents.Event.MOVE_TO_FOREGROUND ||
105                        pkgStats.mLastEvent == UsageEvents.Event.CONTINUE_PREVIOUS_DAY) {
106                    stat.update(pkgStats.mPackageName, stat.lastTimeSaved,
107                            UsageEvents.Event.END_OF_DAY);
108                    notifyStatsChanged();
109                }
110            }
111        }
112    }
113
114    void reportEvent(UsageEvents.Event event) {
115        if (DEBUG) {
116            Slog.d(TAG, mLogPrefix + "Got usage event for " + event.mPackage
117                    + "[" + event.getTimeStamp() + "]: "
118                    + eventToString(event.getEventType()));
119        }
120
121        if (event.getTimeStamp() >= mDailyExpiryDate.getTimeInMillis()) {
122            // Need to rollover
123            rolloverStats();
124        }
125
126        if (mCurrentStats[UsageStatsManager.INTERVAL_DAILY].events == null) {
127            mCurrentStats[UsageStatsManager.INTERVAL_DAILY].events = new TimeSparseArray<>();
128        }
129        mCurrentStats[UsageStatsManager.INTERVAL_DAILY].events.put(event.getTimeStamp(), event);
130
131        for (IntervalStats stats : mCurrentStats) {
132            stats.update(event.mPackage, event.getTimeStamp(),
133                    event.getEventType());
134        }
135
136        notifyStatsChanged();
137    }
138
139    List<UsageStats> queryUsageStats(int bucketType, long beginTime, long endTime) {
140        if (bucketType == UsageStatsManager.INTERVAL_BEST) {
141            bucketType = mDatabase.findBestFitBucket(beginTime, endTime);
142        }
143
144        if (bucketType < 0 || bucketType >= mCurrentStats.length) {
145            if (DEBUG) {
146                Slog.d(TAG, mLogPrefix + "Bad bucketType used " + bucketType);
147            }
148            return null;
149        }
150
151        if (beginTime >= mCurrentStats[bucketType].endTime) {
152            if (DEBUG) {
153                Slog.d(TAG, mLogPrefix + "Requesting stats after " + beginTime + " but latest is "
154                        + mCurrentStats[bucketType].endTime);
155            }
156            // Nothing newer available.
157            return null;
158
159        } else if (beginTime >= mCurrentStats[bucketType].beginTime) {
160            if (DEBUG) {
161                Slog.d(TAG, mLogPrefix + "Returning in-memory stats for bucket " + bucketType);
162            }
163            // Fast path for retrieving in-memory state.
164            ArrayList<UsageStats> results = new ArrayList<>();
165            final int packageCount = mCurrentStats[bucketType].stats.size();
166            for (int i = 0; i < packageCount; i++) {
167                results.add(new UsageStats(mCurrentStats[bucketType].stats.valueAt(i)));
168            }
169            return results;
170        }
171
172        // Flush any changes that were made to disk before we do a disk query.
173        // If we're not grabbing the ongoing stats, no need to persist.
174        persistActiveStats();
175
176        if (DEBUG) {
177            Slog.d(TAG, mLogPrefix + "SELECT * FROM " + bucketType + " WHERE beginTime >= "
178                    + beginTime + " AND endTime < " + endTime);
179        }
180
181        final List<UsageStats> results = mDatabase.queryUsageStats(bucketType, beginTime, endTime);
182        if (DEBUG) {
183            Slog.d(TAG, mLogPrefix + "Results: " + (results == null ? 0 : results.size()));
184        }
185        return results;
186    }
187
188    UsageEvents queryEvents(long beginTime, long endTime) {
189        if (endTime > mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime) {
190            if (beginTime > mCurrentStats[UsageStatsManager.INTERVAL_DAILY].endTime) {
191                return null;
192            }
193
194            TimeSparseArray<UsageEvents.Event> events =
195                    mCurrentStats[UsageStatsManager.INTERVAL_DAILY].events;
196            if (events == null) {
197                return null;
198            }
199
200            final int startIndex = events.closestIndexOnOrAfter(beginTime);
201            if (startIndex < 0) {
202                return null;
203            }
204
205            ArraySet<String> names = new ArraySet<>();
206            ArrayList<UsageEvents.Event> results = new ArrayList<>();
207            final int size = events.size();
208            for (int i = startIndex; i < size; i++) {
209                if (events.keyAt(i) >= endTime) {
210                    break;
211                }
212                final UsageEvents.Event event = events.valueAt(i);
213                names.add(event.mPackage);
214                if (event.mClass != null) {
215                    names.add(event.mClass);
216                }
217                results.add(event);
218            }
219            String[] table = names.toArray(new String[names.size()]);
220            Arrays.sort(table);
221            return new UsageEvents(results, table);
222        }
223
224        // TODO(adamlesinski): Query the previous days.
225        return null;
226    }
227
228    void persistActiveStats() {
229        if (mStatsChanged) {
230            Slog.i(TAG, mLogPrefix + "Flushing usage stats to disk");
231            try {
232                for (int i = 0; i < mCurrentStats.length; i++) {
233                    mDatabase.putUsageStats(i, mCurrentStats[i]);
234                }
235                mStatsChanged = false;
236            } catch (IOException e) {
237                Slog.e(TAG, mLogPrefix + "Failed to persist active stats", e);
238            }
239        }
240    }
241
242    private void rolloverStats() {
243        final long startTime = System.currentTimeMillis();
244        Slog.i(TAG, mLogPrefix + "Rolling over usage stats");
245
246        // Finish any ongoing events with an END_OF_DAY event. Make a note of which components
247        // need a new CONTINUE_PREVIOUS_DAY entry.
248        ArraySet<String> continuePreviousDay = new ArraySet<>();
249        for (IntervalStats stat : mCurrentStats) {
250            final int pkgCount = stat.stats.size();
251            for (int i = 0; i < pkgCount; i++) {
252                UsageStats pkgStats = stat.stats.valueAt(i);
253                if (pkgStats.mLastEvent == UsageEvents.Event.MOVE_TO_FOREGROUND ||
254                        pkgStats.mLastEvent == UsageEvents.Event.CONTINUE_PREVIOUS_DAY) {
255                    continuePreviousDay.add(pkgStats.mPackageName);
256                    stat.update(pkgStats.mPackageName,
257                            mDailyExpiryDate.getTimeInMillis() - 1, UsageEvents.Event.END_OF_DAY);
258                    mStatsChanged = true;
259                }
260            }
261        }
262
263        persistActiveStats();
264        mDatabase.prune();
265        loadActiveStats();
266
267        final int continueCount = continuePreviousDay.size();
268        for (int i = 0; i < continueCount; i++) {
269            String name = continuePreviousDay.valueAt(i);
270            for (IntervalStats stat : mCurrentStats) {
271                stat.update(name, mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime,
272                        UsageEvents.Event.CONTINUE_PREVIOUS_DAY);
273                mStatsChanged = true;
274            }
275        }
276        persistActiveStats();
277
278        final long totalTime = System.currentTimeMillis() - startTime;
279        Slog.i(TAG, mLogPrefix + "Rolling over usage stats complete. Took " + totalTime
280                + " milliseconds");
281    }
282
283    private void notifyStatsChanged() {
284        if (!mStatsChanged) {
285            mStatsChanged = true;
286            mListener.onStatsUpdated();
287        }
288    }
289
290    private void loadActiveStats() {
291        final long timeNow = System.currentTimeMillis();
292
293        Calendar tempCal = mDailyExpiryDate;
294        for (int bucketType = 0; bucketType < mCurrentStats.length; bucketType++) {
295            tempCal.setTimeInMillis(timeNow);
296            UsageStatsUtils.truncateDateTo(bucketType, tempCal);
297
298            if (mCurrentStats[bucketType] != null &&
299                    mCurrentStats[bucketType].beginTime == tempCal.getTimeInMillis()) {
300                // These are the same, no need to load them (in memory stats are always newer
301                // than persisted stats).
302                continue;
303            }
304
305            final long lastBeginTime = mDatabase.getLatestUsageStatsBeginTime(bucketType);
306            if (lastBeginTime >= tempCal.getTimeInMillis()) {
307                if (DEBUG) {
308                    Slog.d(TAG, mLogPrefix + "Loading existing stats (" + lastBeginTime +
309                            ") for bucket " + bucketType);
310                }
311                mCurrentStats[bucketType] = mDatabase.getLatestUsageStats(bucketType);
312                if (DEBUG) {
313                    if (mCurrentStats[bucketType] != null) {
314                        Slog.d(TAG, mLogPrefix + "Found " +
315                                (mCurrentStats[bucketType].events == null ?
316                                        0 : mCurrentStats[bucketType].events.size()) +
317                                " events");
318                    }
319                }
320            } else {
321                mCurrentStats[bucketType] = null;
322            }
323
324            if (mCurrentStats[bucketType] == null) {
325                if (DEBUG) {
326                    Slog.d(TAG, "Creating new stats (" + tempCal.getTimeInMillis() +
327                            ") for bucket " + bucketType);
328
329                }
330                mCurrentStats[bucketType] = new IntervalStats();
331                mCurrentStats[bucketType].beginTime = tempCal.getTimeInMillis();
332                mCurrentStats[bucketType].endTime = timeNow;
333            }
334        }
335        mStatsChanged = false;
336        mDailyExpiryDate.setTimeInMillis(timeNow);
337        mDailyExpiryDate.add(Calendar.DAY_OF_YEAR, 1);
338        UsageStatsUtils.truncateDateTo(UsageStatsManager.INTERVAL_DAILY, mDailyExpiryDate);
339        Slog.i(TAG, mLogPrefix + "Rollover scheduled for "
340                + sDateFormat.format(mDailyExpiryDate.getTime()));
341    }
342
343
344    private static String eventToString(int eventType) {
345        switch (eventType) {
346            case UsageEvents.Event.NONE:
347                return "NONE";
348            case UsageEvents.Event.MOVE_TO_BACKGROUND:
349                return "MOVE_TO_BACKGROUND";
350            case UsageEvents.Event.MOVE_TO_FOREGROUND:
351                return "MOVE_TO_FOREGROUND";
352            case UsageEvents.Event.END_OF_DAY:
353                return "END_OF_DAY";
354            case UsageEvents.Event.CONTINUE_PREVIOUS_DAY:
355                return "CONTINUE_PREVIOUS_DAY";
356            default:
357                return "UNKNOWN";
358        }
359    }
360}
361