1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.app.backup; 18 19 import android.os.ParcelFileDescriptor; 20 import android.util.ArrayMap; 21 import android.util.Log; 22 23 import java.io.ByteArrayInputStream; 24 import java.io.ByteArrayOutputStream; 25 import java.io.DataInputStream; 26 import java.io.DataOutputStream; 27 import java.io.EOFException; 28 import java.io.FileInputStream; 29 import java.io.FileOutputStream; 30 import java.io.IOException; 31 import java.util.zip.CRC32; 32 import java.util.zip.DeflaterOutputStream; 33 import java.util.zip.InflaterInputStream; 34 35 /** 36 * Utility class for writing BackupHelpers whose underlying data is a 37 * fixed set of byte-array blobs. The helper manages diff detection 38 * and compression on the wire. 39 * 40 * @hide 41 */ 42 public abstract class BlobBackupHelper implements BackupHelper { 43 private static final String TAG = "BlobBackupHelper"; 44 private static final boolean DEBUG = false; 45 46 private final int mCurrentBlobVersion; 47 private final String[] mKeys; 48 BlobBackupHelper(int currentBlobVersion, String... keys)49 public BlobBackupHelper(int currentBlobVersion, String... keys) { 50 mCurrentBlobVersion = currentBlobVersion; 51 mKeys = keys; 52 } 53 54 // Client interface 55 56 /** 57 * Generate and return the byte array containing the backup payload describing 58 * the current data state. During a backup operation this method is called once 59 * per key that was supplied to the helper's constructor. 60 * 61 * @return A byte array containing the data blob that the caller wishes to store, 62 * or {@code null} if the current state is empty or undefined. 63 */ getBackupPayload(String key)64 abstract protected byte[] getBackupPayload(String key); 65 66 /** 67 * Given a byte array that was restored from backup, do whatever is appropriate 68 * to apply that described state in the live system. This method is called once 69 * per key/value payload that was delivered for restore. Typically data is delivered 70 * for restore in lexical order by key, <i>not</i> in the order in which the keys 71 * were supplied in the constructor. 72 * 73 * @param payload The byte array that was passed to {@link #getBackupPayload()} 74 * on the ancestral device. 75 */ applyRestoredPayload(String key, byte[] payload)76 abstract protected void applyRestoredPayload(String key, byte[] payload); 77 78 79 // Internal implementation 80 81 /* 82 * State on-disk format: 83 * [Int] : overall blob version number 84 * [Int=N] : number of keys represented in the state blob 85 * N* : 86 * [String] key 87 * [Long] blob checksum, calculated after compression 88 */ 89 @SuppressWarnings("resource") readOldState(ParcelFileDescriptor oldStateFd)90 private ArrayMap<String, Long> readOldState(ParcelFileDescriptor oldStateFd) { 91 final ArrayMap<String, Long> state = new ArrayMap<String, Long>(); 92 93 FileInputStream fis = new FileInputStream(oldStateFd.getFileDescriptor()); 94 DataInputStream in = new DataInputStream(fis); 95 96 try { 97 int version = in.readInt(); 98 if (version <= mCurrentBlobVersion) { 99 final int numKeys = in.readInt(); 100 if (DEBUG) { 101 Log.i(TAG, " " + numKeys + " keys in state record"); 102 } 103 for (int i = 0; i < numKeys; i++) { 104 String key = in.readUTF(); 105 long checksum = in.readLong(); 106 if (DEBUG) { 107 Log.i(TAG, " key '" + key + "' checksum is " + checksum); 108 } 109 state.put(key, checksum); 110 } 111 } else { 112 Log.w(TAG, "Prior state from unrecognized version " + version); 113 } 114 } catch (EOFException e) { 115 // Empty file is expected on first backup, so carry on. If the state 116 // is truncated we just treat it the same way. 117 if (DEBUG) { 118 Log.i(TAG, "Hit EOF reading prior state"); 119 } 120 state.clear(); 121 } catch (Exception e) { 122 Log.e(TAG, "Error examining prior backup state " + e.getMessage()); 123 state.clear(); 124 } 125 126 return state; 127 } 128 129 /** 130 * New overall state record 131 */ writeBackupState(ArrayMap<String, Long> state, ParcelFileDescriptor stateFile)132 private void writeBackupState(ArrayMap<String, Long> state, ParcelFileDescriptor stateFile) { 133 try { 134 FileOutputStream fos = new FileOutputStream(stateFile.getFileDescriptor()); 135 136 // We explicitly don't close 'out' because we must not close the backing fd. 137 // The FileOutputStream will not close it implicitly. 138 @SuppressWarnings("resource") 139 DataOutputStream out = new DataOutputStream(fos); 140 141 out.writeInt(mCurrentBlobVersion); 142 143 final int N = (state != null) ? state.size() : 0; 144 out.writeInt(N); 145 for (int i = 0; i < N; i++) { 146 final String key = state.keyAt(i); 147 final long checksum = state.valueAt(i).longValue(); 148 if (DEBUG) { 149 Log.i(TAG, " writing key " + key + " checksum = " + checksum); 150 } 151 out.writeUTF(key); 152 out.writeLong(checksum); 153 } 154 } catch (IOException e) { 155 Log.e(TAG, "Unable to write updated state", e); 156 } 157 } 158 159 // Also versions the deflated blob internally in case we need to revise it deflate(byte[] data)160 private byte[] deflate(byte[] data) { 161 byte[] result = null; 162 if (data != null) { 163 try { 164 ByteArrayOutputStream sink = new ByteArrayOutputStream(); 165 DataOutputStream headerOut = new DataOutputStream(sink); 166 167 // write the header directly to the sink ahead of the deflated payload 168 headerOut.writeInt(mCurrentBlobVersion); 169 170 DeflaterOutputStream out = new DeflaterOutputStream(sink); 171 out.write(data); 172 out.close(); // finishes and commits the compression run 173 result = sink.toByteArray(); 174 if (DEBUG) { 175 Log.v(TAG, "Deflated " + data.length + " bytes to " + result.length); 176 } 177 } catch (IOException e) { 178 Log.w(TAG, "Unable to process payload: " + e.getMessage()); 179 } 180 } 181 return result; 182 } 183 184 // Returns null if inflation failed inflate(byte[] compressedData)185 private byte[] inflate(byte[] compressedData) { 186 byte[] result = null; 187 if (compressedData != null) { 188 try { 189 ByteArrayInputStream source = new ByteArrayInputStream(compressedData); 190 DataInputStream headerIn = new DataInputStream(source); 191 int version = headerIn.readInt(); 192 if (version > mCurrentBlobVersion) { 193 Log.w(TAG, "Saved payload from unrecognized version " + version); 194 return null; 195 } 196 197 InflaterInputStream in = new InflaterInputStream(source); 198 ByteArrayOutputStream inflated = new ByteArrayOutputStream(); 199 byte[] buffer = new byte[4096]; 200 int nRead; 201 while ((nRead = in.read(buffer)) > 0) { 202 inflated.write(buffer, 0, nRead); 203 } 204 in.close(); 205 inflated.flush(); 206 result = inflated.toByteArray(); 207 if (DEBUG) { 208 Log.v(TAG, "Inflated " + compressedData.length + " bytes to " + result.length); 209 } 210 } catch (IOException e) { 211 // result is still null here 212 Log.w(TAG, "Unable to process restored payload: " + e.getMessage()); 213 } 214 } 215 return result; 216 } 217 checksum(byte[] buffer)218 private long checksum(byte[] buffer) { 219 if (buffer != null) { 220 try { 221 CRC32 crc = new CRC32(); 222 ByteArrayInputStream bis = new ByteArrayInputStream(buffer); 223 byte[] buf = new byte[4096]; 224 int nRead = 0; 225 while ((nRead = bis.read(buf)) >= 0) { 226 crc.update(buf, 0, nRead); 227 } 228 return crc.getValue(); 229 } catch (Exception e) { 230 // whoops; fall through with an explicitly bogus checksum 231 } 232 } 233 return -1; 234 } 235 236 // BackupHelper interface 237 238 @Override performBackup(ParcelFileDescriptor oldStateFd, BackupDataOutput data, ParcelFileDescriptor newStateFd)239 public void performBackup(ParcelFileDescriptor oldStateFd, BackupDataOutput data, 240 ParcelFileDescriptor newStateFd) { 241 if (DEBUG) { 242 Log.i(TAG, "Performing backup for " + this.getClass().getName()); 243 } 244 245 final ArrayMap<String, Long> oldState = readOldState(oldStateFd); 246 final ArrayMap<String, Long> newState = new ArrayMap<String, Long>(); 247 248 try { 249 for (String key : mKeys) { 250 final byte[] payload = deflate(getBackupPayload(key)); 251 final long checksum = checksum(payload); 252 if (DEBUG) { 253 Log.i(TAG, "Key " + key + " backup checksum is " + checksum); 254 } 255 newState.put(key, checksum); 256 257 Long oldChecksum = oldState.get(key); 258 if (oldChecksum == null || checksum != oldChecksum.longValue()) { 259 if (DEBUG) { 260 Log.i(TAG, "Checksum has changed from " + oldChecksum + " to " + checksum 261 + " for key " + key + ", writing"); 262 } 263 if (payload != null) { 264 data.writeEntityHeader(key, payload.length); 265 data.writeEntityData(payload, payload.length); 266 } else { 267 // state's changed but there's no current payload => delete 268 data.writeEntityHeader(key, -1); 269 } 270 } else { 271 if (DEBUG) { 272 Log.i(TAG, "No change under key " + key + " => not writing"); 273 } 274 } 275 } 276 } catch (Exception e) { 277 Log.w(TAG, "Unable to record notification state: " + e.getMessage()); 278 newState.clear(); 279 } finally { 280 // Always rewrite the state even if nothing changed 281 writeBackupState(newState, newStateFd); 282 } 283 } 284 285 @Override restoreEntity(BackupDataInputStream data)286 public void restoreEntity(BackupDataInputStream data) { 287 final String key = data.getKey(); 288 try { 289 // known key? 290 int which; 291 for (which = 0; which < mKeys.length; which++) { 292 if (key.equals(mKeys[which])) { 293 break; 294 } 295 } 296 if (which >= mKeys.length) { 297 Log.e(TAG, "Unrecognized key " + key + ", ignoring"); 298 return; 299 } 300 301 byte[] compressed = new byte[data.size()]; 302 data.read(compressed); 303 byte[] payload = inflate(compressed); 304 applyRestoredPayload(key, payload); 305 } catch (Exception e) { 306 Log.e(TAG, "Exception restoring entity " + key + " : " + e.getMessage()); 307 } 308 } 309 310 @Override writeNewStateDescription(ParcelFileDescriptor newState)311 public void writeNewStateDescription(ParcelFileDescriptor newState) { 312 // Just ensure that we do a full backup the first time after a restore 313 if (DEBUG) { 314 Log.i(TAG, "Writing state description after restore"); 315 } 316 writeBackupState(null, newState); 317 } 318 } 319