Home | History | Annotate | Download | only in usage
      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