Created
December 7, 2014 18:08
-
-
Save shaobin0604/a3d05eb5b08eaf3b4447 to your computer and use it in GitHub Desktop.
ContentProviders in Browser
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright (C) 2010 he 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.accounts.Account; | |
import android.accounts.AccountManager; | |
import android.app.SearchManager; | |
import android.content.ContentResolver; | |
import android.content.ContentUris; | |
import android.content.ContentValues; | |
import android.content.Context; | |
import android.content.Intent; | |
import android.content.UriMatcher; | |
import android.content.res.Resources; | |
import android.content.res.TypedArray; | |
import android.database.AbstractCursor; | |
import android.database.ContentObserver; | |
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.provider.BaseColumns; | |
import android.provider.Browser; | |
import android.provider.Browser.BookmarkColumns; | |
import android.provider.BrowserContract; | |
import android.provider.BrowserContract.Accounts; | |
import android.provider.BrowserContract.Bookmarks; | |
import android.provider.BrowserContract.ChromeSyncColumns; | |
import android.provider.BrowserContract.Combined; | |
import android.provider.BrowserContract.History; | |
import android.provider.BrowserContract.Images; | |
import android.provider.BrowserContract.Searches; | |
import android.provider.BrowserContract.Settings; | |
import android.provider.BrowserContract.SyncState; | |
import android.provider.ContactsContract.RawContacts; | |
import android.provider.SyncStateContract; | |
import android.text.TextUtils; | |
import com.android.browser.R; | |
import com.android.browser.UrlUtils; | |
import com.android.browser.widget.BookmarkThumbnailWidgetProvider; | |
import com.android.common.content.SyncStateContentProviderHelper; | |
import com.google.common.annotations.VisibleForTesting; | |
import java.io.ByteArrayOutputStream; | |
import java.io.File; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.HashMap; | |
import java.util.List; | |
public class BrowserProvider2 extends SQLiteContentProvider { | |
public static final String PARAM_GROUP_BY = "groupBy"; | |
public static final String PARAM_ALLOW_EMPTY_ACCOUNTS = "allowEmptyAccounts"; | |
public static final String LEGACY_AUTHORITY = "browser"; | |
static final Uri LEGACY_AUTHORITY_URI = new Uri.Builder() | |
.authority(LEGACY_AUTHORITY).scheme("content").build(); | |
public static interface Thumbnails { | |
public static final Uri CONTENT_URI = Uri.withAppendedPath( | |
BrowserContract.AUTHORITY_URI, "thumbnails"); | |
public static final String _ID = "_id"; | |
public static final String THUMBNAIL = "thumbnail"; | |
} | |
public static interface OmniboxSuggestions { | |
public static final Uri CONTENT_URI = Uri.withAppendedPath( | |
BrowserContract.AUTHORITY_URI, "omnibox_suggestions"); | |
public static final String _ID = "_id"; | |
public static final String URL = "url"; | |
public static final String TITLE = "title"; | |
public static final String IS_BOOKMARK = "bookmark"; | |
} | |
static final String TABLE_BOOKMARKS = "bookmarks"; | |
static final String TABLE_HISTORY = "history"; | |
static final String TABLE_IMAGES = "images"; | |
static final String TABLE_SEARCHES = "searches"; | |
static final String TABLE_SYNC_STATE = "syncstate"; | |
static final String TABLE_SETTINGS = "settings"; | |
static final String TABLE_SNAPSHOTS = "snapshots"; | |
static final String TABLE_THUMBNAILS = "thumbnails"; | |
static final String TABLE_BOOKMARKS_JOIN_IMAGES = "bookmarks LEFT OUTER JOIN images " + | |
"ON bookmarks.url = images." + Images.URL; | |
static final String TABLE_HISTORY_JOIN_IMAGES = "history LEFT OUTER JOIN images " + | |
"ON history.url = images." + Images.URL; | |
static final String VIEW_ACCOUNTS = "v_accounts"; | |
static final String VIEW_SNAPSHOTS_COMBINED = "v_snapshots_combined"; | |
static final String VIEW_OMNIBOX_SUGGESTIONS = "v_omnibox_suggestions"; | |
static final String FORMAT_COMBINED_JOIN_SUBQUERY_JOIN_IMAGES = | |
"history LEFT OUTER JOIN (%s) bookmarks " + | |
"ON history.url = bookmarks.url LEFT OUTER JOIN images " + | |
"ON history.url = images.url_key"; | |
static final String DEFAULT_SORT_HISTORY = History.DATE_LAST_VISITED + " DESC"; | |
static final String DEFAULT_SORT_ACCOUNTS = | |
Accounts.ACCOUNT_NAME + " IS NOT NULL DESC, " | |
+ Accounts.ACCOUNT_NAME + " ASC"; | |
private static final String TABLE_BOOKMARKS_JOIN_HISTORY = | |
"history LEFT OUTER JOIN bookmarks ON history.url = bookmarks.url"; | |
private static final String[] SUGGEST_PROJECTION = new String[] { | |
qualifyColumn(TABLE_HISTORY, History._ID), | |
qualifyColumn(TABLE_HISTORY, History.URL), | |
bookmarkOrHistoryColumn(Combined.TITLE), | |
bookmarkOrHistoryLiteral(Combined.URL, | |
Integer.toString(R.drawable.ic_bookmark_off_holo_dark), | |
Integer.toString(R.drawable.ic_history_holo_dark)), | |
qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED)}; | |
private static final String SUGGEST_SELECTION = | |
"history.url LIKE ? OR history.url LIKE ? OR history.url LIKE ? OR history.url LIKE ?" | |
+ " OR history.title LIKE ? OR bookmarks.title LIKE ?"; | |
private static final String ZERO_QUERY_SUGGEST_SELECTION = | |
TABLE_HISTORY + "." + History.DATE_LAST_VISITED + " != 0"; | |
private static final String IMAGE_PRUNE = | |
"url_key NOT IN (SELECT url FROM bookmarks " + | |
"WHERE url IS NOT NULL AND deleted == 0) AND url_key NOT IN " + | |
"(SELECT url FROM history WHERE url IS NOT NULL)"; | |
static final int THUMBNAILS = 10; | |
static final int THUMBNAILS_ID = 11; | |
static final int OMNIBOX_SUGGESTIONS = 20; | |
static final int BOOKMARKS = 1000; | |
static final int BOOKMARKS_ID = 1001; | |
static final int BOOKMARKS_FOLDER = 1002; | |
static final int BOOKMARKS_FOLDER_ID = 1003; | |
static final int BOOKMARKS_SUGGESTIONS = 1004; | |
static final int BOOKMARKS_DEFAULT_FOLDER_ID = 1005; | |
static final int HISTORY = 2000; | |
static final int HISTORY_ID = 2001; | |
static final int SEARCHES = 3000; | |
static final int SEARCHES_ID = 3001; | |
static final int SYNCSTATE = 4000; | |
static final int SYNCSTATE_ID = 4001; | |
static final int IMAGES = 5000; | |
static final int COMBINED = 6000; | |
static final int COMBINED_ID = 6001; | |
static final int ACCOUNTS = 7000; | |
static final int SETTINGS = 8000; | |
static final int LEGACY = 9000; | |
static final int LEGACY_ID = 9001; | |
public static final long FIXED_ID_ROOT = 1; | |
// Default sort order for unsync'd bookmarks | |
static final String DEFAULT_BOOKMARKS_SORT_ORDER = | |
Bookmarks.IS_FOLDER + " DESC, position ASC, _id ASC"; | |
// Default sort order for sync'd bookmarks | |
static final String DEFAULT_BOOKMARKS_SORT_ORDER_SYNC = "position ASC, _id ASC"; | |
static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); | |
static final HashMap<String, String> ACCOUNTS_PROJECTION_MAP = new HashMap<String, String>(); | |
static final HashMap<String, String> BOOKMARKS_PROJECTION_MAP = new HashMap<String, String>(); | |
static final HashMap<String, String> OTHER_BOOKMARKS_PROJECTION_MAP = | |
new HashMap<String, String>(); | |
static final HashMap<String, String> HISTORY_PROJECTION_MAP = new HashMap<String, String>(); | |
static final HashMap<String, String> SYNC_STATE_PROJECTION_MAP = new HashMap<String, String>(); | |
static final HashMap<String, String> IMAGES_PROJECTION_MAP = new HashMap<String, String>(); | |
static final HashMap<String, String> COMBINED_HISTORY_PROJECTION_MAP = new HashMap<String, String>(); | |
static final HashMap<String, String> COMBINED_BOOKMARK_PROJECTION_MAP = new HashMap<String, String>(); | |
static final HashMap<String, String> SEARCHES_PROJECTION_MAP = new HashMap<String, String>(); | |
static final HashMap<String, String> SETTINGS_PROJECTION_MAP = new HashMap<String, String>(); | |
static { | |
final UriMatcher matcher = URI_MATCHER; | |
final String authority = BrowserContract.AUTHORITY; | |
matcher.addURI(authority, "accounts", ACCOUNTS); | |
matcher.addURI(authority, "bookmarks", BOOKMARKS); | |
matcher.addURI(authority, "bookmarks/#", BOOKMARKS_ID); | |
matcher.addURI(authority, "bookmarks/folder", BOOKMARKS_FOLDER); | |
matcher.addURI(authority, "bookmarks/folder/#", BOOKMARKS_FOLDER_ID); | |
matcher.addURI(authority, "bookmarks/folder/id", BOOKMARKS_DEFAULT_FOLDER_ID); | |
matcher.addURI(authority, | |
SearchManager.SUGGEST_URI_PATH_QUERY, | |
BOOKMARKS_SUGGESTIONS); | |
matcher.addURI(authority, | |
"bookmarks/" + SearchManager.SUGGEST_URI_PATH_QUERY, | |
BOOKMARKS_SUGGESTIONS); | |
matcher.addURI(authority, "history", HISTORY); | |
matcher.addURI(authority, "history/#", HISTORY_ID); | |
matcher.addURI(authority, "searches", SEARCHES); | |
matcher.addURI(authority, "searches/#", SEARCHES_ID); | |
matcher.addURI(authority, "syncstate", SYNCSTATE); | |
matcher.addURI(authority, "syncstate/#", SYNCSTATE_ID); | |
matcher.addURI(authority, "images", IMAGES); | |
matcher.addURI(authority, "combined", COMBINED); | |
matcher.addURI(authority, "combined/#", COMBINED_ID); | |
matcher.addURI(authority, "settings", SETTINGS); | |
matcher.addURI(authority, "thumbnails", THUMBNAILS); | |
matcher.addURI(authority, "thumbnails/#", THUMBNAILS_ID); | |
matcher.addURI(authority, "omnibox_suggestions", OMNIBOX_SUGGESTIONS); | |
// Legacy | |
matcher.addURI(LEGACY_AUTHORITY, "searches", SEARCHES); | |
matcher.addURI(LEGACY_AUTHORITY, "searches/#", SEARCHES_ID); | |
matcher.addURI(LEGACY_AUTHORITY, "bookmarks", LEGACY); | |
matcher.addURI(LEGACY_AUTHORITY, "bookmarks/#", LEGACY_ID); | |
matcher.addURI(LEGACY_AUTHORITY, | |
SearchManager.SUGGEST_URI_PATH_QUERY, | |
BOOKMARKS_SUGGESTIONS); | |
matcher.addURI(LEGACY_AUTHORITY, | |
"bookmarks/" + SearchManager.SUGGEST_URI_PATH_QUERY, | |
BOOKMARKS_SUGGESTIONS); | |
// Projection maps | |
HashMap<String, String> map; | |
// Accounts | |
map = ACCOUNTS_PROJECTION_MAP; | |
map.put(Accounts.ACCOUNT_TYPE, Accounts.ACCOUNT_TYPE); | |
map.put(Accounts.ACCOUNT_NAME, Accounts.ACCOUNT_NAME); | |
map.put(Accounts.ROOT_ID, Accounts.ROOT_ID); | |
// Bookmarks | |
map = BOOKMARKS_PROJECTION_MAP; | |
map.put(Bookmarks._ID, qualifyColumn(TABLE_BOOKMARKS, 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.TOUCH_ICON, Bookmarks.TOUCH_ICON); | |
map.put(Bookmarks.IS_FOLDER, Bookmarks.IS_FOLDER); | |
map.put(Bookmarks.PARENT, Bookmarks.PARENT); | |
map.put(Bookmarks.POSITION, Bookmarks.POSITION); | |
map.put(Bookmarks.INSERT_AFTER, Bookmarks.INSERT_AFTER); | |
map.put(Bookmarks.IS_DELETED, Bookmarks.IS_DELETED); | |
map.put(Bookmarks.ACCOUNT_NAME, Bookmarks.ACCOUNT_NAME); | |
map.put(Bookmarks.ACCOUNT_TYPE, Bookmarks.ACCOUNT_TYPE); | |
map.put(Bookmarks.SOURCE_ID, Bookmarks.SOURCE_ID); | |
map.put(Bookmarks.VERSION, Bookmarks.VERSION); | |
map.put(Bookmarks.DATE_CREATED, Bookmarks.DATE_CREATED); | |
map.put(Bookmarks.DATE_MODIFIED, Bookmarks.DATE_MODIFIED); | |
map.put(Bookmarks.DIRTY, Bookmarks.DIRTY); | |
map.put(Bookmarks.SYNC1, Bookmarks.SYNC1); | |
map.put(Bookmarks.SYNC2, Bookmarks.SYNC2); | |
map.put(Bookmarks.SYNC3, Bookmarks.SYNC3); | |
map.put(Bookmarks.SYNC4, Bookmarks.SYNC4); | |
map.put(Bookmarks.SYNC5, Bookmarks.SYNC5); | |
map.put(Bookmarks.PARENT_SOURCE_ID, "(SELECT " + Bookmarks.SOURCE_ID + | |
" FROM " + TABLE_BOOKMARKS + " A WHERE " + | |
"A." + Bookmarks._ID + "=" + TABLE_BOOKMARKS + "." + Bookmarks.PARENT + | |
") AS " + Bookmarks.PARENT_SOURCE_ID); | |
map.put(Bookmarks.INSERT_AFTER_SOURCE_ID, "(SELECT " + Bookmarks.SOURCE_ID + | |
" FROM " + TABLE_BOOKMARKS + " A WHERE " + | |
"A." + Bookmarks._ID + "=" + TABLE_BOOKMARKS + "." + Bookmarks.INSERT_AFTER + | |
") AS " + Bookmarks.INSERT_AFTER_SOURCE_ID); | |
map.put(Bookmarks.TYPE, "CASE " | |
+ " WHEN " + Bookmarks.IS_FOLDER + "=0 THEN " | |
+ Bookmarks.BOOKMARK_TYPE_BOOKMARK | |
+ " WHEN " + ChromeSyncColumns.SERVER_UNIQUE + "='" | |
+ ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR + "' THEN " | |
+ Bookmarks.BOOKMARK_TYPE_BOOKMARK_BAR_FOLDER | |
+ " WHEN " + ChromeSyncColumns.SERVER_UNIQUE + "='" | |
+ ChromeSyncColumns.FOLDER_NAME_OTHER_BOOKMARKS + "' THEN " | |
+ Bookmarks.BOOKMARK_TYPE_OTHER_FOLDER | |
+ " ELSE " + Bookmarks.BOOKMARK_TYPE_FOLDER | |
+ " END AS " + Bookmarks.TYPE); | |
// Other bookmarks | |
OTHER_BOOKMARKS_PROJECTION_MAP.putAll(BOOKMARKS_PROJECTION_MAP); | |
OTHER_BOOKMARKS_PROJECTION_MAP.put(Bookmarks.POSITION, | |
Long.toString(Long.MAX_VALUE) + " AS " + Bookmarks.POSITION); | |
// History | |
map = HISTORY_PROJECTION_MAP; | |
map.put(History._ID, qualifyColumn(TABLE_HISTORY, 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.TOUCH_ICON, History.TOUCH_ICON); | |
map.put(History.DATE_CREATED, History.DATE_CREATED); | |
map.put(History.DATE_LAST_VISITED, History.DATE_LAST_VISITED); | |
map.put(History.VISITS, History.VISITS); | |
map.put(History.USER_ENTERED, History.USER_ENTERED); | |
// Sync state | |
map = SYNC_STATE_PROJECTION_MAP; | |
map.put(SyncState._ID, SyncState._ID); | |
map.put(SyncState.ACCOUNT_NAME, SyncState.ACCOUNT_NAME); | |
map.put(SyncState.ACCOUNT_TYPE, SyncState.ACCOUNT_TYPE); | |
map.put(SyncState.DATA, SyncState.DATA); | |
// Images | |
map = IMAGES_PROJECTION_MAP; | |
map.put(Images.URL, Images.URL); | |
map.put(Images.FAVICON, Images.FAVICON); | |
map.put(Images.THUMBNAIL, Images.THUMBNAIL); | |
map.put(Images.TOUCH_ICON, Images.TOUCH_ICON); | |
// Combined history half | |
map = COMBINED_HISTORY_PROJECTION_MAP; | |
map.put(Combined._ID, bookmarkOrHistoryColumn(Combined._ID)); | |
map.put(Combined.TITLE, bookmarkOrHistoryColumn(Combined.TITLE)); | |
map.put(Combined.URL, qualifyColumn(TABLE_HISTORY, Combined.URL)); | |
map.put(Combined.DATE_CREATED, qualifyColumn(TABLE_HISTORY, Combined.DATE_CREATED)); | |
map.put(Combined.DATE_LAST_VISITED, Combined.DATE_LAST_VISITED); | |
map.put(Combined.IS_BOOKMARK, "CASE WHEN " + | |
TABLE_BOOKMARKS + "." + Bookmarks._ID + | |
" IS NOT NULL THEN 1 ELSE 0 END AS " + Combined.IS_BOOKMARK); | |
map.put(Combined.VISITS, Combined.VISITS); | |
map.put(Combined.FAVICON, Combined.FAVICON); | |
map.put(Combined.THUMBNAIL, Combined.THUMBNAIL); | |
map.put(Combined.TOUCH_ICON, Combined.TOUCH_ICON); | |
map.put(Combined.USER_ENTERED, "NULL AS " + Combined.USER_ENTERED); | |
// Combined bookmark half | |
map = COMBINED_BOOKMARK_PROJECTION_MAP; | |
map.put(Combined._ID, Combined._ID); | |
map.put(Combined.TITLE, Combined.TITLE); | |
map.put(Combined.URL, Combined.URL); | |
map.put(Combined.DATE_CREATED, Combined.DATE_CREATED); | |
map.put(Combined.DATE_LAST_VISITED, "NULL AS " + Combined.DATE_LAST_VISITED); | |
map.put(Combined.IS_BOOKMARK, "1 AS " + Combined.IS_BOOKMARK); | |
map.put(Combined.VISITS, "0 AS " + Combined.VISITS); | |
map.put(Combined.FAVICON, Combined.FAVICON); | |
map.put(Combined.THUMBNAIL, Combined.THUMBNAIL); | |
map.put(Combined.TOUCH_ICON, Combined.TOUCH_ICON); | |
map.put(Combined.USER_ENTERED, "NULL AS " + Combined.USER_ENTERED); | |
// Searches | |
map = SEARCHES_PROJECTION_MAP; | |
map.put(Searches._ID, Searches._ID); | |
map.put(Searches.SEARCH, Searches.SEARCH); | |
map.put(Searches.DATE, Searches.DATE); | |
// Settings | |
map = SETTINGS_PROJECTION_MAP; | |
map.put(Settings.KEY, Settings.KEY); | |
map.put(Settings.VALUE, Settings.VALUE); | |
} | |
static final String bookmarkOrHistoryColumn(String column) { | |
return "CASE WHEN bookmarks." + column + " IS NOT NULL THEN " + | |
"bookmarks." + column + " ELSE history." + column + " END AS " + column; | |
} | |
static final String bookmarkOrHistoryLiteral(String column, String bookmarkValue, | |
String historyValue) { | |
return "CASE WHEN bookmarks." + column + " IS NOT NULL THEN \"" + bookmarkValue + | |
"\" ELSE \"" + historyValue + "\" END"; | |
} | |
static final String qualifyColumn(String table, String column) { | |
return table + "." + column + " AS " + column; | |
} | |
DatabaseHelper mOpenHelper; | |
SyncStateContentProviderHelper mSyncHelper = new SyncStateContentProviderHelper(); | |
// This is so provider tests can intercept widget updating | |
ContentObserver mWidgetObserver = null; | |
boolean mUpdateWidgets = false; | |
boolean mSyncToNetwork = true; | |
final class DatabaseHelper extends SQLiteOpenHelper { | |
static final String DATABASE_NAME = "browser2.db"; | |
static final int DATABASE_VERSION = 32; | |
public DatabaseHelper(Context context) { | |
super(context, DATABASE_NAME, null, DATABASE_VERSION); | |
setWriteAheadLoggingEnabled(true); | |
} | |
@Override | |
public void onCreate(SQLiteDatabase db) { | |
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.INSERT_AFTER + " INTEGER," + | |
Bookmarks.IS_DELETED + " INTEGER NOT NULL DEFAULT 0," + | |
Bookmarks.ACCOUNT_NAME + " TEXT," + | |
Bookmarks.ACCOUNT_TYPE + " TEXT," + | |
Bookmarks.SOURCE_ID + " TEXT," + | |
Bookmarks.VERSION + " INTEGER NOT NULL DEFAULT 1," + | |
Bookmarks.DATE_CREATED + " INTEGER," + | |
Bookmarks.DATE_MODIFIED + " INTEGER," + | |
Bookmarks.DIRTY + " INTEGER NOT NULL DEFAULT 0," + | |
Bookmarks.SYNC1 + " TEXT," + | |
Bookmarks.SYNC2 + " TEXT," + | |
Bookmarks.SYNC3 + " TEXT," + | |
Bookmarks.SYNC4 + " TEXT," + | |
Bookmarks.SYNC5 + " TEXT" + | |
");"); | |
// TODO indices | |
db.execSQL("CREATE TABLE " + TABLE_HISTORY + "(" + | |
History._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + | |
History.TITLE + " TEXT," + | |
History.URL + " TEXT NOT NULL," + | |
History.DATE_CREATED + " INTEGER," + | |
History.DATE_LAST_VISITED + " INTEGER," + | |
History.VISITS + " INTEGER NOT NULL DEFAULT 0," + | |
History.USER_ENTERED + " INTEGER" + | |
");"); | |
db.execSQL("CREATE TABLE " + TABLE_IMAGES + " (" + | |
Images.URL + " TEXT UNIQUE NOT NULL," + | |
Images.FAVICON + " BLOB," + | |
Images.THUMBNAIL + " BLOB," + | |
Images.TOUCH_ICON + " BLOB" + | |
");"); | |
db.execSQL("CREATE INDEX imagesUrlIndex ON " + TABLE_IMAGES + | |
"(" + Images.URL + ")"); | |
db.execSQL("CREATE TABLE " + TABLE_SEARCHES + " (" + | |
Searches._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + | |
Searches.SEARCH + " TEXT," + | |
Searches.DATE + " LONG" + | |
");"); | |
db.execSQL("CREATE TABLE " + TABLE_SETTINGS + " (" + | |
Settings.KEY + " TEXT PRIMARY KEY," + | |
Settings.VALUE + " TEXT NOT NULL" + | |
");"); | |
createAccountsView(db); | |
createThumbnails(db); | |
mSyncHelper.createDatabase(db); | |
if (!importFromBrowserProvider(db)) { | |
createDefaultBookmarks(db); | |
} | |
enableSync(db); | |
createOmniboxSuggestions(db); | |
} | |
void createOmniboxSuggestions(SQLiteDatabase db) { | |
db.execSQL(SQL_CREATE_VIEW_OMNIBOX_SUGGESTIONS); | |
} | |
void createThumbnails(SQLiteDatabase db) { | |
db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_THUMBNAILS + " (" + | |
Thumbnails._ID + " INTEGER PRIMARY KEY," + | |
Thumbnails.THUMBNAIL + " BLOB NOT NULL" + | |
");"); | |
} | |
void enableSync(SQLiteDatabase db) { | |
ContentValues values = new ContentValues(); | |
values.put(Settings.KEY, Settings.KEY_SYNC_ENABLED); | |
values.put(Settings.VALUE, 1); | |
insertSettingsInTransaction(db, values); | |
// Enable bookmark sync on all accounts | |
AccountManager am = (AccountManager) getContext().getSystemService( | |
Context.ACCOUNT_SERVICE); | |
if (am == null) { | |
return; | |
} | |
Account[] accounts = am.getAccountsByType("com.google"); | |
if (accounts == null || accounts.length == 0) { | |
return; | |
} | |
for (Account account : accounts) { | |
if (ContentResolver.getIsSyncable( | |
account, BrowserContract.AUTHORITY) == 0) { | |
// Account wasn't syncable, enable it | |
ContentResolver.setIsSyncable( | |
account, BrowserContract.AUTHORITY, 1); | |
ContentResolver.setSyncAutomatically( | |
account, BrowserContract.AUTHORITY, true); | |
} | |
} | |
} | |
boolean importFromBrowserProvider(SQLiteDatabase db) { | |
Context context = getContext(); | |
File oldDbFile = context.getDatabasePath(BrowserProvider.sDatabaseName); | |
if (oldDbFile.exists()) { | |
BrowserProvider.DatabaseHelper helper = | |
new BrowserProvider.DatabaseHelper(context); | |
SQLiteDatabase oldDb = helper.getWritableDatabase(); | |
Cursor c = null; | |
try { | |
String table = BrowserProvider.TABLE_NAMES[BrowserProvider.URI_MATCH_BOOKMARKS]; | |
// Import bookmarks | |
c = oldDb.query(table, | |
new String[] { | |
BookmarkColumns.URL, // 0 | |
BookmarkColumns.TITLE, // 1 | |
BookmarkColumns.FAVICON, // 2 | |
BookmarkColumns.TOUCH_ICON, // 3 | |
BookmarkColumns.CREATED, // 4 | |
}, BookmarkColumns.BOOKMARK + "!=0", null, | |
null, null, null); | |
if (c != null) { | |
while (c.moveToNext()) { | |
String url = c.getString(0); | |
if (TextUtils.isEmpty(url)) | |
continue; // We require a valid URL | |
ContentValues values = new ContentValues(); | |
values.put(Bookmarks.URL, url); | |
values.put(Bookmarks.TITLE, c.getString(1)); | |
values.put(Bookmarks.DATE_CREATED, c.getInt(4)); | |
values.put(Bookmarks.POSITION, 0); | |
values.put(Bookmarks.PARENT, FIXED_ID_ROOT); | |
ContentValues imageValues = new ContentValues(); | |
imageValues.put(Images.URL, url); | |
imageValues.put(Images.FAVICON, c.getBlob(2)); | |
imageValues.put(Images.TOUCH_ICON, c.getBlob(3)); | |
db.insert(TABLE_IMAGES, Images.THUMBNAIL, imageValues); | |
db.insert(TABLE_BOOKMARKS, Bookmarks.DIRTY, values); | |
} | |
c.close(); | |
} | |
// Import history | |
c = oldDb.query(table, | |
new String[] { | |
BookmarkColumns.URL, // 0 | |
BookmarkColumns.TITLE, // 1 | |
BookmarkColumns.VISITS, // 2 | |
BookmarkColumns.DATE, // 3 | |
BookmarkColumns.CREATED, // 4 | |
}, BookmarkColumns.VISITS + " > 0 OR " | |
+ BookmarkColumns.BOOKMARK + " = 0", | |
null, null, null, null); | |
if (c != null) { | |
while (c.moveToNext()) { | |
ContentValues values = new ContentValues(); | |
String url = c.getString(0); | |
if (TextUtils.isEmpty(url)) | |
continue; // We require a valid URL | |
values.put(History.URL, url); | |
values.put(History.TITLE, c.getString(1)); | |
values.put(History.VISITS, c.getInt(2)); | |
values.put(History.DATE_LAST_VISITED, c.getLong(3)); | |
values.put(History.DATE_CREATED, c.getLong(4)); | |
db.insert(TABLE_HISTORY, History.FAVICON, values); | |
} | |
c.close(); | |
} | |
// Wipe the old DB, in case the delete fails. | |
oldDb.delete(table, null, null); | |
} finally { | |
if (c != null) c.close(); | |
oldDb.close(); | |
helper.close(); | |
} | |
if (!oldDbFile.delete()) { | |
oldDbFile.deleteOnExit(); | |
} | |
return true; | |
} | |
return false; | |
} | |
void createAccountsView(SQLiteDatabase db) { | |
db.execSQL("CREATE VIEW IF NOT EXISTS v_accounts AS " | |
+ "SELECT NULL AS " + Accounts.ACCOUNT_NAME | |
+ ", NULL AS " + Accounts.ACCOUNT_TYPE | |
+ ", " + FIXED_ID_ROOT + " AS " + Accounts.ROOT_ID | |
+ " UNION ALL SELECT " + Accounts.ACCOUNT_NAME | |
+ ", " + Accounts.ACCOUNT_TYPE + ", " | |
+ Bookmarks._ID + " AS " + Accounts.ROOT_ID | |
+ " FROM " + TABLE_BOOKMARKS + " WHERE " | |
+ ChromeSyncColumns.SERVER_UNIQUE + " = \"" | |
+ ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR + "\" AND " | |
+ Bookmarks.IS_DELETED + " = 0"); | |
} | |
@Override | |
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { | |
if (oldVersion < 32) { | |
createOmniboxSuggestions(db); | |
} | |
if (oldVersion < 31) { | |
createThumbnails(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); | |
} | |
if (oldVersion < 27) { | |
createAccountsView(db); | |
} | |
if (oldVersion < 26) { | |
db.execSQL("DROP VIEW IF EXISTS combined"); | |
} | |
if (oldVersion < 25) { | |
db.execSQL("DROP TABLE IF EXISTS " + TABLE_BOOKMARKS); | |
db.execSQL("DROP TABLE IF EXISTS " + TABLE_HISTORY); | |
db.execSQL("DROP TABLE IF EXISTS " + TABLE_SEARCHES); | |
db.execSQL("DROP TABLE IF EXISTS " + TABLE_IMAGES); | |
db.execSQL("DROP TABLE IF EXISTS " + TABLE_SETTINGS); | |
mSyncHelper.onAccountsChanged(db, new Account[] {}); // remove all sync info | |
onCreate(db); | |
} | |
} | |
public void onOpen(SQLiteDatabase db) { | |
mSyncHelper.onDatabaseOpened(db); | |
} | |
private void createDefaultBookmarks(SQLiteDatabase db) { | |
ContentValues values = new ContentValues(); | |
// TODO figure out how to deal with localization for the defaults | |
// Bookmarks folder | |
values.put(Bookmarks._ID, FIXED_ID_ROOT); | |
values.put(ChromeSyncColumns.SERVER_UNIQUE, ChromeSyncColumns.FOLDER_NAME_BOOKMARKS); | |
values.put(Bookmarks.TITLE, "Bookmarks"); | |
values.putNull(Bookmarks.PARENT); | |
values.put(Bookmarks.POSITION, 0); | |
values.put(Bookmarks.IS_FOLDER, true); | |
values.put(Bookmarks.DIRTY, true); | |
db.insertOrThrow(TABLE_BOOKMARKS, null, values); | |
addDefaultBookmarks(db, FIXED_ID_ROOT); | |
} | |
private void addDefaultBookmarks(SQLiteDatabase db, long parentId) { | |
Resources res = getContext().getResources(); | |
final CharSequence[] bookmarks = res.getTextArray( | |
R.array.bookmarks); | |
int size = bookmarks.length; | |
TypedArray preloads = res.obtainTypedArray(R.array.bookmark_preloads); | |
try { | |
String parent = Long.toString(parentId); | |
String now = Long.toString(System.currentTimeMillis()); | |
for (int i = 0; i < size; i = i + 2) { | |
CharSequence bookmarkDestination = replaceSystemPropertyInString(getContext(), | |
bookmarks[i + 1]); | |
db.execSQL("INSERT INTO bookmarks (" + | |
Bookmarks.TITLE + ", " + | |
Bookmarks.URL + ", " + | |
Bookmarks.IS_FOLDER + "," + | |
Bookmarks.PARENT + "," + | |
Bookmarks.POSITION + "," + | |
Bookmarks.DATE_CREATED + | |
") VALUES (" + | |
"'" + bookmarks[i] + "', " + | |
"'" + bookmarkDestination + "', " + | |
"0," + | |
parent + "," + | |
Integer.toString(i) + "," + | |
now + | |
");"); | |
int faviconId = preloads.getResourceId(i, 0); | |
int thumbId = preloads.getResourceId(i + 1, 0); | |
byte[] thumb = null, favicon = null; | |
try { | |
thumb = readRaw(res, thumbId); | |
} catch (IOException e) { | |
} | |
try { | |
favicon = readRaw(res, faviconId); | |
} catch (IOException e) { | |
} | |
if (thumb != null || favicon != null) { | |
ContentValues imageValues = new ContentValues(); | |
imageValues.put(Images.URL, bookmarkDestination.toString()); | |
if (favicon != null) { | |
imageValues.put(Images.FAVICON, favicon); | |
} | |
if (thumb != null) { | |
imageValues.put(Images.THUMBNAIL, thumb); | |
} | |
db.insert(TABLE_IMAGES, Images.FAVICON, imageValues); | |
} | |
} | |
} catch (ArrayIndexOutOfBoundsException e) { | |
} finally { | |
preloads.recycle(); | |
} | |
} | |
private byte[] readRaw(Resources res, int id) throws IOException { | |
if (id == 0) { | |
return null; | |
} | |
InputStream is = res.openRawResource(id); | |
try { | |
ByteArrayOutputStream bos = new ByteArrayOutputStream(); | |
byte[] buf = new byte[4096]; | |
int read; | |
while ((read = is.read(buf)) > 0) { | |
bos.write(buf, 0, read); | |
} | |
bos.flush(); | |
return bos.toByteArray(); | |
} finally { | |
is.close(); | |
} | |
} | |
// XXX: This is a major hack to remove our dependency on gsf constants and | |
// its content provider. http://b/issue?id=2425179 | |
private String getClientId(ContentResolver cr) { | |
String ret = "android-google"; | |
Cursor c = null; | |
try { | |
c = cr.query(Uri.parse("content://com.google.settings/partner"), | |
new String[] { "value" }, "name='client_id'", null, null); | |
if (c != null && c.moveToNext()) { | |
ret = c.getString(0); | |
} | |
} catch (RuntimeException ex) { | |
// fall through to return the default | |
} finally { | |
if (c != null) { | |
c.close(); | |
} | |
} | |
return ret; | |
} | |
private CharSequence replaceSystemPropertyInString(Context context, CharSequence srcString) { | |
StringBuffer sb = new StringBuffer(); | |
int lastCharLoc = 0; | |
final String client_id = getClientId(context.getContentResolver()); | |
for (int i = 0; i < srcString.length(); ++i) { | |
char c = srcString.charAt(i); | |
if (c == '{') { | |
sb.append(srcString.subSequence(lastCharLoc, i)); | |
lastCharLoc = i; | |
inner: | |
for (int j = i; j < srcString.length(); ++j) { | |
char k = srcString.charAt(j); | |
if (k == '}') { | |
String propertyKeyValue = srcString.subSequence(i + 1, j).toString(); | |
if (propertyKeyValue.equals("CLIENT_ID")) { | |
sb.append(client_id); | |
} else { | |
sb.append("unknown"); | |
} | |
lastCharLoc = j + 1; | |
i = j; | |
break inner; | |
} | |
} | |
} | |
} | |
if (srcString.length() - lastCharLoc > 0) { | |
// Put on the tail, if there is one | |
sb.append(srcString.subSequence(lastCharLoc, srcString.length())); | |
} | |
return sb; | |
} | |
} | |
@Override | |
public SQLiteOpenHelper getDatabaseHelper(Context context) { | |
synchronized (this) { | |
if (mOpenHelper == null) { | |
mOpenHelper = new DatabaseHelper(context); | |
} | |
return mOpenHelper; | |
} | |
} | |
@Override | |
public boolean isCallerSyncAdapter(Uri uri) { | |
return uri.getBooleanQueryParameter(BrowserContract.CALLER_IS_SYNCADAPTER, false); | |
} | |
@VisibleForTesting | |
public void setWidgetObserver(ContentObserver obs) { | |
mWidgetObserver = obs; | |
} | |
void refreshWidgets() { | |
mUpdateWidgets = true; | |
} | |
@Override | |
protected void onEndTransaction(boolean callerIsSyncAdapter) { | |
super.onEndTransaction(callerIsSyncAdapter); | |
if (mUpdateWidgets) { | |
if (mWidgetObserver == null) { | |
BookmarkThumbnailWidgetProvider.refreshWidgets(getContext()); | |
} else { | |
mWidgetObserver.dispatchChange(false); | |
} | |
mUpdateWidgets = false; | |
} | |
mSyncToNetwork = true; | |
} | |
@Override | |
public String getType(Uri uri) { | |
final int match = URI_MATCHER.match(uri); | |
switch (match) { | |
case LEGACY: | |
case BOOKMARKS: | |
return Bookmarks.CONTENT_TYPE; | |
case LEGACY_ID: | |
case BOOKMARKS_ID: | |
return Bookmarks.CONTENT_ITEM_TYPE; | |
case HISTORY: | |
return History.CONTENT_TYPE; | |
case HISTORY_ID: | |
return History.CONTENT_ITEM_TYPE; | |
case SEARCHES: | |
return Searches.CONTENT_TYPE; | |
case SEARCHES_ID: | |
return Searches.CONTENT_ITEM_TYPE; | |
} | |
return null; | |
} | |
boolean isNullAccount(String account) { | |
if (account == null) return true; | |
account = account.trim(); | |
return account.length() == 0 || account.equals("null"); | |
} | |
Object[] getSelectionWithAccounts(Uri uri, String selection, String[] selectionArgs) { | |
// Look for account info | |
String accountType = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE); | |
String accountName = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME); | |
boolean hasAccounts = false; | |
if (accountType != null && accountName != null) { | |
if (!isNullAccount(accountType) && !isNullAccount(accountName)) { | |
selection = DatabaseUtils.concatenateWhere(selection, | |
Bookmarks.ACCOUNT_TYPE + "=? AND " + Bookmarks.ACCOUNT_NAME + "=? "); | |
selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, | |
new String[] { accountType, accountName }); | |
hasAccounts = true; | |
} else { | |
selection = DatabaseUtils.concatenateWhere(selection, | |
Bookmarks.ACCOUNT_NAME + " IS NULL AND " + | |
Bookmarks.ACCOUNT_TYPE + " IS NULL"); | |
} | |
} | |
return new Object[] { selection, selectionArgs, hasAccounts }; | |
} | |
@Override | |
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, | |
String sortOrder) { | |
SQLiteDatabase db = mOpenHelper.getReadableDatabase(); | |
final int match = URI_MATCHER.match(uri); | |
SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); | |
String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT); | |
String groupBy = uri.getQueryParameter(PARAM_GROUP_BY); | |
switch (match) { | |
case ACCOUNTS: { | |
qb.setTables(VIEW_ACCOUNTS); | |
qb.setProjectionMap(ACCOUNTS_PROJECTION_MAP); | |
String allowEmpty = uri.getQueryParameter(PARAM_ALLOW_EMPTY_ACCOUNTS); | |
if ("false".equals(allowEmpty)) { | |
selection = DatabaseUtils.concatenateWhere(selection, | |
SQL_WHERE_ACCOUNT_HAS_BOOKMARKS); | |
} | |
if (sortOrder == null) { | |
sortOrder = DEFAULT_SORT_ACCOUNTS; | |
} | |
break; | |
} | |
case BOOKMARKS_FOLDER_ID: | |
case BOOKMARKS_ID: | |
case BOOKMARKS: { | |
// Only show deleted bookmarks if requested to do so | |
if (!uri.getBooleanQueryParameter(Bookmarks.QUERY_PARAMETER_SHOW_DELETED, false)) { | |
selection = DatabaseUtils.concatenateWhere( | |
Bookmarks.IS_DELETED + "=0", selection); | |
} | |
if (match == BOOKMARKS_ID) { | |
// Tack on the ID of the specific bookmark requested | |
selection = DatabaseUtils.concatenateWhere(selection, | |
TABLE_BOOKMARKS + "." + Bookmarks._ID + "=?"); | |
selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, | |
new String[] { Long.toString(ContentUris.parseId(uri)) }); | |
} else if (match == BOOKMARKS_FOLDER_ID) { | |
// Tack on the ID of the specific folder requested | |
selection = DatabaseUtils.concatenateWhere(selection, | |
TABLE_BOOKMARKS + "." + Bookmarks.PARENT + "=?"); | |
selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, | |
new String[] { Long.toString(ContentUris.parseId(uri)) }); | |
} | |
Object[] withAccount = getSelectionWithAccounts(uri, selection, selectionArgs); | |
selection = (String) withAccount[0]; | |
selectionArgs = (String[]) withAccount[1]; | |
boolean hasAccounts = (Boolean) withAccount[2]; | |
// Set a default sort order if one isn't specified | |
if (TextUtils.isEmpty(sortOrder)) { | |
if (hasAccounts) { | |
sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER_SYNC; | |
} else { | |
sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER; | |
} | |
} | |
qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP); | |
qb.setTables(TABLE_BOOKMARKS_JOIN_IMAGES); | |
break; | |
} | |
case BOOKMARKS_FOLDER: { | |
// Look for an account | |
boolean useAccount = false; | |
String accountType = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE); | |
String accountName = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME); | |
if (!isNullAccount(accountType) && !isNullAccount(accountName)) { | |
useAccount = true; | |
} | |
qb.setTables(TABLE_BOOKMARKS_JOIN_IMAGES); | |
String[] args; | |
String query; | |
// Set a default sort order if one isn't specified | |
if (TextUtils.isEmpty(sortOrder)) { | |
if (useAccount) { | |
sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER_SYNC; | |
} else { | |
sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER; | |
} | |
} | |
if (!useAccount) { | |
qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP); | |
String where = Bookmarks.PARENT + "=? AND " + Bookmarks.IS_DELETED + "=0"; | |
where = DatabaseUtils.concatenateWhere(where, selection); | |
args = new String[] { Long.toString(FIXED_ID_ROOT) }; | |
if (selectionArgs != null) { | |
args = DatabaseUtils.appendSelectionArgs(args, selectionArgs); | |
} | |
query = qb.buildQuery(projection, where, null, null, sortOrder, null); | |
} else { | |
qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP); | |
String where = Bookmarks.ACCOUNT_TYPE + "=? AND " + | |
Bookmarks.ACCOUNT_NAME + "=? " + | |
"AND parent = " + | |
"(SELECT _id FROM " + TABLE_BOOKMARKS + " WHERE " + | |
ChromeSyncColumns.SERVER_UNIQUE + "=" + | |
"'" + ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR + "' " + | |
"AND account_type = ? AND account_name = ?) " + | |
"AND " + Bookmarks.IS_DELETED + "=0"; | |
where = DatabaseUtils.concatenateWhere(where, selection); | |
String bookmarksBarQuery = qb.buildQuery(projection, | |
where, null, null, null, null); | |
args = new String[] {accountType, accountName, | |
accountType, accountName}; | |
if (selectionArgs != null) { | |
args = DatabaseUtils.appendSelectionArgs(args, selectionArgs); | |
} | |
where = Bookmarks.ACCOUNT_TYPE + "=? AND " + Bookmarks.ACCOUNT_NAME + "=?" + | |
" AND " + ChromeSyncColumns.SERVER_UNIQUE + "=?"; | |
where = DatabaseUtils.concatenateWhere(where, selection); | |
qb.setProjectionMap(OTHER_BOOKMARKS_PROJECTION_MAP); | |
String otherBookmarksQuery = qb.buildQuery(projection, | |
where, null, null, null, null); | |
query = qb.buildUnionQuery( | |
new String[] { bookmarksBarQuery, otherBookmarksQuery }, | |
sortOrder, limit); | |
args = DatabaseUtils.appendSelectionArgs(args, new String[] { | |
accountType, accountName, ChromeSyncColumns.FOLDER_NAME_OTHER_BOOKMARKS, | |
}); | |
if (selectionArgs != null) { | |
args = DatabaseUtils.appendSelectionArgs(args, selectionArgs); | |
} | |
} | |
Cursor cursor = db.rawQuery(query, args); | |
if (cursor != null) { | |
cursor.setNotificationUri(getContext().getContentResolver(), | |
BrowserContract.AUTHORITY_URI); | |
} | |
return cursor; | |
} | |
case BOOKMARKS_DEFAULT_FOLDER_ID: { | |
String accountName = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME); | |
String accountType = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE); | |
long id = queryDefaultFolderId(accountName, accountType); | |
MatrixCursor c = new MatrixCursor(new String[] {Bookmarks._ID}); | |
c.newRow().add(id); | |
return c; | |
} | |
case BOOKMARKS_SUGGESTIONS: { | |
return doSuggestQuery(selection, selectionArgs, limit); | |
} | |
case HISTORY_ID: { | |
selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?"); | |
selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, | |
new String[] { Long.toString(ContentUris.parseId(uri)) }); | |
// fall through | |
} | |
case HISTORY: { | |
filterSearchClient(selectionArgs); | |
if (sortOrder == null) { | |
sortOrder = DEFAULT_SORT_HISTORY; | |
} | |
qb.setProjectionMap(HISTORY_PROJECTION_MAP); | |
qb.setTables(TABLE_HISTORY_JOIN_IMAGES); | |
break; | |
} | |
case SEARCHES_ID: { | |
selection = DatabaseUtils.concatenateWhere(selection, TABLE_SEARCHES + "._id=?"); | |
selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, | |
new String[] { Long.toString(ContentUris.parseId(uri)) }); | |
// fall through | |
} | |
case SEARCHES: { | |
qb.setTables(TABLE_SEARCHES); | |
qb.setProjectionMap(SEARCHES_PROJECTION_MAP); | |
break; | |
} | |
case SYNCSTATE: { | |
return mSyncHelper.query(db, projection, selection, selectionArgs, sortOrder); | |
} | |
case SYNCSTATE_ID: { | |
selection = appendAccountToSelection(uri, selection); | |
String selectionWithId = | |
(SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") | |
+ (selection == null ? "" : " AND (" + selection + ")"); | |
return mSyncHelper.query(db, projection, selectionWithId, selectionArgs, sortOrder); | |
} | |
case IMAGES: { | |
qb.setTables(TABLE_IMAGES); | |
qb.setProjectionMap(IMAGES_PROJECTION_MAP); | |
break; | |
} | |
case LEGACY_ID: | |
case COMBINED_ID: { | |
selection = DatabaseUtils.concatenateWhere( | |
selection, Combined._ID + " = CAST(? AS INTEGER)"); | |
selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, | |
new String[] { Long.toString(ContentUris.parseId(uri)) }); | |
// fall through | |
} | |
case LEGACY: | |
case COMBINED: { | |
if ((match == LEGACY || match == LEGACY_ID) | |
&& projection == null) { | |
projection = Browser.HISTORY_PROJECTION; | |
} | |
String[] args = createCombinedQuery(uri, projection, qb); | |
if (selectionArgs == null) { | |
selectionArgs = args; | |
} else { | |
selectionArgs = DatabaseUtils.appendSelectionArgs(args, selectionArgs); | |
} | |
break; | |
} | |
case SETTINGS: { | |
qb.setTables(TABLE_SETTINGS); | |
qb.setProjectionMap(SETTINGS_PROJECTION_MAP); | |
break; | |
} | |
case THUMBNAILS_ID: { | |
selection = DatabaseUtils.concatenateWhere( | |
selection, Thumbnails._ID + " = ?"); | |
selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, | |
new String[] { Long.toString(ContentUris.parseId(uri)) }); | |
// fall through | |
} | |
case THUMBNAILS: { | |
qb.setTables(TABLE_THUMBNAILS); | |
break; | |
} | |
case OMNIBOX_SUGGESTIONS: { | |
qb.setTables(VIEW_OMNIBOX_SUGGESTIONS); | |
break; | |
} | |
default: { | |
throw new UnsupportedOperationException("Unknown URL " + uri.toString()); | |
} | |
} | |
Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, | |
null, sortOrder, limit); | |
cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.AUTHORITY_URI); | |
return cursor; | |
} | |
private Cursor doSuggestQuery(String selection, String[] selectionArgs, String limit) { | |
if (TextUtils.isEmpty(selectionArgs[0])) { | |
selection = ZERO_QUERY_SUGGEST_SELECTION; | |
selectionArgs = null; | |
} else { | |
String like = selectionArgs[0] + "%"; | |
if (selectionArgs[0].startsWith("http") | |
|| selectionArgs[0].startsWith("file")) { | |
selectionArgs[0] = like; | |
} else { | |
selectionArgs = new String[6]; | |
selectionArgs[0] = "http://" + like; | |
selectionArgs[1] = "http://www." + like; | |
selectionArgs[2] = "https://" + like; | |
selectionArgs[3] = "https://www." + like; | |
// To match against titles. | |
selectionArgs[4] = like; | |
selectionArgs[5] = like; | |
selection = SUGGEST_SELECTION; | |
} | |
selection = DatabaseUtils.concatenateWhere(selection, | |
Bookmarks.IS_DELETED + "=0 AND " + Bookmarks.IS_FOLDER + "=0"); | |
} | |
Cursor c = mOpenHelper.getReadableDatabase().query(TABLE_BOOKMARKS_JOIN_HISTORY, | |
SUGGEST_PROJECTION, selection, selectionArgs, null, null, | |
null, null); | |
return new SuggestionsCursor(c); | |
} | |
private String[] createCombinedQuery( | |
Uri uri, String[] projection, SQLiteQueryBuilder qb) { | |
String[] args = null; | |
StringBuilder whereBuilder = new StringBuilder(128); | |
whereBuilder.append(Bookmarks.IS_DELETED); | |
whereBuilder.append(" = 0"); | |
// Look for account info | |
Object[] withAccount = getSelectionWithAccounts(uri, null, null); | |
String selection = (String) withAccount[0]; | |
String[] selectionArgs = (String[]) withAccount[1]; | |
if (selection != null) { | |
whereBuilder.append(" AND " + selection); | |
if (selectionArgs != null) { | |
// We use the selection twice, hence we need to duplicate the args | |
args = new String[selectionArgs.length * 2]; | |
System.arraycopy(selectionArgs, 0, args, 0, selectionArgs.length); | |
System.arraycopy(selectionArgs, 0, args, selectionArgs.length, | |
selectionArgs.length); | |
} | |
} | |
String where = whereBuilder.toString(); | |
// Build the bookmark subquery for history union subquery | |
qb.setTables(TABLE_BOOKMARKS); | |
String subQuery = qb.buildQuery(null, where, null, null, null, null); | |
// Build the history union subquery | |
qb.setTables(String.format(FORMAT_COMBINED_JOIN_SUBQUERY_JOIN_IMAGES, subQuery)); | |
qb.setProjectionMap(COMBINED_HISTORY_PROJECTION_MAP); | |
String historySubQuery = qb.buildQuery(null, | |
null, null, null, null, null); | |
// Build the bookmark union subquery | |
qb.setTables(TABLE_BOOKMARKS_JOIN_IMAGES); | |
qb.setProjectionMap(COMBINED_BOOKMARK_PROJECTION_MAP); | |
where += String.format(" AND %s NOT IN (SELECT %s FROM %s)", | |
Combined.URL, History.URL, TABLE_HISTORY); | |
String bookmarksSubQuery = qb.buildQuery(null, where, | |
null, null, null, null); | |
// Put it all together | |
String query = qb.buildUnionQuery( | |
new String[] {historySubQuery, bookmarksSubQuery}, | |
null, null); | |
qb.setTables("(" + query + ")"); | |
qb.setProjectionMap(null); | |
return args; | |
} | |
int deleteBookmarks(String selection, String[] selectionArgs, | |
boolean callerIsSyncAdapter) { | |
final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); | |
if (callerIsSyncAdapter) { | |
return db.delete(TABLE_BOOKMARKS, selection, selectionArgs); | |
} | |
Object[] appendedBookmarks = appendBookmarksIfFolder(selection, selectionArgs); | |
selection = (String) appendedBookmarks[0]; | |
selectionArgs = (String[]) appendedBookmarks[1]; | |
ContentValues values = new ContentValues(); | |
values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis()); | |
values.put(Bookmarks.IS_DELETED, 1); | |
return updateBookmarksInTransaction(values, selection, selectionArgs, | |
callerIsSyncAdapter); | |
} | |
private Object[] appendBookmarksIfFolder(String selection, String[] selectionArgs) { | |
final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); | |
final String[] bookmarksProjection = new String[] { | |
Bookmarks._ID, // 0 | |
Bookmarks.IS_FOLDER // 1 | |
}; | |
StringBuilder newSelection = new StringBuilder(selection); | |
List<String> newSelectionArgs = new ArrayList<String>(); | |
Cursor cursor = null; | |
try { | |
cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection, | |
selection, selectionArgs, null, null, null); | |
if (cursor != null) { | |
while (cursor.moveToNext()) { | |
String id = Long.toString(cursor.getLong(0)); | |
newSelectionArgs.add(id); | |
if (cursor.getInt(1) != 0) { | |
// collect bookmarks in this folder | |
Object[] bookmarks = appendBookmarksIfFolder( | |
Bookmarks.PARENT + "=?", new String[] { id }); | |
String[] bookmarkIds = (String[]) bookmarks[1]; | |
if (bookmarkIds.length > 0) { | |
newSelection.append(" OR " + TABLE_BOOKMARKS + "._id IN ("); | |
for (String bookmarkId : bookmarkIds) { | |
newSelection.append("?,"); | |
newSelectionArgs.add(bookmarkId); | |
} | |
newSelection.deleteCharAt(newSelection.length() - 1); | |
newSelection.append(")"); | |
} | |
} | |
} | |
} | |
} finally { | |
if (cursor != null) { | |
cursor.close(); | |
} | |
} | |
return new Object[] { | |
newSelection.toString(), | |
newSelectionArgs.toArray(new String[newSelectionArgs.size()]) | |
}; | |
} | |
@Override | |
public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs, | |
boolean callerIsSyncAdapter) { | |
final int match = URI_MATCHER.match(uri); | |
final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); | |
int deleted = 0; | |
switch (match) { | |
case BOOKMARKS_ID: { | |
selection = DatabaseUtils.concatenateWhere(selection, | |
TABLE_BOOKMARKS + "._id=?"); | |
selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, | |
new String[] { Long.toString(ContentUris.parseId(uri)) }); | |
// fall through | |
} | |
case BOOKMARKS: { | |
// Look for account info | |
Object[] withAccount = getSelectionWithAccounts(uri, selection, selectionArgs); | |
selection = (String) withAccount[0]; | |
selectionArgs = (String[]) withAccount[1]; | |
deleted = deleteBookmarks(selection, selectionArgs, callerIsSyncAdapter); | |
pruneImages(); | |
if (deleted > 0) { | |
refreshWidgets(); | |
} | |
break; | |
} | |
case HISTORY_ID: { | |
selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?"); | |
selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, | |
new String[] { Long.toString(ContentUris.parseId(uri)) }); | |
// fall through | |
} | |
case HISTORY: { | |
filterSearchClient(selectionArgs); | |
deleted = db.delete(TABLE_HISTORY, selection, selectionArgs); | |
pruneImages(); | |
break; | |
} | |
case SEARCHES_ID: { | |
selection = DatabaseUtils.concatenateWhere(selection, TABLE_SEARCHES + "._id=?"); | |
selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, | |
new String[] { Long.toString(ContentUris.parseId(uri)) }); | |
// fall through | |
} | |
case SEARCHES: { | |
deleted = db.delete(TABLE_SEARCHES, selection, selectionArgs); | |
break; | |
} | |
case SYNCSTATE: { | |
deleted = mSyncHelper.delete(db, selection, selectionArgs); | |
break; | |
} | |
case SYNCSTATE_ID: { | |
String selectionWithId = | |
(SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") | |
+ (selection == null ? "" : " AND (" + selection + ")"); | |
deleted = mSyncHelper.delete(db, selectionWithId, selectionArgs); | |
break; | |
} | |
case LEGACY_ID: { | |
selection = DatabaseUtils.concatenateWhere( | |
selection, Combined._ID + " = CAST(? AS INTEGER)"); | |
selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, | |
new String[] { Long.toString(ContentUris.parseId(uri)) }); | |
// fall through | |
} | |
case LEGACY: { | |
String[] projection = new String[] { Combined._ID, | |
Combined.IS_BOOKMARK, Combined.URL }; | |
SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); | |
String[] args = createCombinedQuery(uri, projection, qb); | |
if (selectionArgs == null) { | |
selectionArgs = args; | |
} else { | |
selectionArgs = DatabaseUtils.appendSelectionArgs( | |
args, selectionArgs); | |
} | |
Cursor c = qb.query(db, projection, selection, selectionArgs, | |
null, null, null); | |
while (c.moveToNext()) { | |
long id = c.getLong(0); | |
boolean isBookmark = c.getInt(1) != 0; | |
String url = c.getString(2); | |
if (isBookmark) { | |
deleted += deleteBookmarks(Bookmarks._ID + "=?", | |
new String[] { Long.toString(id) }, | |
callerIsSyncAdapter); | |
db.delete(TABLE_HISTORY, History.URL + "=?", | |
new String[] { url }); | |
} else { | |
deleted += db.delete(TABLE_HISTORY, | |
Bookmarks._ID + "=?", | |
new String[] { Long.toString(id) }); | |
} | |
} | |
c.close(); | |
break; | |
} | |
case THUMBNAILS_ID: { | |
selection = DatabaseUtils.concatenateWhere( | |
selection, Thumbnails._ID + " = ?"); | |
selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, | |
new String[] { Long.toString(ContentUris.parseId(uri)) }); | |
// fall through | |
} | |
case THUMBNAILS: { | |
deleted = db.delete(TABLE_THUMBNAILS, selection, selectionArgs); | |
break; | |
} | |
default: { | |
throw new UnsupportedOperationException("Unknown delete URI " + uri); | |
} | |
} | |
if (deleted > 0) { | |
postNotifyUri(uri); | |
if (shouldNotifyLegacy(uri)) { | |
postNotifyUri(LEGACY_AUTHORITY_URI); | |
} | |
} | |
return deleted; | |
} | |
long queryDefaultFolderId(String accountName, String accountType) { | |
if (!isNullAccount(accountName) && !isNullAccount(accountType)) { | |
final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); | |
Cursor c = db.query(TABLE_BOOKMARKS, new String[] { Bookmarks._ID }, | |
ChromeSyncColumns.SERVER_UNIQUE + " = ?" + | |
" AND account_type = ? AND account_name = ?", | |
new String[] { ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR, | |
accountType, accountName }, null, null, null); | |
try { | |
if (c.moveToFirst()) { | |
return c.getLong(0); | |
} | |
} finally { | |
c.close(); | |
} | |
} | |
return FIXED_ID_ROOT; | |
} | |
@Override | |
public Uri insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { | |
int match = URI_MATCHER.match(uri); | |
final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); | |
long id = -1; | |
if (match == LEGACY) { | |
// Intercept and route to the correct table | |
Integer bookmark = values.getAsInteger(BookmarkColumns.BOOKMARK); | |
values.remove(BookmarkColumns.BOOKMARK); | |
if (bookmark == null || bookmark == 0) { | |
match = HISTORY; | |
} else { | |
match = BOOKMARKS; | |
values.remove(BookmarkColumns.DATE); | |
values.remove(BookmarkColumns.VISITS); | |
values.remove(BookmarkColumns.USER_ENTERED); | |
values.put(Bookmarks.IS_FOLDER, 0); | |
} | |
} | |
switch (match) { | |
case BOOKMARKS: { | |
// Mark rows dirty if they're not coming from a sync adapter | |
if (!callerIsSyncAdapter) { | |
long now = System.currentTimeMillis(); | |
values.put(Bookmarks.DATE_CREATED, now); | |
values.put(Bookmarks.DATE_MODIFIED, now); | |
values.put(Bookmarks.DIRTY, 1); | |
boolean hasAccounts = values.containsKey(Bookmarks.ACCOUNT_TYPE) | |
|| values.containsKey(Bookmarks.ACCOUNT_NAME); | |
String accountType = values | |
.getAsString(Bookmarks.ACCOUNT_TYPE); | |
String accountName = values | |
.getAsString(Bookmarks.ACCOUNT_NAME); | |
boolean hasParent = values.containsKey(Bookmarks.PARENT); | |
if (hasParent && hasAccounts) { | |
// Let's make sure it's valid | |
long parentId = values.getAsLong(Bookmarks.PARENT); | |
hasParent = isValidParent( | |
accountType, accountName, parentId); | |
} else if (hasParent && !hasAccounts) { | |
long parentId = values.getAsLong(Bookmarks.PARENT); | |
hasParent = setParentValues(parentId, values); | |
} | |
// If no parent is set default to the "Bookmarks Bar" folder | |
if (!hasParent) { | |
values.put(Bookmarks.PARENT, | |
queryDefaultFolderId(accountName, accountType)); | |
} | |
} | |
// If no position is requested put the bookmark at the beginning of the list | |
if (!values.containsKey(Bookmarks.POSITION)) { | |
values.put(Bookmarks.POSITION, Long.toString(Long.MIN_VALUE)); | |
} | |
// Extract out the image values so they can be inserted into the images table | |
String url = values.getAsString(Bookmarks.URL); | |
ContentValues imageValues = extractImageValues(values, url); | |
Boolean isFolder = values.getAsBoolean(Bookmarks.IS_FOLDER); | |
if ((isFolder == null || !isFolder) | |
&& imageValues != null && !TextUtils.isEmpty(url)) { | |
int count = db.update(TABLE_IMAGES, imageValues, Images.URL + "=?", | |
new String[] { url }); | |
if (count == 0) { | |
db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, imageValues); | |
} | |
} | |
id = db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.DIRTY, values); | |
refreshWidgets(); | |
break; | |
} | |
case HISTORY: { | |
// If no created time is specified set it to now | |
if (!values.containsKey(History.DATE_CREATED)) { | |
values.put(History.DATE_CREATED, System.currentTimeMillis()); | |
} | |
String url = values.getAsString(History.URL); | |
url = filterSearchClient(url); | |
values.put(History.URL, url); | |
// Extract out the image values so they can be inserted into the images table | |
ContentValues imageValues = extractImageValues(values, | |
values.getAsString(History.URL)); | |
if (imageValues != null) { | |
db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, imageValues); | |
} | |
id = db.insertOrThrow(TABLE_HISTORY, History.VISITS, values); | |
break; | |
} | |
case SEARCHES: { | |
id = insertSearchesInTransaction(db, values); | |
break; | |
} | |
case SYNCSTATE: { | |
id = mSyncHelper.insert(db, values); | |
break; | |
} | |
case SETTINGS: { | |
id = 0; | |
insertSettingsInTransaction(db, values); | |
break; | |
} | |
case THUMBNAILS: { | |
id = db.replaceOrThrow(TABLE_THUMBNAILS, null, values); | |
break; | |
} | |
default: { | |
throw new UnsupportedOperationException("Unknown insert URI " + uri); | |
} | |
} | |
if (id >= 0) { | |
postNotifyUri(uri); | |
if (shouldNotifyLegacy(uri)) { | |
postNotifyUri(LEGACY_AUTHORITY_URI); | |
} | |
return ContentUris.withAppendedId(uri, id); | |
} else { | |
return null; | |
} | |
} | |
private String[] getAccountNameAndType(long id) { | |
if (id <= 0) { | |
return null; | |
} | |
Uri uri = ContentUris.withAppendedId(Bookmarks.CONTENT_URI, id); | |
Cursor c = query(uri, | |
new String[] { Bookmarks.ACCOUNT_NAME, Bookmarks.ACCOUNT_TYPE }, | |
null, null, null); | |
try { | |
if (c.moveToFirst()) { | |
String parentName = c.getString(0); | |
String parentType = c.getString(1); | |
return new String[] { parentName, parentType }; | |
} | |
return null; | |
} finally { | |
c.close(); | |
} | |
} | |
private boolean setParentValues(long parentId, ContentValues values) { | |
String[] parent = getAccountNameAndType(parentId); | |
if (parent == null) { | |
return false; | |
} | |
values.put(Bookmarks.ACCOUNT_NAME, parent[0]); | |
values.put(Bookmarks.ACCOUNT_TYPE, parent[1]); | |
return true; | |
} | |
private boolean isValidParent(String accountType, String accountName, | |
long parentId) { | |
String[] parent = getAccountNameAndType(parentId); | |
if (parent != null | |
&& TextUtils.equals(accountName, parent[0]) | |
&& TextUtils.equals(accountType, parent[1])) { | |
return true; | |
} | |
return false; | |
} | |
private void filterSearchClient(String[] selectionArgs) { | |
if (selectionArgs != null) { | |
for (int i = 0; i < selectionArgs.length; i++) { | |
selectionArgs[i] = filterSearchClient(selectionArgs[i]); | |
} | |
} | |
} | |
// Filters out the client= param for search urls | |
private String filterSearchClient(String url) { | |
// remove "client" before updating it to the history so that it wont | |
// show up in the auto-complete list. | |
int index = url.indexOf("client="); | |
if (index > 0 && url.contains(".google.")) { | |
int end = url.indexOf('&', index); | |
if (end > 0) { | |
url = url.substring(0, index) | |
.concat(url.substring(end + 1)); | |
} else { | |
// the url.charAt(index-1) should be either '?' or '&' | |
url = url.substring(0, index-1); | |
} | |
} | |
return url; | |
} | |
/** | |
* Searches are unique, so perform an UPSERT manually since SQLite doesn't support them. | |
*/ | |
private long insertSearchesInTransaction(SQLiteDatabase db, ContentValues values) { | |
String search = values.getAsString(Searches.SEARCH); | |
if (TextUtils.isEmpty(search)) { | |
throw new IllegalArgumentException("Must include the SEARCH field"); | |
} | |
Cursor cursor = null; | |
try { | |
cursor = db.query(TABLE_SEARCHES, new String[] { Searches._ID }, | |
Searches.SEARCH + "=?", new String[] { search }, null, null, null); | |
if (cursor.moveToNext()) { | |
long id = cursor.getLong(0); | |
db.update(TABLE_SEARCHES, values, Searches._ID + "=?", | |
new String[] { Long.toString(id) }); | |
return id; | |
} else { | |
return db.insertOrThrow(TABLE_SEARCHES, Searches.SEARCH, values); | |
} | |
} finally { | |
if (cursor != null) cursor.close(); | |
} | |
} | |
/** | |
* Settings are unique, so perform an UPSERT manually since SQLite doesn't support them. | |
*/ | |
private long insertSettingsInTransaction(SQLiteDatabase db, ContentValues values) { | |
String key = values.getAsString(Settings.KEY); | |
if (TextUtils.isEmpty(key)) { | |
throw new IllegalArgumentException("Must include the KEY field"); | |
} | |
String[] keyArray = new String[] { key }; | |
Cursor cursor = null; | |
try { | |
cursor = db.query(TABLE_SETTINGS, new String[] { Settings.KEY }, | |
Settings.KEY + "=?", keyArray, null, null, null); | |
if (cursor.moveToNext()) { | |
long id = cursor.getLong(0); | |
db.update(TABLE_SETTINGS, values, Settings.KEY + "=?", keyArray); | |
return id; | |
} else { | |
return db.insertOrThrow(TABLE_SETTINGS, Settings.VALUE, values); | |
} | |
} finally { | |
if (cursor != null) cursor.close(); | |
} | |
} | |
@Override | |
public int updateInTransaction(Uri uri, ContentValues values, String selection, | |
String[] selectionArgs, boolean callerIsSyncAdapter) { | |
int match = URI_MATCHER.match(uri); | |
final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); | |
if (match == LEGACY || match == LEGACY_ID) { | |
// Intercept and route to the correct table | |
Integer bookmark = values.getAsInteger(BookmarkColumns.BOOKMARK); | |
values.remove(BookmarkColumns.BOOKMARK); | |
if (bookmark == null || bookmark == 0) { | |
if (match == LEGACY) { | |
match = HISTORY; | |
} else { | |
match = HISTORY_ID; | |
} | |
} else { | |
if (match == LEGACY) { | |
match = BOOKMARKS; | |
} else { | |
match = BOOKMARKS_ID; | |
} | |
values.remove(BookmarkColumns.DATE); | |
values.remove(BookmarkColumns.VISITS); | |
values.remove(BookmarkColumns.USER_ENTERED); | |
} | |
} | |
int modified = 0; | |
switch (match) { | |
case BOOKMARKS_ID: { | |
selection = DatabaseUtils.concatenateWhere(selection, | |
TABLE_BOOKMARKS + "._id=?"); | |
selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, | |
new String[] { Long.toString(ContentUris.parseId(uri)) }); | |
// fall through | |
} | |
case BOOKMARKS: { | |
Object[] withAccount = getSelectionWithAccounts(uri, selection, selectionArgs); | |
selection = (String) withAccount[0]; | |
selectionArgs = (String[]) withAccount[1]; | |
modified = updateBookmarksInTransaction(values, selection, selectionArgs, | |
callerIsSyncAdapter); | |
if (modified > 0) { | |
refreshWidgets(); | |
} | |
break; | |
} | |
case HISTORY_ID: { | |
selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?"); | |
selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, | |
new String[] { Long.toString(ContentUris.parseId(uri)) }); | |
// fall through | |
} | |
case HISTORY: { | |
modified = updateHistoryInTransaction(values, selection, selectionArgs); | |
break; | |
} | |
case SYNCSTATE: { | |
modified = mSyncHelper.update(mDb, values, | |
appendAccountToSelection(uri, selection), selectionArgs); | |
break; | |
} | |
case SYNCSTATE_ID: { | |
selection = appendAccountToSelection(uri, selection); | |
String selectionWithId = | |
(SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") | |
+ (selection == null ? "" : " AND (" + selection + ")"); | |
modified = mSyncHelper.update(mDb, values, | |
selectionWithId, selectionArgs); | |
break; | |
} | |
case IMAGES: { | |
String url = values.getAsString(Images.URL); | |
if (TextUtils.isEmpty(url)) { | |
throw new IllegalArgumentException("Images.URL is required"); | |
} | |
if (!shouldUpdateImages(db, url, values)) { | |
return 0; | |
} | |
int count = db.update(TABLE_IMAGES, values, Images.URL + "=?", | |
new String[] { url }); | |
if (count == 0) { | |
db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, values); | |
count = 1; | |
} | |
// Only favicon is exposed in the public API. If we updated | |
// the thumbnail or touch icon don't bother notifying the | |
// legacy authority since it can't read it anyway. | |
boolean updatedLegacy = false; | |
if (getUrlCount(db, TABLE_BOOKMARKS, url) > 0) { | |
postNotifyUri(Bookmarks.CONTENT_URI); | |
updatedLegacy = values.containsKey(Images.FAVICON); | |
refreshWidgets(); | |
} | |
if (getUrlCount(db, TABLE_HISTORY, url) > 0) { | |
postNotifyUri(History.CONTENT_URI); | |
updatedLegacy = values.containsKey(Images.FAVICON); | |
} | |
if (pruneImages() > 0 || updatedLegacy) { | |
postNotifyUri(LEGACY_AUTHORITY_URI); | |
} | |
// Even though we may be calling notifyUri on Bookmarks, don't | |
// sync to network as images aren't synced. Otherwise this | |
// unnecessarily triggers a bookmark sync. | |
mSyncToNetwork = false; | |
return count; | |
} | |
case SEARCHES: { | |
modified = db.update(TABLE_SEARCHES, values, selection, selectionArgs); | |
break; | |
} | |
case ACCOUNTS: { | |
Account[] accounts = AccountManager.get(getContext()).getAccounts(); | |
mSyncHelper.onAccountsChanged(mDb, accounts); | |
break; | |
} | |
case THUMBNAILS: { | |
modified = db.update(TABLE_THUMBNAILS, values, | |
selection, selectionArgs); | |
break; | |
} | |
default: { | |
throw new UnsupportedOperationException("Unknown update URI " + uri); | |
} | |
} | |
pruneImages(); | |
if (modified > 0) { | |
postNotifyUri(uri); | |
if (shouldNotifyLegacy(uri)) { | |
postNotifyUri(LEGACY_AUTHORITY_URI); | |
} | |
} | |
return modified; | |
} | |
// We want to avoid sending out more URI notifications than we have to | |
// Thus, we check to see if the images we are about to store are already there | |
// This is used because things like a site's favion or touch icon is rarely | |
// changed, but the browser tries to update it every time the page loads. | |
// Without this, we will always send out 3 URI notifications per page load. | |
// With this, that drops to 0 or 1, depending on if the thumbnail changed. | |
private boolean shouldUpdateImages( | |
SQLiteDatabase db, String url, ContentValues values) { | |
final String[] projection = new String[] { | |
Images.FAVICON, | |
Images.THUMBNAIL, | |
Images.TOUCH_ICON, | |
}; | |
Cursor cursor = db.query(TABLE_IMAGES, projection, Images.URL + "=?", | |
new String[] { url }, null, null, null); | |
byte[] nfavicon = values.getAsByteArray(Images.FAVICON); | |
byte[] nthumb = values.getAsByteArray(Images.THUMBNAIL); | |
byte[] ntouch = values.getAsByteArray(Images.TOUCH_ICON); | |
byte[] cfavicon = null; | |
byte[] cthumb = null; | |
byte[] ctouch = null; | |
try { | |
if (cursor.getCount() <= 0) { | |
return nfavicon != null || nthumb != null || ntouch != null; | |
} | |
while (cursor.moveToNext()) { | |
if (nfavicon != null) { | |
cfavicon = cursor.getBlob(0); | |
if (!Arrays.equals(nfavicon, cfavicon)) { | |
return true; | |
} | |
} | |
if (nthumb != null) { | |
cthumb = cursor.getBlob(1); | |
if (!Arrays.equals(nthumb, cthumb)) { | |
return true; | |
} | |
} | |
if (ntouch != null) { | |
ctouch = cursor.getBlob(2); | |
if (!Arrays.equals(ntouch, ctouch)) { | |
return true; | |
} | |
} | |
} | |
} finally { | |
cursor.close(); | |
} | |
return false; | |
} | |
int getUrlCount(SQLiteDatabase db, String table, String url) { | |
Cursor c = db.query(table, new String[] { "COUNT(*)" }, | |
"url = ?", new String[] { url }, null, null, null); | |
try { | |
int count = 0; | |
if (c.moveToFirst()) { | |
count = c.getInt(0); | |
} | |
return count; | |
} finally { | |
c.close(); | |
} | |
} | |
/** | |
* Does a query to find the matching bookmarks and updates each one with the provided values. | |
*/ | |
int updateBookmarksInTransaction(ContentValues values, String selection, | |
String[] selectionArgs, boolean callerIsSyncAdapter) { | |
int count = 0; | |
final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); | |
final String[] bookmarksProjection = new String[] { | |
Bookmarks._ID, // 0 | |
Bookmarks.VERSION, // 1 | |
Bookmarks.URL, // 2 | |
Bookmarks.TITLE, // 3 | |
Bookmarks.IS_FOLDER, // 4 | |
Bookmarks.ACCOUNT_NAME, // 5 | |
Bookmarks.ACCOUNT_TYPE, // 6 | |
}; | |
Cursor cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection, | |
selection, selectionArgs, null, null, null); | |
boolean updatingParent = values.containsKey(Bookmarks.PARENT); | |
String parentAccountName = null; | |
String parentAccountType = null; | |
if (updatingParent) { | |
long parent = values.getAsLong(Bookmarks.PARENT); | |
Cursor c = db.query(TABLE_BOOKMARKS, new String[] { | |
Bookmarks.ACCOUNT_NAME, Bookmarks.ACCOUNT_TYPE}, | |
"_id = ?", new String[] { Long.toString(parent) }, | |
null, null, null); | |
if (c.moveToFirst()) { | |
parentAccountName = c.getString(0); | |
parentAccountType = c.getString(1); | |
} | |
c.close(); | |
} else if (values.containsKey(Bookmarks.ACCOUNT_NAME) | |
|| values.containsKey(Bookmarks.ACCOUNT_TYPE)) { | |
// TODO: Implement if needed (no one needs this yet) | |
} | |
try { | |
String[] args = new String[1]; | |
// Mark the bookmark dirty if the caller isn't a sync adapter | |
if (!callerIsSyncAdapter) { | |
values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis()); | |
values.put(Bookmarks.DIRTY, 1); | |
} | |
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); | |
args[0] = Long.toString(id); | |
String accountName = cursor.getString(5); | |
String accountType = cursor.getString(6); | |
// If we are updating the parent and either the account name or | |
// type do not match that of the new parent | |
if (updatingParent | |
&& (!TextUtils.equals(accountName, parentAccountName) | |
|| !TextUtils.equals(accountType, parentAccountType))) { | |
// Parent is a different account | |
// First, insert a new bookmark/folder with the new account | |
// Then, if this is a folder, reparent all it's children | |
// Finally, delete the old bookmark/folder | |
ContentValues newValues = valuesFromCursor(cursor); | |
newValues.putAll(values); | |
newValues.remove(Bookmarks._ID); | |
newValues.remove(Bookmarks.VERSION); | |
newValues.put(Bookmarks.ACCOUNT_NAME, parentAccountName); | |
newValues.put(Bookmarks.ACCOUNT_TYPE, parentAccountType); | |
Uri insertUri = insertInTransaction(Bookmarks.CONTENT_URI, | |
newValues, callerIsSyncAdapter); | |
long newId = ContentUris.parseId(insertUri); | |
if (cursor.getInt(4) != 0) { | |
// This is a folder, reparent | |
ContentValues updateChildren = new ContentValues(1); | |
updateChildren.put(Bookmarks.PARENT, newId); | |
count += updateBookmarksInTransaction(updateChildren, | |
Bookmarks.PARENT + "=?", new String[] { | |
Long.toString(id)}, callerIsSyncAdapter); | |
} | |
// Now, delete the old one | |
Uri uri = ContentUris.withAppendedId(Bookmarks.CONTENT_URI, id); | |
deleteInTransaction(uri, null, null, callerIsSyncAdapter); | |
count += 1; | |
} else { | |
if (!callerIsSyncAdapter) { | |
// increase the local version for non-sync changes | |
values.put(Bookmarks.VERSION, cursor.getLong(1) + 1); | |
} | |
count += db.update(TABLE_BOOKMARKS, values, "_id=?", args); | |
} | |
// Update the images over in their table | |
if (imageValues != null) { | |
if (!updatingUrl) { | |
url = cursor.getString(2); | |
imageValues.put(Images.URL, url); | |
} | |
if (!TextUtils.isEmpty(url)) { | |
args[0] = url; | |
if (db.update(TABLE_IMAGES, imageValues, Images.URL + "=?", args) == 0) { | |
db.insert(TABLE_IMAGES, Images.FAVICON, imageValues); | |
} | |
} | |
} | |
} | |
} finally { | |
if (cursor != null) cursor.close(); | |
} | |
return count; | |
} | |
ContentValues valuesFromCursor(Cursor c) { | |
int count = c.getColumnCount(); | |
ContentValues values = new ContentValues(count); | |
String[] colNames = c.getColumnNames(); | |
for (int i = 0; i < count; i++) { | |
switch (c.getType(i)) { | |
case Cursor.FIELD_TYPE_BLOB: | |
values.put(colNames[i], c.getBlob(i)); | |
break; | |
case Cursor.FIELD_TYPE_FLOAT: | |
values.put(colNames[i], c.getFloat(i)); | |
break; | |
case Cursor.FIELD_TYPE_INTEGER: | |
values.put(colNames[i], c.getLong(i)); | |
break; | |
case Cursor.FIELD_TYPE_STRING: | |
values.put(colNames[i], c.getString(i)); | |
break; | |
} | |
} | |
return values; | |
} | |
/** | |
* Does a query to find the matching bookmarks and updates each one with the provided values. | |
*/ | |
int updateHistoryInTransaction(ContentValues values, String selection, String[] selectionArgs) { | |
int count = 0; | |
final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); | |
filterSearchClient(selectionArgs); | |
Cursor cursor = query(History.CONTENT_URI, | |
new String[] { History._ID, History.URL }, | |
selection, selectionArgs, null); | |
try { | |
String[] args = new String[1]; | |
boolean updatingUrl = values.containsKey(History.URL); | |
String url = null; | |
if (updatingUrl) { | |
url = filterSearchClient(values.getAsString(History.URL)); | |
values.put(History.URL, url); | |
} | |
ContentValues imageValues = extractImageValues(values, url); | |
while (cursor.moveToNext()) { | |
args[0] = cursor.getString(0); | |
count += db.update(TABLE_HISTORY, values, "_id=?", args); | |
// Update the images over in their table | |
if (imageValues != null) { | |
if (!updatingUrl) { | |
url = cursor.getString(1); | |
imageValues.put(Images.URL, url); | |
} | |
args[0] = url; | |
if (db.update(TABLE_IMAGES, imageValues, Images.URL + "=?", args) == 0) { | |
db.insert(TABLE_IMAGES, Images.FAVICON, imageValues); | |
} | |
} | |
} | |
} finally { | |
if (cursor != null) cursor.close(); | |
} | |
return count; | |
} | |
String appendAccountToSelection(Uri uri, String selection) { | |
final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); | |
final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE); | |
final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); | |
if (partialUri) { | |
// Throw when either account is incomplete | |
throw new IllegalArgumentException( | |
"Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE for " + uri); | |
} | |
// Accounts are valid by only checking one parameter, since we've | |
// already ruled out partial accounts. | |
final boolean validAccount = !TextUtils.isEmpty(accountName); | |
if (validAccount) { | |
StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "=" | |
+ DatabaseUtils.sqlEscapeString(accountName) + " AND " | |
+ RawContacts.ACCOUNT_TYPE + "=" | |
+ DatabaseUtils.sqlEscapeString(accountType)); | |
if (!TextUtils.isEmpty(selection)) { | |
selectionSb.append(" AND ("); | |
selectionSb.append(selection); | |
selectionSb.append(')'); | |
} | |
return selectionSb.toString(); | |
} else { | |
return selection; | |
} | |
} | |
ContentValues extractImageValues(ContentValues values, String url) { | |
ContentValues imageValues = null; | |
// favicon | |
if (values.containsKey(Bookmarks.FAVICON)) { | |
imageValues = new ContentValues(); | |
imageValues.put(Images.FAVICON, values.getAsByteArray(Bookmarks.FAVICON)); | |
values.remove(Bookmarks.FAVICON); | |
} | |
// thumbnail | |
if (values.containsKey(Bookmarks.THUMBNAIL)) { | |
if (imageValues == null) { | |
imageValues = new ContentValues(); | |
} | |
imageValues.put(Images.THUMBNAIL, values.getAsByteArray(Bookmarks.THUMBNAIL)); | |
values.remove(Bookmarks.THUMBNAIL); | |
} | |
// touch icon | |
if (values.containsKey(Bookmarks.TOUCH_ICON)) { | |
if (imageValues == null) { | |
imageValues = new ContentValues(); | |
} | |
imageValues.put(Images.TOUCH_ICON, values.getAsByteArray(Bookmarks.TOUCH_ICON)); | |
values.remove(Bookmarks.TOUCH_ICON); | |
} | |
if (imageValues != null) { | |
imageValues.put(Images.URL, url); | |
} | |
return imageValues; | |
} | |
int pruneImages() { | |
final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); | |
return db.delete(TABLE_IMAGES, IMAGE_PRUNE, null); | |
} | |
boolean shouldNotifyLegacy(Uri uri) { | |
if (uri.getPathSegments().contains("history") | |
|| uri.getPathSegments().contains("bookmarks") | |
|| uri.getPathSegments().contains("searches")) { | |
return true; | |
} | |
return false; | |
} | |
@Override | |
protected boolean syncToNetwork(Uri uri) { | |
if (BrowserContract.AUTHORITY.equals(uri.getAuthority()) | |
&& uri.getPathSegments().contains("bookmarks")) { | |
return mSyncToNetwork; | |
} | |
if (LEGACY_AUTHORITY.equals(uri.getAuthority())) { | |
// Allow for 3rd party sync adapters | |
return true; | |
} | |
return false; | |
} | |
static class SuggestionsCursor extends AbstractCursor { | |
private static final int ID_INDEX = 0; | |
private static final int URL_INDEX = 1; | |
private static final int TITLE_INDEX = 2; | |
private static final int ICON_INDEX = 3; | |
private static final int LAST_ACCESS_TIME_INDEX = 4; | |
// shared suggestion array index, make sure to match COLUMNS | |
private static final int SUGGEST_COLUMN_INTENT_ACTION_ID = 1; | |
private static final int SUGGEST_COLUMN_INTENT_DATA_ID = 2; | |
private static final int SUGGEST_COLUMN_TEXT_1_ID = 3; | |
private static final int SUGGEST_COLUMN_TEXT_2_TEXT_ID = 4; | |
private static final int SUGGEST_COLUMN_TEXT_2_URL_ID = 5; | |
private static final int SUGGEST_COLUMN_ICON_1_ID = 6; | |
private static final int SUGGEST_COLUMN_LAST_ACCESS_HINT_ID = 7; | |
// shared suggestion columns | |
private static final String[] COLUMNS = new String[] { | |
BaseColumns._ID, | |
SearchManager.SUGGEST_COLUMN_INTENT_ACTION, | |
SearchManager.SUGGEST_COLUMN_INTENT_DATA, | |
SearchManager.SUGGEST_COLUMN_TEXT_1, | |
SearchManager.SUGGEST_COLUMN_TEXT_2, | |
SearchManager.SUGGEST_COLUMN_TEXT_2_URL, | |
SearchManager.SUGGEST_COLUMN_ICON_1, | |
SearchManager.SUGGEST_COLUMN_LAST_ACCESS_HINT}; | |
private final Cursor mSource; | |
public SuggestionsCursor(Cursor cursor) { | |
mSource = cursor; | |
} | |
@Override | |
public String[] getColumnNames() { | |
return COLUMNS; | |
} | |
@Override | |
public String getString(int columnIndex) { | |
switch (columnIndex) { | |
case ID_INDEX: | |
return mSource.getString(columnIndex); | |
case SUGGEST_COLUMN_INTENT_ACTION_ID: | |
return Intent.ACTION_VIEW; | |
case SUGGEST_COLUMN_INTENT_DATA_ID: | |
return mSource.getString(URL_INDEX); | |
case SUGGEST_COLUMN_TEXT_2_TEXT_ID: | |
case SUGGEST_COLUMN_TEXT_2_URL_ID: | |
return UrlUtils.stripUrl(mSource.getString(URL_INDEX)); | |
case SUGGEST_COLUMN_TEXT_1_ID: | |
return mSource.getString(TITLE_INDEX); | |
case SUGGEST_COLUMN_ICON_1_ID: | |
return mSource.getString(ICON_INDEX); | |
case SUGGEST_COLUMN_LAST_ACCESS_HINT_ID: | |
return mSource.getString(LAST_ACCESS_TIME_INDEX); | |
} | |
return null; | |
} | |
@Override | |
public int getCount() { | |
return mSource.getCount(); | |
} | |
@Override | |
public double getDouble(int column) { | |
throw new UnsupportedOperationException(); | |
} | |
@Override | |
public float getFloat(int column) { | |
throw new UnsupportedOperationException(); | |
} | |
@Override | |
public int getInt(int column) { | |
throw new UnsupportedOperationException(); | |
} | |
@Override | |
public long getLong(int column) { | |
switch (column) { | |
case ID_INDEX: | |
return mSource.getLong(ID_INDEX); | |
case SUGGEST_COLUMN_LAST_ACCESS_HINT_ID: | |
return mSource.getLong(LAST_ACCESS_TIME_INDEX); | |
} | |
throw new UnsupportedOperationException(); | |
} | |
@Override | |
public short getShort(int column) { | |
throw new UnsupportedOperationException(); | |
} | |
@Override | |
public boolean isNull(int column) { | |
return mSource.isNull(column); | |
} | |
@Override | |
public boolean onMove(int oldPosition, int newPosition) { | |
return mSource.moveToPosition(newPosition); | |
} | |
} | |
// --------------------------------------------------- | |
// SQL below, be warned | |
// --------------------------------------------------- | |
private static final String SQL_CREATE_VIEW_OMNIBOX_SUGGESTIONS = | |
"CREATE VIEW IF NOT EXISTS v_omnibox_suggestions " | |
+ " AS " | |
+ " SELECT _id, url, title, 1 AS bookmark, 0 AS visits, 0 AS date" | |
+ " FROM bookmarks " | |
+ " WHERE deleted = 0 AND folder = 0 " | |
+ " UNION ALL " | |
+ " SELECT _id, url, title, 0 AS bookmark, visits, date " | |
+ " FROM history " | |
+ " WHERE url NOT IN (SELECT url FROM bookmarks" | |
+ " WHERE deleted = 0 AND folder = 0) " | |
+ " ORDER BY bookmark DESC, visits DESC, date DESC "; | |
private static final String SQL_WHERE_ACCOUNT_HAS_BOOKMARKS = | |
"0 < ( " | |
+ "SELECT count(*) " | |
+ "FROM bookmarks " | |
+ "WHERE deleted = 0 AND folder = 0 " | |
+ " AND ( " | |
+ " v_accounts.account_name = bookmarks.account_name " | |
+ " OR (v_accounts.account_name IS NULL AND bookmarks.account_name IS NULL) " | |
+ " ) " | |
+ " AND ( " | |
+ " v_accounts.account_type = bookmarks.account_type " | |
+ " OR (v_accounts.account_type IS NULL AND bookmarks.account_type IS NULL) " | |
+ " ) " | |
+ ")"; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright (C) 2009 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.ContentProvider; | |
import android.content.ContentProviderOperation; | |
import android.content.ContentProviderResult; | |
import android.content.ContentResolver; | |
import android.content.ContentValues; | |
import android.content.Context; | |
import android.content.OperationApplicationException; | |
import android.database.sqlite.SQLiteDatabase; | |
import android.database.sqlite.SQLiteOpenHelper; | |
import android.net.Uri; | |
import java.util.ArrayList; | |
import java.util.HashSet; | |
import java.util.Set; | |
/** | |
* General purpose {@link ContentProvider} base class that uses SQLiteDatabase for storage. | |
*/ | |
public abstract class SQLiteContentProvider extends ContentProvider { | |
private static final String TAG = "SQLiteContentProvider"; | |
private SQLiteOpenHelper mOpenHelper; | |
private Set<Uri> mChangedUris; | |
protected SQLiteDatabase mDb; | |
private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>(); | |
private static final int SLEEP_AFTER_YIELD_DELAY = 4000; | |
/** | |
* Maximum number of operations allowed in a batch between yield points. | |
*/ | |
private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500; | |
@Override | |
public boolean onCreate() { | |
Context context = getContext(); | |
mOpenHelper = getDatabaseHelper(context); | |
mChangedUris = new HashSet<Uri>(); | |
return true; | |
} | |
/** | |
* Returns a {@link SQLiteOpenHelper} that can open the database. | |
*/ | |
public abstract SQLiteOpenHelper getDatabaseHelper(Context context); | |
/** | |
* The equivalent of the {@link #insert} method, but invoked within a transaction. | |
*/ | |
public abstract Uri insertInTransaction(Uri uri, ContentValues values, | |
boolean callerIsSyncAdapter); | |
/** | |
* The equivalent of the {@link #update} method, but invoked within a transaction. | |
*/ | |
public abstract int updateInTransaction(Uri uri, ContentValues values, String selection, | |
String[] selectionArgs, boolean callerIsSyncAdapter); | |
/** | |
* The equivalent of the {@link #delete} method, but invoked within a transaction. | |
*/ | |
public abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs, | |
boolean callerIsSyncAdapter); | |
/** | |
* Call this to add a URI to the list of URIs to be notified when the transaction | |
* is committed. | |
*/ | |
protected void postNotifyUri(Uri uri) { | |
synchronized (mChangedUris) { | |
mChangedUris.add(uri); | |
} | |
} | |
public boolean isCallerSyncAdapter(Uri uri) { | |
return false; | |
} | |
public SQLiteOpenHelper getDatabaseHelper() { | |
return mOpenHelper; | |
} | |
private boolean applyingBatch() { | |
return mApplyingBatch.get() != null && mApplyingBatch.get(); | |
} | |
@Override | |
public Uri insert(Uri uri, ContentValues values) { | |
Uri result = null; | |
boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); | |
boolean applyingBatch = applyingBatch(); | |
if (!applyingBatch) { | |
mDb = mOpenHelper.getWritableDatabase(); | |
mDb.beginTransaction(); | |
try { | |
result = insertInTransaction(uri, values, callerIsSyncAdapter); | |
mDb.setTransactionSuccessful(); | |
} finally { | |
mDb.endTransaction(); | |
} | |
onEndTransaction(callerIsSyncAdapter); | |
} else { | |
result = insertInTransaction(uri, values, callerIsSyncAdapter); | |
} | |
return result; | |
} | |
@Override | |
public int bulkInsert(Uri uri, ContentValues[] values) { | |
int numValues = values.length; | |
boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); | |
mDb = mOpenHelper.getWritableDatabase(); | |
mDb.beginTransaction(); | |
try { | |
for (int i = 0; i < numValues; i++) { | |
Uri result = insertInTransaction(uri, values[i], callerIsSyncAdapter); | |
mDb.yieldIfContendedSafely(); | |
} | |
mDb.setTransactionSuccessful(); | |
} finally { | |
mDb.endTransaction(); | |
} | |
onEndTransaction(callerIsSyncAdapter); | |
return numValues; | |
} | |
@Override | |
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { | |
int count = 0; | |
boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); | |
boolean applyingBatch = applyingBatch(); | |
if (!applyingBatch) { | |
mDb = mOpenHelper.getWritableDatabase(); | |
mDb.beginTransaction(); | |
try { | |
count = updateInTransaction(uri, values, selection, selectionArgs, | |
callerIsSyncAdapter); | |
mDb.setTransactionSuccessful(); | |
} finally { | |
mDb.endTransaction(); | |
} | |
onEndTransaction(callerIsSyncAdapter); | |
} else { | |
count = updateInTransaction(uri, values, selection, selectionArgs, callerIsSyncAdapter); | |
} | |
return count; | |
} | |
@Override | |
public int delete(Uri uri, String selection, String[] selectionArgs) { | |
int count = 0; | |
boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); | |
boolean applyingBatch = applyingBatch(); | |
if (!applyingBatch) { | |
mDb = mOpenHelper.getWritableDatabase(); | |
mDb.beginTransaction(); | |
try { | |
count = deleteInTransaction(uri, selection, selectionArgs, callerIsSyncAdapter); | |
mDb.setTransactionSuccessful(); | |
} finally { | |
mDb.endTransaction(); | |
} | |
onEndTransaction(callerIsSyncAdapter); | |
} else { | |
count = deleteInTransaction(uri, selection, selectionArgs, callerIsSyncAdapter); | |
} | |
return count; | |
} | |
@Override | |
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) | |
throws OperationApplicationException { | |
int ypCount = 0; | |
int opCount = 0; | |
boolean callerIsSyncAdapter = false; | |
mDb = mOpenHelper.getWritableDatabase(); | |
mDb.beginTransaction(); | |
try { | |
mApplyingBatch.set(true); | |
final int numOperations = operations.size(); | |
final ContentProviderResult[] results = new ContentProviderResult[numOperations]; | |
for (int i = 0; i < numOperations; i++) { | |
if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) { | |
throw new OperationApplicationException( | |
"Too many content provider operations between yield points. " | |
+ "The maximum number of operations per yield point is " | |
+ MAX_OPERATIONS_PER_YIELD_POINT, ypCount); | |
} | |
final ContentProviderOperation operation = operations.get(i); | |
if (!callerIsSyncAdapter && isCallerSyncAdapter(operation.getUri())) { | |
callerIsSyncAdapter = true; | |
} | |
if (i > 0 && operation.isYieldAllowed()) { | |
opCount = 0; | |
if (mDb.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY)) { | |
ypCount++; | |
} | |
} | |
results[i] = operation.apply(this, results, i); | |
} | |
mDb.setTransactionSuccessful(); | |
return results; | |
} finally { | |
mApplyingBatch.set(false); | |
mDb.endTransaction(); | |
onEndTransaction(callerIsSyncAdapter); | |
} | |
} | |
protected void onEndTransaction(boolean callerIsSyncAdapter) { | |
Set<Uri> changed; | |
synchronized (mChangedUris) { | |
changed = new HashSet<Uri>(mChangedUris); | |
mChangedUris.clear(); | |
} | |
ContentResolver resolver = getContext().getContentResolver(); | |
for (Uri uri : changed) { | |
boolean syncToNetwork = !callerIsSyncAdapter && syncToNetwork(uri); | |
resolver.notifyChange(uri, null, syncToNetwork); | |
} | |
} | |
protected boolean syncToNetwork(Uri uri) { | |
return false; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment