UserUsageStatsService.java revision 55717a613c23fc6b05cab86c3b0de283a027e9f6
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) { 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, 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, mDatabase.isFirstUpdate()); 139 } 140 } 141 142 /** 143 * If any of the apps don't have a last-used entry, add one now. 144 * @param currentTimeMillis the current time 145 * @param firstUpdate if it is the first update, touch all installed apps, otherwise only 146 * touch the system apps 147 */ 148 private void initializeDefaultsForApps(long currentTimeMillis, boolean firstUpdate) { 149 PackageManager pm = mContext.getPackageManager(); 150 List<PackageInfo> packages = pm.getInstalledPackages(0, mUserId); 151 final int packageCount = packages.size(); 152 for (int i = 0; i < packageCount; i++) { 153 final PackageInfo pi = packages.get(i); 154 String packageName = pi.packageName; 155 if (pi.applicationInfo != null && (firstUpdate || pi.applicationInfo.isSystemApp()) 156 && getLastPackageAccessTime(packageName) == -1) { 157 for (IntervalStats stats : mCurrentStats) { 158 stats.update(packageName, currentTimeMillis, Event.INTERACTION); 159 mStatsChanged = true; 160 } 161 } 162 } 163 // Persist the new OTA-related access stats. 164 persistActiveStats(); 165 } 166 167 void onTimeChanged(long oldTime, long newTime) { 168 persistActiveStats(); 169 mDatabase.onTimeChanged(newTime - oldTime); 170 loadActiveStats(newTime, true); 171 } 172 173 void reportEvent(UsageEvents.Event event) { 174 if (DEBUG) { 175 Slog.d(TAG, mLogPrefix + "Got usage event for " + event.mPackage 176 + "[" + event.mTimeStamp + "]: " 177 + eventToString(event.mEventType)); 178 } 179 180 if (event.mTimeStamp >= mDailyExpiryDate.getTimeInMillis()) { 181 // Need to rollover 182 rolloverStats(event.mTimeStamp); 183 } 184 185 final IntervalStats currentDailyStats = mCurrentStats[UsageStatsManager.INTERVAL_DAILY]; 186 187 final Configuration newFullConfig = event.mConfiguration; 188 if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE && 189 currentDailyStats.activeConfiguration != null) { 190 // Make the event configuration a delta. 191 event.mConfiguration = Configuration.generateDelta( 192 currentDailyStats.activeConfiguration, newFullConfig); 193 } 194 195 // Add the event to the daily list. 196 if (currentDailyStats.events == null) { 197 currentDailyStats.events = new TimeSparseArray<>(); 198 } 199 if (event.mEventType != UsageEvents.Event.INTERACTION) { 200 currentDailyStats.events.put(event.mTimeStamp, event); 201 } 202 203 for (IntervalStats stats : mCurrentStats) { 204 if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE) { 205 stats.updateConfigurationStats(newFullConfig, event.mTimeStamp); 206 } else { 207 stats.update(event.mPackage, event.mTimeStamp, event.mEventType); 208 } 209 } 210 211 notifyStatsChanged(); 212 } 213 214 private static final StatCombiner<UsageStats> sUsageStatsCombiner = 215 new StatCombiner<UsageStats>() { 216 @Override 217 public void combine(IntervalStats stats, boolean mutable, 218 List<UsageStats> accResult) { 219 if (!mutable) { 220 accResult.addAll(stats.packageStats.values()); 221 return; 222 } 223 224 final int statCount = stats.packageStats.size(); 225 for (int i = 0; i < statCount; i++) { 226 accResult.add(new UsageStats(stats.packageStats.valueAt(i))); 227 } 228 } 229 }; 230 231 private static final StatCombiner<ConfigurationStats> sConfigStatsCombiner = 232 new StatCombiner<ConfigurationStats>() { 233 @Override 234 public void combine(IntervalStats stats, boolean mutable, 235 List<ConfigurationStats> accResult) { 236 if (!mutable) { 237 accResult.addAll(stats.configurations.values()); 238 return; 239 } 240 241 final int configCount = stats.configurations.size(); 242 for (int i = 0; i < configCount; i++) { 243 accResult.add(new ConfigurationStats(stats.configurations.valueAt(i))); 244 } 245 } 246 }; 247 248 /** 249 * Generic query method that selects the appropriate IntervalStats for the specified time range 250 * and bucket, then calls the {@link com.android.server.usage.UsageStatsDatabase.StatCombiner} 251 * provided to select the stats to use from the IntervalStats object. 252 */ 253 private <T> List<T> queryStats(int intervalType, final long beginTime, final long endTime, 254 StatCombiner<T> combiner) { 255 if (intervalType == UsageStatsManager.INTERVAL_BEST) { 256 intervalType = mDatabase.findBestFitBucket(beginTime, endTime); 257 if (intervalType < 0) { 258 // Nothing saved to disk yet, so every stat is just as equal (no rollover has 259 // occurred. 260 intervalType = UsageStatsManager.INTERVAL_DAILY; 261 } 262 } 263 264 if (intervalType < 0 || intervalType >= mCurrentStats.length) { 265 if (DEBUG) { 266 Slog.d(TAG, mLogPrefix + "Bad intervalType used " + intervalType); 267 } 268 return null; 269 } 270 271 final IntervalStats currentStats = mCurrentStats[intervalType]; 272 273 if (DEBUG) { 274 Slog.d(TAG, mLogPrefix + "SELECT * FROM " + intervalType + " WHERE beginTime >= " 275 + beginTime + " AND endTime < " + endTime); 276 } 277 278 if (beginTime >= currentStats.endTime) { 279 if (DEBUG) { 280 Slog.d(TAG, mLogPrefix + "Requesting stats after " + beginTime + " but latest is " 281 + currentStats.endTime); 282 } 283 // Nothing newer available. 284 return null; 285 } 286 287 // Truncate the endTime to just before the in-memory stats. Then, we'll append the 288 // in-memory stats to the results (if necessary) so as to avoid writing to disk too 289 // often. 290 final long truncatedEndTime = Math.min(currentStats.beginTime, endTime); 291 292 // Get the stats from disk. 293 List<T> results = mDatabase.queryUsageStats(intervalType, beginTime, 294 truncatedEndTime, combiner); 295 if (DEBUG) { 296 Slog.d(TAG, "Got " + (results != null ? results.size() : 0) + " results from disk"); 297 Slog.d(TAG, "Current stats beginTime=" + currentStats.beginTime + 298 " endTime=" + currentStats.endTime); 299 } 300 301 // Now check if the in-memory stats match the range and add them if they do. 302 if (beginTime < currentStats.endTime && endTime > currentStats.beginTime) { 303 if (DEBUG) { 304 Slog.d(TAG, mLogPrefix + "Returning in-memory stats"); 305 } 306 307 if (results == null) { 308 results = new ArrayList<>(); 309 } 310 combiner.combine(currentStats, true, results); 311 } 312 313 if (DEBUG) { 314 Slog.d(TAG, mLogPrefix + "Results: " + (results != null ? results.size() : 0)); 315 } 316 return results; 317 } 318 319 List<UsageStats> queryUsageStats(int bucketType, long beginTime, long endTime) { 320 return queryStats(bucketType, beginTime, endTime, sUsageStatsCombiner); 321 } 322 323 List<ConfigurationStats> queryConfigurationStats(int bucketType, long beginTime, long endTime) { 324 return queryStats(bucketType, beginTime, endTime, sConfigStatsCombiner); 325 } 326 327 UsageEvents queryEvents(final long beginTime, final long endTime) { 328 final ArraySet<String> names = new ArraySet<>(); 329 List<UsageEvents.Event> results = queryStats(UsageStatsManager.INTERVAL_DAILY, 330 beginTime, endTime, new StatCombiner<UsageEvents.Event>() { 331 @Override 332 public void combine(IntervalStats stats, boolean mutable, 333 List<UsageEvents.Event> accumulatedResult) { 334 if (stats.events == null) { 335 return; 336 } 337 338 final int startIndex = stats.events.closestIndexOnOrAfter(beginTime); 339 if (startIndex < 0) { 340 return; 341 } 342 343 final int size = stats.events.size(); 344 for (int i = startIndex; i < size; i++) { 345 if (stats.events.keyAt(i) >= endTime) { 346 return; 347 } 348 349 final UsageEvents.Event event = stats.events.valueAt(i); 350 names.add(event.mPackage); 351 if (event.mClass != null) { 352 names.add(event.mClass); 353 } 354 accumulatedResult.add(event); 355 } 356 } 357 }); 358 359 if (results == null || results.isEmpty()) { 360 return null; 361 } 362 363 String[] table = names.toArray(new String[names.size()]); 364 Arrays.sort(table); 365 return new UsageEvents(results, table); 366 } 367 368 long getLastPackageAccessTime(String packageName) { 369 final IntervalStats yearly = mCurrentStats[UsageStatsManager.INTERVAL_YEARLY]; 370 UsageStats packageUsage; 371 if ((packageUsage = yearly.packageStats.get(packageName)) == null) { 372 return -1; 373 } else { 374 return packageUsage.getLastTimeUsed(); 375 } 376 } 377 378 void persistActiveStats() { 379 if (mStatsChanged) { 380 Slog.i(TAG, mLogPrefix + "Flushing usage stats to disk"); 381 try { 382 for (int i = 0; i < mCurrentStats.length; i++) { 383 mDatabase.putUsageStats(i, mCurrentStats[i]); 384 } 385 mStatsChanged = false; 386 } catch (IOException e) { 387 Slog.e(TAG, mLogPrefix + "Failed to persist active stats", e); 388 } 389 } 390 } 391 392 private void rolloverStats(final long currentTimeMillis) { 393 final long startTime = SystemClock.elapsedRealtime(); 394 Slog.i(TAG, mLogPrefix + "Rolling over usage stats"); 395 396 // Finish any ongoing events with an END_OF_DAY event. Make a note of which components 397 // need a new CONTINUE_PREVIOUS_DAY entry. 398 final Configuration previousConfig = 399 mCurrentStats[UsageStatsManager.INTERVAL_DAILY].activeConfiguration; 400 ArraySet<String> continuePreviousDay = new ArraySet<>(); 401 for (IntervalStats stat : mCurrentStats) { 402 final int pkgCount = stat.packageStats.size(); 403 for (int i = 0; i < pkgCount; i++) { 404 UsageStats pkgStats = stat.packageStats.valueAt(i); 405 if (pkgStats.mLastEvent == UsageEvents.Event.MOVE_TO_FOREGROUND || 406 pkgStats.mLastEvent == UsageEvents.Event.CONTINUE_PREVIOUS_DAY) { 407 continuePreviousDay.add(pkgStats.mPackageName); 408 stat.update(pkgStats.mPackageName, mDailyExpiryDate.getTimeInMillis() - 1, 409 UsageEvents.Event.END_OF_DAY); 410 notifyStatsChanged(); 411 } 412 } 413 414 stat.updateConfigurationStats(null, mDailyExpiryDate.getTimeInMillis() - 1); 415 } 416 417 persistActiveStats(); 418 mDatabase.prune(currentTimeMillis); 419 loadActiveStats(currentTimeMillis, false); 420 421 final int continueCount = continuePreviousDay.size(); 422 for (int i = 0; i < continueCount; i++) { 423 String name = continuePreviousDay.valueAt(i); 424 final long beginTime = mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime; 425 for (IntervalStats stat : mCurrentStats) { 426 stat.update(name, beginTime, UsageEvents.Event.CONTINUE_PREVIOUS_DAY); 427 stat.updateConfigurationStats(previousConfig, beginTime); 428 notifyStatsChanged(); 429 } 430 } 431 persistActiveStats(); 432 433 final long totalTime = SystemClock.elapsedRealtime() - startTime; 434 Slog.i(TAG, mLogPrefix + "Rolling over usage stats complete. Took " + totalTime 435 + " milliseconds"); 436 } 437 438 private void notifyStatsChanged() { 439 if (!mStatsChanged) { 440 mStatsChanged = true; 441 mListener.onStatsUpdated(); 442 } 443 } 444 445 /** 446 * @param force To force all in-memory stats to be reloaded. 447 */ 448 private void loadActiveStats(final long currentTimeMillis, boolean force) { 449 final UnixCalendar tempCal = mDailyExpiryDate; 450 for (int intervalType = 0; intervalType < mCurrentStats.length; intervalType++) { 451 tempCal.setTimeInMillis(currentTimeMillis); 452 UnixCalendar.truncateTo(tempCal, intervalType); 453 454 if (!force && mCurrentStats[intervalType] != null && 455 mCurrentStats[intervalType].beginTime == tempCal.getTimeInMillis()) { 456 // These are the same, no need to load them (in memory stats are always newer 457 // than persisted stats). 458 continue; 459 } 460 461 final long lastBeginTime = mDatabase.getLatestUsageStatsBeginTime(intervalType); 462 if (lastBeginTime >= tempCal.getTimeInMillis()) { 463 if (DEBUG) { 464 Slog.d(TAG, mLogPrefix + "Loading existing stats @ " + 465 sDateFormat.format(lastBeginTime) + "(" + lastBeginTime + 466 ") for interval " + intervalType); 467 } 468 mCurrentStats[intervalType] = mDatabase.getLatestUsageStats(intervalType); 469 } else { 470 mCurrentStats[intervalType] = null; 471 } 472 473 if (mCurrentStats[intervalType] == null) { 474 if (DEBUG) { 475 Slog.d(TAG, "Creating new stats @ " + 476 sDateFormat.format(tempCal.getTimeInMillis()) + "(" + 477 tempCal.getTimeInMillis() + ") for interval " + intervalType); 478 479 } 480 mCurrentStats[intervalType] = new IntervalStats(); 481 mCurrentStats[intervalType].beginTime = tempCal.getTimeInMillis(); 482 mCurrentStats[intervalType].endTime = currentTimeMillis; 483 } 484 } 485 mStatsChanged = false; 486 mDailyExpiryDate.setTimeInMillis(currentTimeMillis); 487 mDailyExpiryDate.addDays(1); 488 mDailyExpiryDate.truncateToDay(); 489 Slog.i(TAG, mLogPrefix + "Rollover scheduled @ " + 490 sDateFormat.format(mDailyExpiryDate.getTimeInMillis()) + "(" + 491 tempCal.getTimeInMillis() + ")"); 492 } 493 494 // 495 // -- DUMP related methods -- 496 // 497 498 void checkin(final IndentingPrintWriter pw) { 499 mDatabase.checkinDailyFiles(new UsageStatsDatabase.CheckinAction() { 500 @Override 501 public boolean checkin(IntervalStats stats) { 502 printIntervalStats(pw, stats, false); 503 return true; 504 } 505 }); 506 } 507 508 void dump(IndentingPrintWriter pw) { 509 // This is not a check-in, only dump in-memory stats. 510 for (int interval = 0; interval < mCurrentStats.length; interval++) { 511 pw.print("In-memory "); 512 pw.print(intervalToString(interval)); 513 pw.println(" stats"); 514 printIntervalStats(pw, mCurrentStats[interval], true); 515 } 516 } 517 518 private String formatDateTime(long dateTime, boolean pretty) { 519 if (pretty) { 520 return "\"" + DateUtils.formatDateTime(mContext, dateTime, sDateFormatFlags) + "\""; 521 } 522 return Long.toString(dateTime); 523 } 524 525 private String formatElapsedTime(long elapsedTime, boolean pretty) { 526 if (pretty) { 527 return "\"" + DateUtils.formatElapsedTime(elapsedTime / 1000) + "\""; 528 } 529 return Long.toString(elapsedTime); 530 } 531 532 void printIntervalStats(IndentingPrintWriter pw, IntervalStats stats, boolean prettyDates) { 533 if (prettyDates) { 534 pw.printPair("timeRange", "\"" + DateUtils.formatDateRange(mContext, 535 stats.beginTime, stats.endTime, sDateFormatFlags) + "\""); 536 } else { 537 pw.printPair("beginTime", stats.beginTime); 538 pw.printPair("endTime", stats.endTime); 539 } 540 pw.println(); 541 pw.increaseIndent(); 542 pw.println("packages"); 543 pw.increaseIndent(); 544 final ArrayMap<String, UsageStats> pkgStats = stats.packageStats; 545 final int pkgCount = pkgStats.size(); 546 for (int i = 0; i < pkgCount; i++) { 547 final UsageStats usageStats = pkgStats.valueAt(i); 548 pw.printPair("package", usageStats.mPackageName); 549 pw.printPair("totalTime", formatElapsedTime(usageStats.mTotalTimeInForeground, prettyDates)); 550 pw.printPair("lastTime", formatDateTime(usageStats.mLastTimeUsed, prettyDates)); 551 pw.println(); 552 } 553 pw.decreaseIndent(); 554 555 pw.println("configurations"); 556 pw.increaseIndent(); 557 final ArrayMap<Configuration, ConfigurationStats> configStats = 558 stats.configurations; 559 final int configCount = configStats.size(); 560 for (int i = 0; i < configCount; i++) { 561 final ConfigurationStats config = configStats.valueAt(i); 562 pw.printPair("config", Configuration.resourceQualifierString(config.mConfiguration)); 563 pw.printPair("totalTime", formatElapsedTime(config.mTotalTimeActive, prettyDates)); 564 pw.printPair("lastTime", formatDateTime(config.mLastTimeActive, prettyDates)); 565 pw.printPair("count", config.mActivationCount); 566 pw.println(); 567 } 568 pw.decreaseIndent(); 569 570 pw.println("events"); 571 pw.increaseIndent(); 572 final TimeSparseArray<UsageEvents.Event> events = stats.events; 573 final int eventCount = events != null ? events.size() : 0; 574 for (int i = 0; i < eventCount; i++) { 575 final UsageEvents.Event event = events.valueAt(i); 576 pw.printPair("time", formatDateTime(event.mTimeStamp, prettyDates)); 577 pw.printPair("type", eventToString(event.mEventType)); 578 pw.printPair("package", event.mPackage); 579 if (event.mClass != null) { 580 pw.printPair("class", event.mClass); 581 } 582 if (event.mConfiguration != null) { 583 pw.printPair("config", Configuration.resourceQualifierString(event.mConfiguration)); 584 } 585 pw.println(); 586 } 587 pw.decreaseIndent(); 588 pw.decreaseIndent(); 589 } 590 591 private static String intervalToString(int interval) { 592 switch (interval) { 593 case UsageStatsManager.INTERVAL_DAILY: 594 return "daily"; 595 case UsageStatsManager.INTERVAL_WEEKLY: 596 return "weekly"; 597 case UsageStatsManager.INTERVAL_MONTHLY: 598 return "monthly"; 599 case UsageStatsManager.INTERVAL_YEARLY: 600 return "yearly"; 601 default: 602 return "?"; 603 } 604 } 605 606 private static String eventToString(int eventType) { 607 switch (eventType) { 608 case UsageEvents.Event.NONE: 609 return "NONE"; 610 case UsageEvents.Event.MOVE_TO_BACKGROUND: 611 return "MOVE_TO_BACKGROUND"; 612 case UsageEvents.Event.MOVE_TO_FOREGROUND: 613 return "MOVE_TO_FOREGROUND"; 614 case UsageEvents.Event.END_OF_DAY: 615 return "END_OF_DAY"; 616 case UsageEvents.Event.CONTINUE_PREVIOUS_DAY: 617 return "CONTINUE_PREVIOUS_DAY"; 618 case UsageEvents.Event.CONFIGURATION_CHANGE: 619 return "CONFIGURATION_CHANGE"; 620 case UsageEvents.Event.INTERACTION: 621 return "INTERACTION"; 622 default: 623 return "UNKNOWN"; 624 } 625 } 626} 627