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();