2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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
17 package com.android.server.usage;
19 import android.app.usage.ConfigurationStats;
20 import android.app.usage.TimeSparseArray;
21 import android.app.usage.UsageEvents;
22 import android.app.usage.UsageEvents.Event;
23 import android.app.usage.UsageStats;
24 import android.app.usage.UsageStatsManager;
25 import android.content.pm.PackageInfo;
26 import android.content.pm.PackageManager;
27 import android.content.res.Configuration;
28 import android.os.SystemClock;
29 import android.content.Context;
30 import android.text.format.DateUtils;
31 import android.util.ArrayMap;
32 import android.util.ArraySet;
33 import android.util.Slog;
35 import com.android.internal.util.IndentingPrintWriter;
36 import com.android.server.usage.UsageStatsDatabase.StatCombiner;
39 import java.io.IOException;
40 import java.text.SimpleDateFormat;
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.List;
46 * A per-user UsageStatsService. All methods are meant to be called with the main lock held
47 * in UsageStatsService.
49 class 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;
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;
68 interface StatsUpdatedListener {
69 void onStatsUpdated();
72 UserUsageStatsService(Context context, int userId, File usageStatsDir,
73 StatsUpdatedListener listener) {
75 mDailyExpiryDate = new UnixCalendar(0);
76 mDatabase = new UsageStatsDatabase(usageStatsDir);
77 mCurrentStats = new IntervalStats[UsageStatsManager.INTERVAL_COUNT];
79 mLogPrefix = "User[" + Integer.toString(userId) + "] ";
83 void init(final long currentTimeMillis, final long deviceUsageTime) {
84 mDatabase.init(currentTimeMillis);
87 for (int i = 0; i < mCurrentStats.length; i++) {
88 mCurrentStats[i] = mDatabase.getLatestUsageStats(i);
89 if (mCurrentStats[i] == null) {
90 // Find out how many intervals we don't have data for.
91 // Ideally it should be all or none.
97 if (nullCount != mCurrentStats.length) {
98 // This is weird, but we shouldn't fail if something like this
100 Slog.w(TAG, mLogPrefix + "Some stats have no latest available");
102 // This must be first boot.
105 // By calling loadActiveStats, we will
106 // generate new stats for each bucket.
107 loadActiveStats(currentTimeMillis,/*force=*/ false, /*resetBeginIdleTime=*/ false);
109 // Set up the expiry date to be one day from the latest daily stat.
110 // This may actually be today and we will rollover on the first event
112 mDailyExpiryDate.setTimeInMillis(
113 mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime);
114 mDailyExpiryDate.addDays(1);
115 mDailyExpiryDate.truncateToDay();
116 Slog.i(TAG, mLogPrefix + "Rollover scheduled @ " +
117 sDateFormat.format(mDailyExpiryDate.getTimeInMillis()) +
118 "(" + mDailyExpiryDate.getTimeInMillis() + ")");
121 // Now close off any events that were open at the time this was saved.
122 for (IntervalStats stat : mCurrentStats) {
123 final int pkgCount = stat.packageStats.size();
124 for (int i = 0; i < pkgCount; i++) {
125 UsageStats pkgStats = stat.packageStats.valueAt(i);
126 if (pkgStats.mLastEvent == UsageEvents.Event.MOVE_TO_FOREGROUND ||
127 pkgStats.mLastEvent == UsageEvents.Event.CONTINUE_PREVIOUS_DAY) {
128 stat.update(pkgStats.mPackageName, stat.lastTimeSaved,
129 UsageEvents.Event.END_OF_DAY);
130 notifyStatsChanged();
134 stat.updateConfigurationStats(null, stat.lastTimeSaved);
137 if (mDatabase.isNewUpdate()) {
138 initializeDefaultsForApps(currentTimeMillis, deviceUsageTime,
139 mDatabase.isFirstUpdate());
144 * If any of the apps don't have a last-used entry, add one now.
145 * @param currentTimeMillis the current time
146 * @param firstUpdate if it is the first update, touch all installed apps, otherwise only
147 * touch the system apps
149 private void initializeDefaultsForApps(long currentTimeMillis, long deviceUsageTime,
150 boolean firstUpdate) {
151 PackageManager pm = mContext.getPackageManager();
152 List<PackageInfo> packages = pm.getInstalledPackages(0, mUserId);
153 final int packageCount = packages.size();
154 for (int i = 0; i < packageCount; i++) {
155 final PackageInfo pi = packages.get(i);
156 String packageName = pi.packageName;
157 if (pi.applicationInfo != null && (firstUpdate || pi.applicationInfo.isSystemApp())
158 && getBeginIdleTime(packageName) == -1) {
159 for (IntervalStats stats : mCurrentStats) {
160 stats.update(packageName, currentTimeMillis, Event.SYSTEM_INTERACTION);
161 stats.updateBeginIdleTime(packageName, deviceUsageTime);
162 mStatsChanged = true;
166 // Persist the new OTA-related access stats.
167 persistActiveStats();
170 void onTimeChanged(long oldTime, long newTime, boolean resetBeginIdleTime) {
171 persistActiveStats();
172 mDatabase.onTimeChanged(newTime - oldTime);
173 loadActiveStats(newTime, /* force= */ true, resetBeginIdleTime);
176 void reportEvent(UsageEvents.Event event, long deviceUsageTime) {
178 Slog.d(TAG, mLogPrefix + "Got usage event for " + event.mPackage
179 + "[" + event.mTimeStamp + "]: "
180 + eventToString(event.mEventType));
183 if (event.mTimeStamp >= mDailyExpiryDate.getTimeInMillis()) {
185 rolloverStats(event.mTimeStamp);
188 final IntervalStats currentDailyStats = mCurrentStats[UsageStatsManager.INTERVAL_DAILY];
190 final Configuration newFullConfig = event.mConfiguration;
191 if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE &&
192 currentDailyStats.activeConfiguration != null) {
193 // Make the event configuration a delta.
194 event.mConfiguration = Configuration.generateDelta(
195 currentDailyStats.activeConfiguration, newFullConfig);
198 // Add the event to the daily list.
199 if (currentDailyStats.events == null) {
200 currentDailyStats.events = new TimeSparseArray<>();
202 if (event.mEventType != UsageEvents.Event.SYSTEM_INTERACTION) {
203 currentDailyStats.events.put(event.mTimeStamp, event);
206 for (IntervalStats stats : mCurrentStats) {
207 if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE) {
208 stats.updateConfigurationStats(newFullConfig, event.mTimeStamp);
210 stats.update(event.mPackage, event.mTimeStamp, event.mEventType);
211 stats.updateBeginIdleTime(event.mPackage, deviceUsageTime);
215 notifyStatsChanged();
219 * Sets the beginIdleTime for each of the intervals.
220 * @param beginIdleTime
222 void setBeginIdleTime(String packageName, long beginIdleTime) {
223 for (IntervalStats stats : mCurrentStats) {
224 stats.updateBeginIdleTime(packageName, beginIdleTime);
226 notifyStatsChanged();
229 void setSystemLastUsedTime(String packageName, long lastUsedTime) {
230 for (IntervalStats stats : mCurrentStats) {
231 stats.updateSystemLastUsedTime(packageName, lastUsedTime);
233 notifyStatsChanged();
236 private static final StatCombiner<UsageStats> sUsageStatsCombiner =
237 new StatCombiner<UsageStats>() {
239 public void combine(IntervalStats stats, boolean mutable,
240 List<UsageStats> accResult) {
242 accResult.addAll(stats.packageStats.values());
246 final int statCount = stats.packageStats.size();
247 for (int i = 0; i < statCount; i++) {
248 accResult.add(new UsageStats(stats.packageStats.valueAt(i)));
253 private static final StatCombiner<ConfigurationStats> sConfigStatsCombiner =
254 new StatCombiner<ConfigurationStats>() {
256 public void combine(IntervalStats stats, boolean mutable,
257 List<ConfigurationStats> accResult) {
259 accResult.addAll(stats.configurations.values());
263 final int configCount = stats.configurations.size();
264 for (int i = 0; i < configCount; i++) {
265 accResult.add(new ConfigurationStats(stats.configurations.valueAt(i)));
271 * Generic query method that selects the appropriate IntervalStats for the specified time range
272 * and bucket, then calls the {@link com.android.server.usage.UsageStatsDatabase.StatCombiner}
273 * provided to select the stats to use from the IntervalStats object.
275 private <T> List<T> queryStats(int intervalType, final long beginTime, final long endTime,
276 StatCombiner<T> combiner) {
277 if (intervalType == UsageStatsManager.INTERVAL_BEST) {
278 intervalType = mDatabase.findBestFitBucket(beginTime, endTime);
279 if (intervalType < 0) {
280 // Nothing saved to disk yet, so every stat is just as equal (no rollover has
282 intervalType = UsageStatsManager.INTERVAL_DAILY;
286 if (intervalType < 0 || intervalType >= mCurrentStats.length) {
288 Slog.d(TAG, mLogPrefix + "Bad intervalType used " + intervalType);
293 final IntervalStats currentStats = mCurrentStats[intervalType];
296 Slog.d(TAG, mLogPrefix + "SELECT * FROM " + intervalType + " WHERE beginTime >= "
297 + beginTime + " AND endTime < " + endTime);
300 if (beginTime >= currentStats.endTime) {
302 Slog.d(TAG, mLogPrefix + "Requesting stats after " + beginTime + " but latest is "
303 + currentStats.endTime);
305 // Nothing newer available.
309 // Truncate the endTime to just before the in-memory stats. Then, we'll append the
310 // in-memory stats to the results (if necessary) so as to avoid writing to disk too
312 final long truncatedEndTime = Math.min(currentStats.beginTime, endTime);
314 // Get the stats from disk.
315 List<T> results = mDatabase.queryUsageStats(intervalType, beginTime,
316 truncatedEndTime, combiner);
318 Slog.d(TAG, "Got " + (results != null ? results.size() : 0) + " results from disk");
319 Slog.d(TAG, "Current stats beginTime=" + currentStats.beginTime +
320 " endTime=" + currentStats.endTime);
323 // Now check if the in-memory stats match the range and add them if they do.
324 if (beginTime < currentStats.endTime && endTime > currentStats.beginTime) {
326 Slog.d(TAG, mLogPrefix + "Returning in-memory stats");
329 if (results == null) {
330 results = new ArrayList<>();
332 combiner.combine(currentStats, true, results);
336 Slog.d(TAG, mLogPrefix + "Results: " + (results != null ? results.size() : 0));
341 List<UsageStats> queryUsageStats(int bucketType, long beginTime, long endTime) {
342 return queryStats(bucketType, beginTime, endTime, sUsageStatsCombiner);
345 List<ConfigurationStats> queryConfigurationStats(int bucketType, long beginTime, long endTime) {
346 return queryStats(bucketType, beginTime, endTime, sConfigStatsCombiner);
349 UsageEvents queryEvents(final long beginTime, final long endTime) {
350 final ArraySet<String> names = new ArraySet<>();
351 List<UsageEvents.Event> results = queryStats(UsageStatsManager.INTERVAL_DAILY,
352 beginTime, endTime, new StatCombiner<UsageEvents.Event>() {
354 public void combine(IntervalStats stats, boolean mutable,
355 List<UsageEvents.Event> accumulatedResult) {
356 if (stats.events == null) {
360 final int startIndex = stats.events.closestIndexOnOrAfter(beginTime);
361 if (startIndex < 0) {
365 final int size = stats.events.size();
366 for (int i = startIndex; i < size; i++) {
367 if (stats.events.keyAt(i) >= endTime) {
371 final UsageEvents.Event event = stats.events.valueAt(i);
372 names.add(event.mPackage);
373 if (event.mClass != null) {
374 names.add(event.mClass);
376 accumulatedResult.add(event);
381 if (results == null || results.isEmpty()) {
385 String[] table = names.toArray(new String[names.size()]);
387 return new UsageEvents(results, table);
390 long getBeginIdleTime(String packageName) {
391 final IntervalStats yearly = mCurrentStats[UsageStatsManager.INTERVAL_YEARLY];
392 UsageStats packageUsage;
393 if ((packageUsage = yearly.packageStats.get(packageName)) == null) {
396 return packageUsage.getBeginIdleTime();
400 long getSystemLastUsedTime(String packageName) {
401 final IntervalStats yearly = mCurrentStats[UsageStatsManager.INTERVAL_YEARLY];
402 UsageStats packageUsage;
403 if ((packageUsage = yearly.packageStats.get(packageName)) == null) {
406 return packageUsage.getLastTimeSystemUsed();
410 void persistActiveStats() {
412 Slog.i(TAG, mLogPrefix + "Flushing usage stats to disk");
414 for (int i = 0; i < mCurrentStats.length; i++) {
415 mDatabase.putUsageStats(i, mCurrentStats[i]);
417 mStatsChanged = false;
418 } catch (IOException e) {
419 Slog.e(TAG, mLogPrefix + "Failed to persist active stats", e);
424 private void rolloverStats(final long currentTimeMillis) {
425 final long startTime = SystemClock.elapsedRealtime();
426 Slog.i(TAG, mLogPrefix + "Rolling over usage stats");
428 // Finish any ongoing events with an END_OF_DAY event. Make a note of which components
429 // need a new CONTINUE_PREVIOUS_DAY entry.
430 final Configuration previousConfig =
431 mCurrentStats[UsageStatsManager.INTERVAL_DAILY].activeConfiguration;
432 ArraySet<String> continuePreviousDay = new ArraySet<>();
433 for (IntervalStats stat : mCurrentStats) {
434 final int pkgCount = stat.packageStats.size();
435 for (int i = 0; i < pkgCount; i++) {
436 UsageStats pkgStats = stat.packageStats.valueAt(i);
437 if (pkgStats.mLastEvent == UsageEvents.Event.MOVE_TO_FOREGROUND ||
438 pkgStats.mLastEvent == UsageEvents.Event.CONTINUE_PREVIOUS_DAY) {
439 continuePreviousDay.add(pkgStats.mPackageName);
440 stat.update(pkgStats.mPackageName, mDailyExpiryDate.getTimeInMillis() - 1,
441 UsageEvents.Event.END_OF_DAY);
442 notifyStatsChanged();
446 stat.updateConfigurationStats(null, mDailyExpiryDate.getTimeInMillis() - 1);
449 persistActiveStats();
450 mDatabase.prune(currentTimeMillis);
451 loadActiveStats(currentTimeMillis, /*force=*/ false, /*resetBeginIdleTime=*/ false);
453 final int continueCount = continuePreviousDay.size();
454 for (int i = 0; i < continueCount; i++) {
455 String name = continuePreviousDay.valueAt(i);
456 final long beginTime = mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime;
457 for (IntervalStats stat : mCurrentStats) {
458 stat.update(name, beginTime, UsageEvents.Event.CONTINUE_PREVIOUS_DAY);
459 stat.updateConfigurationStats(previousConfig, beginTime);
460 notifyStatsChanged();
463 persistActiveStats();
465 final long totalTime = SystemClock.elapsedRealtime() - startTime;
466 Slog.i(TAG, mLogPrefix + "Rolling over usage stats complete. Took " + totalTime
470 private void notifyStatsChanged() {
471 if (!mStatsChanged) {
472 mStatsChanged = true;
473 mListener.onStatsUpdated();
478 * @param force To force all in-memory stats to be reloaded.
480 private void loadActiveStats(final long currentTimeMillis, boolean force,
481 boolean resetBeginIdleTime) {
482 final UnixCalendar tempCal = mDailyExpiryDate;
483 for (int intervalType = 0; intervalType < mCurrentStats.length; intervalType++) {
484 tempCal.setTimeInMillis(currentTimeMillis);
485 UnixCalendar.truncateTo(tempCal, intervalType);
487 if (!force && mCurrentStats[intervalType] != null &&
488 mCurrentStats[intervalType].beginTime == tempCal.getTimeInMillis()) {
489 // These are the same, no need to load them (in memory stats are always newer
490 // than persisted stats).
494 final long lastBeginTime = mDatabase.getLatestUsageStatsBeginTime(intervalType);
495 if (lastBeginTime >= tempCal.getTimeInMillis()) {
497 Slog.d(TAG, mLogPrefix + "Loading existing stats @ " +
498 sDateFormat.format(lastBeginTime) + "(" + lastBeginTime +
499 ") for interval " + intervalType);
501 mCurrentStats[intervalType] = mDatabase.getLatestUsageStats(intervalType);
503 mCurrentStats[intervalType] = null;
506 if (mCurrentStats[intervalType] == null) {
508 Slog.d(TAG, "Creating new stats @ " +
509 sDateFormat.format(tempCal.getTimeInMillis()) + "(" +
510 tempCal.getTimeInMillis() + ") for interval " + intervalType);
513 mCurrentStats[intervalType] = new IntervalStats();
514 mCurrentStats[intervalType].beginTime = tempCal.getTimeInMillis();
515 mCurrentStats[intervalType].endTime = currentTimeMillis;
518 if (resetBeginIdleTime) {
519 for (UsageStats usageStats : mCurrentStats[intervalType].packageStats.values()) {
520 usageStats.mBeginIdleTime = 0;
524 mStatsChanged = false;
525 mDailyExpiryDate.setTimeInMillis(currentTimeMillis);
526 mDailyExpiryDate.addDays(1);
527 mDailyExpiryDate.truncateToDay();
528 Slog.i(TAG, mLogPrefix + "Rollover scheduled @ " +
529 sDateFormat.format(mDailyExpiryDate.getTimeInMillis()) + "(" +
530 tempCal.getTimeInMillis() + ")");
534 // -- DUMP related methods --
537 void checkin(final IndentingPrintWriter pw, final long screenOnTime) {
538 mDatabase.checkinDailyFiles(new UsageStatsDatabase.CheckinAction() {
540 public boolean checkin(IntervalStats stats) {
541 printIntervalStats(pw, stats, screenOnTime, false);
547 void dump(IndentingPrintWriter pw, final long screenOnTime) {
548 // This is not a check-in, only dump in-memory stats.
549 for (int interval = 0; interval < mCurrentStats.length; interval++) {
550 pw.print("In-memory ");
551 pw.print(intervalToString(interval));
552 pw.println(" stats");
553 printIntervalStats(pw, mCurrentStats[interval], screenOnTime, true);
557 private String formatDateTime(long dateTime, boolean pretty) {
559 return "\"" + DateUtils.formatDateTime(mContext, dateTime, sDateFormatFlags) + "\"";
561 return Long.toString(dateTime);
564 private String formatElapsedTime(long elapsedTime, boolean pretty) {
566 return "\"" + DateUtils.formatElapsedTime(elapsedTime / 1000) + "\"";
568 return Long.toString(elapsedTime);
571 void printIntervalStats(IndentingPrintWriter pw, IntervalStats stats, long screenOnTime,
572 boolean prettyDates) {
574 pw.printPair("timeRange", "\"" + DateUtils.formatDateRange(mContext,
575 stats.beginTime, stats.endTime, sDateFormatFlags) + "\"");
577 pw.printPair("beginTime", stats.beginTime);
578 pw.printPair("endTime", stats.endTime);
582 pw.println("packages");
584 final ArrayMap<String, UsageStats> pkgStats = stats.packageStats;
585 final int pkgCount = pkgStats.size();
586 for (int i = 0; i < pkgCount; i++) {
587 final UsageStats usageStats = pkgStats.valueAt(i);
588 pw.printPair("package", usageStats.mPackageName);
589 pw.printPair("totalTime",
590 formatElapsedTime(usageStats.mTotalTimeInForeground, prettyDates));
591 pw.printPair("lastTime", formatDateTime(usageStats.mLastTimeUsed, prettyDates));
592 pw.printPair("lastTimeSystem",
593 formatDateTime(usageStats.mLastTimeSystemUsed, prettyDates));
594 pw.printPair("inactiveTime",
595 formatElapsedTime(screenOnTime - usageStats.mBeginIdleTime, prettyDates));
600 pw.println("configurations");
602 final ArrayMap<Configuration, ConfigurationStats> configStats = stats.configurations;
603 final int configCount = configStats.size();
604 for (int i = 0; i < configCount; i++) {
605 final ConfigurationStats config = configStats.valueAt(i);
606 pw.printPair("config", Configuration.resourceQualifierString(config.mConfiguration));
607 pw.printPair("totalTime", formatElapsedTime(config.mTotalTimeActive, prettyDates));
608 pw.printPair("lastTime", formatDateTime(config.mLastTimeActive, prettyDates));
609 pw.printPair("count", config.mActivationCount);
614 pw.println("events");
616 final TimeSparseArray<UsageEvents.Event> events = stats.events;
617 final int eventCount = events != null ? events.size() : 0;
618 for (int i = 0; i < eventCount; i++) {
619 final UsageEvents.Event event = events.valueAt(i);
620 pw.printPair("time", formatDateTime(event.mTimeStamp, prettyDates));
621 pw.printPair("type", eventToString(event.mEventType));
622 pw.printPair("package", event.mPackage);
623 if (event.mClass != null) {
624 pw.printPair("class", event.mClass);
626 if (event.mConfiguration != null) {
627 pw.printPair("config", Configuration.resourceQualifierString(event.mConfiguration));
635 private static String intervalToString(int interval) {
637 case UsageStatsManager.INTERVAL_DAILY:
639 case UsageStatsManager.INTERVAL_WEEKLY:
641 case UsageStatsManager.INTERVAL_MONTHLY:
643 case UsageStatsManager.INTERVAL_YEARLY:
650 private static String eventToString(int eventType) {
652 case UsageEvents.Event.NONE:
654 case UsageEvents.Event.MOVE_TO_BACKGROUND:
655 return "MOVE_TO_BACKGROUND";
656 case UsageEvents.Event.MOVE_TO_FOREGROUND:
657 return "MOVE_TO_FOREGROUND";
658 case UsageEvents.Event.END_OF_DAY:
660 case UsageEvents.Event.CONTINUE_PREVIOUS_DAY:
661 return "CONTINUE_PREVIOUS_DAY";
662 case UsageEvents.Event.CONFIGURATION_CHANGE:
663 return "CONFIGURATION_CHANGE";
664 case UsageEvents.Event.SYSTEM_INTERACTION:
665 return "SYSTEM_INTERACTION";
666 case UsageEvents.Event.USER_INTERACTION:
667 return "USER_INTERACTION";