| /** |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| * use this file except in compliance with the License. You may obtain a copy |
| * of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations |
| * under the License. |
| */ |
| package com.android.server.usage; |
| |
| import static android.app.usage.UsageEvents.Event.ACTIVITY_PAUSED; |
| import static android.app.usage.UsageEvents.Event.ACTIVITY_RESUMED; |
| import static android.app.usage.UsageEvents.Event.ACTIVITY_STOPPED; |
| import static android.app.usage.UsageEvents.Event.CONFIGURATION_CHANGE; |
| import static android.app.usage.UsageEvents.Event.CONTINUE_PREVIOUS_DAY; |
| import static android.app.usage.UsageEvents.Event.CONTINUING_FOREGROUND_SERVICE; |
| import static android.app.usage.UsageEvents.Event.DEVICE_SHUTDOWN; |
| import static android.app.usage.UsageEvents.Event.END_OF_DAY; |
| import static android.app.usage.UsageEvents.Event.FLUSH_TO_DISK; |
| import static android.app.usage.UsageEvents.Event.FOREGROUND_SERVICE_START; |
| import static android.app.usage.UsageEvents.Event.FOREGROUND_SERVICE_STOP; |
| import static android.app.usage.UsageEvents.Event.KEYGUARD_HIDDEN; |
| import static android.app.usage.UsageEvents.Event.KEYGUARD_SHOWN; |
| import static android.app.usage.UsageEvents.Event.NOTIFICATION_INTERRUPTION; |
| import static android.app.usage.UsageEvents.Event.ROLLOVER_FOREGROUND_SERVICE; |
| import static android.app.usage.UsageEvents.Event.SCREEN_INTERACTIVE; |
| import static android.app.usage.UsageEvents.Event.SCREEN_NON_INTERACTIVE; |
| import static android.app.usage.UsageEvents.Event.SHORTCUT_INVOCATION; |
| import static android.app.usage.UsageEvents.Event.STANDBY_BUCKET_CHANGED; |
| import static android.app.usage.UsageEvents.Event.SYSTEM_INTERACTION; |
| |
| import android.app.usage.ConfigurationStats; |
| import android.app.usage.EventList; |
| import android.app.usage.EventStats; |
| import android.app.usage.UsageEvents.Event; |
| import android.app.usage.UsageStats; |
| import android.content.res.Configuration; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.proto.ProtoInputStream; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.io.IOException; |
| import java.util.List; |
| |
| public class IntervalStats { |
| public static final int CURRENT_MAJOR_VERSION = 1; |
| public static final int CURRENT_MINOR_VERSION = 1; |
| public int majorVersion = CURRENT_MAJOR_VERSION; |
| public int minorVersion = CURRENT_MINOR_VERSION; |
| public long beginTime; |
| public long endTime; |
| public long lastTimeSaved; |
| public final EventTracker interactiveTracker = new EventTracker(); |
| public final EventTracker nonInteractiveTracker = new EventTracker(); |
| public final EventTracker keyguardShownTracker = new EventTracker(); |
| public final EventTracker keyguardHiddenTracker = new EventTracker(); |
| public final ArrayMap<String, UsageStats> packageStats = new ArrayMap<>(); |
| public final ArrayMap<Configuration, ConfigurationStats> configurations = new ArrayMap<>(); |
| public Configuration activeConfiguration; |
| public final EventList events = new EventList(); |
| |
| // A string cache. This is important as when we're parsing XML files, we don't want to |
| // keep hundreds of strings that have the same contents. We will read the string |
| // and only keep it if it's not in the cache. The GC will take care of the |
| // strings that had identical copies in the cache. |
| public final ArraySet<String> mStringCache = new ArraySet<>(); |
| |
| public static final class EventTracker { |
| public long curStartTime; |
| public long lastEventTime; |
| public long duration; |
| public int count; |
| |
| public void commitTime(long timeStamp) { |
| if (curStartTime != 0) { |
| duration += timeStamp - curStartTime; |
| curStartTime = 0; |
| } |
| } |
| |
| public void update(long timeStamp) { |
| if (curStartTime == 0) { |
| // If we aren't already running, time to bump the count. |
| count++; |
| } |
| commitTime(timeStamp); |
| curStartTime = timeStamp; |
| lastEventTime = timeStamp; |
| } |
| |
| void addToEventStats(List<EventStats> out, int event, long beginTime, long endTime) { |
| if (count != 0 || duration != 0) { |
| EventStats ev = new EventStats(); |
| ev.mEventType = event; |
| ev.mCount = count; |
| ev.mTotalTime = duration; |
| ev.mLastEventTime = lastEventTime; |
| ev.mBeginTimeStamp = beginTime; |
| ev.mEndTimeStamp = endTime; |
| out.add(ev); |
| } |
| } |
| |
| } |
| |
| public IntervalStats() { |
| } |
| |
| /** |
| * Gets the UsageStats object for the given package, or creates one and adds it internally. |
| */ |
| UsageStats getOrCreateUsageStats(String packageName) { |
| UsageStats usageStats = packageStats.get(packageName); |
| if (usageStats == null) { |
| usageStats = new UsageStats(); |
| usageStats.mPackageName = getCachedStringRef(packageName); |
| usageStats.mBeginTimeStamp = beginTime; |
| usageStats.mEndTimeStamp = endTime; |
| packageStats.put(usageStats.mPackageName, usageStats); |
| } |
| return usageStats; |
| } |
| |
| /** |
| * Gets the ConfigurationStats object for the given configuration, or creates one and adds it |
| * internally. |
| */ |
| ConfigurationStats getOrCreateConfigurationStats(Configuration config) { |
| ConfigurationStats configStats = configurations.get(config); |
| if (configStats == null) { |
| configStats = new ConfigurationStats(); |
| configStats.mBeginTimeStamp = beginTime; |
| configStats.mEndTimeStamp = endTime; |
| configStats.mConfiguration = config; |
| configurations.put(config, configStats); |
| } |
| return configStats; |
| } |
| |
| /** |
| * Builds a UsageEvents.Event, but does not add it internally. |
| */ |
| Event buildEvent(String packageName, String className) { |
| Event event = new Event(); |
| event.mPackage = getCachedStringRef(packageName); |
| if (className != null) { |
| event.mClass = getCachedStringRef(className); |
| } |
| return event; |
| } |
| |
| /** |
| * Builds a UsageEvents.Event from a proto, but does not add it internally. |
| * Built here to take advantage of the cached String Refs |
| */ |
| Event buildEvent(ProtoInputStream parser, List<String> stringPool) |
| throws IOException { |
| final Event event = new Event(); |
| while (true) { |
| switch (parser.nextField()) { |
| case (int) IntervalStatsProto.Event.PACKAGE: |
| event.mPackage = getCachedStringRef( |
| parser.readString(IntervalStatsProto.Event.PACKAGE)); |
| break; |
| case (int) IntervalStatsProto.Event.PACKAGE_INDEX: |
| event.mPackage = getCachedStringRef(stringPool.get( |
| parser.readInt(IntervalStatsProto.Event.PACKAGE_INDEX) - 1)); |
| break; |
| case (int) IntervalStatsProto.Event.CLASS: |
| event.mClass = getCachedStringRef( |
| parser.readString(IntervalStatsProto.Event.CLASS)); |
| break; |
| case (int) IntervalStatsProto.Event.CLASS_INDEX: |
| event.mClass = getCachedStringRef(stringPool.get( |
| parser.readInt(IntervalStatsProto.Event.CLASS_INDEX) - 1)); |
| break; |
| case (int) IntervalStatsProto.Event.TIME_MS: |
| event.mTimeStamp = beginTime + parser.readLong( |
| IntervalStatsProto.Event.TIME_MS); |
| break; |
| case (int) IntervalStatsProto.Event.FLAGS: |
| event.mFlags = parser.readInt(IntervalStatsProto.Event.FLAGS); |
| break; |
| case (int) IntervalStatsProto.Event.TYPE: |
| event.mEventType = parser.readInt(IntervalStatsProto.Event.TYPE); |
| break; |
| case (int) IntervalStatsProto.Event.CONFIG: |
| event.mConfiguration = new Configuration(); |
| event.mConfiguration.readFromProto(parser, IntervalStatsProto.Event.CONFIG); |
| break; |
| case (int) IntervalStatsProto.Event.SHORTCUT_ID: |
| event.mShortcutId = parser.readString( |
| IntervalStatsProto.Event.SHORTCUT_ID).intern(); |
| break; |
| case (int) IntervalStatsProto.Event.STANDBY_BUCKET: |
| event.mBucketAndReason = parser.readInt( |
| IntervalStatsProto.Event.STANDBY_BUCKET); |
| break; |
| case (int) IntervalStatsProto.Event.NOTIFICATION_CHANNEL: |
| event.mNotificationChannelId = parser.readString( |
| IntervalStatsProto.Event.NOTIFICATION_CHANNEL); |
| break; |
| case (int) IntervalStatsProto.Event.NOTIFICATION_CHANNEL_INDEX: |
| event.mNotificationChannelId = getCachedStringRef(stringPool.get( |
| parser.readInt(IntervalStatsProto.Event.NOTIFICATION_CHANNEL_INDEX) |
| - 1)); |
| break; |
| case (int) IntervalStatsProto.Event.INSTANCE_ID: |
| event.mInstanceId = parser.readInt(IntervalStatsProto.Event.INSTANCE_ID); |
| break; |
| case (int) IntervalStatsProto.Event.TASK_ROOT_PACKAGE_INDEX: |
| event.mTaskRootPackage = getCachedStringRef(stringPool.get( |
| parser.readInt(IntervalStatsProto.Event.TASK_ROOT_PACKAGE_INDEX) - 1)); |
| break; |
| case (int) IntervalStatsProto.Event.TASK_ROOT_CLASS_INDEX: |
| event.mTaskRootClass = getCachedStringRef(stringPool.get( |
| parser.readInt(IntervalStatsProto.Event.TASK_ROOT_CLASS_INDEX) - 1)); |
| break; |
| case ProtoInputStream.NO_MORE_FIELDS: |
| // Handle default values for certain events types |
| switch (event.mEventType) { |
| case CONFIGURATION_CHANGE: |
| if (event.mConfiguration == null) { |
| event.mConfiguration = new Configuration(); |
| } |
| break; |
| case SHORTCUT_INVOCATION: |
| if (event.mShortcutId == null) { |
| event.mShortcutId = ""; |
| } |
| break; |
| case NOTIFICATION_INTERRUPTION: |
| if (event.mNotificationChannelId == null) { |
| event.mNotificationChannelId = ""; |
| } |
| break; |
| } |
| if (event.mTimeStamp == 0) { |
| //mTimestamp not set, assume default value 0 plus beginTime |
| event.mTimeStamp = beginTime; |
| } |
| return event; |
| } |
| } |
| } |
| |
| private boolean isStatefulEvent(int eventType) { |
| switch (eventType) { |
| case ACTIVITY_RESUMED: |
| case ACTIVITY_PAUSED: |
| case ACTIVITY_STOPPED: |
| case FOREGROUND_SERVICE_START: |
| case FOREGROUND_SERVICE_STOP: |
| case END_OF_DAY: |
| case ROLLOVER_FOREGROUND_SERVICE: |
| case CONTINUE_PREVIOUS_DAY: |
| case CONTINUING_FOREGROUND_SERVICE: |
| case DEVICE_SHUTDOWN: |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Returns whether the event type is one caused by user visible |
| * interaction. Excludes those that are internally generated. |
| */ |
| private boolean isUserVisibleEvent(int eventType) { |
| return eventType != SYSTEM_INTERACTION |
| && eventType != STANDBY_BUCKET_CHANGED; |
| } |
| |
| /** |
| * Update the IntervalStats by a activity or foreground service event. |
| * @param packageName package name of this event. Is null if event targets to all packages. |
| * @param className class name of a activity or foreground service, could be null to if this |
| * is sent to all activities/services in this package. |
| * @param timeStamp Epoch timestamp in milliseconds. |
| * @param eventType event type as in {@link Event} |
| * @param instanceId if className is an activity, the hashCode of ActivityRecord's appToken. |
| * if className is not an activity, instanceId is not used. |
| * @hide |
| */ |
| @VisibleForTesting |
| public void update(String packageName, String className, long timeStamp, int eventType, |
| int instanceId) { |
| if (eventType == DEVICE_SHUTDOWN |
| || eventType == FLUSH_TO_DISK) { |
| // DEVICE_SHUTDOWN and FLUSH_TO_DISK are sent to all packages. |
| final int size = packageStats.size(); |
| for (int i = 0; i < size; i++) { |
| UsageStats usageStats = packageStats.valueAt(i); |
| usageStats.update(null, timeStamp, eventType, instanceId); |
| } |
| } else { |
| UsageStats usageStats = getOrCreateUsageStats(packageName); |
| usageStats.update(className, timeStamp, eventType, instanceId); |
| } |
| if (timeStamp > endTime) { |
| endTime = timeStamp; |
| } |
| } |
| |
| /** |
| * @hide |
| */ |
| @VisibleForTesting |
| public void addEvent(Event event) { |
| // Cache common use strings |
| event.mPackage = getCachedStringRef(event.mPackage); |
| if (event.mClass != null) { |
| event.mClass = getCachedStringRef(event.mClass); |
| } |
| if (event.mTaskRootPackage != null) { |
| event.mTaskRootPackage = getCachedStringRef(event.mTaskRootPackage); |
| } |
| if (event.mTaskRootClass != null) { |
| event.mTaskRootClass = getCachedStringRef(event.mTaskRootClass); |
| } |
| if (event.mEventType == NOTIFICATION_INTERRUPTION) { |
| event.mNotificationChannelId = getCachedStringRef(event.mNotificationChannelId); |
| } |
| events.insert(event); |
| if (event.mTimeStamp > endTime) { |
| endTime = event.mTimeStamp; |
| } |
| } |
| |
| void updateChooserCounts(String packageName, String category, String action) { |
| UsageStats usageStats = getOrCreateUsageStats(packageName); |
| if (usageStats.mChooserCounts == null) { |
| usageStats.mChooserCounts = new ArrayMap<>(); |
| } |
| ArrayMap<String, Integer> chooserCounts; |
| final int idx = usageStats.mChooserCounts.indexOfKey(action); |
| if (idx < 0) { |
| chooserCounts = new ArrayMap<>(); |
| usageStats.mChooserCounts.put(action, chooserCounts); |
| } else { |
| chooserCounts = usageStats.mChooserCounts.valueAt(idx); |
| } |
| int currentCount = chooserCounts.getOrDefault(category, 0); |
| chooserCounts.put(category, currentCount + 1); |
| } |
| |
| void updateConfigurationStats(Configuration config, long timeStamp) { |
| if (activeConfiguration != null) { |
| ConfigurationStats activeStats = configurations.get(activeConfiguration); |
| activeStats.mTotalTimeActive += timeStamp - activeStats.mLastTimeActive; |
| activeStats.mLastTimeActive = timeStamp - 1; |
| } |
| |
| if (config != null) { |
| ConfigurationStats configStats = getOrCreateConfigurationStats(config); |
| configStats.mLastTimeActive = timeStamp; |
| configStats.mActivationCount += 1; |
| activeConfiguration = configStats.mConfiguration; |
| } |
| if (timeStamp > endTime) { |
| endTime = timeStamp; |
| } |
| } |
| |
| void incrementAppLaunchCount(String packageName) { |
| UsageStats usageStats = getOrCreateUsageStats(packageName); |
| usageStats.mAppLaunchCount += 1; |
| } |
| |
| void commitTime(long timeStamp) { |
| interactiveTracker.commitTime(timeStamp); |
| nonInteractiveTracker.commitTime(timeStamp); |
| keyguardShownTracker.commitTime(timeStamp); |
| keyguardHiddenTracker.commitTime(timeStamp); |
| } |
| |
| void updateScreenInteractive(long timeStamp) { |
| interactiveTracker.update(timeStamp); |
| nonInteractiveTracker.commitTime(timeStamp); |
| } |
| |
| void updateScreenNonInteractive(long timeStamp) { |
| nonInteractiveTracker.update(timeStamp); |
| interactiveTracker.commitTime(timeStamp); |
| } |
| |
| void updateKeyguardShown(long timeStamp) { |
| keyguardShownTracker.update(timeStamp); |
| keyguardHiddenTracker.commitTime(timeStamp); |
| } |
| |
| void updateKeyguardHidden(long timeStamp) { |
| keyguardHiddenTracker.update(timeStamp); |
| keyguardShownTracker.commitTime(timeStamp); |
| } |
| |
| void addEventStatsTo(List<EventStats> out) { |
| interactiveTracker.addToEventStats(out, SCREEN_INTERACTIVE, |
| beginTime, endTime); |
| nonInteractiveTracker.addToEventStats(out, SCREEN_NON_INTERACTIVE, |
| beginTime, endTime); |
| keyguardShownTracker.addToEventStats(out, KEYGUARD_SHOWN, |
| beginTime, endTime); |
| keyguardHiddenTracker.addToEventStats(out, KEYGUARD_HIDDEN, |
| beginTime, endTime); |
| } |
| |
| private String getCachedStringRef(String str) { |
| final int index = mStringCache.indexOf(str); |
| if (index < 0) { |
| mStringCache.add(str); |
| return str; |
| } |
| return mStringCache.valueAt(index); |
| } |
| |
| /** |
| * When an IntervalStats object is deserialized, if the object's version number |
| * is lower than current version number, optionally perform a upgrade. |
| */ |
| void upgradeIfNeeded() { |
| // We only uprade on majorVersion change, no need to upgrade on minorVersion change. |
| if (!(majorVersion < CURRENT_MAJOR_VERSION)) { |
| return; |
| } |
| /* |
| Optional upgrade code here. |
| */ |
| majorVersion = CURRENT_MAJOR_VERSION; |
| } |
| } |