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
17package com.android.hugebackup;
18
19import android.app.backup.BackupAgent;
20import android.app.backup.BackupDataInput;
21import android.app.backup.BackupDataOutput;
22import android.os.ParcelFileDescriptor;
23
24import java.io.ByteArrayInputStream;
25import java.io.ByteArrayOutputStream;
26import java.io.DataInputStream;
27import java.io.DataOutputStream;
28import java.io.File;
29import java.io.FileInputStream;
30import java.io.FileOutputStream;
31import java.io.IOException;
32import 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 */
39public 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