• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 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.browser;
18 
19 import java.io.IOException;
20 
21 import android.app.backup.BackupAgent;
22 import android.app.backup.BackupDataInput;
23 import android.app.backup.BackupDataOutput;
24 import android.database.Cursor;
25 import android.os.ParcelFileDescriptor;
26 import android.provider.Browser;
27 import android.provider.Browser.BookmarkColumns;
28 import android.util.Log;
29 
30 import java.io.ByteArrayOutputStream;
31 import java.io.DataInputStream;
32 import java.io.DataOutputStream;
33 import java.io.EOFException;
34 import java.io.File;
35 import java.io.FileInputStream;
36 import java.io.FileOutputStream;
37 import java.util.ArrayList;
38 import java.util.zip.CRC32;
39 
40 /**
41  * Settings backup agent for the Android browser.  Currently the only thing
42  * stored is the set of bookmarks.  It's okay if I/O exceptions are thrown
43  * out of the agent; the calling code handles it and the backup operation
44  * simply fails.
45  *
46  * @hide
47  */
48 public class BrowserBackupAgent extends BackupAgent {
49     static final String TAG = "BrowserBackupAgent";
50     static final boolean DEBUG = false;
51 
52     static final String BOOKMARK_KEY = "_bookmarks_";
53     /** this version num MUST be incremented if the flattened-file schema ever changes */
54     static final int BACKUP_AGENT_VERSION = 0;
55 
56     /**
57      * In order to determine whether the bookmark set has changed since the
58      * last time we did a backup, we store the following bits of info in the
59      * state file after a backup:
60      *
61      * 1. the size of the flattened bookmark file
62      * 2. the CRC32 of that file
63      * 3. the agent version number [relevant following an OTA]
64      *
65      * After we flatten the bookmarks file here in onBackup, we compare its
66      * metrics with the values from the saved state.  If they match, it means
67      * the bookmarks didn't really change and we don't need to send the data.
68      * (If they don't match, of course, then they've changed and we do indeed
69      * send the new flattened file to be backed up.)
70      */
71     @Override
onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)72     public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
73             ParcelFileDescriptor newState) throws IOException {
74         long savedFileSize = -1;
75         long savedCrc = -1;
76         int savedVersion = -1;
77 
78         // Extract the previous bookmark file size & CRC from the saved state
79         DataInputStream in = new DataInputStream(
80                 new FileInputStream(oldState.getFileDescriptor()));
81         try {
82             savedFileSize = in.readLong();
83             savedCrc = in.readLong();
84             savedVersion = in.readInt();
85         } catch (EOFException e) {
86             // It means we had no previous state; that's fine
87         } finally {
88             if (in != null) {
89                 in.close();
90             }
91         }
92 
93         // Build a flattened representation of the bookmarks table
94         File tmpfile = File.createTempFile("bkp", null, getCacheDir());
95         try {
96             FileOutputStream outfstream = new FileOutputStream(tmpfile);
97             long newCrc = buildBookmarkFile(outfstream);
98             outfstream.close();
99 
100             // Any changes since the last backup?
101             if ((savedVersion != BACKUP_AGENT_VERSION)
102                     || (newCrc != savedCrc)
103                     || (tmpfile.length() != savedFileSize)) {
104                 // Different checksum or different size, so we need to back it up
105                 copyFileToBackup(BOOKMARK_KEY, tmpfile, data);
106             }
107 
108             // Record our backup state and we're done
109             writeBackupState(tmpfile.length(), newCrc, newState);
110         } finally {
111             // Make sure to tidy up when we're done
112             tmpfile.delete();
113         }
114     }
115 
116     /**
117      * Restore from backup -- reads in the flattened bookmark file as supplied from
118      * the backup service, parses that out, and rebuilds the bookmarks table in the
119      * browser database from it.
120      */
121     @Override
onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)122     public void onRestore(BackupDataInput data, int appVersionCode,
123             ParcelFileDescriptor newState) throws IOException {
124         long crc = -1;
125         File tmpfile = File.createTempFile("rst", null, getFilesDir());
126         try {
127             while (data.readNextHeader()) {
128                 if (BOOKMARK_KEY.equals(data.getKey())) {
129                     // Read the flattened bookmark data into a temp file
130                     crc = copyBackupToFile(data, tmpfile, data.getDataSize());
131 
132                     FileInputStream infstream = new FileInputStream(tmpfile);
133                     DataInputStream in = new DataInputStream(infstream);
134 
135                     try {
136                         int count = in.readInt();
137                         ArrayList<Bookmark> bookmarks = new ArrayList<Bookmark>(count);
138 
139                         // Read all the bookmarks, then process later -- if we can't read
140                         // all the data successfully, we don't touch the bookmarks table
141                         for (int i = 0; i < count; i++) {
142                             Bookmark mark = new Bookmark();
143                             mark.url = in.readUTF();
144                             mark.visits = in.readInt();
145                             mark.date = in.readLong();
146                             mark.created = in.readLong();
147                             mark.title = in.readUTF();
148                             bookmarks.add(mark);
149                         }
150 
151                         // Okay, we have all the bookmarks -- now see if we need to add
152                         // them to the browser's database
153                         int N = bookmarks.size();
154                         int nUnique = 0;
155                         if (DEBUG) Log.v(TAG, "Restoring " + N + " bookmarks");
156                         String[] urlCol = new String[] { BookmarkColumns.URL };
157                         for (int i = 0; i < N; i++) {
158                             Bookmark mark = bookmarks.get(i);
159 
160                             // Does this URL exist in the bookmark table?
161                             Cursor cursor = getContentResolver().query(Browser.BOOKMARKS_URI,
162                                     urlCol,  BookmarkColumns.URL + " == '" + mark.url + "' AND " +
163                                     BookmarkColumns.BOOKMARK + " == 1 ", null, null);
164                             // if not, insert it
165                             if (cursor.getCount() <= 0) {
166                                 if (DEBUG) Log.v(TAG, "Did not see url: " + mark.url);
167                                 // Right now we do not reconstruct the db entry in its
168                                 // entirety; we just add a new bookmark with the same data
169                                 Bookmarks.addBookmark(null, getContentResolver(),
170                                         mark.url, mark.title, null, false);
171                                 nUnique++;
172                             } else {
173                                 if (DEBUG) Log.v(TAG, "Skipping extant url: " + mark.url);
174                             }
175                             cursor.close();
176                         }
177                         Log.i(TAG, "Restored " + nUnique + " of " + N + " bookmarks");
178                     } catch (IOException ioe) {
179                         Log.w(TAG, "Bad backup data; not restoring");
180                         crc = -1;
181                     } finally {
182                         if (in != null) {
183                             in.close();
184                         }
185                     }
186                 }
187 
188                 // Last, write the state we just restored from so we can discern
189                 // changes whenever we get invoked for backup in the future
190                 writeBackupState(tmpfile.length(), crc, newState);
191             }
192         } finally {
193             // Whatever happens, delete the temp file
194             tmpfile.delete();
195         }
196     }
197 
198     static class Bookmark {
199         public String url;
200         public int visits;
201         public long date;
202         public long created;
203         public String title;
204     }
205     /*
206      * Utility functions
207      */
208 
209     // Flatten the bookmarks table into the given file, calculating its CRC in the process
buildBookmarkFile(FileOutputStream outfstream)210     private long buildBookmarkFile(FileOutputStream outfstream) throws IOException {
211         CRC32 crc = new CRC32();
212         ByteArrayOutputStream bufstream = new ByteArrayOutputStream(512);
213         DataOutputStream bout = new DataOutputStream(bufstream);
214 
215         Cursor cursor = getContentResolver().query(Browser.BOOKMARKS_URI,
216                 new String[] { BookmarkColumns.URL, BookmarkColumns.VISITS,
217                 BookmarkColumns.DATE, BookmarkColumns.CREATED,
218                 BookmarkColumns.TITLE },
219                 BookmarkColumns.BOOKMARK + " == 1 ", null, null);
220 
221         // The first thing in the file is the row count...
222         int count = cursor.getCount();
223         if (DEBUG) Log.v(TAG, "Backing up " + count + " bookmarks");
224         bout.writeInt(count);
225         byte[] record = bufstream.toByteArray();
226         crc.update(record);
227         outfstream.write(record);
228 
229         // ... followed by the data for each row
230         for (int i = 0; i < count; i++) {
231             cursor.moveToNext();
232 
233             String url = cursor.getString(0);
234             int visits = cursor.getInt(1);
235             long date = cursor.getLong(2);
236             long created = cursor.getLong(3);
237             String title = cursor.getString(4);
238 
239             // construct the flattened record in a byte array
240             bufstream.reset();
241             bout.writeUTF(url);
242             bout.writeInt(visits);
243             bout.writeLong(date);
244             bout.writeLong(created);
245             bout.writeUTF(title);
246 
247             // Update the CRC and write the record to the temp file
248             record = bufstream.toByteArray();
249             crc.update(record);
250             outfstream.write(record);
251 
252             if (DEBUG) Log.v(TAG, "   wrote url " + url);
253         }
254 
255         cursor.close();
256         return crc.getValue();
257     }
258 
259     // Write the file to backup as a single record under the given key
copyFileToBackup(String key, File file, BackupDataOutput data)260     private void copyFileToBackup(String key, File file, BackupDataOutput data)
261             throws IOException {
262         final int CHUNK = 8192;
263         byte[] buf = new byte[CHUNK];
264 
265         int toCopy = (int) file.length();
266         data.writeEntityHeader(key, toCopy);
267 
268         FileInputStream in = new FileInputStream(file);
269         try {
270             int nRead;
271             while (toCopy > 0) {
272                 nRead = in.read(buf, 0, CHUNK);
273                 data.writeEntityData(buf, nRead);
274                 toCopy -= nRead;
275             }
276         } finally {
277             if (in != null) {
278                 in.close();
279             }
280         }
281     }
282 
283     // Read the given file from backup to a file, calculating a CRC32 along the way
copyBackupToFile(BackupDataInput data, File file, int toRead)284     private long copyBackupToFile(BackupDataInput data, File file, int toRead)
285             throws IOException {
286         final int CHUNK = 8192;
287         byte[] buf = new byte[CHUNK];
288         CRC32 crc = new CRC32();
289         FileOutputStream out = new FileOutputStream(file);
290 
291         try {
292             while (toRead > 0) {
293                 int numRead = data.readEntityData(buf, 0, CHUNK);
294                 crc.update(buf, 0, numRead);
295                 out.write(buf, 0, numRead);
296                 toRead -= numRead;
297             }
298         } finally {
299             if (out != null) {
300                 out.close();
301             }
302         }
303         return crc.getValue();
304     }
305 
306     // Write the given metrics to the new state file
writeBackupState(long fileSize, long crc, ParcelFileDescriptor stateFile)307     private void writeBackupState(long fileSize, long crc, ParcelFileDescriptor stateFile)
308             throws IOException {
309         DataOutputStream out = new DataOutputStream(
310                 new FileOutputStream(stateFile.getFileDescriptor()));
311         try {
312             out.writeLong(fileSize);
313             out.writeLong(crc);
314             out.writeInt(BACKUP_AGENT_VERSION);
315         } finally {
316             if (out != null) {
317                 out.close();
318             }
319         }
320     }
321 }
322