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 interface StatsUpdatedListener { 69 void onStatsUpdated(); 70 } 71 72 UserUsageStatsService(Context context, int userId, File usageStatsDir, 73 StatsUpdatedListener listener) { 74 mContext = context; 75 mDailyExpiryDate = new UnixCalendar(0); 76 mDatabase = new UsageStatsDatabase(usageStatsDir); 77 mCurrentStats = new IntervalStats[UsageStatsManager.INTERVAL_COUNT]; 78 mListener = listener; 79 mLogPrefix = "User[" + Integer.toString(userId) + "] "; 80 mUserId = userId; 81 } 82 83 void init(final long currentTimeMillis, final long deviceUsageTime) { 84 mDatabase.init(currentTimeMillis); 85 86 int nullCount = 0; 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. 92 nullCount++; 93 } 94 } 95 96 if (nullCount > 0) { 97 if (nullCount != mCurrentStats.length) { 98 // This is weird, but we shouldn't fail if something like this 99 // happens. 100 Slog.w(TAG, mLogPrefix + "Some stats have no latest available"); 101 } else { 102 // This must be first boot. 103 } 104 105 // By calling loadActiveStats, we will 106 // generate new stats for each bucket. 107 loadActiveStats(currentTimeMillis,/*force=*/ false, /*resetBeginIdleTime=*/ false); 108 } else { 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 111 // that is reported. 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() + ")"); 119 } 120 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(); 131 } 132 } 133 134 stat.updateConfigurationStats(null, stat.lastTimeSaved); 135 } 136 137 if (mDatabase.isNewUpdate()) { 138 initializeDefaultsForApps(currentTimeMillis, deviceUsageTime, 139 mDatabase.isFirstUpdate()); 140 } 141 } 142 143 /** 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 148 */ 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; 163 } 164 } 165 } 166 // Persist the new OTA-related access stats. 167 persistActiveStats(); 168 } 169 170 void onTimeChanged(long oldTime, long newTime, boolean resetBeginIdleTime) { 171 persistActiveStats(); 172 mDatabase.onTimeChanged(newTime - oldTime); 173 loadActiveStats(newTime, /* force= */ true, resetBeginIdleTime); 174 } 175 176 void reportEvent(UsageEvents.Event event, long deviceUsageTime) { 177 if (DEBUG) { 178 Slog.d(TAG, mLogPrefix + "Got usage event for " + event.mPackage 179 + "[" + event.mTimeStamp + "]: " 180 + eventToString(event.mEventType)); 181 } 182 183 if (event.mTimeStamp >= mDailyExpiryDate.getTimeInMillis()) { 184 // Need to rollover 185 rolloverStats(event.mTimeStamp); 186 } 187 188 final IntervalStats currentDailyStats = mCurrentStats[UsageStatsManager.INTERVAL_DAILY]; 189 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); 196 } 197 198 // Add the event to the daily list. 199 if (currentDailyStats.events == null) { 200 currentDailyStats.events = new TimeSparseArray<>(); 201 } 202 if (event.mEventType != UsageEvents.Event.SYSTEM_INTERACTION) { 203 currentDailyStats.events.put(event.mTimeStamp, event); 204 } 205 206 for (IntervalStats stats : mCurrentStats) { 207 if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE) { 208 stats.updateConfigurationStats(newFullConfig, event.mTimeStamp); 209 } else { 210 stats.update(event.mPackage, event.mTimeStamp, event.mEventType); 211 stats.updateBeginIdleTime(event.mPackage, deviceUsageTime); 212 } 213 } 214 215 notifyStatsChanged(); 216 } 217 218 /** 219 * Sets the beginIdleTime for each of the intervals. 220 * @param beginIdleTime 221 */ 222 void setBeginIdleTime(String packageName, long beginIdleTime) { 223 for (IntervalStats stats : mCurrentStats) { 224 stats.updateBeginIdleTime(packageName, beginIdleTime); 225 } 226 notifyStatsChanged(); 227 } 228 229 void setSystemLastUsedTime(String packageName, long lastUsedTime) { 230 for (IntervalStats stats : mCurrentStats) { 231 stats.updateSystemLastUsedTime(packageName, lastUsedTime); 232 } 233 notifyStatsChanged(); 234 } 235 236 private static final StatCombiner<UsageStats> sUsageStatsCombiner = 237 new StatCombiner<UsageStats>() { 238 @Override 239 public void combine(IntervalStats stats, boolean mutable, 240 List<UsageStats> accResult) { 241 if (!mutable) { 242 accResult.addAll(stats.packageStats.values()); 243 return; 244 } 245 246 final int statCount = stats.packageStats.size(); 247 for (int i = 0; i < statCount; i++) { 248 accResult.add(new UsageStats(stats.packageStats.valueAt(i))); 249 } 250 } 251 }; 252 253 private static final StatCombiner<ConfigurationStats> sConfigStatsCombiner = 254 new StatCombiner<ConfigurationStats>() { 255 @Override 256 public void combine(IntervalStats stats, boolean mutable, 257 List<ConfigurationStats> accResult) { 258 if (!mutable) { 259 accResult.addAll(stats.configurations.values()); 260 return; 261 } 262 263 final int configCount = stats.configurations.size(); 264 for (int i = 0; i < configCount; i++) { 265 accResult.add(new ConfigurationStats(stats.configurations.valueAt(i))); 266 } 267 } 268 }; 269 270 /** 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. 274 */ 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 281 // occurred. 282 intervalType = UsageStatsManager.INTERVAL_DAILY; 283 } 284 } 285 286 if (intervalType < 0 || intervalType >= mCurrentStats.length) { 287 if (DEBUG) { 288 Slog.d(TAG, mLogPrefix + "Bad intervalType used " + intervalType); 289 } 290 return null; 291 } 292 293 final IntervalStats currentStats = mCurrentStats[intervalType]; 294 295 if (DEBUG) { 296 Slog.d(TAG, mLogPrefix + "SELECT * FROM " + intervalType + " WHERE beginTime >= " 297 + beginTime + " AND endTime < " + endTime); 298 } 299 300 if (beginTime >= currentStats.endTime) { 301 if (DEBUG) { 302 Slog.d(TAG, mLogPrefix + "Requesting stats after " + beginTime + " but latest is " 303 + currentStats.endTime); 304 } 305 // Nothing newer available. 306 return null; 307 } 308 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 311 // often. 312 final long truncatedEndTime = Math.min(currentStats.beginTime, endTime); 313 314 // Get the stats from disk. 315 List<T> results = mDatabase.queryUsageStats(intervalType, beginTime, 316 truncatedEndTime, combiner); 317 if (DEBUG) { 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); 321 } 322 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) { 325 if (DEBUG) { 326 Slog.d(TAG, mLogPrefix + "Returning in-memory stats"); 327 } 328 329 if (results == null) { 330 results = new ArrayList<>(); 331 } 332 combiner.combine(currentStats, true, results); 333 } 334 335 if (DEBUG) { 336 Slog.d(TAG, mLogPrefix + "Results: " + (results != null ? results.size() : 0)); 337 } 338 return results; 339 } 340 341 List<UsageStats> queryUsageStats(int bucketType, long beginTime, long endTime) { 342 return queryStats(bucketType, beginTime, endTime, sUsageStatsCombiner); 343 } 344 345 List<ConfigurationStats> queryConfigurationStats(int bucketType, long beginTime, long endTime) { 346 return queryStats(bucketType, beginTime, endTime, sConfigStatsCombiner); 347 } 348 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>() { 353 @Override 354 public void combine(IntervalStats stats, boolean mutable, 355 List<UsageEvents.Event> accumulatedResult) { 356 if (stats.events == null) { 357 return; 358 } 359 360 final int startIndex = stats.events.closestIndexOnOrAfter(beginTime); 361 if (startIndex < 0) { 362 return; 363 } 364 365 final int size = stats.events.size(); 366 for (int i = startIndex; i < size; i++) { 367 if (stats.events.keyAt(i) >= endTime) { 368 return; 369 } 370 371 final UsageEvents.Event event = stats.events.valueAt(i); 372 names.add(event.mPackage); 373 if (event.mClass != null) { 374 names.add(event.mClass); 375 } 376 accumulatedResult.add(event); 377 } 378 } 379 }); 380 381 if (results == null || results.isEmpty()) { 382 return null; 383 } 384 385 String[] table = names.toArray(new String[names.size()]); 386 Arrays.sort(table); 387 return new UsageEvents(results, table); 388 } 389 390 long getBeginIdleTime(String packageName) { 391 final IntervalStats yearly = mCurrentStats[UsageStatsManager.INTERVAL_YEARLY]; 392 UsageStats packageUsage; 393 if ((packageUsage = yearly.packageStats.get(packageName)) == null) { 394 return -1; 395 } else { 396 return packageUsage.getBeginIdleTime(); 397 } 398 } 399 400 long getSystemLastUsedTime(String packageName) { 401 final IntervalStats yearly = mCurrentStats[UsageStatsManager.INTERVAL_YEARLY]; 402 UsageStats packageUsage; 403 if ((packageUsage = yearly.packageStats.get(packageName)) == null) { 404 return -1; 405 } else { 406 return packageUsage.getLastTimeSystemUsed(); 407 } 408 } 409 410 void persistActiveStats() { 411 if (mStatsChanged) { 412 Slog.i(TAG, mLogPrefix + "Flushing usage stats to disk"); 413 try { 414 for (int i = 0; i < mCurrentStats.length; i++) { 415 mDatabase.putUsageStats(i, mCurrentStats[i]); 416 } 417 mStatsChanged = false; 418 } catch (IOException e) { 419 Slog.e(TAG, mLogPrefix + "Failed to persist active stats", e); 420 } 421 } 422 } 423 424 private void rolloverStats(final long currentTimeMillis) { 425 final long startTime = SystemClock.elapsedRealtime(); 426 Slog.i(TAG, mLogPrefix + "Rolling over usage stats"); 427 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(); 443 } 444 } 445 446 stat.updateConfigurationStats(null, mDailyExpiryDate.getTimeInMillis() - 1); 447 } 448 449 persistActiveStats(); 450 mDatabase.prune(currentTimeMillis); 451 loadActiveStats(currentTimeMillis, /*force=*/ false, /*resetBeginIdleTime=*/ false); 452 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(); 461 } 462 } 463 persistActiveStats(); 464 465 final long totalTime = SystemClock.elapsedRealtime() - startTime; 466 Slog.i(TAG, mLogPrefix + "Rolling over usage stats complete. Took " + totalTime 467 + " milliseconds"); 468 } 469 470 private void notifyStatsChanged() { 471 if (!mStatsChanged) { 472 mStatsChanged = true; 473 mListener.onStatsUpdated(); 474 } 475 } 476 477 /** 478 * @param force To force all in-memory stats to be reloaded. 479 */ 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); 486 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). 491 continue; 492 } 493 494 final long lastBeginTime = mDatabase.getLatestUsageStatsBeginTime(intervalType); 495 if (lastBeginTime >= tempCal.getTimeInMillis()) { 496 if (DEBUG) { 497 Slog.d(TAG, mLogPrefix + "Loading existing stats @ " + 498 sDateFormat.format(lastBeginTime) + "(" + lastBeginTime + 499 ") for interval " + intervalType); 500 } 501 mCurrentStats[intervalType] = mDatabase.getLatestUsageStats(intervalType); 502 } else { 503 mCurrentStats[intervalType] = null; 504 } 505 506 if (mCurrentStats[intervalType] == null) { 507 if (DEBUG) { 508 Slog.d(TAG, "Creating new stats @ " + 509 sDateFormat.format(tempCal.getTimeInMillis()) + "(" + 510 tempCal.getTimeInMillis() + ") for interval " + intervalType); 511 512 } 513 mCurrentStats[intervalType] = new IntervalStats(); 514 mCurrentStats[intervalType].beginTime = tempCal.getTimeInMillis(); 515 mCurrentStats[intervalType].endTime = currentTimeMillis; 516 } 517 518 if (resetBeginIdleTime) { 519 for (UsageStats usageStats : mCurrentStats[intervalType].packageStats.values()) { 520 usageStats.mBeginIdleTime = 0; 521 } 522 } 523 } 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() + ")"); 531 } 532 533 // 534 // -- DUMP related methods -- 535 // 536 537 void checkin(final IndentingPrintWriter pw, final long screenOnTime) { 538 mDatabase.checkinDailyFiles(new UsageStatsDatabase.CheckinAction() { 539 @Override 540 public boolean checkin(IntervalStats stats) { 541 printIntervalStats(pw, stats, screenOnTime, false); 542 return true; 543 } 544 }); 545 } 546 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); 554 } 555 } 556 557 private String formatDateTime(long dateTime, boolean pretty) { 558 if (pretty) { 559 return "\"" + DateUtils.formatDateTime(mContext, dateTime, sDateFormatFlags) + "\""; 560 } 561 return Long.toString(dateTime); 562 } 563 564 private String formatElapsedTime(long elapsedTime, boolean pretty) { 565 if (pretty) { 566 return "\"" + DateUtils.formatElapsedTime(elapsedTime / 1000) + "\""; 567 } 568 return Long.toString(elapsedTime); 569 } 570 571 void printIntervalStats(IndentingPrintWriter pw, IntervalStats stats, long screenOnTime, 572 boolean prettyDates) { 573 if (prettyDates) { 574 pw.printPair("timeRange", "\"" + DateUtils.formatDateRange(mContext, 575 stats.beginTime, stats.endTime, sDateFormatFlags) + "\""); 576 } else { 577 pw.printPair("beginTime", stats.beginTime); 578 pw.printPair("endTime", stats.endTime); 579 } 580 pw.println(); 581 pw.increaseIndent(); 582 pw.println("packages"); 583 pw.increaseIndent(); 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)); 596 pw.println(); 597 } 598 pw.decreaseIndent(); 599 600 pw.println("configurations"); 601 pw.increaseIndent(); 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); 610 pw.println(); 611 } 612 pw.decreaseIndent(); 613 614 pw.println("events"); 615 pw.increaseIndent(); 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); 625 } 626 if (event.mConfiguration != null) { 627 pw.printPair("config", Configuration.resourceQualifierString(event.mConfiguration)); 628 } 629 pw.println(); 630 } 631 pw.decreaseIndent(); 632 pw.decreaseIndent(); 633 } 634 635 private static String intervalToString(int interval) { 636 switch (interval) { 637 case UsageStatsManager.INTERVAL_DAILY: 638 return "daily"; 639 case UsageStatsManager.INTERVAL_WEEKLY: 640 return "weekly"; 641 case UsageStatsManager.INTERVAL_MONTHLY: 642 return "monthly"; 643 case UsageStatsManager.INTERVAL_YEARLY: 644 return "yearly"; 645 default: 646 return "?"; 647 } 648 } 649 650 private static String eventToString(int eventType) { 651 switch (eventType) { 652 case UsageEvents.Event.NONE: 653 return "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: 659 return "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"; 668 default: 669 return "UNKNOWN"; 670 } 671 } 672} 673