| /* |
| * 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.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 = false; |
| |
| 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()); |
| DataInputStream in = new DataInputStream(fis); |
| |
| try { |
| int version = in.readInt(); |
| if (version <= mCurrentBlobVersion) { |
| final int numKeys = in.readInt(); |
| if (DEBUG) { |
| Log.i(TAG, " " + numKeys + " keys in state record"); |
| } |
| for (int i = 0; i < numKeys; i++) { |
| String key = in.readUTF(); |
| long checksum = in.readLong(); |
| if (DEBUG) { |
| Log.i(TAG, " key '" + key + "' checksum is " + checksum); |
| } |
| 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. |
| if (DEBUG) { |
| Log.i(TAG, "Hit EOF reading prior state"); |
| } |
| 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 != null) ? state.size() : 0; |
| out.writeInt(N); |
| for (int i = 0; i < N; i++) { |
| final String key = state.keyAt(i); |
| final long checksum = state.valueAt(i).longValue(); |
| if (DEBUG) { |
| Log.i(TAG, " writing key " + key + " checksum = " + checksum); |
| } |
| out.writeUTF(key); |
| out.writeLong(checksum); |
| } |
| } 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) { |
| if (DEBUG) { |
| Log.i(TAG, "Performing backup for " + this.getClass().getName()); |
| } |
| |
| 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); |
| if (DEBUG) { |
| Log.i(TAG, "Key " + key + " backup checksum is " + checksum); |
| } |
| newState.put(key, checksum); |
| |
| Long oldChecksum = oldState.get(key); |
| if (oldChecksum == null || checksum != oldChecksum.longValue()) { |
| if (DEBUG) { |
| Log.i(TAG, "Checksum has changed from " + oldChecksum + " to " + checksum |
| + " 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 rewrite 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 |
| if (DEBUG) { |
| Log.i(TAG, "Writing state description after restore"); |
| } |
| writeBackupState(null, newState); |
| } |
| } |