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