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