Change where saved pages are stored
Bug: 5416822
Move saved pages out of external storage (b/5605575)
Save them as files instead of in the database, as the database
has a row size limit
Change-Id: I03b5af2459724d8cab67a9acfcc2827c7129e80f
diff --git a/src/com/android/browser/BrowserSnapshotPage.java b/src/com/android/browser/BrowserSnapshotPage.java
index be4f9af..b0d8205 100644
--- a/src/com/android/browser/BrowserSnapshotPage.java
+++ b/src/com/android/browser/BrowserSnapshotPage.java
@@ -61,7 +61,7 @@
private static final String[] PROJECTION = new String[] {
Snapshots._ID,
Snapshots.TITLE,
- "length(" + Snapshots.VIEWSTATE + ")",
+ Snapshots.VIEWSTATE_SIZE,
Snapshots.THUMBNAIL,
Snapshots.FAVICON,
Snapshots.URL,
@@ -69,7 +69,7 @@
};
private static final int SNAPSHOT_ID = 0;
private static final int SNAPSHOT_TITLE = 1;
- private static final int SNAPSHOT_VIEWSTATE_LENGTH = 2;
+ private static final int SNAPSHOT_VIEWSTATE_SIZE = 2;
private static final int SNAPSHOT_THUMBNAIL = 3;
private static final int SNAPSHOT_FAVICON = 4;
private static final int SNAPSHOT_URL = 5;
@@ -281,7 +281,7 @@
title.setText(cursor.getString(SNAPSHOT_TITLE));
TextView size = (TextView) view.findViewById(R.id.size);
if (size != null) {
- int stateLen = cursor.getInt(SNAPSHOT_VIEWSTATE_LENGTH);
+ int stateLen = cursor.getInt(SNAPSHOT_VIEWSTATE_SIZE);
size.setText(String.format("%.2fMB", stateLen / 1024f / 1024f));
}
long timestamp = cursor.getLong(SNAPSHOT_DATE_CREATED);
diff --git a/src/com/android/browser/SnapshotByteArrayOutputStream.java b/src/com/android/browser/SnapshotByteArrayOutputStream.java
deleted file mode 100644
index 127eee8..0000000
--- a/src/com/android/browser/SnapshotByteArrayOutputStream.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-
-public class SnapshotByteArrayOutputStream extends OutputStream {
-
- // Maximum size, this needs to be small enough such that an entire row
- // can fit in CursorWindow's 2MB limit
- private static final int MAX_SIZE = 1700000;
- private ByteArrayOutputStream mStream;
-
- public SnapshotByteArrayOutputStream() {
- mStream = new ByteArrayOutputStream(MAX_SIZE);
- }
-
- @Override
- public synchronized void write(int oneByte) throws IOException {
- checkError(1);
- mStream.write(oneByte);
- }
-
- @Override
- public void write(byte[] buffer, int offset, int count) throws IOException {
- checkError(count);
- mStream.write(buffer, offset, count);
- }
-
- private void checkError(int expandBy) throws IOException {
- if ((size() + expandBy) > MAX_SIZE) {
- throw new IOException("Exceeded max size!");
- }
- }
-
- public int size() {
- return mStream.size();
- }
-
- public byte[] toByteArray() {
- return mStream.toByteArray();
- }
-
-}
diff --git a/src/com/android/browser/SnapshotTab.java b/src/com/android/browser/SnapshotTab.java
index f58f88b..e14f095 100644
--- a/src/com/android/browser/SnapshotTab.java
+++ b/src/com/android/browser/SnapshotTab.java
@@ -18,11 +18,13 @@
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
+import android.content.Context;
import android.database.Cursor;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
+import android.text.TextUtils;
import android.util.Log;
import android.webkit.WebView;
import android.webkit.WebViewClassic;
@@ -30,6 +32,8 @@
import com.android.browser.provider.SnapshotProvider.Snapshots;
import java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
import java.util.Map;
import java.util.zip.GZIPInputStream;
@@ -75,7 +79,7 @@
void loadData() {
if (mLoadTask == null) {
- mLoadTask = new LoadData(this, mContext.getContentResolver());
+ mLoadTask = new LoadData(this, mContext);
mLoadTask.execute();
}
}
@@ -152,20 +156,31 @@
static final String[] PROJECTION = new String[] {
Snapshots._ID, // 0
- Snapshots.TITLE, // 1
- Snapshots.URL, // 2
+ Snapshots.URL, // 1
+ Snapshots.TITLE, // 2
Snapshots.FAVICON, // 3
Snapshots.VIEWSTATE, // 4
Snapshots.BACKGROUND, // 5
Snapshots.DATE_CREATED, // 6
+ Snapshots.VIEWSTATE_PATH, // 7
};
+ static final int SNAPSHOT_ID = 0;
+ static final int SNAPSHOT_URL = 1;
+ static final int SNAPSHOT_TITLE = 2;
+ static final int SNAPSHOT_FAVICON = 3;
+ static final int SNAPSHOT_VIEWSTATE = 4;
+ static final int SNAPSHOT_BACKGROUND = 5;
+ static final int SNAPSHOT_DATE_CREATED = 6;
+ static final int SNAPSHOT_VIEWSTATE_PATH = 7;
private SnapshotTab mTab;
private ContentResolver mContentResolver;
+ private Context mContext;
- public LoadData(SnapshotTab t, ContentResolver cr) {
+ public LoadData(SnapshotTab t, Context context) {
mTab = t;
- mContentResolver = cr;
+ mContentResolver = context.getContentResolver();
+ mContext = context;
}
@Override
@@ -175,26 +190,35 @@
return mContentResolver.query(uri, PROJECTION, null, null, null);
}
+ private InputStream getInputStream(Cursor c) throws FileNotFoundException {
+ String path = c.getString(SNAPSHOT_VIEWSTATE_PATH);
+ if (!TextUtils.isEmpty(path)) {
+ return mContext.openFileInput(path);
+ }
+ byte[] data = c.getBlob(SNAPSHOT_VIEWSTATE);
+ ByteArrayInputStream bis = new ByteArrayInputStream(data);
+ return bis;
+ }
+
@Override
protected void onPostExecute(Cursor result) {
try {
if (result.moveToFirst()) {
- mTab.mCurrentState.mTitle = result.getString(1);
- mTab.mCurrentState.mUrl = result.getString(2);
- byte[] favicon = result.getBlob(3);
+ mTab.mCurrentState.mTitle = result.getString(SNAPSHOT_TITLE);
+ mTab.mCurrentState.mUrl = result.getString(SNAPSHOT_URL);
+ byte[] favicon = result.getBlob(SNAPSHOT_FAVICON);
if (favicon != null) {
mTab.mCurrentState.mFavicon = BitmapFactory
.decodeByteArray(favicon, 0, favicon.length);
}
WebViewClassic web = mTab.getWebViewClassic();
if (web != null) {
- byte[] data = result.getBlob(4);
- ByteArrayInputStream bis = new ByteArrayInputStream(data);
- GZIPInputStream stream = new GZIPInputStream(bis);
+ InputStream ins = getInputStream(result);
+ GZIPInputStream stream = new GZIPInputStream(ins);
web.loadViewState(stream);
}
- mTab.mBackgroundColor = result.getInt(5);
- mTab.mDateCreated = result.getLong(6);
+ mTab.mBackgroundColor = result.getInt(SNAPSHOT_BACKGROUND);
+ mTab.mDateCreated = result.getLong(SNAPSHOT_DATE_CREATED);
mTab.mWebViewController.onPageFinished(mTab);
}
} catch (Exception e) {
diff --git a/src/com/android/browser/Tab.java b/src/com/android/browser/Tab.java
index 9b5a675..04bee08 100644
--- a/src/com/android/browser/Tab.java
+++ b/src/com/android/browser/Tab.java
@@ -64,8 +64,8 @@
import android.webkit.WebResourceResponse;
import android.webkit.WebStorage;
import android.webkit.WebView;
-import android.webkit.WebViewClassic;
import android.webkit.WebView.PictureListener;
+import android.webkit.WebViewClassic;
import android.webkit.WebViewClient;
import android.widget.CheckBox;
import android.widget.Toast;
@@ -76,12 +76,15 @@
import com.android.common.speech.LoggingEvents;
import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
+import java.util.UUID;
import java.util.Vector;
import java.util.regex.Pattern;
import java.util.zip.GZIPOutputStream;
@@ -2059,9 +2062,10 @@
public ContentValues createSnapshotValues() {
if (mMainView == null) return null;
- SnapshotByteArrayOutputStream bos = new SnapshotByteArrayOutputStream();
+ String path = UUID.randomUUID().toString();
try {
- GZIPOutputStream stream = new GZIPOutputStream(bos);
+ OutputStream outs = mContext.openFileOutput(path, Context.MODE_PRIVATE);
+ GZIPOutputStream stream = new GZIPOutputStream(outs);
if (!getWebViewClassic().saveViewState(stream)) {
return null;
}
@@ -2071,11 +2075,13 @@
Log.w(LOGTAG, "Failed to save view state", e);
return null;
}
- byte[] data = bos.toByteArray();
+ File savedFile = mContext.getFileStreamPath(path);
+ long size = savedFile.length();
ContentValues values = new ContentValues();
values.put(Snapshots.TITLE, mCurrentState.mTitle);
values.put(Snapshots.URL, mCurrentState.mUrl);
- values.put(Snapshots.VIEWSTATE, data);
+ values.put(Snapshots.VIEWSTATE_PATH, path);
+ values.put(Snapshots.VIEWSTATE_SIZE, size);
values.put(Snapshots.BACKGROUND, getWebViewClassic().getPageBackgroundColor());
values.put(Snapshots.DATE_CREATED, System.currentTimeMillis());
values.put(Snapshots.FAVICON, compressBitmap(getFavicon()));
diff --git a/src/com/android/browser/provider/SnapshotProvider.java b/src/com/android/browser/provider/SnapshotProvider.java
index 437a867..291e93b 100644
--- a/src/com/android/browser/provider/SnapshotProvider.java
+++ b/src/com/android/browser/provider/SnapshotProvider.java
@@ -15,13 +15,10 @@
*/
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;
@@ -29,15 +26,11 @@
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
-import android.os.Environment;
+import android.os.FileUtils;
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 {
@@ -45,6 +38,7 @@
public static final Uri CONTENT_URI = Uri.withAppendedPath(
SnapshotProvider.AUTHORITY_URI, "snapshots");
public static final String _ID = "_id";
+ @Deprecated
public static final String VIEWSTATE = "view_state";
public static final String BACKGROUND = "background";
public static final String TITLE = "title";
@@ -52,6 +46,8 @@
public static final String FAVICON = "favicon";
public static final String THUMBNAIL = "thumbnail";
public static final String DATE_CREATED = "date_created";
+ public static final String VIEWSTATE_PATH = "viewstate_path";
+ public static final String VIEWSTATE_SIZE = "viewstate_size";
}
public static final String AUTHORITY = "com.android.browser.snapshots";
@@ -61,6 +57,8 @@
static final int SNAPSHOTS = 10;
static final int SNAPSHOTS_ID = 11;
static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+ // Workaround that we can't remove the "NOT NULL" constraint on VIEWSTATE
+ static final byte[] NULL_BLOB_HACK = new byte[0];
SnapshotDatabaseHelper mOpenHelper;
@@ -72,15 +70,10 @@
final static class SnapshotDatabaseHelper extends SQLiteOpenHelper {
static final String DATABASE_NAME = "snapshots.db";
- static final int DATABASE_VERSION = 2;
+ static final int DATABASE_VERSION = 3;
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();
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
@@ -93,7 +86,9 @@
Snapshots.FAVICON + " BLOB," +
Snapshots.THUMBNAIL + " BLOB," +
Snapshots.BACKGROUND + " INTEGER," +
- Snapshots.VIEWSTATE + " BLOB NOT NULL" +
+ Snapshots.VIEWSTATE + " BLOB NOT NULL," +
+ Snapshots.VIEWSTATE_PATH + " TEXT," +
+ Snapshots.VIEWSTATE_SIZE + "INTEGER" +
");");
}
@@ -103,64 +98,52 @@
db.execSQL("DROP TABLE " + TABLE_SNAPSHOTS);
onCreate(db);
}
+ if (oldVersion < 3) {
+ db.execSQL("ALTER TABLE " + TABLE_SNAPSHOTS + " ADD COLUMN "
+ + Snapshots.VIEWSTATE_PATH + " TEXT");
+ db.execSQL("ALTER TABLE " + TABLE_SNAPSHOTS + " ADD COLUMN "
+ + Snapshots.VIEWSTATE_SIZE + " INTEGER");
+ db.execSQL("UPDATE " + TABLE_SNAPSHOTS + " SET "
+ + Snapshots.VIEWSTATE_SIZE + " = length("
+ + Snapshots.VIEWSTATE + ")");
+ }
}
}
+ static File getOldDatabasePath(Context context) {
+ File dir = context.getExternalFilesDir(null);
+ return new File(dir, SnapshotDatabaseHelper.DATABASE_NAME);
+ }
+
+ private void migrateToDataFolder() {
+ File dbPath = getContext().getDatabasePath(SnapshotDatabaseHelper.DATABASE_NAME);
+ if (dbPath.exists()) return;
+ File oldPath = getOldDatabasePath(getContext());
+ if (oldPath.exists()) {
+ // Try to move
+ if (!oldPath.renameTo(dbPath)) {
+ // Failed, do a copy
+ FileUtils.copyFile(oldPath, dbPath);
+ }
+ // Cleanup
+ oldPath.delete();
+ }
+ }
+
@Override
public boolean onCreate() {
- IntentFilter filter = new IntentFilter(Intent.ACTION_MEDIA_EJECT);
- filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
- getContext().registerReceiver(mExternalStorageReceiver, filter);
+ migrateToDataFolder();
+ mOpenHelper = new SnapshotDatabaseHelper(getContext());
return true;
}
- final BroadcastReceiver mExternalStorageReceiver = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (mOpenHelper != null) {
- 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 {
- if (mOpenHelper == null) {
- mOpenHelper = new SnapshotDatabaseHelper(getContext());
- }
- return mOpenHelper.getWritableDatabase();
- } catch (Throwable t) {
- return null;
- }
- }
- return null;
+ return mOpenHelper.getWritableDatabase();
}
SQLiteDatabase getReadableDatabase() {
- String state = Environment.getExternalStorageState();
- if (Environment.MEDIA_MOUNTED.equals(state)
- || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
- try {
- if (mOpenHelper == null) {
- mOpenHelper = new SnapshotDatabaseHelper(getContext());
- }
- return mOpenHelper.getReadableDatabase();
- } catch (Throwable t) {
- return null;
- }
- }
- return null;
+ return mOpenHelper.getReadableDatabase();
}
@Override
@@ -186,15 +169,11 @@
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;
- }
+ Cursor cursor = qb.query(db, projection, selection, selectionArgs,
+ null, null, sortOrder, limit);
+ cursor.setNotificationUri(getContext().getContentResolver(),
+ AUTHORITY_URI);
+ return cursor;
}
@Override
@@ -212,11 +191,10 @@
long id = -1;
switch (match) {
case SNAPSHOTS:
- try {
- id = db.insert(TABLE_SNAPSHOTS, Snapshots.TITLE, values);
- } catch (Throwable t) {
- id = -1;
+ if (!values.containsKey(Snapshots.VIEWSTATE)) {
+ values.put(Snapshots.VIEWSTATE, NULL_BLOB_HACK);
}
+ id = db.insert(TABLE_SNAPSHOTS, Snapshots.TITLE, values);
break;
default:
throw new UnsupportedOperationException("Unknown insert URI " + uri);
@@ -229,6 +207,25 @@
return inserted;
}
+ static final String[] DELETE_PROJECTION = new String[] {
+ Snapshots.VIEWSTATE_PATH,
+ };
+ private void deleteDataFiles(SQLiteDatabase db, String selection,
+ String[] selectionArgs) {
+ Cursor c = db.query(TABLE_SNAPSHOTS, DELETE_PROJECTION, selection,
+ selectionArgs, null, null, null);
+ final Context context = getContext();
+ while (c.moveToNext()) {
+ File f = context.getFileStreamPath(c.getString(0));
+ if (f.exists()) {
+ if (!f.delete()) {
+ f.deleteOnExit();
+ }
+ }
+ }
+ c.close();
+ }
+
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = getWritableDatabase();
@@ -245,10 +242,8 @@
// fall through
}
case SNAPSHOTS:
- try {
- deleted = db.delete(TABLE_SNAPSHOTS, selection, selectionArgs);
- } catch (Throwable t) {
- }
+ deleteDataFiles(db, selection, selectionArgs);
+ deleted = db.delete(TABLE_SNAPSHOTS, selection, selectionArgs);
break;
default:
throw new UnsupportedOperationException("Unknown delete URI " + uri);