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);
+    }
 }