Move Snapshots to own DB on sdcard

 Bug: 4982126

Change-Id: Ib66b2880d163de4feb4d880e1d01996301bbea08
diff --git a/src/com/android/browser/BrowserSnapshotPage.java b/src/com/android/browser/BrowserSnapshotPage.java
index 06b2e42..72da15b 100644
--- a/src/com/android/browser/BrowserSnapshotPage.java
+++ b/src/com/android/browser/BrowserSnapshotPage.java
@@ -43,7 +43,7 @@
 import android.widget.ResourceCursorAdapter;
 import android.widget.TextView;
 
-import com.android.browser.provider.BrowserProvider2.Snapshots;
+import com.android.browser.provider.SnapshotProvider.Snapshots;
 
 import java.text.DateFormat;
 import java.util.Date;
@@ -61,12 +61,14 @@
         Snapshots.THUMBNAIL,
         Snapshots.FAVICON,
         Snapshots.URL,
+        Snapshots.DATE_CREATED,
     };
     private static final int SNAPSHOT_TITLE = 1;
     private static final int SNAPSHOT_VIEWSTATE_LENGTH = 2;
     private static final int SNAPSHOT_THUMBNAIL = 3;
     private static final int SNAPSHOT_FAVICON = 4;
     private static final int SNAPSHOT_URL = 5;
+    private static final int SNAPSHOT_DATE_CREATED = 6;
 
     GridView mGrid;
     View mEmpty;
@@ -113,10 +115,9 @@
     @Override
     public Loader<Cursor> onCreateLoader(int id, Bundle args) {
         if (id == LOADER_SNAPSHOTS) {
-            // TODO: Sort by date created
             return new CursorLoader(getActivity(),
                     Snapshots.CONTENT_URI, PROJECTION,
-                    null, null, null);
+                    null, null, Snapshots.DATE_CREATED + " DESC");
         }
         return null;
     }
@@ -216,12 +217,11 @@
             title.setText(cursor.getString(SNAPSHOT_TITLE));
             TextView size = (TextView) view.findViewById(R.id.size);
             int stateLen = cursor.getInt(SNAPSHOT_VIEWSTATE_LENGTH);
-            size.setText(String.format("%.1fMB", stateLen / 1024f / 1024f));
-            // We don't actually have the date in the database yet
-            // Use the current date as a placeholder
+            size.setText(String.format("%.2fMB", stateLen / 1024f / 1024f));
+            long timestamp = cursor.getLong(SNAPSHOT_DATE_CREATED);
             TextView date = (TextView) view.findViewById(R.id.date);
             DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.SHORT);
-            date.setText(dateFormat.format(new Date()));
+            date.setText(dateFormat.format(new Date(timestamp)));
         }
 
         @Override
diff --git a/src/com/android/browser/Controller.java b/src/com/android/browser/Controller.java
index 2e66c84..d02a843 100644
--- a/src/com/android/browser/Controller.java
+++ b/src/com/android/browser/Controller.java
@@ -79,7 +79,7 @@
 import com.android.browser.UI.ComboViews;
 import com.android.browser.UI.DropdownChangeListener;
 import com.android.browser.provider.BrowserProvider;
-import com.android.browser.provider.BrowserProvider2.Snapshots;
+import com.android.browser.provider.SnapshotProvider.Snapshots;
 import com.android.browser.search.SearchEngine;
 import com.android.common.Search;
 
@@ -1945,7 +1945,7 @@
         return null;
     }
 
