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.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 
     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      */
     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      */
     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")
     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      */
    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
    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
    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 
    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
    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
    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
    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