UserUsageStatsService.java revision 37a46b48dcb7e34ee3669cfb2ed78af08bfca3c7
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.util.ArraySet; 26import android.util.Slog; 27 28import com.android.server.usage.UsageStatsDatabase.StatCombiner; 29 30import java.io.File; 31import java.io.IOException; 32import java.text.SimpleDateFormat; 33import java.util.ArrayList; 34import java.util.Arrays; 35import java.util.List; 36 37/** 38 * A per-user UsageStatsService. All methods are meant to be called with the main lock held 39 * in UsageStatsService. 40 */ 41class UserUsageStatsService { 42 private static final String TAG = "UsageStatsService"; 43 private static final boolean DEBUG = UsageStatsService.DEBUG; 44 private static final SimpleDateFormat sDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 45 46 private final UsageStatsDatabase mDatabase; 47 private final IntervalStats[] mCurrentStats; 48 private boolean mStatsChanged = false; 49 private final UnixCalendar mDailyExpiryDate; 50 private final StatsUpdatedListener mListener; 51 private final String mLogPrefix; 52 53 interface StatsUpdatedListener { 54 void onStatsUpdated(); 55 } 56 57 UserUsageStatsService(int userId, File usageStatsDir, StatsUpdatedListener listener) { 58 mDailyExpiryDate = new UnixCalendar(0); 59 mDatabase = new UsageStatsDatabase(usageStatsDir); 60 mCurrentStats = new IntervalStats[UsageStatsManager.INTERVAL_COUNT]; 61 mListener = listener; 62 mLogPrefix = "User[" + Integer.toString(userId) + "] "; 63 } 64 65 void init() { 66 mDatabase.init(); 67 68 final long timeNow = System.currentTimeMillis(); 69 int nullCount = 0; 70 for (int i = 0; i < mCurrentStats.length; i++) { 71 mCurrentStats[i] = mDatabase.getLatestUsageStats(i); 72 if (mCurrentStats[i] == null) { 73 // Find out how many intervals we don't have data for. 74 // Ideally it should be all or none. 75 nullCount++; 76 } else if (mCurrentStats[i].beginTime > timeNow) { 77 Slog.e(TAG, mLogPrefix + "Interval " + i + " has stat in the future " + 78 mCurrentStats[i].beginTime); 79 mCurrentStats[i] = null; 80 nullCount++; 81 } 82 } 83 84 if (nullCount > 0) { 85 if (nullCount != mCurrentStats.length) { 86 // This is weird, but we shouldn't fail if something like this 87 // happens. 88 Slog.w(TAG, mLogPrefix + "Some stats have no latest available"); 89 } else { 90 // This must be first boot. 91 } 92 93 // By calling loadActiveStats, we will 94 // generate new stats for each bucket. 95 loadActiveStats(); 96 } else { 97 // Set up the expiry date to be one day from the latest daily stat. 98 // This may actually be today and we will rollover on the first event 99 // that is reported. 100 mDailyExpiryDate.setTimeInMillis( 101 mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime); 102 mDailyExpiryDate.addDays(1); 103 mDailyExpiryDate.truncateToDay(); 104 Slog.i(TAG, mLogPrefix + "Rollover scheduled @ " + 105 sDateFormat.format(mDailyExpiryDate.getTimeInMillis()) + 106 "(" + mDailyExpiryDate.getTimeInMillis() + ")"); 107 } 108 109 // Now close off any events that were open at the time this was saved. 110 for (IntervalStats stat : mCurrentStats) { 111 final int pkgCount = stat.packageStats.size(); 112 for (int i = 0; i < pkgCount; i++) { 113 UsageStats pkgStats = stat.packageStats.valueAt(i); 114 if (pkgStats.mLastEvent == UsageEvents.Event.MOVE_TO_FOREGROUND || 115 pkgStats.mLastEvent == UsageEvents.Event.CONTINUE_PREVIOUS_DAY) { 116 stat.update(pkgStats.mPackageName, stat.lastTimeSaved, 117 UsageEvents.Event.END_OF_DAY); 118 notifyStatsChanged(); 119 } 120 } 121 122 stat.updateConfigurationStats(null, stat.lastTimeSaved); 123 } 124 } 125 126 void reportEvent(UsageEvents.Event event) { 127 if (DEBUG) { 128 Slog.d(TAG, mLogPrefix + "Got usage event for " + event.mPackage 129 + "[" + event.mTimeStamp + "]: " 130 + eventToString(event.mEventType)); 131 } 132 133 if (event.mTimeStamp >= mDailyExpiryDate.getTimeInMillis()) { 134 // Need to rollover 135 rolloverStats(); 136 } 137 138 final IntervalStats currentDailyStats = mCurrentStats[UsageStatsManager.INTERVAL_DAILY]; 139 140 final Configuration newFullConfig = event.mConfiguration; 141 if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE && 142 currentDailyStats.activeConfiguration != null) { 143 // Make the event configuration a delta. 144 event.mConfiguration = Configuration.generateDelta( 145 currentDailyStats.activeConfiguration, newFullConfig); 146 } 147 148 // Add the event to the daily list. 149 if (currentDailyStats.events == null) { 150 currentDailyStats.events = new TimeSparseArray<>(); 151 } 152 currentDailyStats.events.put(event.mTimeStamp, event); 153 154 for (IntervalStats stats : mCurrentStats) { 155 if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE) { 156 stats.updateConfigurationStats(newFullConfig, event.mTimeStamp); 157 } else { 158 stats.update(event.mPackage, event.mTimeStamp, event.mEventType); 159 } 160 } 161 162 notifyStatsChanged(); 163 } 164 165 private static final StatCombiner<UsageStats> sUsageStatsCombiner = 166 new StatCombiner<UsageStats>() { 167 @Override 168 public void combine(IntervalStats stats, boolean mutable, 169 List<UsageStats> accResult) { 170 if (!mutable) { 171 accResult.addAll(stats.packageStats.values()); 172 return; 173 } 174 175 final int statCount = stats.packageStats.size(); 176 for (int i = 0; i < statCount; i++) { 177 accResult.add(new UsageStats(stats.packageStats.valueAt(i))); 178 } 179 } 180 }; 181 182 private static final StatCombiner<ConfigurationStats> sConfigStatsCombiner = 183 new StatCombiner<ConfigurationStats>() { 184 @Override 185 public void combine(IntervalStats stats, boolean mutable, 186 List<ConfigurationStats> accResult) { 187 if (!mutable) { 188 accResult.addAll(stats.configurations.values()); 189 return; 190 } 191 192 final int configCount = stats.configurations.size(); 193 for (int i = 0; i < configCount; i++) { 194 accResult.add(new ConfigurationStats(stats.configurations.valueAt(i))); 195 } 196 } 197 }; 198 199 /** 200 * Generic query method that selects the appropriate IntervalStats for the specified time range 201 * and bucket, then calls the {@link com.android.server.usage.UsageStatsDatabase.StatCombiner} 202 * provided to select the stats to use from the IntervalStats object. 203 */ 204 private <T> List<T> queryStats(int intervalType, final long beginTime, final long endTime, 205 StatCombiner<T> combiner) { 206 if (intervalType == UsageStatsManager.INTERVAL_BEST) { 207 intervalType = mDatabase.findBestFitBucket(beginTime, endTime); 208 if (intervalType < 0) { 209 // Nothing saved to disk yet, so every stat is just as equal (no rollover has 210 // occurred. 211 intervalType = UsageStatsManager.INTERVAL_DAILY; 212 } 213 } 214 215 if (intervalType < 0 || intervalType >= mCurrentStats.length) { 216 if (DEBUG) { 217 Slog.d(TAG, mLogPrefix + "Bad intervalType used " + intervalType); 218 } 219 return null; 220 } 221 222 final IntervalStats currentStats = mCurrentStats[intervalType]; 223 224 if (DEBUG) { 225 Slog.d(TAG, mLogPrefix + "SELECT * FROM " + intervalType + " WHERE beginTime >= " 226 + beginTime + " AND endTime < " + endTime); 227 } 228 229 if (beginTime >= currentStats.endTime) { 230 if (DEBUG) { 231 Slog.d(TAG, mLogPrefix + "Requesting stats after " + beginTime + " but latest is " 232 + currentStats.endTime); 233 } 234 // Nothing newer available. 235 return null; 236 } 237 238 // Truncate the endTime to just before the in-memory stats. Then, we'll append the 239 // in-memory stats to the results (if necessary) so as to avoid writing to disk too 240 // often. 241 final long truncatedEndTime = Math.min(currentStats.beginTime, endTime); 242 243 // Get the stats from disk. 244 List<T> results = mDatabase.queryUsageStats(intervalType, beginTime, 245 truncatedEndTime, combiner); 246 if (DEBUG) { 247 Slog.d(TAG, "Got " + (results != null ? results.size() : 0) + " results from disk"); 248 Slog.d(TAG, "Current stats beginTime=" + currentStats.beginTime + 249 " endTime=" + currentStats.endTime); 250 } 251 252 // Now check if the in-memory stats match the range and add them if they do. 253 if (beginTime < currentStats.endTime && endTime > currentStats.beginTime) { 254 if (DEBUG) { 255 Slog.d(TAG, mLogPrefix + "Returning in-memory stats"); 256 } 257 258 if (results == null) { 259 results = new ArrayList<>(); 260 } 261 combiner.combine(currentStats, true, results); 262 } 263 264 if (DEBUG) { 265 Slog.d(TAG, mLogPrefix + "Results: " + (results != null ? results.size() : 0)); 266 } 267 return results; 268 } 269 270 List<UsageStats> queryUsageStats(int bucketType, long beginTime, long endTime) { 271 return queryStats(bucketType, beginTime, endTime, sUsageStatsCombiner); 272 } 273 274 List<ConfigurationStats> queryConfigurationStats(int bucketType, long beginTime, long endTime) { 275 return queryStats(bucketType, beginTime, endTime, sConfigStatsCombiner); 276 } 277 278 UsageEvents queryEvents(final long beginTime, final long endTime) { 279 final ArraySet<String> names = new ArraySet<>(); 280 List<UsageEvents.Event> results = queryStats(UsageStatsManager.INTERVAL_DAILY, 281 beginTime, endTime, new StatCombiner<UsageEvents.Event>() { 282 @Override 283 public void combine(IntervalStats stats, boolean mutable, 284 List<UsageEvents.Event> accumulatedResult) { 285 if (stats.events == null) { 286 return; 287 } 288 289 final int startIndex = stats.events.closestIndexOnOrAfter(beginTime); 290 if (startIndex < 0) { 291 return; 292 } 293 294 final int size = stats.events.size(); 295 for (int i = startIndex; i < size; i++) { 296 if (stats.events.keyAt(i) >= endTime) { 297 return; 298 } 299 300 final UsageEvents.Event event = stats.events.valueAt(i); 301 names.add(event.mPackage); 302 if (event.mClass != null) { 303 names.add(event.mClass); 304 } 305 accumulatedResult.add(event); 306 } 307 } 308 }); 309 310 if (results == null || results.isEmpty()) { 311 return null; 312 } 313 314 String[] table = names.toArray(new String[names.size()]); 315 Arrays.sort(table); 316 return new UsageEvents(results, table); 317 } 318 319 void persistActiveStats() { 320 if (mStatsChanged) { 321 Slog.i(TAG, mLogPrefix + "Flushing usage stats to disk"); 322 try { 323 for (int i = 0; i < mCurrentStats.length; i++) { 324 mDatabase.putUsageStats(i, mCurrentStats[i]); 325 } 326 mStatsChanged = false; 327 } catch (IOException e) { 328 Slog.e(TAG, mLogPrefix + "Failed to persist active stats", e); 329 } 330 } 331 } 332 333 private void rolloverStats() { 334 final long startTime = System.currentTimeMillis(); 335 Slog.i(TAG, mLogPrefix + "Rolling over usage stats"); 336 337 // Finish any ongoing events with an END_OF_DAY event. Make a note of which components 338 // need a new CONTINUE_PREVIOUS_DAY entry. 339 final Configuration previousConfig = 340 mCurrentStats[UsageStatsManager.INTERVAL_DAILY].activeConfiguration; 341 ArraySet<String> continuePreviousDay = new ArraySet<>(); 342 for (IntervalStats stat : mCurrentStats) { 343 final int pkgCount = stat.packageStats.size(); 344 for (int i = 0; i < pkgCount; i++) { 345 UsageStats pkgStats = stat.packageStats.valueAt(i); 346 if (pkgStats.mLastEvent == UsageEvents.Event.MOVE_TO_FOREGROUND || 347 pkgStats.mLastEvent == UsageEvents.Event.CONTINUE_PREVIOUS_DAY) { 348 continuePreviousDay.add(pkgStats.mPackageName); 349 stat.update(pkgStats.mPackageName, mDailyExpiryDate.getTimeInMillis() - 1, 350 UsageEvents.Event.END_OF_DAY); 351 mStatsChanged = true; 352 } 353 } 354 355 stat.updateConfigurationStats(null, mDailyExpiryDate.getTimeInMillis() - 1); 356 } 357 358 persistActiveStats(); 359 mDatabase.prune(); 360 loadActiveStats(); 361 362 final int continueCount = continuePreviousDay.size(); 363 for (int i = 0; i < continueCount; i++) { 364 String name = continuePreviousDay.valueAt(i); 365 final long beginTime = mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime; 366 for (IntervalStats stat : mCurrentStats) { 367 stat.update(name, beginTime, UsageEvents.Event.CONTINUE_PREVIOUS_DAY); 368 stat.updateConfigurationStats(previousConfig, beginTime); 369 mStatsChanged = true; 370 } 371 } 372 persistActiveStats(); 373 374 final long totalTime = System.currentTimeMillis() - startTime; 375 Slog.i(TAG, mLogPrefix + "Rolling over usage stats complete. Took " + totalTime 376 + " milliseconds"); 377 } 378 379 private void notifyStatsChanged() { 380 if (!mStatsChanged) { 381 mStatsChanged = true; 382 mListener.onStatsUpdated(); 383 } 384 } 385 386 private void loadActiveStats() { 387 final long timeNow = System.currentTimeMillis(); 388 389 final UnixCalendar tempCal = mDailyExpiryDate; 390 for (int intervalType = 0; intervalType < mCurrentStats.length; intervalType++) { 391 tempCal.setTimeInMillis(timeNow); 392 UnixCalendar.truncateTo(tempCal, intervalType); 393 394 if (mCurrentStats[intervalType] != null && 395 mCurrentStats[intervalType].beginTime == tempCal.getTimeInMillis()) { 396 // These are the same, no need to load them (in memory stats are always newer 397 // than persisted stats). 398 continue; 399 } 400 401 final long lastBeginTime = mDatabase.getLatestUsageStatsBeginTime(intervalType); 402 if (lastBeginTime > timeNow) { 403 Slog.e(TAG, mLogPrefix + "Latest usage stats for interval " + 404 intervalType + " begins in the future"); 405 mCurrentStats[intervalType] = null; 406 } else if (lastBeginTime >= tempCal.getTimeInMillis()) { 407 if (DEBUG) { 408 Slog.d(TAG, mLogPrefix + "Loading existing stats @ " + 409 sDateFormat.format(lastBeginTime) + "(" + lastBeginTime + 410 ") for interval " + intervalType); 411 } 412 mCurrentStats[intervalType] = mDatabase.getLatestUsageStats(intervalType); 413 } else { 414 mCurrentStats[intervalType] = null; 415 } 416 417 if (mCurrentStats[intervalType] == null) { 418 if (DEBUG) { 419 Slog.d(TAG, "Creating new stats @ " + 420 sDateFormat.format(tempCal.getTimeInMillis()) + "(" + 421 tempCal.getTimeInMillis() + ") for interval " + intervalType); 422 423 } 424 mCurrentStats[intervalType] = new IntervalStats(); 425 mCurrentStats[intervalType].beginTime = tempCal.getTimeInMillis(); 426 mCurrentStats[intervalType].endTime = timeNow; 427 } 428 } 429 mStatsChanged = false; 430 mDailyExpiryDate.setTimeInMillis(timeNow); 431 mDailyExpiryDate.addDays(1); 432 mDailyExpiryDate.truncateToDay(); 433 Slog.i(TAG, mLogPrefix + "Rollover scheduled @ " + 434 sDateFormat.format(mDailyExpiryDate.getTimeInMillis()) + "(" + 435 tempCal.getTimeInMillis() + ")"); 436 } 437 438 439 private static String eventToString(int eventType) { 440 switch (eventType) { 441 case UsageEvents.Event.NONE: 442 return "NONE"; 443 case UsageEvents.Event.MOVE_TO_BACKGROUND: 444 return "MOVE_TO_BACKGROUND"; 445 case UsageEvents.Event.MOVE_TO_FOREGROUND: 446 return "MOVE_TO_FOREGROUND"; 447 case UsageEvents.Event.END_OF_DAY: 448 return "END_OF_DAY"; 449 case UsageEvents.Event.CONTINUE_PREVIOUS_DAY: 450 return "CONTINUE_PREVIOUS_DAY"; 451 case UsageEvents.Event.CONFIGURATION_CHANGE: 452 return "CONFIGURATION_CHANGE"; 453 default: 454 return "UNKNOWN"; 455 } 456 } 457} 458