/* * 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 != null) ? state.size() : 0; 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); } }