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