/**
* 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 android.app.usage.ConfigurationStats;
import android.app.usage.TimeSparseArray;
import android.app.usage.UsageEvents;
import android.app.usage.UsageEvents.Event;
import android.app.usage.UsageStats;
import android.app.usage.UsageStatsManager;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.SystemClock;
import android.content.Context;
import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Slog;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.usage.UsageStatsDatabase.StatCombiner;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* A per-user UsageStatsService. All methods are meant to be called with the main lock held
* in UsageStatsService.
*/
class UserUsageStatsService {
private static final String TAG = "UsageStatsService";
private static final boolean DEBUG = UsageStatsService.DEBUG;
private static final SimpleDateFormat sDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private static final int sDateFormatFlags =
DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_SHOW_TIME
| DateUtils.FORMAT_SHOW_YEAR
| DateUtils.FORMAT_NUMERIC_DATE;
private final Context mContext;
private final UsageStatsDatabase mDatabase;
private final IntervalStats[] mCurrentStats;
private boolean mStatsChanged = false;
private final UnixCalendar mDailyExpiryDate;
private final StatsUpdatedListener mListener;
private final String mLogPrefix;
private final int mUserId;
interface StatsUpdatedListener {
void onStatsUpdated();
}
UserUsageStatsService(Context context, int userId, File usageStatsDir,
StatsUpdatedListener listener) {
mContext = context;
mDailyExpiryDate = new UnixCalendar(0);
mDatabase = new UsageStatsDatabase(usageStatsDir);
mCurrentStats = new IntervalStats[UsageStatsManager.INTERVAL_COUNT];
mListener = listener;
mLogPrefix = "User[" + Integer.toString(userId) + "] ";
mUserId = userId;
}
void init(final long currentTimeMillis, final long deviceUsageTime) {
mDatabase.init(currentTimeMillis);
int nullCount = 0;
for (int i = 0; i < mCurrentStats.length; i++) {
mCurrentStats[i] = mDatabase.getLatestUsageStats(i);
if (mCurrentStats[i] == null) {
// Find out how many intervals we don't have data for.
// Ideally it should be all or none.
nullCount++;
}
}
if (nullCount > 0) {
if (nullCount != mCurrentStats.length) {
// This is weird, but we shouldn't fail if something like this
// happens.
Slog.w(TAG, mLogPrefix + "Some stats have no latest available");
} else {
// This must be first boot.
}
// By calling loadActiveStats, we will
// generate new stats for each bucket.
loadActiveStats(currentTimeMillis,/*force=*/ false, /*resetBeginIdleTime=*/ false);
} else {
// Set up the expiry date to be one day from the latest daily stat.
// This may actually be today and we will rollover on the first event
// that is reported.
mDailyExpiryDate.setTimeInMillis(
mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime);
mDailyExpiryDate.addDays(1);
mDailyExpiryDate.truncateToDay();
Slog.i(TAG, mLogPrefix + "Rollover scheduled @ " +
sDateFormat.format(mDailyExpiryDate.getTimeInMillis()) +
"(" + mDailyExpiryDate.getTimeInMillis() + ")");
}
// Now close off any events that were open at the time this was saved.
for (IntervalStats stat : mCurrentStats) {
final int pkgCount = stat.packageStats.size();
for (int i = 0; i < pkgCount; i++) {
UsageStats pkgStats = stat.packageStats.valueAt(i);
if (pkgStats.mLastEvent == UsageEvents.Event.MOVE_TO_FOREGROUND ||
pkgStats.mLastEvent == UsageEvents.Event.CONTINUE_PREVIOUS_DAY) {
stat.update(pkgStats.mPackageName, stat.lastTimeSaved,
UsageEvents.Event.END_OF_DAY);
notifyStatsChanged();
}
}
stat.updateConfigurationStats(null, stat.lastTimeSaved);
}
if (mDatabase.isNewUpdate()) {
initializeDefaultsForApps(currentTimeMillis, deviceUsageTime,
mDatabase.isFirstUpdate());
}
}
/**
* If any of the apps don't have a last-used entry, add one now.
* @param currentTimeMillis the current time
* @param firstUpdate if it is the first update, touch all installed apps, otherwise only
* touch the system apps
*/
private void initializeDefaultsForApps(long currentTimeMillis, long deviceUsageTime,
boolean firstUpdate) {
PackageManager pm = mContext.getPackageManager();
List<PackageInfo> packages = pm.getInstalledPackages(0, mUserId);
final int packageCount = packages.size();
for (int i = 0; i < packageCount; i++) {
final PackageInfo pi = packages.get(i);
String packageName = pi.packageName;
if (pi.applicationInfo != null && (firstUpdate || pi.applicationInfo.isSystemApp())
&& getBeginIdleTime(packageName) == -1) {
for (IntervalStats stats : mCurrentStats) {
stats.update(packageName, currentTimeMillis, Event.SYSTEM_INTERACTION);
stats.updateBeginIdleTime(packageName, deviceUsageTime);
mStatsChanged = true;
}
}
}
// Persist the new OTA-related access stats.
persistActiveStats();
}
void onTimeChanged(long oldTime, long newTime, boolean resetBeginIdleTime) {
persistActiveStats();
mDatabase.onTimeChanged(newTime - oldTime);
loadActiveStats(newTime, /* force= */ true, resetBeginIdleTime);
}
void reportEvent(UsageEvents.Event event, long deviceUsageTime) {
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Got usage event for " + event.mPackage
+ "[" + event.mTimeStamp + "]: "
+ eventToString(event.mEventType));
}
if (event.mTimeStamp >= mDailyExpiryDate.getTimeInMillis()) {
// Need to rollover
rolloverStats(event.mTimeStamp);
}
final IntervalStats currentDailyStats = mCurrentStats[UsageStatsManager.INTERVAL_DAILY];
final Configuration newFullConfig = event.mConfiguration;
if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE &&
currentDailyStats.activeConfiguration != null) {
// Make the event configuration a delta.
event.mConfiguration = Configuration.generateDelta(
currentDailyStats.activeConfiguration, newFullConfig);
}
// Add the event to the daily list.
if (currentDailyStats.events == null) {
currentDailyStats.events = new TimeSparseArray<>();
}
if (event.mEventType != UsageEvents.Event.SYSTEM_INTERACTION) {
currentDailyStats.events.put(event.mTimeStamp, event);
}
for (IntervalStats stats : mCurrentStats) {
if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE) {
stats.updateConfigurationStats(newFullConfig, event.mTimeStamp);
} else {
stats.update(event.mPackage, event.mTimeStamp, event.mEventType);
stats.updateBeginIdleTime(event.mPackage, deviceUsageTime);
}
}
notifyStatsChanged();
}
/**
* Sets the beginIdleTime for each of the intervals.
* @param beginIdleTime
*/
void setBeginIdleTime(String packageName, long beginIdleTime) {
for (IntervalStats stats : mCurrentStats) {
stats.updateBeginIdleTime(packageName, beginIdleTime);
}
notifyStatsChanged();
}
void setSystemLastUsedTime(String packageName, long lastUsedTime) {
for (IntervalStats stats : mCurrentStats) {
stats.updateSystemLastUsedTime(packageName, lastUsedTime);
}
notifyStatsChanged();
}
private static final StatCombiner<UsageStats> sUsageStatsCombiner =
new StatCombiner<UsageStats>() {
@Override
public void combine(IntervalStats stats, boolean mutable,
List<UsageStats> accResult) {
if (!mutable) {
accResult.addAll(stats.packageStats.values());
return;
}
final int statCount = stats.packageStats.size();
for (int i = 0; i < statCount; i++) {
accResult.add(new UsageStats(stats.packageStats.valueAt(i)));
}
}
};
private static final StatCombiner<ConfigurationStats> sConfigStatsCombiner =
new StatCombiner<ConfigurationStats>() {
@Override
public void combine(IntervalStats stats, boolean mutable,
List<ConfigurationStats> accResult) {
if (!mutable) {
accResult.addAll(stats.configurations.values());
return;
}
final int configCount = stats.configurations.size();
for (int i = 0; i < configCount; i++) {
accResult.add(new ConfigurationStats(stats.configurations.valueAt(i)));
}
}
};
/**
* Generic query method that selects the appropriate IntervalStats for the specified time range
* and bucket, then calls the {@link com.android.server.usage.UsageStatsDatabase.StatCombiner}
* provided to select the stats to use from the IntervalStats object.
*/
private <T> List<T> queryStats(int intervalType, final long beginTime, final long endTime,
StatCombiner<T> combiner) {
if (intervalType == UsageStatsManager.INTERVAL_BEST) {
intervalType = mDatabase.findBestFitBucket(beginTime, endTime);
if (intervalType < 0) {
// Nothing saved to disk yet, so every stat is just as equal (no rollover has
// occurred.
intervalType = UsageStatsManager.INTERVAL_DAILY;
}
}
if (intervalType < 0 || intervalType >= mCurrentStats.length) {
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Bad intervalType used " + intervalType);
}
return null;
}
final IntervalStats currentStats = mCurrentStats[intervalType];
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "SELECT * FROM " + intervalType + " WHERE beginTime >= "
+ beginTime + " AND endTime < " + endTime);
}
if (beginTime >= currentStats.endTime) {
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Requesting stats after " + beginTime + " but latest is "
+ currentStats.endTime);
}
// Nothing newer available.
return null;
}
// Truncate the endTime to just before the in-memory stats. Then, we'll append the
// in-memory stats to the results (if necessary) so as to avoid writing to disk too
// often.
final long truncatedEndTime = Math.min(currentStats.beginTime, endTime);
// Get the stats from disk.
List<T> results = mDatabase.queryUsageStats(intervalType, beginTime,
truncatedEndTime, combiner);
if (DEBUG) {
Slog.d(TAG, "Got " + (results != null ? results.size() : 0) + " results from disk");
Slog.d(TAG, "Current stats beginTime=" + currentStats.beginTime +
" endTime=" + currentStats.endTime);
}
// Now check if the in-memory stats match the range and add them if they do.
if (beginTime < currentStats.endTime && endTime > currentStats.beginTime) {
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Returning in-memory stats");
}
if (results == null) {
results = new ArrayList<>();
}
combiner.combine(currentStats, true, results);
}
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Results: " + (results != null ? results.size() : 0));
}
return results;
}
List<UsageStats> queryUsageStats(int bucketType, long beginTime, long endTime) {
return queryStats(bucketType, beginTime, endTime, sUsageStatsCombiner);
}
List<ConfigurationStats> queryConfigurationStats(int bucketType, long beginTime, long endTime) {
return queryStats(bucketType, beginTime, endTime, sConfigStatsCombiner);
}
UsageEvents queryEvents(final long beginTime, final long endTime) {
final ArraySet<String> names = new ArraySet<>();
List<UsageEvents.Event> results = queryStats(UsageStatsManager.INTERVAL_DAILY,
beginTime, endTime, new StatCombiner<UsageEvents.Event>() {
@Override
public void combine(IntervalStats stats, boolean mutable,
List<UsageEvents.Event> accumulatedResult) {
if (stats.events == null) {
return;
}
final int startIndex = stats.events.closestIndexOnOrAfter(beginTime);
if (startIndex < 0) {
return;
}
final int size = stats.events.size();
for (int i = startIndex; i < size; i++) {
if (stats.events.keyAt(i) >= endTime) {
return;
}
final UsageEvents.Event event = stats.events.valueAt(i);
names.add(event.mPackage);
if (event.mClass != null) {
names.add(event.mClass);
}
accumulatedResult.add(event);
}
}
});
if (results == null || results.isEmpty()) {
return null;
}
String[] table = names.toArray(new String[names.size()]);
Arrays.sort(table);
return new UsageEvents(results, table);
}
long getBeginIdleTime(String packageName) {
final IntervalStats yearly = mCurrentStats[UsageStatsManager.INTERVAL_YEARLY];
UsageStats packageUsage;
if ((packageUsage = yearly.packageStats.get(packageName)) == null) {
return -1;
} else {
return packageUsage.getBeginIdleTime();
}
}
long getSystemLastUsedTime(String packageName) {
final IntervalStats yearly = mCurrentStats[UsageStatsManager.INTERVAL_YEARLY];
UsageStats packageUsage;
if ((packageUsage = yearly.packageStats.get(packageName)) == null) {
return -1;
} else {
return packageUsage.getLastTimeSystemUsed();
}
}
void persistActiveStats() {
if (mStatsChanged) {
Slog.i(TAG, mLogPrefix + "Flushing usage stats to disk");
try {
for (int i = 0; i < mCurrentStats.length; i++) {
mDatabase.putUsageStats(i, mCurrentStats[i]);
}
mStatsChanged = false;
} catch (IOException e) {
Slog.e(TAG, mLogPrefix + "Failed to persist active stats", e);
}
}
}
private void rolloverStats(final long currentTimeMillis) {
final long startTime = SystemClock.elapsedRealtime();
Slog.i(TAG, mLogPrefix + "Rolling over usage stats");
// Finish any ongoing events with an END_OF_DAY event. Make a note of which components
// need a new CONTINUE_PREVIOUS_DAY entry.
final Configuration previousConfig =
mCurrentStats[UsageStatsManager.INTERVAL_DAILY].activeConfiguration;
ArraySet<String> continuePreviousDay = new ArraySet<>();
for (IntervalStats stat : mCurrentStats) {
final int pkgCount = stat.packageStats.size();
for (int i = 0; i < pkgCount; i++) {
UsageStats pkgStats = stat.packageStats.valueAt(i);
if (pkgStats.mLastEvent == UsageEvents.Event.MOVE_TO_FOREGROUND ||
pkgStats.mLastEvent == UsageEvents.Event.CONTINUE_PREVIOUS_DAY) {
continuePreviousDay.add(pkgStats.mPackageName);
stat.update(pkgStats.mPackageName, mDailyExpiryDate.getTimeInMillis() - 1,
UsageEvents.Event.END_OF_DAY);
notifyStatsChanged();
}
}
stat.updateConfigurationStats(null, mDailyExpiryDate.getTimeInMillis() - 1);
}
persistActiveStats();
mDatabase.prune(currentTimeMillis);
loadActiveStats(currentTimeMillis, /*force=*/ false, /*resetBeginIdleTime=*/ false);
final int continueCount = continuePreviousDay.size();
for (int i = 0; i < continueCount; i++) {
String name = continuePreviousDay.valueAt(i);
final long beginTime = mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime;
for (IntervalStats stat : mCurrentStats) {
stat.update(name, beginTime, UsageEvents.Event.CONTINUE_PREVIOUS_DAY);
stat.updateConfigurationStats(previousConfig, beginTime);
notifyStatsChanged();
}
}
persistActiveStats();
final long totalTime = SystemClock.elapsedRealtime() - startTime;
Slog.i(TAG, mLogPrefix + "Rolling over usage stats complete. Took " + totalTime
+ " milliseconds");
}
private void notifyStatsChanged() {
if (!mStatsChanged) {
mStatsChanged = true;
mListener.onStatsUpdated();
}
}
/**
* @param force To force all in-memory stats to be reloaded.
*/
private void loadActiveStats(final long currentTimeMillis, boolean force,
boolean resetBeginIdleTime) {
final UnixCalendar tempCal = mDailyExpiryDate;
for (int intervalType = 0; intervalType < mCurrentStats.length; intervalType++) {
tempCal.setTimeInMillis(currentTimeMillis);
UnixCalendar.truncateTo(tempCal, intervalType);
if (!force && mCurrentStats[intervalType] != null &&
mCurrentStats[intervalType].beginTime == tempCal.getTimeInMillis()) {
// These are the same, no need to load them (in memory stats are always newer
// than persisted stats).
continue;
}
final long lastBeginTime = mDatabase.getLatestUsageStatsBeginTime(intervalType);
if (lastBeginTime >= tempCal.getTimeInMillis()) {
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Loading existing stats @ " +
sDateFormat.format(lastBeginTime) + "(" + lastBeginTime +
") for interval " + intervalType);
}
mCurrentStats[intervalType] = mDatabase.getLatestUsageStats(intervalType);
} else {
mCurrentStats[intervalType] = null;
}
if (mCurrentStats[intervalType] == null) {
if (DEBUG) {
Slog.d(TAG, "Creating new stats @ " +
sDateFormat.format(tempCal.getTimeInMillis()) + "(" +
tempCal.getTimeInMillis() + ") for interval " + intervalType);
}
mCurrentStats[intervalType] = new IntervalStats();
mCurrentStats[intervalType].beginTime = tempCal.getTimeInMillis();
mCurrentStats[intervalType].endTime = currentTimeMillis;
}
if (resetBeginIdleTime) {
for (UsageStats usageStats : mCurrentStats[intervalType].packageStats.values()) {
usageStats.mBeginIdleTime = 0;
}
}
}
mStatsChanged = false;
mDailyExpiryDate.setTimeInMillis(currentTimeMillis);
mDailyExpiryDate.addDays(1);
mDailyExpiryDate.truncateToDay();
Slog.i(TAG, mLogPrefix + "Rollover scheduled @ " +
sDateFormat.format(mDailyExpiryDate.getTimeInMillis()) + "(" +
tempCal.getTimeInMillis() + ")");
}
//
// -- DUMP related methods --
//
void checkin(final IndentingPrintWriter pw, final long screenOnTime) {
mDatabase.checkinDailyFiles(new UsageStatsDatabase.CheckinAction() {
@Override
public boolean checkin(IntervalStats stats) {
printIntervalStats(pw, stats, screenOnTime, false);
return true;
}
});
}
void dump(IndentingPrintWriter pw, final long screenOnTime) {
// This is not a check-in, only dump in-memory stats.
for (int interval = 0; interval < mCurrentStats.length; interval++) {
pw.print("In-memory ");
pw.print(intervalToString(interval));
pw.println(" stats");
printIntervalStats(pw, mCurrentStats[interval], screenOnTime, true);
}
}
private String formatDateTime(long dateTime, boolean pretty) {
if (pretty) {
return "\"" + DateUtils.formatDateTime(mContext, dateTime, sDateFormatFlags) + "\"";
}
return Long.toString(dateTime);
}
private String formatElapsedTime(long elapsedTime, boolean pretty) {
if (pretty) {
return "\"" + DateUtils.formatElapsedTime(elapsedTime / 1000) + "\"";
}
return Long.toString(elapsedTime);
}
void printIntervalStats(IndentingPrintWriter pw, IntervalStats stats, long screenOnTime,
boolean prettyDates) {
if (prettyDates) {
pw.printPair("timeRange", "\"" + DateUtils.formatDateRange(mContext,
stats.beginTime, stats.endTime, sDateFormatFlags) + "\"");
} else {
pw.printPair("beginTime", stats.beginTime);
pw.printPair("endTime", stats.endTime);
}
pw.println();
pw.increaseIndent();
pw.println("packages");
pw.increaseIndent();
final ArrayMap<String, UsageStats> pkgStats = stats.packageStats;
final int pkgCount = pkgStats.size();
for (int i = 0; i < pkgCount; i++) {
final UsageStats usageStats = pkgStats.valueAt(i);
pw.printPair("package", usageStats.mPackageName);
pw.printPair("totalTime",
formatElapsedTime(usageStats.mTotalTimeInForeground, prettyDates));
pw.printPair("lastTime", formatDateTime(usageStats.mLastTimeUsed, prettyDates));
pw.printPair("lastTimeSystem",
formatDateTime(usageStats.mLastTimeSystemUsed, prettyDates));
pw.printPair("inactiveTime",
formatElapsedTime(screenOnTime - usageStats.mBeginIdleTime, prettyDates));
pw.println();
}
pw.decreaseIndent();
pw.println("configurations");
pw.increaseIndent();
final ArrayMap<Configuration, ConfigurationStats> configStats = stats.configurations;
final int configCount = configStats.size();
for (int i = 0; i < configCount; i++) {
final ConfigurationStats config = configStats.valueAt(i);
pw.printPair("config", Configuration.resourceQualifierString(config.mConfiguration));
pw.printPair("totalTime", formatElapsedTime(config.mTotalTimeActive, prettyDates));
pw.printPair("lastTime", formatDateTime(config.mLastTimeActive, prettyDates));
pw.printPair("count", config.mActivationCount);
pw.println();
}
pw.decreaseIndent();
pw.println("events");
pw.increaseIndent();
final TimeSparseArray<UsageEvents.Event> events = stats.events;
final int eventCount = events != null ? events.size() : 0;
for (int i = 0; i < eventCount; i++) {
final UsageEvents.Event event = events.valueAt(i);
pw.printPair("time", formatDateTime(event.mTimeStamp, prettyDates));
pw.printPair("type", eventToString(event.mEventType));
pw.printPair("package", event.mPackage);
if (event.mClass != null) {
pw.printPair("class", event.mClass);
}
if (event.mConfiguration != null) {
pw.printPair("config", Configuration.resourceQualifierString(event.mConfiguration));
}
pw.println();
}
pw.decreaseIndent();
pw.decreaseIndent();
}
private static String intervalToString(int interval) {
switch (interval) {
case UsageStatsManager.INTERVAL_DAILY:
return "daily";
case UsageStatsManager.INTERVAL_WEEKLY:
return "weekly";
case UsageStatsManager.INTERVAL_MONTHLY:
return "monthly";
case UsageStatsManager.INTERVAL_YEARLY:
return "yearly";
default:
return "?";
}
}
private static String eventToString(int eventType) {
switch (eventType) {
case UsageEvents.Event.NONE:
return "NONE";
case UsageEvents.Event.MOVE_TO_BACKGROUND:
return "MOVE_TO_BACKGROUND";
case UsageEvents.Event.MOVE_TO_FOREGROUND:
return "MOVE_TO_FOREGROUND";
case UsageEvents.Event.END_OF_DAY:
return "END_OF_DAY";
case UsageEvents.Event.CONTINUE_PREVIOUS_DAY:
return "CONTINUE_PREVIOUS_DAY";
case UsageEvents.Event.CONFIGURATION_CHANGE:
return "CONFIGURATION_CHANGE";
case UsageEvents.Event.SYSTEM_INTERACTION:
return "SYSTEM_INTERACTION";
case UsageEvents.Event.USER_INTERACTION:
return "USER_INTERACTION";
default:
return "UNKNOWN";
}
}
}