blob: e489fb3416bb6f5d25120a26f9603086043888f3 [file] [log] [blame]
/**
* 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.TimeSparseArray;
import android.app.usage.UsageEvents;
import android.app.usage.UsageStats;
import android.app.usage.UsageStatsManager;
import android.content.ComponentName;
import android.util.ArraySet;
import android.util.Slog;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
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 final UsageStatsDatabase mDatabase;
private final IntervalStats[] mCurrentStats;
private boolean mStatsChanged = false;
private final Calendar mDailyExpiryDate;
private final StatsUpdatedListener mListener;
private final String mLogPrefix;
interface StatsUpdatedListener {
void onStatsUpdated();
}
UserUsageStatsService(int userId, File usageStatsDir, StatsUpdatedListener listener) {
mDailyExpiryDate = Calendar.getInstance();
mDatabase = new UsageStatsDatabase(usageStatsDir);
mCurrentStats = new IntervalStats[UsageStatsManager.INTERVAL_COUNT];
mListener = listener;
mLogPrefix = "User[" + Integer.toString(userId) + "] ";
}
void init() {
mDatabase.init();
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();
} 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.add(Calendar.DAY_OF_YEAR, 1);
UsageStatsUtils.truncateDateTo(UsageStatsManager.INTERVAL_DAILY, mDailyExpiryDate);
Slog.i(TAG, mLogPrefix + "Rollover scheduled for "
+ sDateFormat.format(mDailyExpiryDate.getTime()));
}
// Now close off any events that were open at the time this was saved.
for (IntervalStats stat : mCurrentStats) {
final int pkgCount = stat.stats.size();
for (int i = 0; i < pkgCount; i++) {
UsageStats pkgStats = stat.stats.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();
}
}
}
}
void reportEvent(UsageEvents.Event event) {
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Got usage event for " + event.getComponent().getPackageName()
+ "[" + event.getTimeStamp() + "]: "
+ eventToString(event.getEventType()));
}
if (event.getTimeStamp() >= mDailyExpiryDate.getTimeInMillis()) {
// Need to rollover
rolloverStats();
}
if (mCurrentStats[UsageStatsManager.INTERVAL_DAILY].events == null) {
mCurrentStats[UsageStatsManager.INTERVAL_DAILY].events = new TimeSparseArray<>();
}
mCurrentStats[UsageStatsManager.INTERVAL_DAILY].events.put(event.getTimeStamp(), event);
for (IntervalStats stats : mCurrentStats) {
stats.update(event.getComponent().getPackageName(), event.getTimeStamp(),
event.getEventType());
}
notifyStatsChanged();
}
List<UsageStats> queryUsageStats(int bucketType, long beginTime, long endTime) {
if (bucketType == UsageStatsManager.INTERVAL_BEST) {
bucketType = mDatabase.findBestFitBucket(beginTime, endTime);
}
if (bucketType < 0 || bucketType >= mCurrentStats.length) {
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Bad bucketType used " + bucketType);
}
return null;
}
if (beginTime >= mCurrentStats[bucketType].endTime) {
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Requesting stats after " + beginTime + " but latest is "
+ mCurrentStats[bucketType].endTime);
}
// Nothing newer available.
return null;
} else if (beginTime >= mCurrentStats[bucketType].beginTime) {
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Returning in-memory stats for bucket " + bucketType);
}
// Fast path for retrieving in-memory state.
ArrayList<UsageStats> results = new ArrayList<>();
final int packageCount = mCurrentStats[bucketType].stats.size();
for (int i = 0; i < packageCount; i++) {
results.add(new UsageStats(mCurrentStats[bucketType].stats.valueAt(i)));
}
return results;
}
// Flush any changes that were made to disk before we do a disk query.
// If we're not grabbing the ongoing stats, no need to persist.
persistActiveStats();
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "SELECT * FROM " + bucketType + " WHERE beginTime >= "
+ beginTime + " AND endTime < " + endTime);
}
final List<UsageStats> results = mDatabase.queryUsageStats(bucketType, beginTime, endTime);
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Results: " + (results == null ? 0 : results.size()));
}
return results;
}
UsageEvents queryEvents(long beginTime, long endTime) {
if (endTime > mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime) {
if (beginTime > mCurrentStats[UsageStatsManager.INTERVAL_DAILY].endTime) {
return null;
}
TimeSparseArray<UsageEvents.Event> events =
mCurrentStats[UsageStatsManager.INTERVAL_DAILY].events;
if (events == null) {
return null;
}
final int startIndex = events.closestIndexOnOrAfter(beginTime);
if (startIndex < 0) {
return null;
}
ArraySet<ComponentName> names = new ArraySet<>();
ArrayList<UsageEvents.Event> results = new ArrayList<>();
final int size = events.size();
for (int i = startIndex; i < size; i++) {
if (events.keyAt(i) >= endTime) {
break;
}
names.add(events.valueAt(i).getComponent());
results.add(events.valueAt(i));
}
ComponentName[] table = names.toArray(new ComponentName[names.size()]);
Arrays.sort(table);
return new UsageEvents(results, table);
}
// TODO(adamlesinski): Query the previous days.
return null;
}
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 startTime = System.currentTimeMillis();
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.
ArraySet<String> continuePreviousDay = new ArraySet<>();
for (IntervalStats stat : mCurrentStats) {
final int pkgCount = stat.stats.size();
for (int i = 0; i < pkgCount; i++) {
UsageStats pkgStats = stat.stats.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);
mStatsChanged = true;
}
}
}
persistActiveStats();
mDatabase.prune();
loadActiveStats();
final int continueCount = continuePreviousDay.size();
for (int i = 0; i < continueCount; i++) {
String name = continuePreviousDay.valueAt(i);
for (IntervalStats stat : mCurrentStats) {
stat.update(name, mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime,
UsageEvents.Event.CONTINUE_PREVIOUS_DAY);
mStatsChanged = true;
}
}
persistActiveStats();
final long totalTime = System.currentTimeMillis() - startTime;
Slog.i(TAG, mLogPrefix + "Rolling over usage stats complete. Took " + totalTime
+ " milliseconds");
}
private void notifyStatsChanged() {
if (!mStatsChanged) {
mStatsChanged = true;
mListener.onStatsUpdated();
}
}
private void loadActiveStats() {
final long timeNow = System.currentTimeMillis();
Calendar tempCal = mDailyExpiryDate;
for (int bucketType = 0; bucketType < mCurrentStats.length; bucketType++) {
tempCal.setTimeInMillis(timeNow);
UsageStatsUtils.truncateDateTo(bucketType, tempCal);
if (mCurrentStats[bucketType] != null &&
mCurrentStats[bucketType].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(bucketType);
if (lastBeginTime >= tempCal.getTimeInMillis()) {
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Loading existing stats (" + lastBeginTime +
") for bucket " + bucketType);
}
mCurrentStats[bucketType] = mDatabase.getLatestUsageStats(bucketType);
if (DEBUG) {
if (mCurrentStats[bucketType] != null) {
Slog.d(TAG, mLogPrefix + "Found " +
(mCurrentStats[bucketType].events == null ?
0 : mCurrentStats[bucketType].events.size()) +
" events");
}
}
} else {
mCurrentStats[bucketType] = null;
}
if (mCurrentStats[bucketType] == null) {
if (DEBUG) {
Slog.d(TAG, "Creating new stats (" + tempCal.getTimeInMillis() +
") for bucket " + bucketType);
}
mCurrentStats[bucketType] = new IntervalStats();
mCurrentStats[bucketType].beginTime = tempCal.getTimeInMillis();
mCurrentStats[bucketType].endTime = timeNow;
}
}
mStatsChanged = false;
mDailyExpiryDate.setTimeInMillis(timeNow);
mDailyExpiryDate.add(Calendar.DAY_OF_YEAR, 1);
UsageStatsUtils.truncateDateTo(UsageStatsManager.INTERVAL_DAILY, mDailyExpiryDate);
Slog.i(TAG, mLogPrefix + "Rollover scheduled for "
+ sDateFormat.format(mDailyExpiryDate.getTime()));
}
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";
default:
return "UNKNOWN";
}
}
}