-    private static Bitmap createScreenshot(WebView view, int width, int height) {
+    static Bitmap createScreenshot(WebView view, int width, int height) {
         // We render to a bitmap 2x the desired size so that we can then
         // re-scale it with filtering since canvas.scale doesn't filter
         // This helps reduce aliasing at the cost of being slightly blurry
diff --git a/src/com/android/browser/SnapshotTab.java b/src/com/android/browser/SnapshotTab.java
index adccdf3..f0abf58 100644
--- a/src/com/android/browser/SnapshotTab.java
+++ b/src/com/android/browser/SnapshotTab.java
@@ -20,19 +20,21 @@
 import android.content.ContentValues;
 import android.database.Cursor;
 import android.graphics.BitmapFactory;
-import android.graphics.Color;
 import android.net.Uri;
 import android.os.AsyncTask;
-import android.os.Bundle;
+import android.util.Log;
 import android.webkit.WebView;
 
-import com.android.browser.provider.BrowserProvider2.Snapshots;
+import com.android.browser.provider.SnapshotProvider.Snapshots;
 
 import java.io.ByteArrayInputStream;
+import java.util.zip.GZIPInputStream;
 
 
 public class SnapshotTab extends Tab {
 
+    private static final String LOGTAG = "SnapshotTab";
+
     private long mSnapshotId;
     private LoadData mLoadTask;
     private WebViewFactory mWebViewFactory;
@@ -145,8 +147,13 @@
                     WebView web = mTab.getWebView();
                     if (web != null) {
                         byte[] data = result.getBlob(4);
-                        ByteArrayInputStream stream = new ByteArrayInputStream(data);
-                        web.loadViewState(stream);
+                        ByteArrayInputStream bis = new ByteArrayInputStream(data);
+                        try {
+                            GZIPInputStream stream = new GZIPInputStream(bis);
+                            web.loadViewState(stream);
+                        } catch (Exception e) {
+                            Log.w(LOGTAG, "Failed to load view state", e);
+                        }
                     }
                     mTab.mBackgroundColor = result.getInt(5);
                     mTab.mWebViewController.onPageFinished(mTab);
diff --git a/src/com/android/browser/Tab.java b/src/com/android/browser/Tab.java
index a38c5f3..911726c 100644
--- a/src/com/android/browser/Tab.java
+++ b/src/com/android/browser/Tab.java
@@ -27,6 +27,7 @@
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
+import android.graphics.Bitmap.CompressFormat;
 import android.net.Uri;
 import android.net.http.SslError;
 import android.os.Bundle;
@@ -63,7 +64,7 @@
 import android.widget.Toast;
 
 import com.android.browser.homepages.HomeProvider;
-import com.android.browser.provider.BrowserProvider2.Snapshots;
+import com.android.browser.provider.SnapshotProvider.Snapshots;
 import com.android.common.speech.LoggingEvents;
 
 import java.io.ByteArrayOutputStream;
@@ -73,6 +74,7 @@
 import java.util.LinkedList;
 import java.util.Map;
 import java.util.Vector;
+import java.util.zip.GZIPOutputStream;
 
 /**
  * Class for maintaining Tabs with a main WebView and a subwindow.
@@ -1875,28 +1877,42 @@
 
     public ContentValues createSnapshotValues() {
         if (mMainView == null) return null;
-        /*
-         * TODO: Compression
-         * Some quick tests indicate GZIPing the stream will result in
-         * some decent savings. There is little overhead for sites with mostly
-         * images (such as the "Most Visited" page), dropping from 235kb
-         * to 200kb. Sites with a decent amount of text (hardocp.com), the size
-         * drops from 522kb to 381kb. Do this as part of the switch to saving
-         * to the SD card.
-         */
-        ByteArrayOutputStream stream = new ByteArrayOutputStream();
-        if (!mMainView.saveViewState(stream)) {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        try {
+            GZIPOutputStream stream = new GZIPOutputStream(bos);
+            if (!mMainView.saveViewState(stream)) {
+                return null;
+            }
+            stream.flush();
+            stream.close();
+        } catch (Exception e) {
+            Log.w(LOGTAG, "Failed to save view state", e);
             return null;
         }
-        byte[] data = stream.toByteArray();
+        byte[] data = bos.toByteArray();
         ContentValues values = new ContentValues();
         values.put(Snapshots.TITLE, mCurrentState.mTitle);
         values.put(Snapshots.URL, mCurrentState.mUrl);
         values.put(Snapshots.VIEWSTATE, data);
         values.put(Snapshots.BACKGROUND, mMainView.getPageBackgroundColor());
+        values.put(Snapshots.DATE_CREATED, System.currentTimeMillis());
+        values.put(Snapshots.FAVICON, compressBitmap(getFavicon()));
+        Bitmap screenshot = Controller.createScreenshot(mMainView,
+                Controller.getDesiredThumbnailWidth(mContext),
+                Controller.getDesiredThumbnailHeight(mContext));
+        values.put(Snapshots.THUMBNAIL, compressBitmap(screenshot));
         return values;
     }
 
+    public byte[] compressBitmap(Bitmap bitmap) {
+        if (bitmap == null) {
+            return null;
+        }
+        ByteArrayOutputStream stream = new ByteArrayOutputStream();
+        bitmap.compress(CompressFormat.PNG, 100, stream);
+        return stream.toByteArray();
+    }
+
     public void loadUrl(String url, Map<String, String> headers) {
         if (mMainView != null) {
             mCurrentState = new PageState(mContext, false, url, null);
diff --git a/src/com/android/browser/provider/BrowserProvider2.java b/src/com/android/browser/provider/BrowserProvider2.java
index 32fa172..b974c0e 100644
--- a/src/com/android/browser/provider/BrowserProvider2.java
+++ b/src/com/android/browser/provider/BrowserProvider2.java
@@ -68,19 +68,6 @@
 
 public class BrowserProvider2 extends SQLiteContentProvider {
 
-    public static interface Snapshots {
-
-        public static final Uri CONTENT_URI = Uri.withAppendedPath(
-                BrowserContract.AUTHORITY_URI, "snapshots");
-        public static final String _ID = "_id";
-        public static final String VIEWSTATE = "view_state";
-        public static final String BACKGROUND = "background";
-        public static final String TITLE = History.TITLE;
-        public static final String URL = History.URL;
-        public static final String FAVICON = History.FAVICON;
-        public static final String THUMBNAIL = History.THUMBNAIL;
-    }
-
     public static final String PARAM_GROUP_BY = "groupBy";
 
     public static final String LEGACY_AUTHORITY = "browser";
@@ -152,9 +139,6 @@
     static final int LEGACY = 9000;
     static final int LEGACY_ID = 9001;
 
-    static final int SNAPSHOTS = 10000;
-    static final int SNAPSHOTS_ID = 10001;
-
     public static final long FIXED_ID_ROOT = 1;
 
     // Default sort order for unsync'd bookmarks
@@ -216,9 +200,6 @@
                 "bookmarks/" + SearchManager.SUGGEST_URI_PATH_QUERY,
                 BOOKMARKS_SUGGESTIONS);
 
-        matcher.addURI(authority, "snapshots", SNAPSHOTS);
-        matcher.addURI(authority, "snapshots/#", SNAPSHOTS_ID);
-
         // Projection maps
         HashMap<String, String> map;
 
@@ -352,7 +333,7 @@
 
     final class DatabaseHelper extends SQLiteOpenHelper {
         static final String DATABASE_NAME = "browser2.db";
-        static final int DATABASE_VERSION = 29;
+        static final int DATABASE_VERSION = 30;
         public DatabaseHelper(Context context) {
             super(context, DATABASE_NAME, null, DATABASE_VERSION);
         }
@@ -423,8 +404,6 @@
             }
 
             enableSync(db);
-
-            createSnapshots(db);
         }
 
         void enableSync(SQLiteDatabase db) {
@@ -521,8 +500,9 @@
 
         @Override
         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-            if (oldVersion < 29) {
-                createSnapshots(db);
+            if (oldVersion < 30) {
+                db.execSQL("DROP VIEW IF EXISTS " + VIEW_SNAPSHOTS_COMBINED);
+                db.execSQL("DROP TABLE IF EXISTS " + TABLE_SNAPSHOTS);
             }
             if (oldVersion < 28) {
                 enableSync(db);
@@ -544,23 +524,6 @@
             }
         }
 
-        void createSnapshots(SQLiteDatabase db) {
-            db.execSQL("DROP TABLE IF EXISTS " + TABLE_SNAPSHOTS);
-            db.execSQL("CREATE TABLE " + TABLE_SNAPSHOTS + " (" +
-                    Snapshots._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
-                    Snapshots.URL + " TEXT NOT NULL," +
-                    Snapshots.TITLE + " TEXT," +
-                    Snapshots.BACKGROUND + " INTEGER," +
-                    Snapshots.VIEWSTATE + " BLOB NOT NULL" +
-                    ");");
-            db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_SNAPSHOTS_COMBINED +
-                    " AS SELECT * FROM " + TABLE_SNAPSHOTS +
-                    " LEFT OUTER JOIN " + TABLE_IMAGES +
-                    " ON " + TABLE_SNAPSHOTS + "." + Snapshots.URL +
-                    " = images.url_key");
-        }
-
-        @Override
         public void onOpen(SQLiteDatabase db) {
             db.enableWriteAheadLogging();
             mSyncHelper.onDatabaseOpened(db);
@@ -1011,17 +974,6 @@
                 break;
             }
 
-            case SNAPSHOTS_ID: {
-                selection = DatabaseUtils.concatenateWhere(selection, "_id=?");
-                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
-                        new String[] { Long.toString(ContentUris.parseId(uri)) });
-                // fall through
-            }
-            case SNAPSHOTS: {
-                qb.setTables(VIEW_SNAPSHOTS_COMBINED);
-                break;
-            }
-
             default: {
                 throw new UnsupportedOperationException("Unknown URL " + uri.toString());
             }
@@ -1221,16 +1173,6 @@
                 }
                 break;
             }
-            case SNAPSHOTS_ID: {
-                selection = DatabaseUtils.concatenateWhere(selection, TABLE_SNAPSHOTS + "._id=?");
-                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
-                        new String[] { Long.toString(ContentUris.parseId(uri)) });
-                // fall through
-            }
-            case SNAPSHOTS: {
-                deleted = db.delete(TABLE_SNAPSHOTS, selection, selectionArgs);
-                break;
-            }
             default: {
                 throw new UnsupportedOperationException("Unknown delete URI " + uri);
             }
