From 8cc9235816ac9b3f1b3551d6234684f0455746dc Mon Sep 17 00:00:00 2001 From: John Reck Date: Wed, 6 Jul 2011 17:41:52 -0700 Subject: Move Snapshots to own DB on sdcard Bug: 4982126 Change-Id: Ib66b2880d163de4feb4d880e1d01996301bbea08 --- AndroidManifest.xml | 4 + res/layout/snapshot_item.xml | 5 +- src/com/android/browser/BrowserSnapshotPage.java | 14 +- src/com/android/browser/Controller.java | 4 +- src/com/android/browser/SnapshotTab.java | 17 +- src/com/android/browser/Tab.java | 42 ++-- .../android/browser/provider/BrowserProvider2.java | 71 +----- .../android/browser/provider/SnapshotProvider.java | 258 +++++++++++++++++++++ 8 files changed, 319 insertions(+), 96 deletions(-) create mode 100644 src/com/android/browser/provider/SnapshotProvider.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 7133a1a..0f11f8c 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -254,6 +254,10 @@ + + diff --git a/res/layout/snapshot_item.xml b/res/layout/snapshot_item.xml index 76cd501..2fc6ca8 100644 --- a/res/layout/snapshot_item.xml +++ b/res/layout/snapshot_item.xml @@ -40,8 +40,9 @@ android:textSize="12sp" android:typeface="sans" android:textColor="@android:color/white" - android:paddingLeft="2dip" - android:paddingRight="2dip" /> + android:paddingLeft="6dip" + android:paddingRight="2dip" + android:gravity="center_vertical" /> 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 @@ public class BrowserSnapshotPage extends Fragment implements 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.IntentHandler.UrlData; 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 @@ public class Controller 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.ContentUris; 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 @@ public class SnapshotTab extends Tab { 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.DialogInterface.OnCancelListener; 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.TextView; 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.Iterator; 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 @@ class Tab { 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 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 @@ import java.util.HashMap; 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 @@ public class BrowserProvider2 extends SQLiteContentProvider { 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 @@ public class BrowserProvider2 extends SQLiteContentProvider { "bookmarks/" + SearchManager.SUGGEST_URI_PATH_QUERY, BOOKMARKS_SUGGESTIONS); - matcher.addURI(authority, "snapshots", SNAPSHOTS); - matcher.addURI(authority, "snapshots/#", SNAPSHOTS_ID); - // Projection maps HashMap map; @@ -352,7 +333,7 @@ public class BrowserProvider2 extends SQLiteContentProvider { 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 @@ public class BrowserProvider2 extends SQLiteContentProvider { } enableSync(db); - - createSnapshots(db); } void enableSync(SQLiteDatabase db) { @@ -521,8 +500,9 @@ public class BrowserProvider2 extends SQLiteContentProvider { @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 @@ public class BrowserProvider2 extends SQLiteContentProvider { } } - 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 @@ public class BrowserProvider2 extends SQLiteContentProvider { 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 @@ public class BrowserProvider2 extends SQLiteContentProvider { } 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 @@ public class BrowserProvider2 extends SQLiteContentProvider { 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"); + } + +} -- cgit v1.1