UserUsageStatsService.java revision 9d9607527f5bbf49c96565b63b90e36276b0dda7
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.TimeSparseArray; 20import android.app.usage.UsageEvents; 21import android.app.usage.UsageStats; 22import android.app.usage.UsageStatsManager; 23import android.util.ArraySet; 24import android.util.Slog; 25 26import java.io.File; 27import java.io.IOException; 28import java.text.SimpleDateFormat; 29import java.util.ArrayList; 30import java.util.Arrays; 31import java.util.Calendar; 32import java.util.List; 33 34/** 35 * A per-user UsageStatsService. All methods are meant to be called with the main lock held 36 * in UsageStatsService. 37 */ 38class UserUsageStatsService { 39 private static final String TAG = "UsageStatsService"; 40 private static final boolean DEBUG = UsageStatsService.DEBUG; 41 private static final SimpleDateFormat sDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 42 43 private final UsageStatsDatabase mDatabase; 44 private final IntervalStats[] mCurrentStats; 45 private boolean mStatsChanged = false; 46 private final Calendar mDailyExpiryDate; 47 private final StatsUpdatedListener mListener; 48 private final String mLogPrefix; 49 50 interface StatsUpdatedListener { 51 void onStatsUpdated(); 52 } 53 54 UserUsageStatsService(int userId, File usageStatsDir, StatsUpdatedListener listener) { 55 mDailyExpiryDate = Calendar.getInstance(); 56 mDatabase = new UsageStatsDatabase(usageStatsDir); 57 mCurrentStats = new IntervalStats[UsageStatsManager.INTERVAL_COUNT]; 58 mListener = listener; 59 mLogPrefix = "User[" + Integer.toString(userId) + "] "; 60 } 61 62 void init() { 63 mDatabase.init(); 64 65 int nullCount = 0; 66 for (int i = 0; i < mCurrentStats.length; i++) { 67 mCurrentStats[i] = mDatabase.getLatestUsageStats(i); 68 if (mCurrentStats[i] == null) { 69 // Find out how many intervals we don't have data for. 70 // Ideally it should be all or none. 71 nullCount++; 72 } 73 } 74 75 if (nullCount > 0) { 76 if (nullCount != mCurrentStats.length) { 77 // This is weird, but we shouldn't fail if something like this 78 // happens. 79 Slog.w(TAG, mLogPrefix + "Some stats have no latest available"); 80 } else { 81 // This must be first boot. 82 } 83 84 // By calling loadActiveStats, we will 85 // generate new stats for each bucket. 86 loadActiveStats(); 87 } else { 88 // Set up the expiry date to be one day from the latest daily stat. 89 // This may actually be today and we will rollover on the first event 90 // that is reported. 91 mDailyExpiryDate.setTimeInMillis( 92 mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime); 93 mDailyExpiryDate.add(Calendar.DAY_OF_YEAR, 1); 94 UsageStatsUtils.truncateDateTo(UsageStatsManager.INTERVAL_DAILY, mDailyExpiryDate); 95 Slog.i(TAG, mLogPrefix + "Rollover scheduled for " 96 + sDateFormat.format(mDailyExpiryDate.getTime())); 97 } 98 99 // Now close off any events that were open at the time this was saved. 100 for (IntervalStats stat : mCurrentStats) { 101 final int pkgCount = stat.stats.size(); 102 for (int i = 0; i < pkgCount; i++) { 103 UsageStats pkgStats = stat.stats.valueAt(i); 104 if (pkgStats.mLastEvent == UsageEvents.Event.MOVE_TO_FOREGROUND || 105 pkgStats.mLastEvent == UsageEvents.Event.CONTINUE_PREVIOUS_DAY) { 106 stat.update(pkgStats.mPackageName, stat.lastTimeSaved, 107 UsageEvents.Event.END_OF_DAY); 108 notifyStatsChanged(); 109 } 110 } 111 } 112 } 113 114 void reportEvent(UsageEvents.Event event) { 115 if (DEBUG) { 116 Slog.d(TAG, mLogPrefix + "Got usage event for " + event.mPackage 117 + "[" + event.getTimeStamp() + "]: " 118 + eventToString(event.getEventType())); 119 } 120 121 if (event.getTimeStamp() >= mDailyExpiryDate.getTimeInMillis()) { 122 // Need to rollover 123 rolloverStats(); 124 } 125 126 if (mCurrentStats[UsageStatsManager.INTERVAL_DAILY].events == null) { 127 mCurrentStats[UsageStatsManager.INTERVAL_DAILY].events = new TimeSparseArray<>(); 128 } 129 mCurrentStats[UsageStatsManager.INTERVAL_DAILY].events.put(event.getTimeStamp(), event); 130 131 for (IntervalStats stats : mCurrentStats) { 132 stats.update(event.mPackage, event.getTimeStamp(), 133 event.getEventType()); 134 } 135 136 notifyStatsChanged(); 137 } 138 139 List<UsageStats> queryUsageStats(int bucketType, long beginTime, long endTime) { 140 if (bucketType == UsageStatsManager.INTERVAL_BEST) { 141 bucketType = mDatabase.findBestFitBucket(beginTime, endTime); 142 } 143 144 if (bucketType < 0 || bucketType >= mCurrentStats.length) { 145 if (DEBUG) { 146 Slog.d(TAG, mLogPrefix + "Bad bucketType used " + bucketType); 147 } 148 return null; 149 } 150 151 if (beginTime >= mCurrentStats[bucketType].endTime) { 152 if (DEBUG) { 153 Slog.d(TAG, mLogPrefix + "Requesting stats after " + beginTime + " but latest is " 154 + mCurrentStats[bucketType].endTime); 155 } 156 // Nothing newer available. 157 return null; 158 159 } else if (beginTime >= mCurrentStats[bucketType].beginTime) { 160 if (DEBUG) { 161 Slog.d(TAG, mLogPrefix + "Returning in-memory stats for bucket " + bucketType); 162 } 163 // Fast path for retrieving in-memory state. 164 ArrayList<UsageStats> results = new ArrayList<>(); 165 final int packageCount = mCurrentStats[bucketType].stats.size(); 166 for (int i = 0; i < packageCount; i++) { 167 results.add(new UsageStats(mCurrentStats[bucketType].stats.valueAt(i))); 168 } 169 return results; 170 } 171 172 // Flush any changes that were made to disk before we do a disk query. 173 // If we're not grabbing the ongoing stats, no need to persist. 174 persistActiveStats(); 175 176 if (DEBUG) { 177 Slog.d(TAG, mLogPrefix + "SELECT * FROM " + bucketType + " WHERE beginTime >= " 178 + beginTime + " AND endTime < " + endTime); 179 } 180 181 final List<UsageStats> results = mDatabase.queryUsageStats(bucketType, beginTime, endTime); 182 if (DEBUG) { 183 Slog.d(TAG, mLogPrefix + "Results: " + (results == null ? 0 : results.size())); 184 } 185 return results; 186 } 187 188 UsageEvents queryEvents(long beginTime, long endTime) { 189 if (endTime > mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime) { 190 if (beginTime > mCurrentStats[UsageStatsManager.INTERVAL_DAILY].endTime) { 191 return null; 192 } 193 194 TimeSparseArray<UsageEvents.Event> events = 195 mCurrentStats[UsageStatsManager.INTERVAL_DAILY].events; 196 if (events == null) { 197 return null; 198 } 199 200 final int startIndex = events.closestIndexOnOrAfter(beginTime); 201 if (startIndex < 0) { 202 return null; 203 } 204 205 ArraySet<String> names = new ArraySet<>(); 206 ArrayList<UsageEvents.Event> results = new ArrayList<>(); 207 final int size = events.size(); 208 for (int i = startIndex; i < size; i++) { 209 if (events.keyAt(i) >= endTime) { 210 break; 211 } 212 final UsageEvents.Event event = events.valueAt(i); 213 names.add(event.mPackage); 214 if (event.mClass != null) { 215 names.add(event.mClass); 216 } 217 results.add(event); 218 } 219 String[] table = names.toArray(new String[names.size()]); 220 Arrays.sort(table); 221 return new UsageEvents(results, table); 222 } 223 224 // TODO(adamlesinski): Query the previous days. 225 return null; 226 } 227 228 void persistActiveStats() { 229 if (mStatsChanged) { 230 Slog.i(TAG, mLogPrefix + "Flushing usage stats to disk"); 231 try { 232 for (int i = 0; i < mCurrentStats.length; i++) { 233 mDatabase.putUsageStats(i, mCurrentStats[i]); 234 } 235 mStatsChanged = false; 236 } catch (IOException e) { 237 Slog.e(TAG, mLogPrefix + "Failed to persist active stats", e); 238 } 239 } 240 } 241 242 private void rolloverStats() { 243 final long startTime = System.currentTimeMillis(); 244 Slog.i(TAG, mLogPrefix + "Rolling over usage stats"); 245 246 // Finish any ongoing events with an END_OF_DAY event. Make a note of which components 247 // need a new CONTINUE_PREVIOUS_DAY entry. 248 ArraySet<String> continuePreviousDay = new ArraySet<>(); 249 for (IntervalStats stat : mCurrentStats) { 250 final int pkgCount = stat.stats.size(); 251 for (int i = 0; i < pkgCount; i++) { 252 UsageStats pkgStats = stat.stats.valueAt(i); 253 if (pkgStats.mLastEvent == UsageEvents.Event.MOVE_TO_FOREGROUND || 254 pkgStats.mLastEvent == UsageEvents.Event.CONTINUE_PREVIOUS_DAY) { 255 continuePreviousDay.add(pkgStats.mPackageName); 256 stat.update(pkgStats.mPackageName, 257 mDailyExpiryDate.getTimeInMillis() - 1, UsageEvents.Event.END_OF_DAY); 258 mStatsChanged = true; 259 } 260 } 261 } 262 263 persistActiveStats(); 264 mDatabase.prune(); 265 loadActiveStats(); 266 267 final int continueCount = continuePreviousDay.size(); 268 for (int i = 0; i < continueCount; i++) { 269 String name = continuePreviousDay.valueAt(i); 270 for (IntervalStats stat : mCurrentStats) { 271 stat.update(name, mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime, 272 UsageEvents.Event.CONTINUE_PREVIOUS_DAY); 273 mStatsChanged = true; 274 } 275 } 276 persistActiveStats(); 277 278 final long totalTime = System.currentTimeMillis() - startTime; 279 Slog.i(TAG, mLogPrefix + "Rolling over usage stats complete. Took " + totalTime 280 + " milliseconds"); 281 } 282 283 private void notifyStatsChanged() { 284 if (!mStatsChanged) { 285 mStatsChanged = true; 286 mListener.onStatsUpdated(); 287 } 288 } 289 290 private void loadActiveStats() { 291 final long timeNow = System.currentTimeMillis(); 292 293 Calendar tempCal = mDailyExpiryDate; 294 for (int bucketType = 0; bucketType < mCurrentStats.length; bucketType++) { 295 tempCal.setTimeInMillis(timeNow); 296 UsageStatsUtils.truncateDateTo(bucketType, tempCal); 297 298 if (mCurrentStats[bucketType] != null && 299 mCurrentStats[bucketType].beginTime == tempCal.getTimeInMillis()) { 300 // These are the same, no need to load them (in memory stats are always newer 301 // than persisted stats). 302 continue; 303 } 304 305 final long lastBeginTime = mDatabase.getLatestUsageStatsBeginTime(bucketType); 306 if (lastBeginTime >= tempCal.getTimeInMillis()) { 307 if (DEBUG) { 308 Slog.d(TAG, mLogPrefix + "Loading existing stats (" + lastBeginTime + 309 ") for bucket " + bucketType); 310 } 311 mCurrentStats[bucketType] = mDatabase.getLatestUsageStats(bucketType); 312 if (DEBUG) { 313 if (mCurrentStats[bucketType] != null) { 314 Slog.d(TAG, mLogPrefix + "Found " + 315 (mCurrentStats[bucketType].events == null ? 316 0 : mCurrentStats[bucketType].events.size()) + 317 " events"); 318 } 319 } 320 } else { 321 mCurrentStats[bucketType] = null; 322 } 323 324 if (mCurrentStats[bucketType] == null) { 325 if (DEBUG) { 326 Slog.d(TAG, "Creating new stats (" + tempCal.getTimeInMillis() + 327 ") for bucket " + bucketType); 328 329 } 330 mCurrentStats[bucketType] = new IntervalStats(); 331 mCurrentStats[bucketType].beginTime = tempCal.getTimeInMillis(); 332 mCurrentStats[bucketType].endTime = timeNow; 333 } 334 } 335 mStatsChanged = false; 336 mDailyExpiryDate.setTimeInMillis(timeNow); 337 mDailyExpiryDate.add(Calendar.DAY_OF_YEAR, 1); 338 UsageStatsUtils.truncateDateTo(UsageStatsManager.INTERVAL_DAILY, mDailyExpiryDate); 339 Slog.i(TAG, mLogPrefix + "Rollover scheduled for " 340 + sDateFormat.format(mDailyExpiryDate.getTime())); 341 } 342 343 344 private static String eventToString(int eventType) { 345 switch (eventType) { 346 case UsageEvents.Event.NONE: 347 return "NONE"; 348 case UsageEvents.Event.MOVE_TO_BACKGROUND: 349 return "MOVE_TO_BACKGROUND"; 350 case UsageEvents.Event.MOVE_TO_FOREGROUND: 351 return "MOVE_TO_FOREGROUND"; 352 case UsageEvents.Event.END_OF_DAY: 353 return "END_OF_DAY"; 354 case UsageEvents.Event.CONTINUE_PREVIOUS_DAY: 355 return "CONTINUE_PREVIOUS_DAY"; 356 default: 357 return "UNKNOWN"; 358 } 359 } 360} 361