mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
1496 lines
57 KiB
Java
1496 lines
57 KiB
Java
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
|
* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (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.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is Mozilla Android code.
|
|
*
|
|
* The Initial Developer of the Original Code is Mozilla Foundation.
|
|
* Portions created by the Initial Developer are Copyright (C) 2011
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Lucas Rocha <lucasr@mozilla.com>
|
|
* Jason Voll <jvoll@mozilla.com>
|
|
* Richard Newman <rnewman@mozilla.com>
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
#filter substitution
|
|
package @ANDROID_PACKAGE_NAME@.db;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Random;
|
|
|
|
import org.mozilla.gecko.GeckoAppShell;
|
|
import org.mozilla.gecko.GeckoDirProvider;
|
|
import org.mozilla.gecko.R;
|
|
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
|
|
import org.mozilla.gecko.db.BrowserContract.CommonColumns;
|
|
import org.mozilla.gecko.db.BrowserContract.History;
|
|
import org.mozilla.gecko.db.BrowserContract.Images;
|
|
import org.mozilla.gecko.db.BrowserContract.Schema;
|
|
import org.mozilla.gecko.db.BrowserContract.SyncColumns;
|
|
import org.mozilla.gecko.db.BrowserContract.URLColumns;
|
|
import org.mozilla.gecko.db.BrowserContract;
|
|
import org.mozilla.gecko.db.DBUtils;
|
|
import org.mozilla.gecko.sync.Utils;
|
|
|
|
import android.content.ContentProvider;
|
|
import android.content.ContentUris;
|
|
import android.content.ContentValues;
|
|
import android.content.Context;
|
|
import android.content.UriMatcher;
|
|
import android.database.Cursor;
|
|
import android.database.DatabaseUtils;
|
|
import android.database.MatrixCursor;
|
|
import android.database.sqlite.SQLiteDatabase;
|
|
import android.database.sqlite.SQLiteOpenHelper;
|
|
import android.database.sqlite.SQLiteQueryBuilder;
|
|
import android.net.Uri;
|
|
import android.os.Build;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
|
|
public class BrowserProvider extends ContentProvider {
|
|
private static final String LOGTAG = "GeckoBrowserProvider";
|
|
private Context mContext;
|
|
|
|
static final String DATABASE_NAME = "browser.db";
|
|
|
|
static final int DATABASE_VERSION = 2;
|
|
|
|
// Maximum age of deleted records to be cleaned up (20 days in ms)
|
|
static final long MAX_AGE_OF_DELETED_RECORDS = 86400000 * 20;
|
|
|
|
// Number of records marked as deleted to be removed
|
|
static final long DELETED_RECORDS_PURGE_LIMIT = 5;
|
|
|
|
static final String TABLE_BOOKMARKS = "bookmarks";
|
|
static final String TABLE_HISTORY = "history";
|
|
static final String TABLE_IMAGES = "images";
|
|
|
|
static final String TABLE_BOOKMARKS_TMP = TABLE_BOOKMARKS + "_tmp";
|
|
|
|
static final String VIEW_BOOKMARKS_WITH_IMAGES = "bookmarks_with_images";
|
|
static final String VIEW_HISTORY_WITH_IMAGES = "history_with_images";
|
|
|
|
// Bookmark matches
|
|
static final int BOOKMARKS = 100;
|
|
static final int BOOKMARKS_ID = 101;
|
|
static final int BOOKMARKS_FOLDER_ID = 102;
|
|
static final int BOOKMARKS_PARENT = 103;
|
|
|
|
// History matches
|
|
static final int HISTORY = 200;
|
|
static final int HISTORY_ID = 201;
|
|
|
|
// Image matches
|
|
static final int IMAGES = 300;
|
|
static final int IMAGES_ID = 301;
|
|
|
|
// Schema matches
|
|
static final int SCHEMA = 400;
|
|
|
|
static final String DEFAULT_BOOKMARKS_SORT_ORDER = Bookmarks.IS_FOLDER
|
|
+ " DESC, " + Bookmarks.POSITION + " ASC, " + Bookmarks._ID
|
|
+ " ASC";
|
|
|
|
static final String DEFAULT_HISTORY_SORT_ORDER = History.DATE_LAST_VISITED + " DESC";
|
|
|
|
static final String TABLE_BOOKMARKS_JOIN_IMAGES = TABLE_BOOKMARKS + " LEFT OUTER JOIN " +
|
|
"(SELECT " + Images.URL + ", " + Images.FAVICON + ", " + Images.THUMBNAIL + " FROM " +
|
|
TABLE_IMAGES + ", " + TABLE_BOOKMARKS + " WHERE " +
|
|
qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " +
|
|
qualifyColumn(TABLE_IMAGES, Images.URL) + ") AS bookmark_images ON " +
|
|
qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " +
|
|
qualifyColumn("bookmark_images", Images.URL);
|
|
|
|
static final String TABLE_HISTORY_JOIN_IMAGES = TABLE_HISTORY + " LEFT OUTER JOIN " +
|
|
"(SELECT " + Images.URL + ", " + Images.FAVICON + ", " + Images.THUMBNAIL + " FROM " +
|
|
TABLE_IMAGES + ", " + TABLE_HISTORY + " WHERE " +
|
|
qualifyColumn(TABLE_HISTORY, History.URL) + " = " +
|
|
qualifyColumn(TABLE_IMAGES, Images.URL) + ") AS history_images ON " +
|
|
qualifyColumn(TABLE_HISTORY, History.URL) + " = " +
|
|
qualifyColumn("history_images", Images.URL);
|
|
|
|
static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
|
|
|
|
static final HashMap<String, String> BOOKMARKS_PROJECTION_MAP = new HashMap<String, String>();
|
|
static final HashMap<String, String> HISTORY_PROJECTION_MAP = new HashMap<String, String>();
|
|
static final HashMap<String, String> IMAGES_PROJECTION_MAP = new HashMap<String, String>();
|
|
static final HashMap<String, String> SCHEMA_PROJECTION_MAP = new HashMap<String, String>();
|
|
|
|
static {
|
|
HashMap<String, String> map;
|
|
|
|
// Bookmarks
|
|
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks", BOOKMARKS);
|
|
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/#", BOOKMARKS_ID);
|
|
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/parents", BOOKMARKS_PARENT);
|
|
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/folder/#", BOOKMARKS_FOLDER_ID);
|
|
|
|
map = BOOKMARKS_PROJECTION_MAP;
|
|
map.put(Bookmarks._ID, Bookmarks._ID);
|
|
map.put(Bookmarks.TITLE, Bookmarks.TITLE);
|
|
map.put(Bookmarks.URL, Bookmarks.URL);
|
|
map.put(Bookmarks.FAVICON, Bookmarks.FAVICON);
|
|
map.put(Bookmarks.THUMBNAIL, Bookmarks.THUMBNAIL);
|
|
map.put(Bookmarks.IS_FOLDER, Bookmarks.IS_FOLDER);
|
|
map.put(Bookmarks.PARENT, Bookmarks.PARENT);
|
|
map.put(Bookmarks.POSITION, Bookmarks.POSITION);
|
|
map.put(Bookmarks.TAGS, Bookmarks.TAGS);
|
|
map.put(Bookmarks.DESCRIPTION, Bookmarks.DESCRIPTION);
|
|
map.put(Bookmarks.KEYWORD, Bookmarks.KEYWORD);
|
|
map.put(Bookmarks.DATE_CREATED, Bookmarks.DATE_CREATED);
|
|
map.put(Bookmarks.DATE_MODIFIED, Bookmarks.DATE_MODIFIED);
|
|
map.put(Bookmarks.GUID, Bookmarks.GUID);
|
|
map.put(Bookmarks.IS_DELETED, Bookmarks.IS_DELETED);
|
|
|
|
// History
|
|
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history", HISTORY);
|
|
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history/#", HISTORY_ID);
|
|
|
|
map = HISTORY_PROJECTION_MAP;
|
|
map.put(History._ID, History._ID);
|
|
map.put(History.TITLE, History.TITLE);
|
|
map.put(History.URL, History.URL);
|
|
map.put(History.FAVICON, History.FAVICON);
|
|
map.put(History.THUMBNAIL, History.THUMBNAIL);
|
|
map.put(History.VISITS, History.VISITS);
|
|
map.put(History.DATE_LAST_VISITED, History.DATE_LAST_VISITED);
|
|
map.put(History.DATE_CREATED, History.DATE_CREATED);
|
|
map.put(History.DATE_MODIFIED, History.DATE_MODIFIED);
|
|
map.put(History.GUID, History.GUID);
|
|
map.put(History.IS_DELETED, History.IS_DELETED);
|
|
|
|
// Images
|
|
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "images", IMAGES);
|
|
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "images/#", IMAGES_ID);
|
|
|
|
map = IMAGES_PROJECTION_MAP;
|
|
map.put(Images._ID, Images._ID);
|
|
map.put(Images.URL, Images.URL);
|
|
map.put(Images.FAVICON, Images.FAVICON);
|
|
map.put(Images.FAVICON_URL, Images.FAVICON_URL);
|
|
map.put(Images.THUMBNAIL, Images.THUMBNAIL);
|
|
map.put(Images.DATE_CREATED, Images.DATE_CREATED);
|
|
map.put(Images.DATE_MODIFIED, Images.DATE_MODIFIED);
|
|
map.put(Images.GUID, Images.GUID);
|
|
map.put(Images.IS_DELETED, Images.IS_DELETED);
|
|
|
|
// Schema
|
|
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "schema", SCHEMA);
|
|
|
|
map = SCHEMA_PROJECTION_MAP;
|
|
map.put(Schema.VERSION, Schema.VERSION);
|
|
}
|
|
|
|
private HashMap<String, DatabaseHelper> mDatabasePerProfile;
|
|
|
|
static final String qualifyColumn(String table, String column) {
|
|
return table + "." + column;
|
|
}
|
|
|
|
private static boolean hasImagesInProjection(String[] projection) {
|
|
if (projection == null) return true;
|
|
for (int i = 0; i < projection.length; ++i) {
|
|
if (projection[i].equals(Images.FAVICON) ||
|
|
projection[i].equals(Images.THUMBNAIL))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Calculate these once, at initialization. isLoggable is too expensive to
|
|
// have in-line in each log call.
|
|
private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
|
|
private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
|
|
protected static void trace(String message) {
|
|
if (logVerbose) {
|
|
Log.v(LOGTAG, message);
|
|
}
|
|
}
|
|
|
|
protected static void debug(String message) {
|
|
if (logDebug) {
|
|
Log.d(LOGTAG, message);
|
|
}
|
|
}
|
|
|
|
final class DatabaseHelper extends SQLiteOpenHelper {
|
|
public DatabaseHelper(Context context, String databasePath) {
|
|
super(context, databasePath, null, DATABASE_VERSION);
|
|
}
|
|
|
|
private void createBookmarksTable(SQLiteDatabase db) {
|
|
debug("Creating " + TABLE_BOOKMARKS + " table");
|
|
|
|
// Android versions older than Froyo ship with an sqlite
|
|
// that doesn't support foreign keys.
|
|
String foreignKeyOnParent = null;
|
|
if (Build.VERSION.SDK_INT >= 8) {
|
|
foreignKeyOnParent = ", FOREIGN KEY (" + Bookmarks.PARENT +
|
|
") REFERENCES " + TABLE_BOOKMARKS + "(" + Bookmarks._ID + ")";
|
|
}
|
|
|
|
db.execSQL("CREATE TABLE " + TABLE_BOOKMARKS + "(" +
|
|
Bookmarks._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
|
|
Bookmarks.TITLE + " TEXT," +
|
|
Bookmarks.URL + " TEXT," +
|
|
Bookmarks.IS_FOLDER + " INTEGER NOT NULL DEFAULT 0," +
|
|
Bookmarks.PARENT + " INTEGER," +
|
|
Bookmarks.POSITION + " INTEGER NOT NULL," +
|
|
Bookmarks.KEYWORD + " TEXT," +
|
|
Bookmarks.DESCRIPTION + " TEXT," +
|
|
Bookmarks.TAGS + " TEXT," +
|
|
Bookmarks.DATE_CREATED + " INTEGER," +
|
|
Bookmarks.DATE_MODIFIED + " INTEGER," +
|
|
Bookmarks.GUID + " TEXT," +
|
|
Bookmarks.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
|
|
(foreignKeyOnParent != null ? foreignKeyOnParent : "") +
|
|
");");
|
|
|
|
db.execSQL("CREATE INDEX bookmarks_url_index ON " + TABLE_BOOKMARKS + "("
|
|
+ Bookmarks.URL + ")");
|
|
db.execSQL("CREATE UNIQUE INDEX bookmarks_guid_index ON " + TABLE_BOOKMARKS + "("
|
|
+ Bookmarks.GUID + ")");
|
|
db.execSQL("CREATE INDEX bookmarks_modified_index ON " + TABLE_BOOKMARKS + "("
|
|
+ Bookmarks.DATE_MODIFIED + ")");
|
|
}
|
|
|
|
private void createHistoryTable(SQLiteDatabase db) {
|
|
debug("Creating " + TABLE_HISTORY + " table");
|
|
db.execSQL("CREATE TABLE " + TABLE_HISTORY + "(" +
|
|
History._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
|
|
History.TITLE + " TEXT," +
|
|
History.URL + " TEXT NOT NULL," +
|
|
History.VISITS + " INTEGER NOT NULL DEFAULT 0," +
|
|
History.DATE_LAST_VISITED + " INTEGER," +
|
|
History.DATE_CREATED + " INTEGER," +
|
|
History.DATE_MODIFIED + " INTEGER," +
|
|
History.GUID + " TEXT," +
|
|
History.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
|
|
");");
|
|
|
|
db.execSQL("CREATE INDEX history_url_index ON " + TABLE_HISTORY + "("
|
|
+ History.URL + ")");
|
|
db.execSQL("CREATE UNIQUE INDEX history_guid_index ON " + TABLE_HISTORY + "("
|
|
+ History.GUID + ")");
|
|
db.execSQL("CREATE INDEX history_modified_index ON " + TABLE_HISTORY + "("
|
|
+ History.DATE_MODIFIED + ")");
|
|
db.execSQL("CREATE INDEX history_visited_index ON " + TABLE_HISTORY + "("
|
|
+ History.DATE_LAST_VISITED + ")");
|
|
}
|
|
|
|
private void createImagesTable(SQLiteDatabase db) {
|
|
debug("Creating " + TABLE_IMAGES + " table");
|
|
db.execSQL("CREATE TABLE " + TABLE_IMAGES + " (" +
|
|
Images._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
|
|
Images.URL + " TEXT UNIQUE NOT NULL," +
|
|
Images.FAVICON + " BLOB," +
|
|
Images.FAVICON_URL + " TEXT," +
|
|
Images.THUMBNAIL + " BLOB," +
|
|
Images.DATE_CREATED + " INTEGER," +
|
|
Images.DATE_MODIFIED + " INTEGER," +
|
|
Images.GUID + " TEXT," +
|
|
Images.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
|
|
");");
|
|
|
|
db.execSQL("CREATE INDEX images_url_index ON " + TABLE_IMAGES + "("
|
|
+ Images.URL + ")");
|
|
db.execSQL("CREATE UNIQUE INDEX images_guid_index ON " + TABLE_IMAGES + "("
|
|
+ Images.GUID + ")");
|
|
db.execSQL("CREATE INDEX images_modified_index ON " + TABLE_IMAGES + "("
|
|
+ Images.DATE_MODIFIED + ")");
|
|
}
|
|
|
|
private void createBookmarksWithImagesView(SQLiteDatabase db) {
|
|
db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_BOOKMARKS_WITH_IMAGES + " AS " +
|
|
"SELECT " + qualifyColumn(TABLE_BOOKMARKS, "*") +
|
|
", " + Images.FAVICON + ", " + Images.THUMBNAIL + " FROM " +
|
|
TABLE_BOOKMARKS_JOIN_IMAGES);
|
|
}
|
|
|
|
private void createHistoryWithImagesView(SQLiteDatabase db) {
|
|
db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_HISTORY_WITH_IMAGES + " AS " +
|
|
"SELECT " + qualifyColumn(TABLE_HISTORY, "*") +
|
|
", " + Images.FAVICON + ", " + Images.THUMBNAIL + " FROM " +
|
|
TABLE_HISTORY_JOIN_IMAGES);
|
|
}
|
|
|
|
@Override
|
|
public void onCreate(SQLiteDatabase db) {
|
|
debug("Creating browser.db: " + db.getPath());
|
|
|
|
createBookmarksTable(db);
|
|
createHistoryTable(db);
|
|
createImagesTable(db);
|
|
|
|
createBookmarksWithImagesView(db);
|
|
createHistoryWithImagesView(db);
|
|
|
|
createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID,
|
|
R.string.bookmarks_folder_places, 0);
|
|
|
|
createOrUpdateAllSpecialFolders(db);
|
|
|
|
// FIXME: Create default bookmarks here (bug 728224)
|
|
}
|
|
|
|
private void createOrUpdateAllSpecialFolders(SQLiteDatabase db) {
|
|
createOrUpdateSpecialFolder(db, Bookmarks.MOBILE_FOLDER_GUID,
|
|
R.string.bookmarks_folder_mobile, 0);
|
|
createOrUpdateSpecialFolder(db, Bookmarks.TOOLBAR_FOLDER_GUID,
|
|
R.string.bookmarks_folder_toolbar, 1);
|
|
createOrUpdateSpecialFolder(db, Bookmarks.MENU_FOLDER_GUID,
|
|
R.string.bookmarks_folder_menu, 2);
|
|
createOrUpdateSpecialFolder(db, Bookmarks.TAGS_FOLDER_GUID,
|
|
R.string.bookmarks_folder_tags, 3);
|
|
createOrUpdateSpecialFolder(db, Bookmarks.UNFILED_FOLDER_GUID,
|
|
R.string.bookmarks_folder_unfiled, 4);
|
|
}
|
|
|
|
private void createOrUpdateSpecialFolder(SQLiteDatabase db,
|
|
String guid, int titleId, int position) {
|
|
ContentValues values = new ContentValues();
|
|
values.put(Bookmarks.GUID, guid);
|
|
values.put(Bookmarks.IS_FOLDER, 1);
|
|
values.put(Bookmarks.POSITION, position);
|
|
|
|
if (guid.equals(Bookmarks.PLACES_FOLDER_GUID))
|
|
values.put(Bookmarks._ID, Bookmarks.FIXED_ROOT_ID);
|
|
|
|
// Set the parent to 0, which sync assumes is the root
|
|
values.put(Bookmarks.PARENT, Bookmarks.FIXED_ROOT_ID);
|
|
|
|
String title = mContext.getResources().getString(titleId);
|
|
values.put(Bookmarks.TITLE, title);
|
|
|
|
long now = System.currentTimeMillis();
|
|
values.put(Bookmarks.DATE_CREATED, now);
|
|
values.put(Bookmarks.DATE_MODIFIED, now);
|
|
|
|
int updated = db.update(TABLE_BOOKMARKS, values,
|
|
Bookmarks.GUID + " = ?",
|
|
new String[] { guid });
|
|
|
|
if (updated == 0) {
|
|
db.insert(TABLE_BOOKMARKS, Bookmarks.GUID, values);
|
|
debug("Inserted special folder: " + guid);
|
|
} else {
|
|
debug("Updated special folder: " + guid);
|
|
}
|
|
}
|
|
|
|
private boolean isSpecialFolder(ContentValues values) {
|
|
String guid = values.getAsString(Bookmarks.GUID);
|
|
if (guid == null)
|
|
return false;
|
|
|
|
return guid.equals(Bookmarks.MOBILE_FOLDER_GUID) ||
|
|
guid.equals(Bookmarks.MENU_FOLDER_GUID) ||
|
|
guid.equals(Bookmarks.TOOLBAR_FOLDER_GUID) ||
|
|
guid.equals(Bookmarks.UNFILED_FOLDER_GUID) ||
|
|
guid.equals(Bookmarks.TAGS_FOLDER_GUID);
|
|
}
|
|
|
|
private void migrateBookmarkFolder(SQLiteDatabase db, int folderId) {
|
|
Cursor c = null;
|
|
|
|
debug("Migrating bookmark folder with id = " + folderId);
|
|
|
|
String selection = Bookmarks.PARENT + " = " + folderId;
|
|
String[] selectionArgs = null;
|
|
|
|
boolean isRootFolder = (folderId == Bookmarks.FIXED_ROOT_ID);
|
|
|
|
// If we're loading the root folder, we have to account for
|
|
// any previously created special folder that was created without
|
|
// setting a parent id (e.g. mobile folder) and making sure we're
|
|
// not adding any infinite recursion as root's parent is root itself.
|
|
if (isRootFolder) {
|
|
selection = Bookmarks.GUID + " != ?" + " AND (" +
|
|
selection + " OR " + Bookmarks.PARENT + " = NULL)";
|
|
selectionArgs = new String[] { Bookmarks.PLACES_FOLDER_GUID };
|
|
}
|
|
|
|
List<Integer> subFolders = new ArrayList<Integer>();
|
|
List<ContentValues> invalidSpecialEntries = new ArrayList<ContentValues>();
|
|
|
|
try {
|
|
c = db.query(TABLE_BOOKMARKS_TMP,
|
|
null,
|
|
selection,
|
|
selectionArgs,
|
|
null, null, null);
|
|
|
|
// The key point here is that bookmarks should be added in
|
|
// parent order to avoid any problems with the foreign key
|
|
// in Bookmarks.PARENT.
|
|
while (c.moveToNext()) {
|
|
ContentValues values = new ContentValues();
|
|
|
|
// We're using a null projection in the query which
|
|
// means we're getting all columns from the table.
|
|
// It's safe to simply transform the row into the
|
|
// values to be inserted on the new table.
|
|
DatabaseUtils.cursorRowToContentValues(c, values);
|
|
|
|
boolean isSpecialFolder = isSpecialFolder(values);
|
|
|
|
// The mobile folder used to be created with PARENT = NULL.
|
|
// We want fix that here.
|
|
if (values.getAsLong(Bookmarks.PARENT) == null && isSpecialFolder)
|
|
values.put(Bookmarks.PARENT, Bookmarks.FIXED_ROOT_ID);
|
|
|
|
if (isRootFolder && !isSpecialFolder) {
|
|
invalidSpecialEntries.add(values);
|
|
continue;
|
|
}
|
|
|
|
debug("Migrating bookmark: " + values.getAsString(Bookmarks.TITLE));
|
|
db.insert(TABLE_BOOKMARKS, Bookmarks.URL, values);
|
|
|
|
Integer isFolder = values.getAsInteger(Bookmarks.IS_FOLDER);
|
|
if (isFolder != null && isFolder == 1)
|
|
subFolders.add(values.getAsInteger(Bookmarks._ID));
|
|
}
|
|
} finally {
|
|
if (c != null)
|
|
c.close();
|
|
}
|
|
|
|
// At this point is safe to assume that the mobile folder is
|
|
// in the new table given that we've always created it on
|
|
// database creation time.
|
|
final int nInvalidSpecialEntries = invalidSpecialEntries.size();
|
|
if (nInvalidSpecialEntries > 0) {
|
|
Long mobileFolderId = guidToID(db, Bookmarks.MOBILE_FOLDER_GUID);
|
|
|
|
debug("Found " + nInvalidSpecialEntries + " invalid special folder entries");
|
|
for (int i = 0; i < nInvalidSpecialEntries; i++) {
|
|
ContentValues values = invalidSpecialEntries.get(i);
|
|
values.put(Bookmarks.PARENT, mobileFolderId);
|
|
|
|
db.insert(TABLE_BOOKMARKS, Bookmarks.URL, values);
|
|
}
|
|
}
|
|
|
|
final int nSubFolders = subFolders.size();
|
|
for (int i = 0; i < nSubFolders; i++) {
|
|
int subFolderId = subFolders.get(i);
|
|
migrateBookmarkFolder(db, subFolderId);
|
|
}
|
|
}
|
|
|
|
private void upgradeDatabaseFrom1to2(SQLiteDatabase db) {
|
|
debug("Renaming bookmarks table to " + TABLE_BOOKMARKS_TMP);
|
|
db.execSQL("ALTER TABLE " + TABLE_BOOKMARKS +
|
|
" RENAME TO " + TABLE_BOOKMARKS_TMP);
|
|
|
|
debug("Dropping views and indexes related to " + TABLE_BOOKMARKS);
|
|
db.execSQL("DROP VIEW IF EXISTS " + VIEW_BOOKMARKS_WITH_IMAGES);
|
|
|
|
db.execSQL("DROP INDEX IF EXISTS bookmarks_url_index");
|
|
db.execSQL("DROP INDEX IF EXISTS bookmarks_guid_index");
|
|
db.execSQL("DROP INDEX IF EXISTS bookmarks_modified_index");
|
|
|
|
createBookmarksTable(db);
|
|
createBookmarksWithImagesView(db);
|
|
|
|
createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID,
|
|
R.string.bookmarks_folder_places, 0);
|
|
|
|
migrateBookmarkFolder(db, Bookmarks.FIXED_ROOT_ID);
|
|
|
|
// Ensure all special folders exist and have the
|
|
// right folder hierarchy.
|
|
createOrUpdateAllSpecialFolders(db);
|
|
|
|
debug("Dropping bookmarks temporary table");
|
|
db.execSQL("DROP TABLE IF EXISTS " + TABLE_BOOKMARKS_TMP);
|
|
}
|
|
|
|
@Override
|
|
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
|
debug("Upgrading browser.db: " + db.getPath() + " from " +
|
|
oldVersion + " to " + newVersion);
|
|
|
|
db.beginTransaction();
|
|
|
|
// We have to do incremental upgrades until we reach the current
|
|
// database schema version.
|
|
for (int v = oldVersion + 1; v <= newVersion; v++) {
|
|
switch(v) {
|
|
case 2:
|
|
upgradeDatabaseFrom1to2(db);
|
|
break;
|
|
}
|
|
}
|
|
|
|
db.endTransaction();
|
|
}
|
|
|
|
@Override
|
|
public void onOpen(SQLiteDatabase db) {
|
|
debug("Opening browser.db: " + db.getPath());
|
|
|
|
// From Honeycomb on, it's possible to run several db
|
|
// commands in parallel using multiple connections.
|
|
if (Build.VERSION.SDK_INT >= 11) {
|
|
db.enableWriteAheadLogging();
|
|
} else {
|
|
// Pre-Honeycomb, we can do some lesser optimizations.
|
|
Cursor cursor = null;
|
|
try {
|
|
cursor = db.rawQuery("PRAGMA synchronous=NORMAL", null);
|
|
} finally {
|
|
if (cursor != null)
|
|
cursor.close();
|
|
}
|
|
cursor = null;
|
|
try {
|
|
cursor = db.rawQuery("PRAGMA journal_mode=PERSIST", null);
|
|
} finally {
|
|
if (cursor != null)
|
|
cursor.close();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private Long guidToID(SQLiteDatabase db, String guid) {
|
|
Cursor c = null;
|
|
|
|
try {
|
|
c = db.query(TABLE_BOOKMARKS,
|
|
new String[] { Bookmarks._ID },
|
|
Bookmarks.GUID + " = ?",
|
|
new String[] { guid },
|
|
null, null, null);
|
|
|
|
if (c == null || !c.moveToFirst())
|
|
return null;
|
|
|
|
return c.getLong(c.getColumnIndex(Bookmarks._ID));
|
|
} finally {
|
|
if (c != null)
|
|
c.close();
|
|
}
|
|
}
|
|
|
|
private DatabaseHelper getDatabaseHelperForProfile(String profile) {
|
|
// Each profile has a separate browser.db database. The target
|
|
// profile is provided using a URI query argument in each request
|
|
// to our content provider.
|
|
|
|
// Always fallback to default profile if none has been provided.
|
|
if (TextUtils.isEmpty(profile)) {
|
|
profile = BrowserContract.DEFAULT_PROFILE;
|
|
}
|
|
|
|
DatabaseHelper dbHelper;
|
|
synchronized (this) {
|
|
dbHelper = mDatabasePerProfile.get(profile);
|
|
if (dbHelper != null) {
|
|
return dbHelper;
|
|
}
|
|
dbHelper = new DatabaseHelper(getContext(), getDatabasePath(profile));
|
|
mDatabasePerProfile.put(profile, dbHelper);
|
|
}
|
|
|
|
debug("Created database helper for profile: " + profile);
|
|
return dbHelper;
|
|
}
|
|
|
|
private String getDatabasePath(String profile) {
|
|
trace("Getting database path for profile: " + profile);
|
|
|
|
// On Android releases older than 2.3, it's not possible to use
|
|
// SQLiteOpenHelper with a full path. Fallback to using separate
|
|
// db files per profile in the app directory.
|
|
if (Build.VERSION.SDK_INT <= 8) {
|
|
return "browser-" + profile + ".db";
|
|
}
|
|
|
|
File profileDir = null;
|
|
try {
|
|
profileDir = GeckoDirProvider.getProfileDir(mContext, profile);
|
|
} catch (IOException ex) {
|
|
Log.e(LOGTAG, "Error getting profile dir", ex);
|
|
}
|
|
|
|
if (profileDir == null) {
|
|
debug("Couldn't find directory for profile: " + profile);
|
|
return null;
|
|
}
|
|
|
|
String databasePath = new File(profileDir, DATABASE_NAME).getAbsolutePath();
|
|
debug("Successfully created database path for profile: " + databasePath);
|
|
|
|
return databasePath;
|
|
}
|
|
|
|
private SQLiteDatabase getReadableDatabase(Uri uri) {
|
|
trace("Getting readable database for URI: " + uri);
|
|
|
|
String profile = null;
|
|
|
|
if (uri != null)
|
|
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
|
|
|
|
return getDatabaseHelperForProfile(profile).getReadableDatabase();
|
|
}
|
|
|
|
private SQLiteDatabase getWritableDatabase(Uri uri) {
|
|
trace("Getting writable database for URI: " + uri);
|
|
|
|
String profile = null;
|
|
|
|
if (uri != null)
|
|
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
|
|
|
|
return getDatabaseHelperForProfile(profile).getWritableDatabase();
|
|
}
|
|
|
|
private void cleanupSomeDeletedRecords(Uri fromUri, Uri targetUri, String tableName) {
|
|
// we cleanup records marked as deleted that are older than a
|
|
// predefined max age. It's important not be too greedy here and
|
|
// remove only a few old deleted records at a time.
|
|
|
|
String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE);
|
|
|
|
// The PARAM_SHOW_DELETED argument is necessary to return the records
|
|
// that were marked as deleted. We use PARAM_IS_SYNC here to ensure
|
|
// that we'll be actually deleting records instead of flagging them.
|
|
Uri uriWithArgs = targetUri.buildUpon()
|
|
.appendQueryParameter(BrowserContract.PARAM_PROFILE, profile)
|
|
.appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(DELETED_RECORDS_PURGE_LIMIT))
|
|
.appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1")
|
|
.appendQueryParameter(BrowserContract.PARAM_IS_SYNC, "1")
|
|
.build();
|
|
|
|
Cursor cursor = null;
|
|
|
|
try {
|
|
long now = System.currentTimeMillis();
|
|
String selection = SyncColumns.IS_DELETED + " = 1 AND " +
|
|
SyncColumns.DATE_MODIFIED + " <= " + (now - MAX_AGE_OF_DELETED_RECORDS);
|
|
|
|
cursor = query(uriWithArgs,
|
|
new String[] { CommonColumns._ID },
|
|
selection,
|
|
null,
|
|
null);
|
|
|
|
while (cursor.moveToNext()) {
|
|
Uri uriWithId = ContentUris.withAppendedId(uriWithArgs, cursor.getLong(0));
|
|
delete(uriWithId, null, null);
|
|
|
|
debug("Removed old deleted item with URI: " + uriWithId);
|
|
}
|
|
} finally {
|
|
if (cursor != null)
|
|
cursor.close();
|
|
}
|
|
}
|
|
|
|
private boolean isCallerSync(Uri uri) {
|
|
String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
|
|
return !TextUtils.isEmpty(isSync);
|
|
}
|
|
|
|
private boolean shouldShowDeleted(Uri uri) {
|
|
String showDeleted = uri.getQueryParameter(BrowserContract.PARAM_SHOW_DELETED);
|
|
return !TextUtils.isEmpty(showDeleted);
|
|
}
|
|
|
|
@Override
|
|
public boolean onCreate() {
|
|
debug("Creating BrowserProvider");
|
|
|
|
GeckoAppShell.getHandler().post(new Runnable() {
|
|
public void run() {
|
|
// Kick this off early. It is synchronized so that other callers will wait
|
|
try {
|
|
GeckoDirProvider.getProfileDir(getContext());
|
|
} catch (Exception ex) {
|
|
Log.e(LOGTAG, "Error getting profile dir", ex);
|
|
}
|
|
}
|
|
});
|
|
synchronized (this) {
|
|
mContext = getContext();
|
|
mDatabasePerProfile = new HashMap<String, DatabaseHelper>();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public String getType(Uri uri) {
|
|
final int match = URI_MATCHER.match(uri);
|
|
|
|
trace("Getting URI type: " + uri);
|
|
|
|
switch (match) {
|
|
case BOOKMARKS:
|
|
trace("URI is BOOKMARKS: " + uri);
|
|
return Bookmarks.CONTENT_TYPE;
|
|
case BOOKMARKS_ID:
|
|
trace("URI is BOOKMARKS_ID: " + uri);
|
|
return Bookmarks.CONTENT_ITEM_TYPE;
|
|
case HISTORY:
|
|
trace("URI is HISTORY: " + uri);
|
|
return History.CONTENT_TYPE;
|
|
case HISTORY_ID:
|
|
trace("URI is HISTORY_ID: " + uri);
|
|
return History.CONTENT_ITEM_TYPE;
|
|
}
|
|
|
|
debug("URI has unrecognized type: " + uri);
|
|
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
|
trace("Calling delete on URI: " + uri);
|
|
|
|
final SQLiteDatabase db = getWritableDatabase(uri);
|
|
int deleted = 0;
|
|
|
|
if (Build.VERSION.SDK_INT >= 11) {
|
|
trace("Beginning delete transaction: " + uri);
|
|
db.beginTransaction();
|
|
try {
|
|
deleted = deleteInTransaction(uri, selection, selectionArgs);
|
|
db.setTransactionSuccessful();
|
|
trace("Successful delete transaction: " + uri);
|
|
} finally {
|
|
db.endTransaction();
|
|
}
|
|
} else {
|
|
deleted = deleteInTransaction(uri, selection, selectionArgs);
|
|
}
|
|
|
|
if (deleted > 0)
|
|
getContext().getContentResolver().notifyChange(uri, null);
|
|
|
|
return deleted;
|
|
}
|
|
|
|
@SuppressWarnings("fallthrough")
|
|
public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
|
|
trace("Calling delete in transaction on URI: " + uri);
|
|
|
|
final int match = URI_MATCHER.match(uri);
|
|
int deleted = 0;
|
|
|
|
switch (match) {
|
|
case BOOKMARKS_ID:
|
|
trace("Delete on BOOKMARKS_ID: " + uri);
|
|
|
|
selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?");
|
|
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
|
|
new String[] { Long.toString(ContentUris.parseId(uri)) });
|
|
// fall through
|
|
case BOOKMARKS: {
|
|
trace("Deleting bookmarks: " + uri);
|
|
deleted = deleteBookmarks(uri, selection, selectionArgs);
|
|
deleteUnusedImages(uri);
|
|
break;
|
|
}
|
|
|
|
case HISTORY_ID:
|
|
trace("Delete on HISTORY_ID: " + uri);
|
|
|
|
selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?");
|
|
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
|
|
new String[] { Long.toString(ContentUris.parseId(uri)) });
|
|
// fall through
|
|
case HISTORY: {
|
|
trace("Deleting history: " + uri);
|
|
deleted = deleteHistory(uri, selection, selectionArgs);
|
|
deleteUnusedImages(uri);
|
|
break;
|
|
}
|
|
|
|
case IMAGES_ID:
|
|
debug("Delete on IMAGES_ID: " + uri);
|
|
|
|
selection = DBUtils.concatenateWhere(selection, TABLE_IMAGES + "._id = ?");
|
|
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
|
|
new String[] { Long.toString(ContentUris.parseId(uri)) });
|
|
// fall through
|
|
case IMAGES: {
|
|
trace("Deleting images: " + uri);
|
|
deleted = deleteImages(uri, selection, selectionArgs);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
throw new UnsupportedOperationException("Unknown delete URI " + uri);
|
|
}
|
|
|
|
debug("Deleted " + deleted + " rows for URI: " + uri);
|
|
|
|
return deleted;
|
|
}
|
|
|
|
@Override
|
|
public Uri insert(Uri uri, ContentValues values) {
|
|
trace("Calling insert on URI: " + uri);
|
|
|
|
final SQLiteDatabase db = getWritableDatabase(uri);
|
|
Uri result = null;
|
|
|
|
if (Build.VERSION.SDK_INT >= 11) {
|
|
trace("Beginning insert transaction: " + uri);
|
|
db.beginTransaction();
|
|
try {
|
|
result = insertInTransaction(uri, values);
|
|
db.setTransactionSuccessful();
|
|
trace("Successful insert transaction: " + uri);
|
|
} finally {
|
|
db.endTransaction();
|
|
}
|
|
} else {
|
|
result = insertInTransaction(uri, values);
|
|
}
|
|
|
|
if (result != null)
|
|
getContext().getContentResolver().notifyChange(uri, null);
|
|
|
|
return result;
|
|
}
|
|
|
|
public Uri insertInTransaction(Uri uri, ContentValues values) {
|
|
trace("Calling insert in transaction on URI: " + uri);
|
|
|
|
final SQLiteDatabase db = getWritableDatabase(uri);
|
|
int match = URI_MATCHER.match(uri);
|
|
long id = -1;
|
|
|
|
switch (match) {
|
|
case BOOKMARKS: {
|
|
trace("Insert on BOOKMARKS: " + uri);
|
|
|
|
// Generate values if not specified. Don't overwrite
|
|
// if specified by caller.
|
|
long now = System.currentTimeMillis();
|
|
if (!values.containsKey(Bookmarks.DATE_CREATED)) {
|
|
values.put(Bookmarks.DATE_CREATED, now);
|
|
}
|
|
|
|
if (!values.containsKey(Bookmarks.DATE_MODIFIED)) {
|
|
values.put(Bookmarks.DATE_MODIFIED, now);
|
|
}
|
|
|
|
if (!values.containsKey(Bookmarks.GUID)) {
|
|
values.put(Bookmarks.GUID, Utils.generateGuid());
|
|
}
|
|
|
|
if (!values.containsKey(Bookmarks.POSITION)) {
|
|
debug("Inserting bookmark with no position for URI");
|
|
values.put(Bookmarks.POSITION, Long.toString(Long.MIN_VALUE));
|
|
}
|
|
|
|
String url = values.getAsString(Bookmarks.URL);
|
|
ContentValues imageValues = extractImageValues(values, url);
|
|
Integer isFolder = values.getAsInteger(Bookmarks.IS_FOLDER);
|
|
|
|
if ((isFolder == null || isFolder != 1) && imageValues != null
|
|
&& !TextUtils.isEmpty(url)) {
|
|
debug("Inserting bookmark image for URL: " + url);
|
|
updateOrInsertImage(uri, imageValues, Images.URL + " = ?",
|
|
new String[] { url });
|
|
}
|
|
|
|
debug("Inserting bookmark in database with URL: " + url);
|
|
id = db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.TITLE, values);
|
|
break;
|
|
}
|
|
|
|
case HISTORY: {
|
|
trace("Insert on HISTORY: " + uri);
|
|
|
|
long now = System.currentTimeMillis();
|
|
values.put(History.DATE_CREATED, now);
|
|
values.put(History.DATE_MODIFIED, now);
|
|
|
|
// Generate GUID for new history entry. Don't override specified GUIDs.
|
|
if (!values.containsKey(History.GUID)) {
|
|
values.put(History.GUID, Utils.generateGuid());
|
|
}
|
|
|
|
String url = values.getAsString(History.URL);
|
|
|
|
ContentValues imageValues = extractImageValues(values,
|
|
values.getAsString(History.URL));
|
|
|
|
if (imageValues != null) {
|
|
debug("Inserting history image for URL: " + url);
|
|
updateOrInsertImage(uri, imageValues, Images.URL + " = ?",
|
|
new String[] { url });
|
|
}
|
|
|
|
debug("Inserting history in database with URL: " + url);
|
|
id = db.insertOrThrow(TABLE_HISTORY, History.VISITS, values);
|
|
break;
|
|
}
|
|
|
|
case IMAGES: {
|
|
trace("Insert on IMAGES: " + uri);
|
|
|
|
long now = System.currentTimeMillis();
|
|
values.put(History.DATE_CREATED, now);
|
|
values.put(History.DATE_MODIFIED, now);
|
|
|
|
// Generate GUID for new history entry
|
|
values.put(History.GUID, Utils.generateGuid());
|
|
|
|
String url = values.getAsString(Images.URL);
|
|
|
|
debug("Inserting image in database with URL: " + url);
|
|
id = db.insertOrThrow(TABLE_IMAGES, Images.URL, values);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
throw new UnsupportedOperationException("Unknown insert URI " + uri);
|
|
}
|
|
|
|
debug("Inserted ID in database: " + id);
|
|
|
|
if (id >= 0)
|
|
return ContentUris.withAppendedId(uri, id);
|
|
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public int update(Uri uri, ContentValues values, String selection,
|
|
String[] selectionArgs) {
|
|
trace("Calling update on URI: " + uri);
|
|
|
|
final SQLiteDatabase db = getWritableDatabase(uri);
|
|
int updated = 0;
|
|
|
|
if (Build.VERSION.SDK_INT >= 11) {
|
|
trace("Beginning update transaction: " + uri);
|
|
db.beginTransaction();
|
|
try {
|
|
updated = updateInTransaction(uri, values, selection, selectionArgs);
|
|
db.setTransactionSuccessful();
|
|
trace("Successful update transaction: " + uri);
|
|
} finally {
|
|
db.endTransaction();
|
|
}
|
|
} else {
|
|
updated = updateInTransaction(uri, values, selection, selectionArgs);
|
|
}
|
|
|
|
if (updated > 0)
|
|
getContext().getContentResolver().notifyChange(uri, null);
|
|
|
|
return updated;
|
|
}
|
|
|
|
@SuppressWarnings("fallthrough")
|
|
public int updateInTransaction(Uri uri, ContentValues values, String selection,
|
|
String[] selectionArgs) {
|
|
trace("Calling update in transaction on URI: " + uri);
|
|
|
|
int match = URI_MATCHER.match(uri);
|
|
int updated = 0;
|
|
|
|
switch (match) {
|
|
case BOOKMARKS_PARENT: {
|
|
debug("Update on BOOKMARKS_PARENT: " + uri);
|
|
updated = updateBookmarkParents(uri, values, selection, selectionArgs);
|
|
break;
|
|
}
|
|
|
|
case BOOKMARKS_ID:
|
|
debug("Update on BOOKMARKS_ID: " + uri);
|
|
|
|
selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?");
|
|
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
|
|
new String[] { Long.toString(ContentUris.parseId(uri)) });
|
|
// fall through
|
|
case BOOKMARKS: {
|
|
debug("Updating bookmark: " + uri);
|
|
updated = updateBookmarks(uri, values, selection, selectionArgs);
|
|
break;
|
|
}
|
|
|
|
case HISTORY_ID:
|
|
debug("Update on HISTORY_ID: " + uri);
|
|
|
|
selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?");
|
|
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
|
|
new String[] { Long.toString(ContentUris.parseId(uri)) });
|
|
// fall through
|
|
case HISTORY: {
|
|
debug("Updating history: " + uri);
|
|
updated = updateHistory(uri, values, selection, selectionArgs);
|
|
break;
|
|
}
|
|
|
|
case IMAGES: {
|
|
debug("Update on IMAGES: " + uri);
|
|
|
|
String url = values.getAsString(Images.URL);
|
|
|
|
if (TextUtils.isEmpty(url))
|
|
throw new IllegalArgumentException("Images.URL is required");
|
|
|
|
updated = updateExistingImage(uri, values, Images.URL + " = ?",
|
|
new String[] { url });
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
throw new UnsupportedOperationException("Unknown update URI " + uri);
|
|
}
|
|
|
|
debug("Updated " + updated + " rows for URI: " + uri);
|
|
|
|
return updated;
|
|
}
|
|
|
|
@Override
|
|
public Cursor query(Uri uri, String[] projection, String selection,
|
|
String[] selectionArgs, String sortOrder) {
|
|
SQLiteDatabase db = getReadableDatabase(uri);
|
|
final int match = URI_MATCHER.match(uri);
|
|
|
|
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
|
|
String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
|
|
|
|
switch (match) {
|
|
case BOOKMARKS_FOLDER_ID:
|
|
case BOOKMARKS_ID:
|
|
case BOOKMARKS: {
|
|
debug("Query is on bookmarks: " + uri);
|
|
|
|
if (match == BOOKMARKS_ID) {
|
|
selection = DBUtils.concatenateWhere(selection, Bookmarks._ID + " = ?");
|
|
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
|
|
new String[] { Long.toString(ContentUris.parseId(uri)) });
|
|
} else if (match == BOOKMARKS_FOLDER_ID) {
|
|
selection = DBUtils.concatenateWhere(selection, Bookmarks.PARENT + " = ?");
|
|
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
|
|
new String[] { Long.toString(ContentUris.parseId(uri)) });
|
|
}
|
|
|
|
if (!shouldShowDeleted(uri))
|
|
selection = DBUtils.concatenateWhere(Bookmarks.IS_DELETED + " = 0", selection);
|
|
|
|
if (TextUtils.isEmpty(sortOrder)) {
|
|
sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER;
|
|
} else {
|
|
debug("Using sort order " + sortOrder + ".");
|
|
}
|
|
|
|
qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
|
|
|
|
if (hasImagesInProjection(projection))
|
|
qb.setTables(VIEW_BOOKMARKS_WITH_IMAGES);
|
|
else
|
|
qb.setTables(TABLE_BOOKMARKS);
|
|
|
|
break;
|
|
}
|
|
|
|
case HISTORY_ID:
|
|
case HISTORY: {
|
|
debug("Query is on history: " + uri);
|
|
|
|
if (match == HISTORY_ID) {
|
|
selection = DBUtils.concatenateWhere(selection, History._ID + " = ?");
|
|
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
|
|
new String[] { Long.toString(ContentUris.parseId(uri)) });
|
|
}
|
|
|
|
if (!shouldShowDeleted(uri))
|
|
selection = DBUtils.concatenateWhere(History.IS_DELETED + " = 0", selection);
|
|
|
|
if (TextUtils.isEmpty(sortOrder))
|
|
sortOrder = DEFAULT_HISTORY_SORT_ORDER;
|
|
|
|
qb.setProjectionMap(HISTORY_PROJECTION_MAP);
|
|
|
|
if (hasImagesInProjection(projection))
|
|
qb.setTables(VIEW_HISTORY_WITH_IMAGES);
|
|
else
|
|
qb.setTables(TABLE_HISTORY);
|
|
|
|
break;
|
|
}
|
|
|
|
case IMAGES_ID:
|
|
case IMAGES: {
|
|
debug("Query is on images: " + uri);
|
|
|
|
if (match == IMAGES_ID) {
|
|
selection = DBUtils.concatenateWhere(selection, Images._ID + " = ?");
|
|
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
|
|
new String[] { Long.toString(ContentUris.parseId(uri)) });
|
|
}
|
|
|
|
if (!shouldShowDeleted(uri))
|
|
selection = DBUtils.concatenateWhere(Images.IS_DELETED + " = 0", selection);
|
|
|
|
qb.setProjectionMap(IMAGES_PROJECTION_MAP);
|
|
qb.setTables(TABLE_IMAGES);
|
|
|
|
break;
|
|
}
|
|
|
|
case SCHEMA: {
|
|
debug("Query is on schema.");
|
|
MatrixCursor schemaCursor = new MatrixCursor(new String[] { Schema.VERSION });
|
|
schemaCursor.newRow().add(DATABASE_VERSION);
|
|
|
|
return schemaCursor;
|
|
}
|
|
|
|
default:
|
|
throw new UnsupportedOperationException("Unknown query URI " + uri);
|
|
}
|
|
|
|
trace("Running built query.");
|
|
Cursor cursor = qb.query(db, projection, selection, selectionArgs, null,
|
|
null, sortOrder, limit);
|
|
cursor.setNotificationUri(getContext().getContentResolver(),
|
|
BrowserContract.AUTHORITY_URI);
|
|
|
|
return cursor;
|
|
}
|
|
|
|
ContentValues extractImageValues(ContentValues values, String url) {
|
|
trace("Extracting image values for URI: " + url);
|
|
|
|
ContentValues imageValues = null;
|
|
|
|
if (values.containsKey(Bookmarks.FAVICON)) {
|
|
debug("Has favicon value on URL: " + url);
|
|
imageValues = new ContentValues();
|
|
imageValues.put(Images.FAVICON,
|
|
values.getAsByteArray(Bookmarks.FAVICON));
|
|
values.remove(Bookmarks.FAVICON);
|
|
}
|
|
|
|
if (values.containsKey(Bookmarks.THUMBNAIL)) {
|
|
debug("Has favicon value on URL: " + url);
|
|
if (imageValues == null)
|
|
imageValues = new ContentValues();
|
|
|
|
imageValues.put(Images.THUMBNAIL,
|
|
values.getAsByteArray(Bookmarks.THUMBNAIL));
|
|
values.remove(Bookmarks.THUMBNAIL);
|
|
}
|
|
|
|
if (imageValues != null && url != null) {
|
|
debug("Has URL value");
|
|
imageValues.put(Images.URL, url);
|
|
}
|
|
|
|
return imageValues;
|
|
}
|
|
|
|
int getUrlCount(SQLiteDatabase db, String table, String url) {
|
|
Cursor c = db.query(table, new String[] { "COUNT(*)" },
|
|
URLColumns.URL + " = ?", new String[] { url }, null, null,
|
|
null);
|
|
|
|
int count = 0;
|
|
|
|
try {
|
|
if (c.moveToFirst())
|
|
count = c.getInt(0);
|
|
} finally {
|
|
c.close();
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Construct an update expression that will modify the parents of any records
|
|
* that match.
|
|
*/
|
|
int updateBookmarkParents(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
|
trace("Updating bookmark parents of " + selection + " (" + selectionArgs[0] + ")");
|
|
String where = Bookmarks._ID + " IN (" +
|
|
" SELECT DISTINCT " + Bookmarks.PARENT +
|
|
" FROM " + TABLE_BOOKMARKS +
|
|
" WHERE " + selection + " )";
|
|
return getWritableDatabase(uri).update(TABLE_BOOKMARKS, values, where, selectionArgs);
|
|
}
|
|
|
|
int updateBookmarks(Uri uri, ContentValues values, String selection,
|
|
String[] selectionArgs) {
|
|
trace("Updating bookmarks on URI: " + uri);
|
|
|
|
final SQLiteDatabase db = getWritableDatabase(uri);
|
|
int updated = 0;
|
|
|
|
final String[] bookmarksProjection = new String[] {
|
|
Bookmarks._ID, // 0
|
|
Bookmarks.URL, // 1
|
|
};
|
|
|
|
trace("Quering bookmarks to update on URI: " + uri);
|
|
|
|
Cursor cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection,
|
|
selection, selectionArgs, null, null, null);
|
|
|
|
try {
|
|
if (!values.containsKey(Bookmarks.DATE_MODIFIED))
|
|
values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
|
|
|
|
boolean updatingUrl = values.containsKey(Bookmarks.URL);
|
|
String url = null;
|
|
|
|
if (updatingUrl)
|
|
url = values.getAsString(Bookmarks.URL);
|
|
|
|
ContentValues imageValues = extractImageValues(values, url);
|
|
|
|
while (cursor.moveToNext()) {
|
|
long id = cursor.getLong(0);
|
|
|
|
trace("Updating bookmark with ID: " + id);
|
|
|
|
updated += db.update(TABLE_BOOKMARKS, values, "_id = ?",
|
|
new String[] { Long.toString(id) });
|
|
|
|
if (imageValues == null)
|
|
continue;
|
|
|
|
if (!updatingUrl) {
|
|
url = cursor.getString(1);
|
|
imageValues.put(Images.URL, url);
|
|
}
|
|
|
|
if (!TextUtils.isEmpty(url)) {
|
|
trace("Updating bookmark image for URL: " + url);
|
|
updateOrInsertImage(uri, imageValues, Images.URL + " = ?",
|
|
new String[] { url });
|
|
}
|
|
}
|
|
} finally {
|
|
if (cursor != null)
|
|
cursor.close();
|
|
}
|
|
|
|
return updated;
|
|
}
|
|
|
|
int updateHistory(Uri uri, ContentValues values, String selection,
|
|
String[] selectionArgs) {
|
|
trace("Updating history on URI: " + uri);
|
|
|
|
final SQLiteDatabase db = getWritableDatabase(uri);
|
|
int updated = 0;
|
|
|
|
final String[] historyProjection = new String[] { History._ID, // 0
|
|
History.URL, // 1
|
|
};
|
|
|
|
Cursor cursor = db.query(TABLE_HISTORY, historyProjection, selection,
|
|
selectionArgs, null, null, null);
|
|
|
|
try {
|
|
values.put(History.DATE_MODIFIED, System.currentTimeMillis());
|
|
|
|
boolean updatingUrl = values.containsKey(History.URL);
|
|
String url = null;
|
|
|
|
if (updatingUrl)
|
|
url = values.getAsString(History.URL);
|
|
|
|
ContentValues imageValues = extractImageValues(values, url);
|
|
|
|
while (cursor.moveToNext()) {
|
|
long id = cursor.getLong(0);
|
|
|
|
trace("Updating history entry with ID: " + id);
|
|
|
|
updated += db.update(TABLE_HISTORY, values, "_id = ?",
|
|
new String[] { Long.toString(id) });
|
|
|
|
if (imageValues == null)
|
|
continue;
|
|
|
|
if (!updatingUrl) {
|
|
url = cursor.getString(1);
|
|
imageValues.put(Images.URL, url);
|
|
}
|
|
|
|
if (!TextUtils.isEmpty(url)) {
|
|
trace("Updating history image for URL: " + url);
|
|
updateOrInsertImage(uri, imageValues, Images.URL + " = ?",
|
|
new String[] { url });
|
|
}
|
|
}
|
|
} finally {
|
|
if (cursor != null)
|
|
cursor.close();
|
|
}
|
|
|
|
return updated;
|
|
}
|
|
|
|
int updateOrInsertImage(Uri uri, ContentValues values, String selection,
|
|
String[] selectionArgs) {
|
|
return updateImage(uri, values, selection, selectionArgs,
|
|
true /* insert if needed */);
|
|
}
|
|
|
|
int updateExistingImage(Uri uri, ContentValues values, String selection,
|
|
String[] selectionArgs) {
|
|
return updateImage(uri, values, selection, selectionArgs,
|
|
false /* only update, no insert */);
|
|
}
|
|
|
|
int updateImage(Uri uri, ContentValues values, String selection,
|
|
String[] selectionArgs, boolean insertIfNeeded) {
|
|
String url = values.getAsString(Images.URL);
|
|
|
|
trace("Updating image for URL: " + url);
|
|
|
|
final SQLiteDatabase db = getWritableDatabase(uri);
|
|
|
|
long now = System.currentTimeMillis();
|
|
|
|
// Thumbnails update on every page load. We don't want to flood
|
|
// sync with meaningless last modified date. Only update modified
|
|
// date when favicons bits change.
|
|
if (values.containsKey(Images.FAVICON) || values.containsKey(Images.FAVICON_URL))
|
|
values.put(Images.DATE_MODIFIED, now);
|
|
|
|
// Restore and update an existing image record marked as
|
|
// deleted if possible.
|
|
if (insertIfNeeded)
|
|
values.put(Images.IS_DELETED, 0);
|
|
|
|
debug("Trying to update image for URL: " + url);
|
|
int updated = db.update(TABLE_IMAGES, values, selection, selectionArgs);
|
|
|
|
if (updated == 0 && insertIfNeeded) {
|
|
// Generate GUID for new image, if one is not already provided.
|
|
if (!values.containsKey(Images.GUID)) {
|
|
values.put(Images.GUID, Utils.generateGuid());
|
|
}
|
|
|
|
values.put(Images.DATE_CREATED, now);
|
|
values.put(Images.DATE_MODIFIED, now);
|
|
|
|
trace("No update, inserting image for URL: " + url);
|
|
db.insert(TABLE_IMAGES, Images.FAVICON, values);
|
|
updated = 1;
|
|
}
|
|
|
|
return updated;
|
|
}
|
|
|
|
int deleteHistory(Uri uri, String selection, String[] selectionArgs) {
|
|
debug("Deleting history entry for URI: " + uri);
|
|
|
|
final SQLiteDatabase db = getWritableDatabase(uri);
|
|
|
|
if (isCallerSync(uri)) {
|
|
return db.delete(TABLE_HISTORY, selection, selectionArgs);
|
|
} else {
|
|
debug("Marking history entry as deleted for URI: " + uri);
|
|
|
|
ContentValues values = new ContentValues();
|
|
values.put(History.IS_DELETED, 1);
|
|
|
|
cleanupSomeDeletedRecords(uri, History.CONTENT_URI, TABLE_HISTORY);
|
|
return updateHistory(uri, values, selection, selectionArgs);
|
|
}
|
|
}
|
|
|
|
int deleteBookmarks(Uri uri, String selection, String[] selectionArgs) {
|
|
debug("Deleting bookmarks for URI: " + uri);
|
|
|
|
final SQLiteDatabase db = getWritableDatabase(uri);
|
|
|
|
if (isCallerSync(uri)) {
|
|
return db.delete(TABLE_BOOKMARKS, selection, selectionArgs);
|
|
} else {
|
|
debug("Marking bookmarks as deleted for URI: " + uri);
|
|
|
|
ContentValues values = new ContentValues();
|
|
values.put(Bookmarks.IS_DELETED, 1);
|
|
|
|
cleanupSomeDeletedRecords(uri, Bookmarks.CONTENT_URI, TABLE_BOOKMARKS);
|
|
return updateBookmarks(uri, values, selection, selectionArgs);
|
|
}
|
|
}
|
|
|
|
int deleteImages(Uri uri, String selection, String[] selectionArgs) {
|
|
debug("Deleting images for URI: " + uri);
|
|
|
|
final SQLiteDatabase db = getWritableDatabase(uri);
|
|
|
|
if (isCallerSync(uri)) {
|
|
return db.delete(TABLE_IMAGES, selection, null);
|
|
} else {
|
|
debug("Marking images as deleted for URI: " + uri);
|
|
|
|
ContentValues values = new ContentValues();
|
|
values.put(History.IS_DELETED, 1);
|
|
|
|
cleanupSomeDeletedRecords(uri, Images.CONTENT_URI, TABLE_IMAGES);
|
|
return updateExistingImage(uri, values, selection, null);
|
|
}
|
|
}
|
|
|
|
int deleteUnusedImages(Uri uri) {
|
|
debug("Deleting all unused images for URI: " + uri);
|
|
|
|
String selection = Images.URL + " NOT IN (SELECT " + Bookmarks.URL +
|
|
" FROM " + TABLE_BOOKMARKS + " WHERE " + Bookmarks.URL + " IS NOT NULL AND " +
|
|
qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0) AND " +
|
|
Images.URL + " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY +
|
|
" WHERE " + History.URL + " IS NOT NULL AND " +
|
|
qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0)";
|
|
|
|
return deleteImages(uri, selection, null);
|
|
}
|
|
}
|