Enabled Backup/Restore of Package UsageStatistics
Backing up UsageStatistics using a new API in
UsageStatsService like the Notifications backup.
The backup logic is in the same package as UsageStatsDatabase
while the BackupAgentHelper resides in the android package
alongside SystemBackupAgent.
Bug: 26179323
Change-Id: I022d85fbcd4abb763230bec6eea50a7e723b5152
diff --git a/core/java/android/app/usage/UsageStatsManagerInternal.java b/core/java/android/app/usage/UsageStatsManagerInternal.java
index 498ff81..b6f1567 100644
--- a/core/java/android/app/usage/UsageStatsManagerInternal.java
+++ b/core/java/android/app/usage/UsageStatsManagerInternal.java
@@ -19,6 +19,8 @@
import android.content.ComponentName;
import android.content.res.Configuration;
+import java.io.IOException;
+
/**
* UsageStatsManager local system service interface.
*
@@ -109,4 +111,9 @@
public abstract void onParoleStateChanged(boolean isParoleOn);
}
+ /* Backup/Restore API */
+ public abstract byte[] getBackupPayload(int user, String key);
+
+ public abstract void applyRestoredPayload(int user, String key, byte[] payload);
+
}
diff --git a/core/java/com/android/server/backup/SystemBackupAgent.java b/core/java/com/android/server/backup/SystemBackupAgent.java
index 3f76e13..cee98b8 100644
--- a/core/java/com/android/server/backup/SystemBackupAgent.java
+++ b/core/java/com/android/server/backup/SystemBackupAgent.java
@@ -47,6 +47,7 @@
private static final String PREFERRED_HELPER = "preferred_activities";
private static final String NOTIFICATION_HELPER = "notifications";
private static final String PERMISSION_HELPER = "permissions";
+ private static final String USAGE_STATS_HELPER = "usage_stats";
// These paths must match what the WallpaperManagerService uses. The leaf *_FILENAME
// are also used in the full-backup file format, so must not change unless steps are
@@ -96,7 +97,7 @@
addHelper(PREFERRED_HELPER, new PreferredActivityBackupHelper());
addHelper(NOTIFICATION_HELPER, new NotificationBackupHelper(this));
addHelper(PERMISSION_HELPER, new PermissionBackupHelper());
-
+ addHelper(USAGE_STATS_HELPER, new UsageStatsBackupHelper(this));
super.onBackup(oldState, data, newState);
}
@@ -131,6 +132,7 @@
addHelper(PREFERRED_HELPER, new PreferredActivityBackupHelper());
addHelper(NOTIFICATION_HELPER, new NotificationBackupHelper(this));
addHelper(PERMISSION_HELPER, new PermissionBackupHelper());
+ addHelper(USAGE_STATS_HELPER, new UsageStatsBackupHelper(this));
try {
super.onRestore(data, appVersionCode, newState);
@@ -183,7 +185,7 @@
if (restoredWallpaper) {
IWallpaperManager wallpaper =
(IWallpaperManager)ServiceManager.getService(
- Context.WALLPAPER_SERVICE);
+ Context.WALLPAPER_SERVICE);
if (wallpaper != null) {
try {
wallpaper.settingsRestored();
diff --git a/core/java/com/android/server/backup/UsageStatsBackupHelper.java b/core/java/com/android/server/backup/UsageStatsBackupHelper.java
new file mode 100644
index 0000000..bde2396
--- /dev/null
+++ b/core/java/com/android/server/backup/UsageStatsBackupHelper.java
@@ -0,0 +1,70 @@
+package com.android.server.backup;
+
+
+import android.app.backup.BlobBackupHelper;
+import android.app.usage.IUsageStatsManager;
+import android.app.usage.UsageStatsManagerInternal;
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.server.LocalServices;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+public class UsageStatsBackupHelper extends BlobBackupHelper {
+ static final String TAG = "UsgStatsBackupHelper"; // must be < 23 chars
+ static final boolean DEBUG = false;
+
+ // Current version of the blob schema
+ static final int BLOB_VERSION = 1;
+
+ // Key under which the payload blob is stored
+ // same as UsageStatsBackupHelperAssistant.KEY_USAGE_STATS
+ static final String KEY_USAGE_STATS = "usage_stats";
+
+ public UsageStatsBackupHelper(Context context) {
+ super(BLOB_VERSION, KEY_USAGE_STATS);
+ }
+
+ @Override
+ protected byte[] getBackupPayload(String key) {
+ if(KEY_USAGE_STATS.equals(key)) {
+ UsageStatsManagerInternal localUsageStatsManager = LocalServices.getService(UsageStatsManagerInternal.class);
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DataOutputStream out = new DataOutputStream(baos);
+ try{
+ out.writeInt(UserHandle.USER_SYSTEM);
+ out.write(localUsageStatsManager.getBackupPayload(UserHandle.USER_SYSTEM, key));
+ } catch (IOException ioe){
+ if (DEBUG) Log.e(TAG, "Failed to backup Usage Stats", ioe);
+ baos.reset();
+ }
+ return baos.toByteArray();
+ }
+ return null;
+ }
+
+
+ @Override
+ protected void applyRestoredPayload(String key, byte[] payload) {
+ if (KEY_USAGE_STATS.equals(key)) {
+ UsageStatsManagerInternal localUsageStatsManager = LocalServices.getService(UsageStatsManagerInternal.class);
+ DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload));
+ try{
+ int user = in.readInt();
+ byte[] restoreData = new byte[payload.length - 4];
+ in.read(restoreData, 0, payload.length-4);
+ localUsageStatsManager.applyRestoredPayload(user, key, restoreData);
+ } catch (IOException ioe){
+ if (DEBUG) Log.e(TAG, "Failed to restore Usage Stats", ioe);
+ }
+ }
+ }
+}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
index 0ca4bd8..87c5ba0 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
@@ -17,6 +17,7 @@
package com.android.server.usage;
import android.app.usage.TimeSparseArray;
+import android.app.usage.UsageStats;
import android.app.usage.UsageStatsManager;
import android.os.Build;
import android.util.AtomicFile;
@@ -25,6 +26,10 @@
import java.io.BufferedReader;
import java.io.BufferedWriter;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
@@ -39,6 +44,14 @@
class UsageStatsDatabase {
private static final int CURRENT_VERSION = 3;
+ // Current version of the backup schema
+ static final int BACKUP_STATE_VERSION = 1;
+
+ // Key under which the payload blob is stored
+ // same as UsageStatsBackupHelper.KEY_USAGE_STATS
+ static final String KEY_USAGE_STATS = "usage_stats";
+
+
private static final String TAG = "UsageStatsDatabase";
private static final boolean DEBUG = UsageStatsService.DEBUG;
private static final String BAK_SUFFIX = ".bak";
@@ -539,4 +552,186 @@
stats.lastTimeSaved = f.getLastModifiedTime();
}
}
+
+
+ /* Backup/Restore Code */
+ protected byte[] getBackupPayload(String key){
+ synchronized (mLock) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ if (KEY_USAGE_STATS.equals(key)) {
+ prune(System.currentTimeMillis());
+ DataOutputStream out = new DataOutputStream(baos);
+ try {
+ out.writeInt(BACKUP_STATE_VERSION);
+
+ out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].size());
+ for(int i = 0; i<mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].size(); i++){
+ writeIntervalStatsToStream(out, mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].valueAt(i));
+ }
+
+ out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].size());
+ for(int i = 0; i<mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].size(); i++){
+ writeIntervalStatsToStream(out, mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].valueAt(i));
+ }
+
+ out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].size());
+ for(int i = 0; i<mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].size(); i++){
+ writeIntervalStatsToStream(out, mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].valueAt(i));
+ }
+
+ out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].size());
+ for(int i = 0; i<mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].size(); i++){
+ writeIntervalStatsToStream(out, mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].valueAt(i));
+ }
+ if (DEBUG) Slog.i(TAG, "Written " + baos.size() + " bytes of data");
+ } catch (IOException ioe){
+ Slog.d(TAG, "Failed to write data to output stream", ioe);
+ baos.reset();
+ }
+ }
+ return baos.toByteArray();
+ }
+
+ }
+
+ protected void applyRestoredPayload(String key, byte[] payload){
+ synchronized (mLock) {
+ if (KEY_USAGE_STATS.equals(key)) {
+ // Read stats files for the current device configs
+ IntervalStats dailyConfigSource = getLatestUsageStats(UsageStatsManager.INTERVAL_DAILY);
+ IntervalStats weeklyConfigSource = getLatestUsageStats(UsageStatsManager.INTERVAL_WEEKLY);
+ IntervalStats monthlyConfigSource = getLatestUsageStats(UsageStatsManager.INTERVAL_MONTHLY);
+ IntervalStats yearlyConfigSource = getLatestUsageStats(UsageStatsManager.INTERVAL_YEARLY);
+
+ // Delete all stats files
+ for(int i = 0; i<mIntervalDirs.length; i++){
+ deleteDirectoryContents(mIntervalDirs[i]);
+ }
+ try {
+ DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload));
+ int stateVersion = in.readInt();
+
+ int fileCount = in.readInt();
+ for(int i = 0; i<fileCount; i++){
+ IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in));
+ stats = mergeStats(stats, dailyConfigSource);
+ putUsageStats(UsageStatsManager.INTERVAL_DAILY, stats);
+ }
+
+ fileCount = in.readInt();
+ for(int i = 0; i<fileCount; i++){
+ IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in));
+ stats = mergeStats(stats, weeklyConfigSource);
+ putUsageStats(UsageStatsManager.INTERVAL_WEEKLY, stats);
+ }
+
+ fileCount = in.readInt();
+ for(int i = 0; i<fileCount; i++){
+ IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in));
+ stats = mergeStats(stats, monthlyConfigSource);
+ putUsageStats(UsageStatsManager.INTERVAL_MONTHLY, stats);
+ }
+
+ fileCount = in.readInt();
+ for(int i = 0; i<fileCount; i++){
+ IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in));
+ stats = mergeStats(stats, yearlyConfigSource);
+ putUsageStats(UsageStatsManager.INTERVAL_YEARLY, stats);
+ }
+ if (DEBUG) Slog.i(TAG, "Completed Restoring UsageStats");
+ } catch (IOException ioe){
+ Slog.d(TAG, "Failed to read data from input stream", ioe);
+ }
+ finally {
+ indexFilesLocked();
+ }
+ }
+ }
+ }
+
+ /**
+ * Get the Configuration Statistics from the current device statistics and merge them
+ * with the backed up usage statistics.
+ */
+ private IntervalStats mergeStats(IntervalStats beingRestored, IntervalStats onDevice) {
+ beingRestored.activeConfiguration = onDevice.activeConfiguration;
+ beingRestored.configurations.putAll(onDevice.configurations);
+ beingRestored.events = onDevice.events;
+ return beingRestored;
+ }
+
+ private void writeIntervalStatsToStream(DataOutputStream out, AtomicFile statsFile) throws IOException{
+ IntervalStats stats = new IntervalStats();
+ try {
+ UsageStatsXml.read(statsFile, stats);
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to read usage stats file", e);
+ out.writeInt(0);
+ return;
+ }
+ sanitizeIntervalStatsForBackup(stats);
+ byte[] data = serializeIntervalStats(stats);
+ out.writeInt(data.length);
+ out.write(data);
+ }
+
+ private static byte[] getIntervalStatsBytes(DataInputStream in) throws IOException {
+ int length = in.readInt();
+ byte[] buffer = new byte[length];
+ in.read(buffer, 0, length);
+ return buffer;
+ }
+
+ private static void sanitizeIntervalStatsForBackup(IntervalStats stats) {
+ if (stats == null) return;
+ stats.activeConfiguration = null;
+ stats.configurations.clear();
+ if (stats.events != null) stats.events.clear();
+ }
+
+ private static byte[] serializeIntervalStats(IntervalStats stats) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DataOutputStream out = new DataOutputStream(baos);
+ try {
+ out.writeLong(stats.beginTime);
+ UsageStatsXml.write(out, stats);
+ } catch (IOException ioe) {
+ Slog.d(TAG, "Serializing IntervalStats Failed", ioe);
+ baos.reset();
+ }
+ return baos.toByteArray();
+ }
+
+ private static IntervalStats deserializeIntervalStats(byte[] data) {
+ ByteArrayInputStream bais = new ByteArrayInputStream(data);
+ DataInputStream in = new DataInputStream(bais);
+ IntervalStats stats = new IntervalStats();
+ try {
+ stats.beginTime = in.readLong();
+ UsageStatsXml.read(in, stats);
+ } catch (IOException ioe) {
+ Slog.d(TAG, "DeSerializing IntervalStats Failed", ioe);
+ stats = null;
+ }
+ return stats;
+ }
+
+ private static void deleteDirectoryContents(File directory){
+ File[] files = directory.listFiles();
+ for (File file : files) {
+ deleteDirectory(file);
+ }
+ }
+
+ private static void deleteDirectory(File directory) {
+ File[] files = directory.listFiles();
+ for (File file : files) {
+ if (!file.isDirectory()) {
+ file.delete();
+ } else {
+ deleteDirectory(file);
+ }
+ }
+ directory.delete();
+ }
}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index f2949bf4..81031da 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -1310,6 +1310,8 @@
}
UsageStatsService.this.dump(args, pw);
}
+
+
}
/**
@@ -1416,5 +1418,26 @@
AppIdleStateChangeListener listener) {
UsageStatsService.this.removeListener(listener);
}
+
+ @Override
+ public byte[] getBackupPayload(int user, String key) {
+ // Check to ensure that only user 0's data is b/r for now
+ if (user == UserHandle.USER_SYSTEM) {
+ final UserUsageStatsService userStats =
+ getUserDataAndInitializeIfNeededLocked(user, checkAndGetTimeLocked());
+ return userStats.getBackupPayload(key);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void applyRestoredPayload(int user, String key, byte[] payload) {
+ if (user == UserHandle.USER_SYSTEM) {
+ final UserUsageStatsService userStats =
+ getUserDataAndInitializeIfNeededLocked(user, checkAndGetTimeLocked());
+ userStats.applyRestoredPayload(key, payload);
+ }
+ }
}
}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsXml.java b/services/usage/java/com/android/server/usage/UsageStatsXml.java
index 543f361..e7db741 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsXml.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsXml.java
@@ -87,7 +87,7 @@
}
}
- private static void read(InputStream in, IntervalStats statsOut) throws IOException {
+ static void read(InputStream in, IntervalStats statsOut) throws IOException {
XmlPullParser parser = Xml.newPullParser();
try {
parser.setInput(in, "utf-8");
@@ -113,7 +113,7 @@
}
}
- private static void write(OutputStream out, IntervalStats stats) throws IOException {
+ static void write(OutputStream out, IntervalStats stats) throws IOException {
FastXmlSerializer xml = new FastXmlSerializer();
xml.setOutput(out, "utf-8");
xml.startDocument("utf-8", true);
@@ -126,4 +126,4 @@
xml.endTag(null, USAGESTATS_TAG);
xml.endDocument();
}
-}
+}
\ No newline at end of file
diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
index 224faf4..aa2cf77 100644
--- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
@@ -741,4 +741,12 @@
return "UNKNOWN";
}
}
+
+ byte[] getBackupPayload(String key){
+ return mDatabase.getBackupPayload(key);
+ }
+
+ void applyRestoredPayload(String key, byte[] payload){
+ mDatabase.applyRestoredPayload(key, payload);
+ }
}