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