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 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 } 88 89 // Build a flattened representation of the bookmarks table 90 File tmpfile = File.createTempFile("bkp", null, getCacheDir()); 91 try { 92 FileOutputStream outfstream = new FileOutputStream(tmpfile); 93 long newCrc = buildBookmarkFile(outfstream); 94 outfstream.close(); 95 96 // Any changes since the last backup? 97 if ((savedVersion != BACKUP_AGENT_VERSION) 98 || (newCrc != savedCrc) 99 || (tmpfile.length() != savedFileSize)) { 100 // Different checksum or different size, so we need to back it up 101 copyFileToBackup(BOOKMARK_KEY, tmpfile, data); 102 } 103 104 // Record our backup state and we're done 105 writeBackupState(tmpfile.length(), newCrc, newState); 106 } finally { 107 // Make sure to tidy up when we're done 108 tmpfile.delete(); 109 } 110 } 111 112 /** 113 * Restore from backup -- reads in the flattened bookmark file as supplied from 114 * the backup service, parses that out, and rebuilds the bookmarks table in the 115 * browser database from it. 116 */ 117 @Override 118 public void onRestore(BackupDataInput data, int appVersionCode, 119 ParcelFileDescriptor newState) throws IOException { 120 long crc = -1; 121 File tmpfile = File.createTempFile("rst", null, getFilesDir()); 122 try { 123 while (data.readNextHeader()) { 124 if (BOOKMARK_KEY.equals(data.getKey())) { 125 // Read the flattened bookmark data into a temp file 126 crc = copyBackupToFile(data, tmpfile, data.getDataSize()); 127 128 FileInputStream infstream = new FileInputStream(tmpfile); 129 DataInputStream in = new DataInputStream(infstream); 130 131 try { 132 int count = in.readInt(); 133 ArrayList<Bookmark> bookmarks = new ArrayList<Bookmark>(count); 134 135 // Read all the bookmarks, then process later -- if we can't read 136 // all the data successfully, we don't touch the bookmarks table 137 for (int i = 0; i < count; i++) { 138 Bookmark mark = new Bookmark(); 139 mark.url = in.readUTF(); 140 mark.visits = in.readInt(); 141 mark.date = in.readLong(); 142 mark.created = in.readLong(); 143 mark.title = in.readUTF(); 144 bookmarks.add(mark); 145 } 146 147 // Okay, we have all the bookmarks -- now see if we need to add 148 // them to the browser's database 149 int N = bookmarks.size(); 150 int nUnique = 0; 151 if (DEBUG) Log.v(TAG, "Restoring " + N + " bookmarks"); 152 String[] urlCol = new String[] { BookmarkColumns.URL }; 153 for (int i = 0; i < N; i++) { 154 Bookmark mark = bookmarks.get(i); 155 156 // Does this URL exist in the bookmark table? 157 Cursor cursor = getContentResolver().query(Browser.BOOKMARKS_URI, 158 urlCol, BookmarkColumns.URL + " == '" + mark.url + "' AND " + 159 BookmarkColumns.BOOKMARK + " == 1 ", null, null); 160 // if not, insert it 161 if (cursor.getCount() <= 0) { 162 if (DEBUG) Log.v(TAG, "Did not see url: " + mark.url); 163 // Right now we do not reconstruct the db entry in its 164 // entirety; we just add a new bookmark with the same data 165 Bookmarks.addBookmark(null, getContentResolver(), 166 mark.url, mark.title, null, false); 167 nUnique++; 168 } else { 169 if (DEBUG) Log.v(TAG, "Skipping extant url: " + mark.url); 170 } 171 cursor.close(); 172 } 173 Log.i(TAG, "Restored " + nUnique + " of " + N + " bookmarks"); 174 } catch (IOException ioe) { 175 Log.w(TAG, "Bad backup data; not restoring"); 176 crc = -1; 177 } 178 } 179 180 // Last, write the state we just restored from so we can discern 181 // changes whenever we get invoked for backup in the future 182 writeBackupState(tmpfile.length(), crc, newState); 183 } 184 } finally { 185 // Whatever happens, delete the temp file 186 tmpfile.delete(); 187 } 188 } 189 190 class Bookmark { 191 public String url; 192 public int visits; 193 public long date; 194 public long created; 195 public String title; 196 } 197 /* 198 * Utility functions 199 */ 200 201 // Flatten the bookmarks table into the given file, calculating its CRC in the process 202 private long buildBookmarkFile(FileOutputStream outfstream) throws IOException { 203 CRC32 crc = new CRC32(); 204 ByteArrayOutputStream bufstream = new ByteArrayOutputStream(512); 205 DataOutputStream bout = new DataOutputStream(bufstream); 206 207 Cursor cursor = getContentResolver().query(Browser.BOOKMARKS_URI, 208 new String[] { BookmarkColumns.URL, BookmarkColumns.VISITS, 209 BookmarkColumns.DATE, BookmarkColumns.CREATED, 210 BookmarkColumns.TITLE }, 211 BookmarkColumns.BOOKMARK + " == 1 ", null, null); 212 213 // The first thing in the file is the row count... 214 int count = cursor.getCount(); 215 if (DEBUG) Log.v(TAG, "Backing up " + count + " bookmarks"); 216 bout.writeInt(count); 217 byte[] record = bufstream.toByteArray(); 218 crc.update(record); 219 outfstream.write(record); 220 221 // ... followed by the data for each row 222 for (int i = 0; i < count; i++) { 223 cursor.moveToNext(); 224 225 String url = cursor.getString(0); 226 int visits = cursor.getInt(1); 227 long date = cursor.getLong(2); 228 long created = cursor.getLong(3); 229 String title = cursor.getString(4); 230 231 // construct the flattened record in a byte array 232 bufstream.reset(); 233 bout.writeUTF(url); 234 bout.writeInt(visits); 235 bout.writeLong(date); 236 bout.writeLong(created); 237 bout.writeUTF(title); 238 239 // Update the CRC and write the record to the temp file 240 record = bufstream.toByteArray(); 241 crc.update(record); 242 outfstream.write(record); 243 244 if (DEBUG) Log.v(TAG, " wrote url " + url); 245 } 246 247 cursor.close(); 248 return crc.getValue(); 249 } 250 251 // Write the file to backup as a single record under the given key 252 private void copyFileToBackup(String key, File file, BackupDataOutput data) 253 throws IOException { 254 final int CHUNK = 8192; 255 byte[] buf = new byte[CHUNK]; 256 257 int toCopy = (int) file.length(); 258 data.writeEntityHeader(key, toCopy); 259 260 FileInputStream in = new FileInputStream(file); 261 int nRead; 262 while (toCopy > 0) { 263 nRead = in.read(buf, 0, CHUNK); 264 data.writeEntityData(buf, nRead); 265 toCopy -= nRead; 266 } 267 in.close(); 268 } 269 270 // Read the given file from backup to a file, calculating a CRC32 along the way 271 private long copyBackupToFile(BackupDataInput data, File file, int toRead) 272 throws IOException { 273 final int CHUNK = 8192; 274 byte[] buf = new byte[CHUNK]; 275 CRC32 crc = new CRC32(); 276 FileOutputStream out = new FileOutputStream(file); 277 278 while (toRead > 0) { 279 int numRead = data.readEntityData(buf, 0, CHUNK); 280 crc.update(buf, 0, numRead); 281 out.write(buf, 0, numRead); 282 toRead -= numRead; 283 } 284 285 out.close(); 286 return crc.getValue(); 287 } 288 289 // Write the given metrics to the new state file 290 private void writeBackupState(long fileSize, long crc, ParcelFileDescriptor stateFile) 291 throws IOException { 292 DataOutputStream out = new DataOutputStream( 293 new FileOutputStream(stateFile.getFileDescriptor())); 294 out.writeLong(fileSize); 295 out.writeLong(crc); 296 out.writeInt(BACKUP_AGENT_VERSION); 297 } 298 } 299