diff options
5 files changed, 389 insertions, 0 deletions
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 913159ae8b1f..c177a527c6d8 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -89,4 +89,7 @@ interface INotificationManager 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 000000000000..8e4002d7e4e8 --- /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 000000000000..6f5c7e8149b4 --- /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 19d9e292f9fb..8e97aa9f408d 100644 --- a/core/java/com/android/server/backup/SystemBackupAgent.java +++ b/core/java/com/android/server/backup/SystemBackupAgent.java @@ -48,6 +48,7 @@ public class SystemBackupAgent extends BackupAgentHelper { 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 @@ public class SystemBackupAgent extends BackupAgentHelper { 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 997d5468da96..b6f8e9886304 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 @@ public class NotificationManagerService extends SystemService { 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(); |