Merge "Infrastructure for notification backup/restore"
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index 913159a..c177a52 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -89,4 +89,7 @@
     boolean isNotificationPolicyTokenValid(String pkg, in NotificationManager.Policy.Token token);
     NotificationManager.Policy getNotificationPolicy(in NotificationManager.Policy.Token token);
     void setNotificationPolicy(in NotificationManager.Policy.Token token, in NotificationManager.Policy policy);
+
+    byte[] getBackupPayload(int user);
+    void applyRestore(in byte[] payload, int user);
 }
diff --git a/core/java/android/app/backup/BlobBackupHelper.java b/core/java/android/app/backup/BlobBackupHelper.java
new file mode 100644
index 0000000..8e4002d
--- /dev/null
+++ b/core/java/android/app/backup/BlobBackupHelper.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2015 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 android.app.backup;
+
+import android.os.ParcelFileDescriptor;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.zip.CRC32;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.InflaterInputStream;
+
+/**
+ * Utility class for writing BackupHelpers whose underlying data is a
+ * fixed set of byte-array blobs.  The helper manages diff detection
+ * and compression on the wire.
+ *
+ * @hide
+ */
+public abstract class BlobBackupHelper implements BackupHelper {
+    private static final String TAG = "BlobBackupHelper";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private final int mCurrentBlobVersion;
+    private final String[] mKeys;
+
+    public BlobBackupHelper(int currentBlobVersion, String... keys) {
+        mCurrentBlobVersion = currentBlobVersion;
+        mKeys = keys;
+    }
+
+    // Client interface
+
+    /**
+     * Generate and return the byte array containing the backup payload describing
+     * the current data state.  During a backup operation this method is called once
+     * per key that was supplied to the helper's constructor.
+     *
+     * @return A byte array containing the data blob that the caller wishes to store,
+     *     or {@code null} if the current state is empty or undefined.
+     */
+    abstract protected byte[] getBackupPayload(String key);
+
+    /**
+     * Given a byte array that was restored from backup, do whatever is appropriate
+     * to apply that described state in the live system.  This method is called once
+     * per key/value payload that was delivered for restore.  Typically data is delivered
+     * for restore in lexical order by key, <i>not</i> in the order in which the keys
+     * were supplied in the constructor.
+     *
+     * @param payload The byte array that was passed to {@link #getBackupPayload()}
+     *     on the ancestral device.
+     */
+    abstract protected void applyRestoredPayload(String key, byte[] payload);
+
+
+    // Internal implementation
+
+    /*
+     * State on-disk format:
+     * [Int]    : overall blob version number
+     * [Int=N] : number of keys represented in the state blob
+     * N* :
+     *     [String] key
+     *     [Long]   blob checksum, calculated after compression
+     */
+    @SuppressWarnings("resource")
+    private ArrayMap<String, Long> readOldState(ParcelFileDescriptor oldStateFd) {
+        final ArrayMap<String, Long> state = new ArrayMap<String, Long>();
+
+        FileInputStream fis = new FileInputStream(oldStateFd.getFileDescriptor());
+        BufferedInputStream bis = new BufferedInputStream(fis);
+        DataInputStream in = new DataInputStream(bis);
+
+        try {
+            int version = in.readInt();
+            if (version <= mCurrentBlobVersion) {
+                final int numKeys = in.readInt();
+                for (int i = 0; i < numKeys; i++) {
+                    String key = in.readUTF();
+                    long checksum = in.readLong();
+                    state.put(key, checksum);
+                }
+            } else {
+                Log.w(TAG, "Prior state from unrecognized version " + version);
+            }
+        } catch (EOFException e) {
+            // Empty file is expected on first backup,  so carry on. If the state
+            // is truncated we just treat it the same way.
+            state.clear();
+        } catch (Exception e) {
+            Log.e(TAG, "Error examining prior backup state " + e.getMessage());
+            state.clear();
+        }
+
+        return state;
+    }
+
+    /**
+     * New overall state record
+     */
+    private void writeBackupState(ArrayMap<String, Long> state, ParcelFileDescriptor stateFile) {
+        try {
+            FileOutputStream fos = new FileOutputStream(stateFile.getFileDescriptor());
+
+            // We explicitly don't close 'out' because we must not close the backing fd.
+            // The FileOutputStream will not close it implicitly.
+            @SuppressWarnings("resource")
+            DataOutputStream out = new DataOutputStream(fos);
+
+            out.writeInt(mCurrentBlobVersion);
+
+            final int N = state.size();
+            out.writeInt(N);
+            for (int i = 0; i < N; i++) {
+                out.writeUTF(state.keyAt(i));
+                out.writeLong(state.valueAt(i).longValue());
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to write updated state", e);
+        }
+    }
+
+    // Also versions the deflated blob internally in case we need to revise it
+    private byte[] deflate(byte[] data) {
+        byte[] result = null;
+        if (data != null) {
+            try {
+                ByteArrayOutputStream sink = new ByteArrayOutputStream();
+                DataOutputStream headerOut = new DataOutputStream(sink);
+
+                // write the header directly to the sink ahead of the deflated payload
+                headerOut.writeInt(mCurrentBlobVersion);
+
+                DeflaterOutputStream out = new DeflaterOutputStream(sink);
+                out.write(data);
+                out.close();  // finishes and commits the compression run
+                result = sink.toByteArray();
+                if (DEBUG) {
+                    Log.v(TAG, "Deflated " + data.length + " bytes to " + result.length);
+                }
+            } catch (IOException e) {
+                Log.w(TAG, "Unable to process payload: " + e.getMessage());
+            }
+        }
+        return result;
+    }
+
+    // Returns null if inflation failed
+    private byte[] inflate(byte[] compressedData) {
+        byte[] result = null;
+        if (compressedData != null) {
+            try {
+                ByteArrayInputStream source = new ByteArrayInputStream(compressedData);
+                DataInputStream headerIn = new DataInputStream(source);
+                int version = headerIn.readInt();
+                if (version > mCurrentBlobVersion) {
+                    Log.w(TAG, "Saved payload from unrecognized version " + version);
+                    return null;
+                }
+
+                InflaterInputStream in = new InflaterInputStream(source);
+                ByteArrayOutputStream inflated = new ByteArrayOutputStream();
+                byte[] buffer = new byte[4096];
+                int nRead;
+                while ((nRead = in.read(buffer)) > 0) {
+                    inflated.write(buffer, 0, nRead);
+                }
+                in.close();
+                inflated.flush();
+                result = inflated.toByteArray();
+                if (DEBUG) {
+                    Log.v(TAG, "Inflated " + compressedData.length + " bytes to " + result.length);
+                }
+            } catch (IOException e) {
+                // result is still null here
+                Log.w(TAG, "Unable to process restored payload: " + e.getMessage());
+            }
+        }
+        return result;
+    }
+
+    private long checksum(byte[] buffer) {
+        if (buffer != null) {
+            try {
+                CRC32 crc = new CRC32();
+                ByteArrayInputStream bis = new ByteArrayInputStream(buffer);
+                byte[] buf = new byte[4096];
+                int nRead = 0;
+                while ((nRead = bis.read(buf)) >= 0) {
+                    crc.update(buf, 0, nRead);
+                }
+                return crc.getValue();
+            } catch (Exception e) {
+                // whoops; fall through with an explicitly bogus checksum
+            }
+        }
+        return -1;
+    }
+
+    // BackupHelper interface
+
+    @Override
+    public void performBackup(ParcelFileDescriptor oldStateFd, BackupDataOutput data,
+            ParcelFileDescriptor newStateFd) {
+
+        final ArrayMap<String, Long> oldState = readOldState(oldStateFd);
+        final ArrayMap<String, Long> newState = new ArrayMap<String, Long>();
+
+        try {
+            for (String key : mKeys) {
+                final byte[] payload = deflate(getBackupPayload(key));
+                final long checksum = checksum(payload);
+                newState.put(key, checksum);
+
+                Long oldChecksum = oldState.get(key);
+                if (oldChecksum == null || checksum != oldChecksum) {
+                    if (DEBUG) {
+                        Log.i(TAG, "State has changed for key " + key + ", writing");
+                    }
+                    if (payload != null) {
+                        data.writeEntityHeader(key, payload.length);
+                        data.writeEntityData(payload, payload.length);
+                    } else {
+                        // state's changed but there's no current payload => delete
+                        data.writeEntityHeader(key, -1);
+                    }
+                } else {
+                    if (DEBUG) {
+                        Log.i(TAG, "No change under key " + key + " => not writing");
+                    }
+                }
+            }
+        } catch (Exception e) {
+            Log.w(TAG,  "Unable to record notification state: " + e.getMessage());
+            newState.clear();
+        } finally {
+            // Always recommit the state even if nothing changed
+            writeBackupState(newState, newStateFd);
+        }
+    }
+
+    @Override
+    public void restoreEntity(BackupDataInputStream data) {
+        final String key = data.getKey();
+        try {
+            // known key?
+            int which;
+            for (which = 0; which < mKeys.length; which++) {
+                if (key.equals(mKeys[which])) {
+                    break;
+                }
+            }
+            if (which >= mKeys.length) {
+                Log.e(TAG, "Unrecognized key " + key + ", ignoring");
+                return;
+            }
+
+            byte[] compressed = new byte[data.size()];
+            data.read(compressed);
+            byte[] payload = inflate(compressed);
+            applyRestoredPayload(key, payload);
+        } catch (Exception e) {
+            Log.e(TAG, "Exception restoring entity " + key + " : " + e.getMessage());
+        }
+    }
+
+    @Override
+    public void writeNewStateDescription(ParcelFileDescriptor newState) {
+        // Just ensure that we do a full backup the first time after a restore
+        writeBackupState(null, newState);
+    }
+}
diff --git a/core/java/com/android/server/backup/NotificationBackupHelper.java b/core/java/com/android/server/backup/NotificationBackupHelper.java
new file mode 100644
index 0000000..6f5c7e8
--- /dev/null
+++ b/core/java/com/android/server/backup/NotificationBackupHelper.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2015 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.backup;
+
+import android.app.INotificationManager;
+import android.app.backup.BlobBackupHelper;
+import android.content.Context;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.Slog;
+
+public class NotificationBackupHelper extends BlobBackupHelper {
+    static final String TAG = "NotifBackupHelper";   // must be < 23 chars
+    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    // Current version of the blob schema
+    static final int BLOB_VERSION = 1;
+
+    // Key under which the payload blob is stored
+    static final String KEY_NOTIFICATIONS = "notifications";
+
+    public NotificationBackupHelper(Context context) {
+        super(BLOB_VERSION, KEY_NOTIFICATIONS);
+        // context is currently unused
+    }
+
+    @Override
+    protected byte[] getBackupPayload(String key) {
+        byte[] newPayload = null;
+        if (KEY_NOTIFICATIONS.equals(key)) {
+            try {
+                INotificationManager nm = INotificationManager.Stub.asInterface(
+                        ServiceManager.getService("notification"));
+                newPayload = nm.getBackupPayload(UserHandle.USER_OWNER);
+            } catch (Exception e) {
+                // Treat as no data
+                Slog.e(TAG, "Couldn't communicate with notification manager");
+                newPayload = null;
+            }
+        }
+        return newPayload;
+    }
+
+    @Override
+    protected void applyRestoredPayload(String key, byte[] payload) {
+        if (DEBUG) {
+            Slog.v(TAG, "Got restore of " + key);
+        }
+
+        if (KEY_NOTIFICATIONS.equals(key)) {
+            try {
+                INotificationManager nm = INotificationManager.Stub.asInterface(
+                        ServiceManager.getService("notification"));
+                nm.applyRestore(payload, UserHandle.USER_OWNER);
+            } catch (Exception e) {
+                Slog.e(TAG, "Couldn't communicate with notification manager");
+            }
+        }
+    }
+
+}
diff --git a/core/java/com/android/server/backup/SystemBackupAgent.java b/core/java/com/android/server/backup/SystemBackupAgent.java
index 19d9e29..8e97aa9 100644
--- a/core/java/com/android/server/backup/SystemBackupAgent.java
+++ b/core/java/com/android/server/backup/SystemBackupAgent.java
@@ -48,6 +48,7 @@
     private static final String RECENTS_HELPER = "recents";
     private static final String SYNC_SETTINGS_HELPER = "account_sync_settings";
     private static final String PREFERRED_HELPER = "preferred_activities";
+    private static final String NOTIFICATION_HELPER = "notifications";
 
     // 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
@@ -94,6 +95,7 @@
         addHelper(RECENTS_HELPER, new RecentsBackupHelper(this));
         addHelper(SYNC_SETTINGS_HELPER, new AccountSyncSettingsBackupHelper(this));
         addHelper(PREFERRED_HELPER, new PreferredActivityBackupHelper(this));
+        addHelper(NOTIFICATION_HELPER, new NotificationBackupHelper(this));
 
         super.onBackup(oldState, data, newState);
     }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 997d546..b6f8e98 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1610,6 +1610,18 @@
             return mConditionProviders.isSystemProviderEnabled(path);
         }
 
+        // Backup/restore interface
+        @Override
+        public byte[] getBackupPayload(int user) {
+            // TODO: build a payload of whatever is appropriate
+            return null;
+        }
+
+        @Override
+        public void applyRestore(byte[] payload, int user) {
+            // TODO: apply the restored payload as new current state
+        }
+
         @Override
         public Policy.Token getPolicyTokenFromListener(INotificationListener listener) {
             final long identity = Binder.clearCallingIdentity();