Home | History | Annotate | Download | only in browser
      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