@@ -1368,11 +1310,6 @@
                 break;
             }
 
-            case SNAPSHOTS: {
-                id = db.insertOrThrow(TABLE_SNAPSHOTS, Snapshots.TITLE, values);
-                break;
-            }
-
             default: {
                 throw new UnsupportedOperationException("Unknown insert URI " + uri);
             }
diff --git a/src/com/android/browser/provider/SnapshotProvider.java b/src/com/android/browser/provider/SnapshotProvider.java
new file mode 100644
index 0000000..49557f7
--- /dev/null
+++ b/src/com/android/browser/provider/SnapshotProvider.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.browser.provider;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.BrowserContract;
+
+import java.io.File;
+
+/**
+ * This provider is expected to be potentially flaky. It uses a database
+ * stored on external storage, which could be yanked unexpectedly.
+ */
+public class SnapshotProvider extends ContentProvider {
+
+    public static interface Snapshots {
+
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(
+                SnapshotProvider.AUTHORITY_URI, "snapshots");
+        public static final String _ID = "_id";
+        public static final String VIEWSTATE = "view_state";
+        public static final String BACKGROUND = "background";
+        public static final String TITLE = "title";
+        public static final String URL = "url";
+        public static final String FAVICON = "favicon";
+        public static final String THUMBNAIL = "thumbnail";
+        public static final String DATE_CREATED = "date_created";
+    }
+
+    public static final String AUTHORITY = "com.android.browser.snapshots";
+    public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
+
+    static final String TABLE_SNAPSHOTS = "snapshots";
+    static final int SNAPSHOTS = 10;
+    static final int SNAPSHOTS_ID = 11;
+    static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+
+    SnapshotDatabaseHelper mOpenHelper;
+
+    static {
+        URI_MATCHER.addURI(AUTHORITY, "snapshots", SNAPSHOTS);
+        URI_MATCHER.addURI(AUTHORITY, "snapshots/#", SNAPSHOTS_ID);
+    }
+
+    final static class SnapshotDatabaseHelper extends SQLiteOpenHelper {
+
+        static final String DATABASE_NAME = "snapshots.db";
+        static final int DATABASE_VERSION = 1;
+
+        public SnapshotDatabaseHelper(Context context) {
+            super(context, getFullDatabaseName(context), null, DATABASE_VERSION);
+        }
+
+        static String getFullDatabaseName(Context context) {
+            File dir = context.getExternalFilesDir(null);
+            return new File(dir, DATABASE_NAME).getAbsolutePath();
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            db.execSQL("CREATE TABLE " + TABLE_SNAPSHOTS + "(" +
+                    Snapshots._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+                    Snapshots.TITLE + " TEXT," +
+                    Snapshots.URL + " TEXT NOT NULL," +
+                    Snapshots.DATE_CREATED + " INTEGER," +
+                    Snapshots.FAVICON + " BLOB," +
+                    Snapshots.THUMBNAIL + " BLOB," +
+                    Snapshots.BACKGROUND + " INTEGER," +
+                    Snapshots.VIEWSTATE + " BLOB NOT NULL" +
+                    ");");
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            // Not needed yet
+        }
+
+    }
+
+    @Override
+    public boolean onCreate() {
+        mOpenHelper = new SnapshotDatabaseHelper(getContext());
+        IntentFilter filter = new IntentFilter(Intent.ACTION_MEDIA_EJECT);
+        filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
+        getContext().registerReceiver(mExternalStorageReceiver, filter);
+        return true;
+    }
+
+    final BroadcastReceiver mExternalStorageReceiver = new BroadcastReceiver() {
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            try {
+                mOpenHelper.close();
+            } catch (Throwable t) {
+                // We failed to close the open helper, which most likely means
+                // another thread is busy attempting to open the database
+                // or use the database. Let that thread try to gracefully
+                // deal with the error
+            }
+        }
+    };
+
+    SQLiteDatabase getWritableDatabase() {
+        String state = Environment.getExternalStorageState();
+        if (Environment.MEDIA_MOUNTED.equals(state)) {
+            try {
+                return mOpenHelper.getWritableDatabase();
+            } catch (Throwable t) {
+                return null;
+            }
+        }
+        return null;
+    }
+
+    SQLiteDatabase getReadableDatabase() {
+        String state = Environment.getExternalStorageState();
+        if (Environment.MEDIA_MOUNTED.equals(state)
+                || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
+            try {
+                return mOpenHelper.getReadableDatabase();
+            } catch (Throwable t) {
+                return null;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        SQLiteDatabase db = getReadableDatabase();
+        if (db == null) {
+            return null;
+        }
+        final int match = URI_MATCHER.match(uri);
+        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+        String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
+        switch (match) {
+        case SNAPSHOTS_ID:
+            selection = DatabaseUtils.concatenateWhere(selection, "_id=?");
+            selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+                    new String[] { Long.toString(ContentUris.parseId(uri)) });
+            // fall through
+        case SNAPSHOTS:
+            qb.setTables(TABLE_SNAPSHOTS);
+            break;
+
+        default:
+            throw new UnsupportedOperationException("Unknown URL " + uri.toString());
+        }
+        try {
+            Cursor cursor = qb.query(db, projection, selection, selectionArgs,
+                    null, null, sortOrder, limit);
+            cursor.setNotificationUri(getContext().getContentResolver(),
+                    AUTHORITY_URI);
+            return cursor;
+        } catch (Throwable t) {
+            return null;
+        }
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return null;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        SQLiteDatabase db = getWritableDatabase();
+        if (db == null) {
+            return null;
+        }
+        int match = URI_MATCHER.match(uri);
+        long id = -1;
+        switch (match) {
+        case SNAPSHOTS:
+            try {
+                id = db.insert(TABLE_SNAPSHOTS, Snapshots.TITLE, values);
+            } catch (Throwable t) {
+                id = -1;
+            }
+            break;
+        default:
+            throw new UnsupportedOperationException("Unknown insert URI " + uri);
+        }
+        if (id < 0) {
+            return null;
+        }
+        Uri inserted = ContentUris.withAppendedId(uri, id);
+        getContext().getContentResolver().notifyChange(inserted, null, false);
+        return inserted;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        SQLiteDatabase db = getWritableDatabase();
+        if (db == null) {
+            return 0;
+        }
+        int match = URI_MATCHER.match(uri);
+        int deleted = 0;
+        switch (match) {
+        case SNAPSHOTS_ID: {
+            selection = DatabaseUtils.concatenateWhere(selection, TABLE_SNAPSHOTS + "._id=?");
+            selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+                    new String[] { Long.toString(ContentUris.parseId(uri)) });
+            // fall through
+        }
+        case SNAPSHOTS:
+            try {
+                deleted = db.delete(TABLE_SNAPSHOTS, selection, selectionArgs);
+            } catch (Throwable t) {
+            }
+            break;
+        default:
+            throw new UnsupportedOperationException("Unknown delete URI " + uri);
+        }
+        if (deleted > 0) {
+            getContext().getContentResolver().notifyChange(uri, null, false);
+        }
+        return deleted;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
+        throw new UnsupportedOperationException("not implemented");
+    }
+
+}