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 java.io.ByteArrayOutputStream;
20import java.io.ByteArrayInputStream;
21import java.io.DataInputStream;
22import java.io.DataOutputStream;
23import java.io.File;
24import java.io.FileInputStream;
25import java.io.FileOutputStream;
26import java.io.IOException;
27import java.io.RandomAccessFile;
28
29import android.app.backup.BackupAgent;
30import android.app.backup.BackupDataInput;
31import android.app.backup.BackupDataOutput;
32import android.os.ParcelFileDescriptor;
33
34/**
35 * This agent implementation is similar to the {@link ExampleAgent} one, but
36 * stores each distinct piece of application data in a separate record within
37 * the backup data set.  These records are updated independently: if the user
38 * changes the state of one of the UI's checkboxes, for example, only that
39 * datum's backup record is updated, not the entire data file.
40 */
41public class MultiRecordExampleAgent extends BackupAgent {
42    // Key strings for each record in the backup set
43    static final String FILLING_KEY = "filling";
44    static final String MAYO_KEY = "mayo";
45    static final String TOMATO_KEY = "tomato";
46
47    // Current live data, read from the application's data file
48    int mFilling;
49    boolean mAddMayo;
50    boolean mAddTomato;
51
52    /** The location of the application's persistent data file */
53    File mDataFile;
54
55    @Override
56    public void onCreate() {
57        // Cache a File for the app's data
58        mDataFile = new File(getFilesDir(), BackupRestoreActivity.DATA_FILE_NAME);
59    }
60
61    @Override
62    public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
63            ParcelFileDescriptor newState) throws IOException {
64        // First, get the current data from the application's file.  This
65        // may throw an IOException, but in that case something has gone
66        // badly wrong with the app's data on disk, and we do not want
67        // to back up garbage data.  If we just let the exception go, the
68        // Backup Manager will handle it and simply skip the current
69        // backup operation.
70        synchronized (BackupRestoreActivity.sDataLock) {
71            RandomAccessFile file = new RandomAccessFile(mDataFile, "r");
72            mFilling = file.readInt();
73            mAddMayo = file.readBoolean();
74            mAddTomato = file.readBoolean();
75        }
76
77        // If this is the first backup ever, we have to back up everything
78        boolean forceBackup = (oldState == null);
79
80        // Now read the state as of the previous backup pass, if any
81        int lastFilling = 0;
82        boolean lastMayo = false;
83        boolean lastTomato = false;
84
85        if (!forceBackup) {
86
87            FileInputStream instream = new FileInputStream(oldState.getFileDescriptor());
88            DataInputStream in = new DataInputStream(instream);
89
90            try {
91                // Read the state as of the last backup
92                lastFilling = in.readInt();
93                lastMayo = in.readBoolean();
94                lastTomato = in.readBoolean();
95            } catch (IOException e) {
96                // If something went wrong reading the state file, be safe and
97                // force a backup of all the data again.
98                forceBackup = true;
99            }
100        }
101
102        // Okay, now check each datum to see whether we need to back up a new value.  We'll
103        // reuse the bytearray buffering stream for each datum.  We also use a little
104        // helper routine to avoid some code duplication when writing the two boolean
105        // records.
106        ByteArrayOutputStream bufStream = new ByteArrayOutputStream();
107        DataOutputStream out = new DataOutputStream(bufStream);
108
109        if (forceBackup || (mFilling != lastFilling)) {
110            // bufStream.reset();   // not necessary the first time, but good to remember
111            out.writeInt(mFilling);
112            writeBackupEntity(data, bufStream, FILLING_KEY);
113        }
114
115        if (forceBackup || (mAddMayo != lastMayo)) {
116            bufStream.reset();
117            out.writeBoolean(mAddMayo);
118            writeBackupEntity(data, bufStream, MAYO_KEY);
119        }
120
121        if (forceBackup || (mAddTomato != lastTomato)) {
122            bufStream.reset();
123            out.writeBoolean(mAddTomato);
124            writeBackupEntity(data, bufStream, TOMATO_KEY);
125        }
126
127        // Finally, write the state file that describes our data as of this backup pass
128        writeStateFile(newState);
129    }
130
131    /**
132     * Write out the new state file:  the version number, followed by the
133     * three bits of data as we sent them off to the backup transport.
134     */
135    void writeStateFile(ParcelFileDescriptor stateFile) throws IOException {
136        FileOutputStream outstream = new FileOutputStream(stateFile.getFileDescriptor());
137        DataOutputStream out = new DataOutputStream(outstream);
138
139        out.writeInt(mFilling);
140        out.writeBoolean(mAddMayo);
141        out.writeBoolean(mAddTomato);
142    }
143
144    // Helper: write the boolean 'value' as a backup record under the given 'key',
145    // reusing the given buffering stream & data writer objects to do so.
146    void writeBackupEntity(BackupDataOutput data, ByteArrayOutputStream bufStream, String key)
147            throws IOException {
148        byte[] buf = bufStream.toByteArray();
149        data.writeEntityHeader(key, buf.length);
150        data.writeEntityData(buf, buf.length);
151    }
152
153    /**
154     * On restore, we pull the various bits of data out of the restore stream,
155     * then reconstruct the application's data file inside the shared lock.  A
156     * restore data set will always be the full set of records supplied by the
157     * application's backup operations.
158     */
159    @Override
160    public void onRestore(BackupDataInput data, int appVersionCode,
161            ParcelFileDescriptor newState) throws IOException {
162
163        // Consume the restore data set, remembering each bit of application state
164        // that we see along the way
165        while (data.readNextHeader()) {
166            String key = data.getKey();
167            int dataSize = data.getDataSize();
168
169            // In this implementation, we trust that we won't see any record keys
170            // that we don't understand.  Since we expect to handle them all, we
171            // go ahead and extract the data for each record before deciding how
172            // it will be handled.
173            byte[] dataBuf = new byte[dataSize];
174            data.readEntityData(dataBuf, 0, dataSize);
175            ByteArrayInputStream instream = new ByteArrayInputStream(dataBuf);
176            DataInputStream in = new DataInputStream(instream);
177
178            if (FILLING_KEY.equals(key)) {
179                mFilling = in.readInt();
180            } else if (MAYO_KEY.equals(key)) {
181                mAddMayo = in.readBoolean();
182            } else if (TOMATO_KEY.equals(key)) {
183                mAddTomato = in.readBoolean();
184            }
185        }
186
187        // Now we're ready to write out a full new dataset for the application.  Note that
188        // the restore process is intended to *replace* any existing or default data, so
189        // we can just go ahead and overwrite it all.
190        synchronized (BackupRestoreActivity.sDataLock) {
191            RandomAccessFile file = new RandomAccessFile(mDataFile, "rw");
192            file.setLength(0L);
193            file.writeInt(mFilling);
194            file.writeBoolean(mAddMayo);
195            file.writeBoolean(mAddTomato);
196        }
197
198        // Finally, write the state file that describes our data as of this restore pass.
199        writeStateFile(newState);
200    }
201}
202