Home | History | Annotate | Download | only in hugebackup
      1 /*
      2  * Copyright (C) 2011 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 com.android.hugebackup;
     18 
     19 import android.app.backup.BackupAgent;
     20 import android.app.backup.BackupDataInput;
     21 import android.app.backup.BackupDataOutput;
     22 import android.os.ParcelFileDescriptor;
     23 
     24 import java.io.ByteArrayInputStream;
     25 import java.io.ByteArrayOutputStream;
     26 import java.io.DataInputStream;
     27 import java.io.DataOutputStream;
     28 import java.io.File;
     29 import java.io.FileInputStream;
     30 import java.io.FileOutputStream;
     31 import java.io.IOException;
     32 import java.io.RandomAccessFile;
     33 
     34 /**
     35  * This is the backup/restore agent class for the BackupRestore sample
     36  * application.  This particular agent illustrates using the backup and
     37  * restore APIs directly, without taking advantage of any helper classes.
     38  */
     39 public class HugeAgent extends BackupAgent {
     40     /**
     41      * We put a simple version number into the state files so that we can
     42      * tell properly how to read "old" versions if at some point we want
     43      * to change what data we back up and how we store the state blob.
     44      */
     45     static final int AGENT_VERSION = 1;
     46 
     47     /**
     48      * Pick an arbitrary string to use as the "key" under which the
     49      * data is backed up.  This key identifies different data records
     50      * within this one application's data set.  Since we only maintain
     51      * one piece of data we don't need to distinguish, so we just pick
     52      * some arbitrary tag to use.
     53      */
     54     static final String APP_DATA_KEY = "alldata";
     55     static final String HUGE_DATA_KEY = "colossus";
     56 
     57     /** The app's current data, read from the live disk file */
     58     boolean mAddMayo;
     59     boolean mAddTomato;
     60     int mFilling;
     61 
     62     /** The location of the application's persistent data file */
     63     File mDataFile;
     64 
     65     /** For convenience, we set up the File object for the app's data on creation */
     66     @Override
     67     public void onCreate() {
     68         mDataFile = new File(getFilesDir(), HugeBackupActivity.DATA_FILE_NAME);
     69     }
     70 
     71     /**
     72      * The set of data backed up by this application is very small: just
     73      * two booleans and an integer.  With such a simple dataset, it's
     74      * easiest to simply store a copy of the backed-up data as the state
     75      * blob describing the last dataset backed up.  The state file
     76      * contents can be anything; it is private to the agent class, and
     77      * is never stored off-device.
     78      *
     79      * <p>One thing that an application may wish to do is tag the state
     80      * blob contents with a version number.  This is so that if the
     81      * application is upgraded, the next time it attempts to do a backup,
     82      * it can detect that the last backup operation was performed by an
     83      * older version of the agent, and might therefore require different
     84      * handling.
     85      */
     86     @Override
     87     public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
     88             ParcelFileDescriptor newState) throws IOException {
     89         // First, get the current data from the application's file.  This
     90         // may throw an IOException, but in that case something has gone
     91         // badly wrong with the app's data on disk, and we do not want
     92         // to back up garbage data.  If we just let the exception go, the
     93         // Backup Manager will handle it and simply skip the current
     94         // backup operation.
     95         synchronized (HugeBackupActivity.sDataLock) {
     96             RandomAccessFile file = new RandomAccessFile(mDataFile, "r");
     97             mFilling = file.readInt();
     98             mAddMayo = file.readBoolean();
     99             mAddTomato = file.readBoolean();
    100         }
    101 
    102         // If the new state file descriptor is null, this is the first time
    103         // a backup is being performed, so we know we have to write the
    104         // data.  If there <em>is</em> a previous state blob, we want to
    105         // double check whether the current data is actually different from
    106         // our last backup, so that we can avoid transmitting redundant
    107         // data to the storage backend.
    108         boolean doBackup = (oldState == null);
    109         if (!doBackup) {
    110             doBackup = compareStateFile(oldState);
    111         }
    112 
    113         // If we decided that we do in fact need to write our dataset, go
    114         // ahead and do that.  The way this agent backs up the data is to
    115         // flatten it into a single buffer, then write that to the backup
    116         // transport under the single key string.
    117         if (doBackup) {
    118             ByteArrayOutputStream bufStream = new ByteArrayOutputStream();
    119 
    120             // We use a DataOutputStream to write structured data into
    121             // the buffering stream
    122             DataOutputStream outWriter = new DataOutputStream(bufStream);
    123             outWriter.writeInt(mFilling);
    124             outWriter.writeBoolean(mAddMayo);
    125             outWriter.writeBoolean(mAddTomato);
    126 
    127             // Okay, we've flattened the data for transmission.  Pull it
    128             // out of the buffering stream object and send it off.
    129             byte[] buffer = bufStream.toByteArray();
    130             int len = buffer.length;
    131             data.writeEntityHeader(APP_DATA_KEY, len);
    132             data.writeEntityData(buffer, len);
    133 
    134             // ***** pathological behavior *****
    135             // Now, in order to incur deliberate too-much-data failures,
    136             // try to back up 20 MB of data besides what we already pushed.
    137             final int MEGABYTE = 1024*1024;
    138             final int NUM_MEGS = 20;
    139             buffer = new byte[MEGABYTE];
    140             data.writeEntityHeader(HUGE_DATA_KEY, NUM_MEGS * MEGABYTE);
    141             for (int i = 0; i < NUM_MEGS; i++) {
    142                 data.writeEntityData(buffer, MEGABYTE);
    143             }
    144         }
    145 
    146         // Finally, in all cases, we need to write the new state blob
    147         writeStateFile(newState);
    148     }
    149 
    150     /**
    151      * Helper routine - read a previous state file and decide whether to
    152      * perform a backup based on its contents.
    153      *
    154      * @return <code>true</code> if the application's data has changed since
    155      *   the last backup operation; <code>false</code> otherwise.
    156      */
    157     boolean compareStateFile(ParcelFileDescriptor oldState) {
    158         FileInputStream instream = new FileInputStream(oldState.getFileDescriptor());
    159         DataInputStream in = new DataInputStream(instream);
    160 
    161         try {
    162             int stateVersion = in.readInt();
    163             if (stateVersion > AGENT_VERSION) {
    164                 // Whoops; the last version of the app that backed up
    165                 // data on this device was <em>newer</em> than the current
    166                 // version -- the user has downgraded.  That's problematic.
    167                 // In this implementation, we recover by simply rewriting
    168                 // the backup.
    169                 return true;
    170             }
    171 
    172             // The state data we store is just a mirror of the app's data;
    173             // read it from the state file then return 'true' if any of
    174             // it differs from the current data.
    175             int lastFilling = in.readInt();
    176             boolean lastMayo = in.readBoolean();
    177             boolean lastTomato = in.readBoolean();
    178 
    179             return (lastFilling != mFilling)
    180                     || (lastTomato != mAddTomato)
    181                     || (lastMayo != mAddMayo);
    182         } catch (IOException e) {
    183             // If something went wrong reading the state file, be safe
    184             // and back up the data again.
    185             return true;
    186         }
    187     }
    188 
    189     /**
    190      * Write out the new state file:  the version number, followed by the
    191      * three bits of data as we sent them off to the backup transport.
    192      */
    193     void writeStateFile(ParcelFileDescriptor stateFile) throws IOException {
    194         FileOutputStream outstream = new FileOutputStream(stateFile.getFileDescriptor());
    195         DataOutputStream out = new DataOutputStream(outstream);
    196 
    197         out.writeInt(AGENT_VERSION);
    198         out.writeInt(mFilling);
    199         out.writeBoolean(mAddMayo);
    200         out.writeBoolean(mAddTomato);
    201     }
    202 
    203     /**
    204      * This application does not do any "live" restores of its own data,
    205      * so the only time a restore will happen is when the application is
    206      * installed.  This means that the activity itself is not going to
    207      * be running while we change its data out from under it.  That, in
    208      * turn, means that there is no need to send out any sort of notification
    209      * of the new data:  we only need to read the data from the stream
    210      * provided here, build the application's new data file, and then
    211      * write our new backup state blob that will be consulted at the next
    212      * backup operation.
    213      *
    214      * <p>We don't bother checking the versionCode of the app who originated
    215      * the data because we have never revised the backup data format.  If
    216      * we had, the 'appVersionCode' parameter would tell us how we should
    217      * interpret the data we're about to read.
    218      */
    219     @Override
    220     public void onRestore(BackupDataInput data, int appVersionCode,
    221             ParcelFileDescriptor newState) throws IOException {
    222         // We should only see one entity in the data stream, but the safest
    223         // way to consume it is using a while() loop
    224         while (data.readNextHeader()) {
    225             String key = data.getKey();
    226             int dataSize = data.getDataSize();
    227 
    228             if (APP_DATA_KEY.equals(key)) {
    229                 // It's our saved data, a flattened chunk of data all in
    230                 // one buffer.  Use some handy structured I/O classes to
    231                 // extract it.
    232                 byte[] dataBuf = new byte[dataSize];
    233                 data.readEntityData(dataBuf, 0, dataSize);
    234                 ByteArrayInputStream baStream = new ByteArrayInputStream(dataBuf);
    235                 DataInputStream in = new DataInputStream(baStream);
    236 
    237                 mFilling = in.readInt();
    238                 mAddMayo = in.readBoolean();
    239                 mAddTomato = in.readBoolean();
    240 
    241                 // Now we are ready to construct the app's data file based
    242                 // on the data we are restoring from.
    243                 synchronized (HugeBackupActivity.sDataLock) {
    244                     RandomAccessFile file = new RandomAccessFile(mDataFile, "rw");
    245                     file.setLength(0L);
    246                     file.writeInt(mFilling);
    247                     file.writeBoolean(mAddMayo);
    248                     file.writeBoolean(mAddTomato);
    249                 }
    250             } else {
    251                 // Curious!  This entity is data under a key we do not
    252                 // understand how to process.  Just skip it.
    253                 data.skipEntityData();
    254             }
    255         }
    256 
    257         // The last thing to do is write the state blob that describes the
    258         // app's data as restored from backup.
    259         writeStateFile(newState);
    260     }
    261 }
    262