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