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