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 if (event.mShortcutId != null) { 553 pw.printPair("shortcutId", event.mShortcutId); 554 } 555 pw.println(); 556 } 557 pw.decreaseIndent(); 558 pw.decreaseIndent(); 559 } 560 561 private static String intervalToString(int interval) { 562 switch (interval) { 563 case UsageStatsManager.INTERVAL_DAILY: 564 return "daily"; 565 case UsageStatsManager.INTERVAL_WEEKLY: 566 return "weekly"; 567 case UsageStatsManager.INTERVAL_MONTHLY: 568 return "monthly"; 569 case UsageStatsManager.INTERVAL_YEARLY: 570 return "yearly"; 571 default: 572 return "?"; 573 } 574 } 575 576 private static String eventToString(int eventType) { 577 switch (eventType) { 578 case UsageEvents.Event.NONE: 579 return "NONE"; 580 case UsageEvents.Event.MOVE_TO_BACKGROUND: 581 return "MOVE_TO_BACKGROUND"; 582 case UsageEvents.Event.MOVE_TO_FOREGROUND: 583 return "MOVE_TO_FOREGROUND"; 584 case UsageEvents.Event.END_OF_DAY: 585 return "END_OF_DAY"; 586 case UsageEvents.Event.CONTINUE_PREVIOUS_DAY: 587 return "CONTINUE_PREVIOUS_DAY"; 588 case UsageEvents.Event.CONFIGURATION_CHANGE: 589 return "CONFIGURATION_CHANGE"; 590 case UsageEvents.Event.SYSTEM_INTERACTION: 591 return "SYSTEM_INTERACTION"; 592 case UsageEvents.Event.USER_INTERACTION: 593 return "USER_INTERACTION"; 594 case UsageEvents.Event.SHORTCUT_INVOCATION: 595 return "SHORTCUT_INVOCATION"; 596 default: 597 return "UNKNOWN"; 598 } 599 } 600 601 byte[] getBackupPayload(String key){ 602 return mDatabase.getBackupPayload(key); 603 } 604 605 void applyRestoredPayload(String key, byte[] payload){ 606 mDatabase.applyRestoredPayload(key, payload); 607 } 608} 609