blob: 9c5d65b4c6a3ac8334daee75177f0ec1efe5125c [file] [log] [blame]
Christopher Tatede6f1312009-07-07 13:11:41 -07001/*
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
17package com.android.browser;
18
19import java.io.IOException;
20
Christopher Tateb6a65442010-03-05 15:47:48 -080021import android.app.backup.BackupAgent;
22import android.app.backup.BackupDataInput;
23import android.app.backup.BackupDataOutput;
Christopher Tatede6f1312009-07-07 13:11:41 -070024import android.database.Cursor;
25import android.os.ParcelFileDescriptor;
26import android.provider.Browser;
27import android.provider.Browser.BookmarkColumns;
Christopher Tate9c0dd8c2009-07-10 17:51:48 -070028import android.util.Log;
Christopher Tatede6f1312009-07-07 13:11:41 -070029
Christopher Tate9c0dd8c2009-07-10 17:51:48 -070030import java.io.ByteArrayOutputStream;
Christopher Tatede6f1312009-07-07 13:11:41 -070031import java.io.DataInputStream;
32import java.io.DataOutputStream;
33import java.io.EOFException;
34import java.io.File;
35import java.io.FileInputStream;
36import java.io.FileOutputStream;
Christopher Tate9c0dd8c2009-07-10 17:51:48 -070037import java.util.ArrayList;
Christopher Tatede6f1312009-07-07 13:11:41 -070038import 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.
Christopher Tate9c0dd8c2009-07-10 17:51:48 -070045 *
46 * @hide
Christopher Tatede6f1312009-07-07 13:11:41 -070047 */
48public class BrowserBackupAgent extends BackupAgent {
Christopher Tatef8b59982009-09-29 12:40:25 -070049 static final String TAG = "BrowserBackupAgent";
50 static final boolean DEBUG = false;
Christopher Tate9c0dd8c2009-07-10 17:51:48 -070051
Christopher Tatede6f1312009-07-07 13:11:41 -070052 static final String BOOKMARK_KEY = "_bookmarks_";
Christopher Tate9c0dd8c2009-07-10 17:51:48 -070053 /** this version num MUST be incremented if the flattened-file schema ever changes */
54 static final int BACKUP_AGENT_VERSION = 0;
Christopher Tatede6f1312009-07-07 13:11:41 -070055
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
Christopher Tate9c0dd8c2009-07-10 17:51:48 -070063 * 3. the agent version number [relevant following an OTA]
Christopher Tatede6f1312009-07-07 13:11:41 -070064 *
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
72 public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
73 ParcelFileDescriptor newState) throws IOException {
74 long savedFileSize = -1;
75 long savedCrc = -1;
Christopher Tate9c0dd8c2009-07-10 17:51:48 -070076 int savedVersion = -1;
Christopher Tatede6f1312009-07-07 13:11:41 -070077
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();
Christopher Tate9c0dd8c2009-07-10 17:51:48 -070084 savedVersion = in.readInt();
Christopher Tatede6f1312009-07-07 13:11:41 -070085 } catch (EOFException e) {
86 // It means we had no previous state; that's fine
Henrik Baard37550402010-04-21 12:36:33 +020087 } finally {
88 if (in != null) {
89 in.close();
90 }
Christopher Tatede6f1312009-07-07 13:11:41 -070091 }
92
Christopher Tate9c0dd8c2009-07-10 17:51:48 -070093 // Build a flattened representation of the bookmarks table
94 File tmpfile = File.createTempFile("bkp", null, getCacheDir());
Christopher Tatede6f1312009-07-07 13:11:41 -070095 try {
Christopher Tate9c0dd8c2009-07-10 17:51:48 -070096 FileOutputStream outfstream = new FileOutputStream(tmpfile);
97 long newCrc = buildBookmarkFile(outfstream);
98 outfstream.close();
Christopher Tateb4645a12009-07-10 13:36:58 -070099
Christopher Tate9c0dd8c2009-07-10 17:51:48 -0700100 // Any changes since the last backup?
101 if ((savedVersion != BACKUP_AGENT_VERSION)
102 || (newCrc != savedCrc)
103 || (tmpfile.length() != savedFileSize)) {
Christopher Tatede6f1312009-07-07 13:11:41 -0700104 // Different checksum or different size, so we need to back it up
105 copyFileToBackup(BOOKMARK_KEY, tmpfile, data);
106 }
107
Christopher Tate9c0dd8c2009-07-10 17:51:48 -0700108 // Record our backup state and we're done
109 writeBackupState(tmpfile.length(), newCrc, newState);
Christopher Tatede6f1312009-07-07 13:11:41 -0700110 } 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
122 public void onRestore(BackupDataInput data, int appVersionCode,
123 ParcelFileDescriptor newState) throws IOException {
124 long crc = -1;
Christopher Tate9c0dd8c2009-07-10 17:51:48 -0700125 File tmpfile = File.createTempFile("rst", null, getFilesDir());
Christopher Tatede6f1312009-07-07 13:11:41 -0700126 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
Christopher Tate9c0dd8c2009-07-10 17:51:48 -0700132 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();
Christopher Tatef8b59982009-09-29 12:40:25 -0700154 int nUnique = 0;
Christopher Tate9c0dd8c2009-07-10 17:51:48 -0700155 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) {
Christopher Tatef8b59982009-09-29 12:40:25 -0700166 if (DEBUG) Log.v(TAG, "Did not see url: " + mark.url);
Christopher Tate9c0dd8c2009-07-10 17:51:48 -0700167 // Right now we do not reconstruct the db entry in its
168 // entirety; we just add a new bookmark with the same data
Leon Scroggins III052ce662010-09-13 14:44:16 -0400169 // FIXME: This file needs to be reworked
170 // anyway For now, add the bookmark at
171 // the root level.
Jeff Hamilton7f6cf3e2010-09-17 17:22:21 -0500172 Bookmarks.addBookmark(this, false,
Leon Scroggins III052ce662010-09-13 14:44:16 -0400173 mark.url, mark.title, null, false, 0);
Christopher Tatef8b59982009-09-29 12:40:25 -0700174 nUnique++;
Christopher Tate9c0dd8c2009-07-10 17:51:48 -0700175 } else {
Christopher Tatef8b59982009-09-29 12:40:25 -0700176 if (DEBUG) Log.v(TAG, "Skipping extant url: " + mark.url);
Christopher Tate9c0dd8c2009-07-10 17:51:48 -0700177 }
178 cursor.close();
179 }
Christopher Tatef8b59982009-09-29 12:40:25 -0700180 Log.i(TAG, "Restored " + nUnique + " of " + N + " bookmarks");
Christopher Tate9c0dd8c2009-07-10 17:51:48 -0700181 } catch (IOException ioe) {
182 Log.w(TAG, "Bad backup data; not restoring");
183 crc = -1;
Henrik Baard37550402010-04-21 12:36:33 +0200184 } finally {
185 if (in != null) {
186 in.close();
187 }
Christopher Tate9c0dd8c2009-07-10 17:51:48 -0700188 }
Christopher Tatede6f1312009-07-07 13:11:41 -0700189 }
190
191 // Last, write the state we just restored from so we can discern
192 // changes whenever we get invoked for backup in the future
193 writeBackupState(tmpfile.length(), crc, newState);
194 }
195 } finally {
196 // Whatever happens, delete the temp file
197 tmpfile.delete();
198 }
199 }
200
Henrik Baard37550402010-04-21 12:36:33 +0200201 static class Bookmark {
Christopher Tate9c0dd8c2009-07-10 17:51:48 -0700202 public String url;
203 public int visits;
204 public long date;
205 public long created;
206 public String title;
207 }
Christopher Tatede6f1312009-07-07 13:11:41 -0700208 /*
209 * Utility functions
210 */
211
Christopher Tate9c0dd8c2009-07-10 17:51:48 -0700212 // Flatten the bookmarks table into the given file, calculating its CRC in the process
213 private long buildBookmarkFile(FileOutputStream outfstream) throws IOException {
214 CRC32 crc = new CRC32();
215 ByteArrayOutputStream bufstream = new ByteArrayOutputStream(512);
216 DataOutputStream bout = new DataOutputStream(bufstream);
217
218 Cursor cursor = getContentResolver().query(Browser.BOOKMARKS_URI,
219 new String[] { BookmarkColumns.URL, BookmarkColumns.VISITS,
220 BookmarkColumns.DATE, BookmarkColumns.CREATED,
221 BookmarkColumns.TITLE },
222 BookmarkColumns.BOOKMARK + " == 1 ", null, null);
223
224 // The first thing in the file is the row count...
225 int count = cursor.getCount();
226 if (DEBUG) Log.v(TAG, "Backing up " + count + " bookmarks");
227 bout.writeInt(count);
228 byte[] record = bufstream.toByteArray();
229 crc.update(record);
230 outfstream.write(record);
231
232 // ... followed by the data for each row
233 for (int i = 0; i < count; i++) {
234 cursor.moveToNext();
235
236 String url = cursor.getString(0);
237 int visits = cursor.getInt(1);
238 long date = cursor.getLong(2);
239 long created = cursor.getLong(3);
240 String title = cursor.getString(4);
241
242 // construct the flattened record in a byte array
243 bufstream.reset();
244 bout.writeUTF(url);
245 bout.writeInt(visits);
246 bout.writeLong(date);
247 bout.writeLong(created);
248 bout.writeUTF(title);
249
250 // Update the CRC and write the record to the temp file
251 record = bufstream.toByteArray();
252 crc.update(record);
253 outfstream.write(record);
254
255 if (DEBUG) Log.v(TAG, " wrote url " + url);
256 }
257
258 cursor.close();
259 return crc.getValue();
260 }
261
Christopher Tatede6f1312009-07-07 13:11:41 -0700262 // Write the file to backup as a single record under the given key
263 private void copyFileToBackup(String key, File file, BackupDataOutput data)
264 throws IOException {
265 final int CHUNK = 8192;
266 byte[] buf = new byte[CHUNK];
267
268 int toCopy = (int) file.length();
269 data.writeEntityHeader(key, toCopy);
270
271 FileInputStream in = new FileInputStream(file);
Henrik Baard37550402010-04-21 12:36:33 +0200272 try {
273 int nRead;
274 while (toCopy > 0) {
275 nRead = in.read(buf, 0, CHUNK);
276 data.writeEntityData(buf, nRead);
277 toCopy -= nRead;
278 }
279 } finally {
280 if (in != null) {
281 in.close();
282 }
Christopher Tatede6f1312009-07-07 13:11:41 -0700283 }
Christopher Tatede6f1312009-07-07 13:11:41 -0700284 }
285
286 // Read the given file from backup to a file, calculating a CRC32 along the way
287 private long copyBackupToFile(BackupDataInput data, File file, int toRead)
288 throws IOException {
289 final int CHUNK = 8192;
290 byte[] buf = new byte[CHUNK];
291 CRC32 crc = new CRC32();
Christopher Tate9c0dd8c2009-07-10 17:51:48 -0700292 FileOutputStream out = new FileOutputStream(file);
Christopher Tatede6f1312009-07-07 13:11:41 -0700293
Henrik Baard37550402010-04-21 12:36:33 +0200294 try {
295 while (toRead > 0) {
296 int numRead = data.readEntityData(buf, 0, CHUNK);
297 crc.update(buf, 0, numRead);
298 out.write(buf, 0, numRead);
299 toRead -= numRead;
300 }
301 } finally {
302 if (out != null) {
303 out.close();
304 }
Christopher Tatede6f1312009-07-07 13:11:41 -0700305 }
Christopher Tatede6f1312009-07-07 13:11:41 -0700306 return crc.getValue();
307 }
308
309 // Write the given metrics to the new state file
310 private void writeBackupState(long fileSize, long crc, ParcelFileDescriptor stateFile)
311 throws IOException {
312 DataOutputStream out = new DataOutputStream(
313 new FileOutputStream(stateFile.getFileDescriptor()));
Henrik Baard37550402010-04-21 12:36:33 +0200314 try {
315 out.writeLong(fileSize);
316 out.writeLong(crc);
317 out.writeInt(BACKUP_AGENT_VERSION);
318 } finally {
319 if (out != null) {
320 out.close();
321 }
322 }
Christopher Tatede6f1312009-07-07 13:11:41 -0700323 }
324}