1 | // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | package org.chromium.chrome.browser; |
6 | |
7 | import android.app.Activity; |
8 | import android.app.SearchManager; |
9 | import android.content.ContentProvider; |
10 | import android.content.ContentUris; |
11 | import android.content.ContentValues; |
12 | import android.content.Context; |
13 | import android.content.Intent; |
14 | import android.content.SharedPreferences; |
15 | import android.content.UriMatcher; |
16 | import android.database.Cursor; |
17 | import android.database.MatrixCursor; |
18 | import android.graphics.Bitmap; |
19 | import android.net.Uri; |
20 | import android.os.Binder; |
21 | import android.os.Build; |
22 | import android.os.Bundle; |
23 | import android.os.Parcel; |
24 | import android.os.Parcelable; |
25 | import android.preference.PreferenceManager; |
26 | import android.provider.BaseColumns; |
27 | import android.provider.Browser; |
28 | import android.provider.Browser.BookmarkColumns; |
29 | import android.provider.Browser.SearchColumns; |
30 | import android.text.TextUtils; |
31 | import android.util.Log; |
32 | |
33 | import com.google.common.annotations.VisibleForTesting; |
34 | |
35 | import org.chromium.base.CalledByNative; |
36 | import org.chromium.base.CalledByNativeUnchecked; |
37 | import org.chromium.base.ThreadUtils; |
38 | import org.chromium.chrome.browser.database.SQLiteCursor; |
39 | import org.chromium.sync.notifier.SyncStatusHelper; |
40 | |
41 | import java.util.ArrayList; |
42 | import java.util.Arrays; |
43 | import java.util.HashMap; |
44 | import java.util.List; |
45 | import java.util.Vector; |
46 | import java.util.concurrent.atomic.AtomicBoolean; |
47 | |
48 | /** |
49 | * This class provides various information of Chrome, like bookmarks, most |
50 | * visited page etc. It is used to support android.provider.Browser. |
51 | * |
52 | */ |
53 | public class ChromeBrowserProvider extends ContentProvider { |
54 | private static final String TAG = "ChromeBrowserProvider"; |
55 | |
56 | // The permission required for using the bookmark folders API. Android build system does |
57 | // not generate Manifest.java for java libraries, hence use the permission name string. When |
58 | // making changes to this permission, also update the permission in AndroidManifest.xml. |
59 | private static final String PERMISSION_READ_WRITE_BOOKMARKS = "READ_WRITE_BOOKMARK_FOLDERS"; |
60 | |
61 | // Defines the API methods that the Client can call by name. |
62 | static final String CLIENT_API_BOOKMARK_NODE_EXISTS = "BOOKMARK_NODE_EXISTS"; |
63 | static final String CLIENT_API_CREATE_BOOKMARKS_FOLDER_ONCE = "CREATE_BOOKMARKS_FOLDER_ONCE"; |
64 | static final String CLIENT_API_GET_BOOKMARK_FOLDER_HIERARCHY = "GET_BOOKMARK_FOLDER_HIERARCHY"; |
65 | static final String CLIENT_API_GET_BOOKMARK_NODE = "GET_BOOKMARK_NODE"; |
66 | static final String CLIENT_API_GET_DEFAULT_BOOKMARK_FOLDER = "GET_DEFAULT_BOOKMARK_FOLDER"; |
67 | static final String CLIENT_API_GET_MOBILE_BOOKMARKS_FOLDER_ID = |
68 | "GET_MOBILE_BOOKMARKS_FOLDER_ID"; |
69 | static final String CLIENT_API_IS_BOOKMARK_IN_MOBILE_BOOKMARKS_BRANCH = |
70 | "IS_BOOKMARK_IN_MOBILE_BOOKMARKS_BRANCH"; |
71 | static final String CLIENT_API_DELETE_ALL_BOOKMARKS = "DELETE_ALL_BOOKMARKS"; |
72 | static final String CLIENT_API_RESULT_KEY = "result"; |
73 | |
74 | |
75 | // Defines Chrome's API authority, so it can be run and tested |
76 | // independently. |
77 | private static final String API_AUTHORITY_SUFFIX = ".browser"; |
78 | |
79 | private static final String BROWSER_CONTRACT_API_AUTHORITY = |
80 | "com.google.android.apps.chrome.browser-contract"; |
81 | |
82 | // These values are taken from android.provider.BrowserContract.java since |
83 | // that class is hidden from the SDK. |
84 | private static final String BROWSER_CONTRACT_AUTHORITY = "com.android.browser"; |
85 | private static final String BROWSER_CONTRACT_HISTORY_CONTENT_TYPE = |
86 | "vnd.android.cursor.dir/browser-history"; |
87 | private static final String BROWSER_CONTRACT_HISTORY_CONTENT_ITEM_TYPE = |
88 | "vnd.android.cursor.item/browser-history"; |
89 | |
90 | // This Authority is for internal interface. It's concatenated with |
91 | // Context.getPackageName() so that we can install different channels |
92 | // SxS and have different authorities. |
93 | private static final String AUTHORITY_SUFFIX = ".ChromeBrowserProvider"; |
94 | private static final String BOOKMARKS_PATH = "bookmarks"; |
95 | private static final String SEARCHES_PATH = "searches"; |
96 | private static final String HISTORY_PATH = "history"; |
97 | private static final String COMBINED_PATH = "combined"; |
98 | private static final String BOOKMARK_FOLDER_PATH = "hierarchy"; |
99 | |
100 | public static final Uri BROWSER_CONTRACTS_BOOKMAKRS_API_URI = buildContentUri( |
101 | BROWSER_CONTRACT_API_AUTHORITY, BOOKMARKS_PATH); |
102 | |
103 | public static final Uri BROWSER_CONTRACTS_SEARCHES_API_URI = buildContentUri( |
104 | BROWSER_CONTRACT_API_AUTHORITY, SEARCHES_PATH); |
105 | |
106 | public static final Uri BROWSER_CONTRACTS_HISTORY_API_URI = buildContentUri( |
107 | BROWSER_CONTRACT_API_AUTHORITY, HISTORY_PATH); |
108 | |
109 | public static final Uri BROWSER_CONTRACTS_COMBINED_API_URI = buildContentUri( |
110 | BROWSER_CONTRACT_API_AUTHORITY, COMBINED_PATH); |
111 | |
112 | /** The parameter used to specify a bookmark parent ID in ContentValues. */ |
113 | public static final String BOOKMARK_PARENT_ID_PARAM = "parentId"; |
114 | |
115 | /** The parameter used to specify whether this is a bookmark folder. */ |
116 | public static final String BOOKMARK_IS_FOLDER_PARAM = "isFolder"; |
117 | |
118 | /** Invalid id value for the Android ContentProvider API calls. */ |
119 | public static final long INVALID_CONTENT_PROVIDER_ID = 0; |
120 | |
121 | // ID used to indicate an invalid id for bookmark nodes. |
122 | // Client API queries should use ChromeBrowserProviderClient.INVALID_BOOKMARK_ID. |
123 | static final long INVALID_BOOKMARK_ID = -1; |
124 | |
125 | private static final String LAST_MODIFIED_BOOKMARK_FOLDER_ID_KEY = "last_bookmark_folder_id"; |
126 | |
127 | private static final int URI_MATCH_BOOKMARKS = 0; |
128 | private static final int URI_MATCH_BOOKMARKS_ID = 1; |
129 | private static final int URL_MATCH_API_BOOKMARK = 2; |
130 | private static final int URL_MATCH_API_BOOKMARK_ID = 3; |
131 | private static final int URL_MATCH_API_SEARCHES = 4; |
132 | private static final int URL_MATCH_API_SEARCHES_ID = 5; |
133 | private static final int URL_MATCH_API_HISTORY_CONTENT = 6; |
134 | private static final int URL_MATCH_API_HISTORY_CONTENT_ID = 7; |
135 | private static final int URL_MATCH_API_BOOKMARK_CONTENT = 8; |
136 | private static final int URL_MATCH_API_BOOKMARK_CONTENT_ID = 9; |
137 | private static final int URL_MATCH_BOOKMARK_SUGGESTIONS_ID = 10; |
138 | private static final int URL_MATCH_BOOKMARK_HISTORY_SUGGESTIONS_ID = 11; |
139 | |
140 | // TODO : Using Android.provider.Browser.HISTORY_PROJECTION once THUMBNAIL, |
141 | // TOUCH_ICON, and USER_ENTERED fields are supported. |
142 | private static final String[] BOOKMARK_DEFAULT_PROJECTION = new String[] { |
143 | BookmarkColumns._ID, BookmarkColumns.URL, BookmarkColumns.VISITS, |
144 | BookmarkColumns.DATE, BookmarkColumns.BOOKMARK, BookmarkColumns.TITLE, |
145 | BookmarkColumns.FAVICON, BookmarkColumns.CREATED}; |
146 | |
147 | private static final String[] SUGGEST_PROJECTION = new String[] { |
148 | BookmarkColumns._ID, |
149 | BookmarkColumns.TITLE, |
150 | BookmarkColumns.URL, |
151 | BookmarkColumns.DATE, |
152 | BookmarkColumns.BOOKMARK |
153 | }; |
154 | |
155 | private final Object mInitializeUriMatcherLock = new Object(); |
156 | private final Object mLoadNativeLock = new Object(); |
157 | private UriMatcher mUriMatcher; |
158 | private long mLastModifiedBookmarkFolderId = INVALID_BOOKMARK_ID; |
159 | private int mNativeChromeBrowserProvider; |
160 | private BookmarkNode mMobileBookmarksFolder; |
161 | |
162 | /** |
163 | * Records whether we've received a call to one of the public ContentProvider APIs. |
164 | */ |
165 | protected boolean mContentProviderApiCalled; |
166 | |
167 | private void ensureUriMatcherInitialized() { |
168 | synchronized (mInitializeUriMatcherLock) { |
169 | if (mUriMatcher != null) return; |
170 | |
171 | mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); |
172 | // The internal URIs |
173 | String authority = getContext().getPackageName() + AUTHORITY_SUFFIX; |
174 | mUriMatcher.addURI(authority, BOOKMARKS_PATH, URI_MATCH_BOOKMARKS); |
175 | mUriMatcher.addURI(authority, BOOKMARKS_PATH + "/#", URI_MATCH_BOOKMARKS_ID); |
176 | // The internal authority for public APIs |
177 | String apiAuthority = getContext().getPackageName() + API_AUTHORITY_SUFFIX; |
178 | mUriMatcher.addURI(apiAuthority, BOOKMARKS_PATH, URL_MATCH_API_BOOKMARK); |
179 | mUriMatcher.addURI(apiAuthority, BOOKMARKS_PATH + "/#", URL_MATCH_API_BOOKMARK_ID); |
180 | mUriMatcher.addURI(apiAuthority, SEARCHES_PATH, URL_MATCH_API_SEARCHES); |
181 | mUriMatcher.addURI(apiAuthority, SEARCHES_PATH + "/#", URL_MATCH_API_SEARCHES_ID); |
182 | mUriMatcher.addURI(apiAuthority, HISTORY_PATH, URL_MATCH_API_HISTORY_CONTENT); |
183 | mUriMatcher.addURI(apiAuthority, HISTORY_PATH + "/#", URL_MATCH_API_HISTORY_CONTENT_ID); |
184 | mUriMatcher.addURI(apiAuthority, COMBINED_PATH, URL_MATCH_API_BOOKMARK); |
185 | mUriMatcher.addURI(apiAuthority, COMBINED_PATH + "/#", URL_MATCH_API_BOOKMARK_ID); |
186 | // The internal authority for BrowserContracts |
187 | mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, HISTORY_PATH, |
188 | URL_MATCH_API_HISTORY_CONTENT); |
189 | mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, HISTORY_PATH + "/#", |
190 | URL_MATCH_API_HISTORY_CONTENT_ID); |
191 | mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, COMBINED_PATH, |
192 | URL_MATCH_API_BOOKMARK); |
193 | mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, COMBINED_PATH + "/#", |
194 | URL_MATCH_API_BOOKMARK_ID); |
195 | mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, SEARCHES_PATH, |
196 | URL_MATCH_API_SEARCHES); |
197 | mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, SEARCHES_PATH + "/#", |
198 | URL_MATCH_API_SEARCHES_ID); |
199 | mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, BOOKMARKS_PATH, |
200 | URL_MATCH_API_BOOKMARK_CONTENT); |
201 | mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, BOOKMARKS_PATH + "/#", |
202 | URL_MATCH_API_BOOKMARK_CONTENT_ID); |
203 | // Added the Android Framework URIs, so the provider can easily switched |
204 | // by adding 'browser' and 'com.android.browser' in manifest. |
205 | // The Android's BrowserContract |
206 | mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, HISTORY_PATH, |
207 | URL_MATCH_API_HISTORY_CONTENT); |
208 | mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, HISTORY_PATH + "/#", |
209 | URL_MATCH_API_HISTORY_CONTENT_ID); |
210 | mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, "combined", URL_MATCH_API_BOOKMARK); |
211 | mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, "combined/#", URL_MATCH_API_BOOKMARK_ID); |
212 | mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, SEARCHES_PATH, URL_MATCH_API_SEARCHES); |
213 | mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, SEARCHES_PATH + "/#", |
214 | URL_MATCH_API_SEARCHES_ID); |
215 | mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, BOOKMARKS_PATH, |
216 | URL_MATCH_API_BOOKMARK_CONTENT); |
217 | mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, BOOKMARKS_PATH + "/#", |
218 | URL_MATCH_API_BOOKMARK_CONTENT_ID); |
219 | // For supporting android.provider.browser.BookmarkColumns and |
220 | // SearchColumns |
221 | mUriMatcher.addURI("browser", BOOKMARKS_PATH, URL_MATCH_API_BOOKMARK); |
222 | mUriMatcher.addURI("browser", BOOKMARKS_PATH + "/#", URL_MATCH_API_BOOKMARK_ID); |
223 | mUriMatcher.addURI("browser", SEARCHES_PATH, URL_MATCH_API_SEARCHES); |
224 | mUriMatcher.addURI("browser", SEARCHES_PATH + "/#", URL_MATCH_API_SEARCHES_ID); |
225 | |
226 | mUriMatcher.addURI(apiAuthority, |
227 | BOOKMARKS_PATH + "/" + SearchManager.SUGGEST_URI_PATH_QUERY, |
228 | URL_MATCH_BOOKMARK_SUGGESTIONS_ID); |
229 | mUriMatcher.addURI(apiAuthority, |
230 | SearchManager.SUGGEST_URI_PATH_QUERY, |
231 | URL_MATCH_BOOKMARK_HISTORY_SUGGESTIONS_ID); |
232 | } |
233 | } |
234 | |
235 | @Override |
236 | public boolean onCreate() { |
237 | // Pre-load shared preferences object, this happens on a separate thread |
238 | PreferenceManager.getDefaultSharedPreferences(getContext()); |
239 | return true; |
240 | } |
241 | |
242 | /** |
243 | * Lazily fetches the last modified bookmark folder id. |
244 | */ |
245 | private long getLastModifiedBookmarkFolderId() { |
246 | if (mLastModifiedBookmarkFolderId == INVALID_BOOKMARK_ID) { |
247 | SharedPreferences sharedPreferences = |
248 | PreferenceManager.getDefaultSharedPreferences(getContext()); |
249 | mLastModifiedBookmarkFolderId = sharedPreferences.getLong( |
250 | LAST_MODIFIED_BOOKMARK_FOLDER_ID_KEY, INVALID_BOOKMARK_ID); |
251 | } |
252 | return mLastModifiedBookmarkFolderId; |
253 | } |
254 | |
255 | private String buildSuggestWhere(String selection, int argc) { |
256 | StringBuilder sb = new StringBuilder(selection); |
257 | for (int i = 0; i < argc - 1; i++) { |
258 | sb.append(" OR "); |
259 | sb.append(selection); |
260 | } |
261 | return sb.toString(); |
262 | } |
263 | |
264 | private String getReadWritePermissionNameForBookmarkFolders() { |
265 | return getContext().getApplicationContext().getPackageName() + ".permission." |
266 | + PERMISSION_READ_WRITE_BOOKMARKS; |
267 | } |
268 | |
269 | private Cursor getBookmarkHistorySuggestions(String selection, String[] selectionArgs, |
270 | String sortOrder, boolean excludeHistory) { |
271 | boolean matchTitles = false; |
272 | Vector<String> args = new Vector<String>(); |
273 | String like = selectionArgs[0] + "%"; |
274 | if (selectionArgs[0].startsWith("http") || selectionArgs[0].startsWith("file")) { |
275 | args.add(like); |
276 | } else { |
277 | // Match against common URL prefixes. |
278 | args.add("http://" + like); |
279 | args.add("https://" + like); |
280 | args.add("http://www." + like); |
281 | args.add("https://www." + like); |
282 | args.add("file://" + like); |
283 | matchTitles = true; |
284 | } |
285 | |
286 | StringBuilder urlWhere = new StringBuilder("("); |
287 | urlWhere.append(buildSuggestWhere(selection, args.size())); |
288 | if (matchTitles) { |
289 | args.add(like); |
290 | urlWhere.append(" OR title LIKE ?"); |
291 | } |
292 | urlWhere.append(")"); |
293 | |
294 | if (excludeHistory) { |
295 | urlWhere.append(" AND bookmark=?"); |
296 | args.add("1"); |
297 | } |
298 | |
299 | selectionArgs = args.toArray(selectionArgs); |
300 | Cursor cursor = queryBookmarkFromAPI(SUGGEST_PROJECTION, urlWhere.toString(), |
301 | selectionArgs, sortOrder); |
302 | return new ChromeBrowserProviderSuggestionsCursor(cursor); |
303 | } |
304 | |
305 | @Override |
306 | public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, |
307 | String sortOrder) { |
308 | if (!canHandleContentProviderApiCall()) return null; |
309 | |
310 | // Check for invalid id values if provided. |
311 | // If it represents a bookmark node then it's the root node. Don't provide access here. |
312 | // Otherwise it represents a SQLite row id, so 0 is invalid. |
313 | long bookmarkId = INVALID_CONTENT_PROVIDER_ID; |
314 | try { |
315 | bookmarkId = ContentUris.parseId(uri); |
316 | if (bookmarkId == INVALID_CONTENT_PROVIDER_ID) return null; |
317 | } catch (Exception e) { |
318 | } |
319 | |
320 | int match = mUriMatcher.match(uri); |
321 | Cursor cursor = null; |
322 | switch (match) { |
323 | case URL_MATCH_BOOKMARK_SUGGESTIONS_ID: |
324 | cursor = getBookmarkHistorySuggestions(selection, selectionArgs, sortOrder, true); |
325 | break; |
326 | case URL_MATCH_BOOKMARK_HISTORY_SUGGESTIONS_ID: |
327 | cursor = getBookmarkHistorySuggestions(selection, selectionArgs, sortOrder, false); |
328 | break; |
329 | case URL_MATCH_API_BOOKMARK: |
330 | cursor = queryBookmarkFromAPI(projection, selection, selectionArgs, sortOrder); |
331 | break; |
332 | case URL_MATCH_API_BOOKMARK_ID: |
333 | cursor = queryBookmarkFromAPI(projection, buildWhereClause(bookmarkId, selection), |
334 | selectionArgs, sortOrder); |
335 | break; |
336 | case URL_MATCH_API_SEARCHES: |
337 | cursor = querySearchTermFromAPI(projection, selection, selectionArgs, sortOrder); |
338 | break; |
339 | case URL_MATCH_API_SEARCHES_ID: |
340 | cursor = querySearchTermFromAPI(projection, buildWhereClause(bookmarkId, selection), |
341 | selectionArgs, sortOrder); |
342 | break; |
343 | case URL_MATCH_API_HISTORY_CONTENT: |
344 | cursor = queryBookmarkFromAPI(projection, buildHistoryWhereClause(selection), |
345 | selectionArgs, sortOrder); |
346 | break; |
347 | case URL_MATCH_API_HISTORY_CONTENT_ID: |
348 | cursor = queryBookmarkFromAPI(projection, |
349 | buildHistoryWhereClause(bookmarkId, selection), selectionArgs, sortOrder); |
350 | break; |
351 | case URL_MATCH_API_BOOKMARK_CONTENT: |
352 | cursor = queryBookmarkFromAPI(projection, buildBookmarkWhereClause(selection), |
353 | selectionArgs, sortOrder); |
354 | break; |
355 | case URL_MATCH_API_BOOKMARK_CONTENT_ID: |
356 | cursor = queryBookmarkFromAPI(projection, |
357 | buildBookmarkWhereClause(bookmarkId, selection), selectionArgs, sortOrder); |
358 | break; |
359 | default: |
360 | throw new IllegalArgumentException(TAG + ": query - unknown URL uri = " + uri); |
361 | } |
362 | if (cursor == null) { |
363 | cursor = new MatrixCursor(new String[] { }); |
364 | } |
365 | cursor.setNotificationUri(getContext().getContentResolver(), uri); |
366 | return cursor; |
367 | } |
368 | |
369 | @Override |
370 | public Uri insert(Uri uri, ContentValues values) { |
371 | if (!canHandleContentProviderApiCall()) return null; |
372 | |
373 | int match = mUriMatcher.match(uri); |
374 | Uri res = null; |
375 | long id; |
376 | switch (match) { |
377 | case URI_MATCH_BOOKMARKS: |
378 | id = addBookmark(values); |
379 | if (id == INVALID_BOOKMARK_ID) return null; |
380 | break; |
381 | case URL_MATCH_API_BOOKMARK_CONTENT: |
382 | values.put(BookmarkColumns.BOOKMARK, 1); |
383 | //$FALL-THROUGH$ |
384 | case URL_MATCH_API_BOOKMARK: |
385 | case URL_MATCH_API_HISTORY_CONTENT: |
386 | id = addBookmarkFromAPI(values); |
387 | if (id == INVALID_CONTENT_PROVIDER_ID) return null; |
388 | break; |
389 | case URL_MATCH_API_SEARCHES: |
390 | id = addSearchTermFromAPI(values); |
391 | if (id == INVALID_CONTENT_PROVIDER_ID) return null; |
392 | break; |
393 | default: |
394 | throw new IllegalArgumentException(TAG + ": insert - unknown URL " + uri); |
395 | } |
396 | |
397 | res = ContentUris.withAppendedId(uri, id); |
398 | getContext().getContentResolver().notifyChange(res, null); |
399 | return res; |
400 | } |
401 | |
402 | @Override |
403 | public int delete(Uri uri, String selection, String[] selectionArgs) { |
404 | if (!canHandleContentProviderApiCall()) return 0; |
405 | |
406 | // Check for invalid id values if provided. |
407 | // If it represents a bookmark node then it's the root node and not mutable. |
408 | // Otherwise it represents a SQLite row id, so 0 is invalid. |
409 | long bookmarkId = INVALID_CONTENT_PROVIDER_ID; |
410 | try { |
411 | bookmarkId = ContentUris.parseId(uri); |
412 | if (bookmarkId == INVALID_CONTENT_PROVIDER_ID) return 0; |
413 | } catch (Exception e) { |
414 | } |
415 | |
416 | int match = mUriMatcher.match(uri); |
417 | int result; |
418 | switch (match) { |
419 | case URI_MATCH_BOOKMARKS_ID : |
420 | result = nativeRemoveBookmark(mNativeChromeBrowserProvider, bookmarkId); |
421 | break; |
422 | case URL_MATCH_API_BOOKMARK_ID: |
423 | result = removeBookmarkFromAPI( |
424 | buildWhereClause(bookmarkId, selection), selectionArgs); |
425 | break; |
426 | case URL_MATCH_API_BOOKMARK: |
427 | result = removeBookmarkFromAPI(selection, selectionArgs); |
428 | break; |
429 | case URL_MATCH_API_SEARCHES_ID: |
430 | result = removeSearchFromAPI(buildWhereClause(bookmarkId, selection), |
431 | selectionArgs); |
432 | break; |
433 | case URL_MATCH_API_SEARCHES: |
434 | result = removeSearchFromAPI(selection, selectionArgs); |
435 | break; |
436 | case URL_MATCH_API_HISTORY_CONTENT: |
437 | result = removeHistoryFromAPI(selection, selectionArgs); |
438 | break; |
439 | case URL_MATCH_API_HISTORY_CONTENT_ID: |
440 | result = removeHistoryFromAPI(buildWhereClause(bookmarkId, selection), |
441 | selectionArgs); |
442 | break; |
443 | case URL_MATCH_API_BOOKMARK_CONTENT: |
444 | result = removeBookmarkFromAPI(buildBookmarkWhereClause(selection), selectionArgs); |
445 | break; |
446 | case URL_MATCH_API_BOOKMARK_CONTENT_ID: |
447 | result = removeBookmarkFromAPI(buildBookmarkWhereClause(bookmarkId, selection), |
448 | selectionArgs); |
449 | break; |
450 | default: |
451 | throw new IllegalArgumentException(TAG + ": delete - unknown URL " + uri); |
452 | } |
453 | if (result != 0) { |
454 | getContext().getContentResolver().notifyChange(uri, null); |
455 | } |
456 | return result; |
457 | } |
458 | |
459 | @Override |
460 | public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { |
461 | if (!canHandleContentProviderApiCall()) return 0; |
462 | |
463 | // Check for invalid id values if provided. |
464 | // If it represents a bookmark node then it's the root node and not mutable. |
465 | // Otherwise it represents a SQLite row id, so 0 is invalid. |
466 | long bookmarkId = INVALID_CONTENT_PROVIDER_ID; |
467 | try { |
468 | bookmarkId = ContentUris.parseId(uri); |
469 | if (bookmarkId == INVALID_CONTENT_PROVIDER_ID) return 0; |
470 | } catch (Exception e) { |
471 | } |
472 | |
473 | int match = mUriMatcher.match(uri); |
474 | int result; |
475 | switch (match) { |
476 | case URI_MATCH_BOOKMARKS_ID: |
477 | String url = null; |
478 | if (values.containsKey(Browser.BookmarkColumns.URL)) { |
479 | url = values.getAsString(Browser.BookmarkColumns.URL); |
480 | } |
481 | String title = values.getAsString(Browser.BookmarkColumns.TITLE); |
482 | long parentId = INVALID_BOOKMARK_ID; |
483 | if (values.containsKey(BOOKMARK_PARENT_ID_PARAM)) { |
484 | parentId = values.getAsLong(BOOKMARK_PARENT_ID_PARAM); |
485 | } |
486 | result = nativeUpdateBookmark(mNativeChromeBrowserProvider, bookmarkId, url, title, |
487 | parentId); |
488 | updateLastModifiedBookmarkFolder(parentId); |
489 | break; |
490 | case URL_MATCH_API_BOOKMARK_ID: |
491 | result = updateBookmarkFromAPI(values, buildWhereClause(bookmarkId, selection), |
492 | selectionArgs); |
493 | break; |
494 | case URL_MATCH_API_BOOKMARK: |
495 | result = updateBookmarkFromAPI(values, selection, selectionArgs); |
496 | break; |
497 | case URL_MATCH_API_SEARCHES_ID: |
498 | result = updateSearchTermFromAPI(values, buildWhereClause(bookmarkId, selection), |
499 | selectionArgs); |
500 | break; |
501 | case URL_MATCH_API_SEARCHES: |
502 | result = updateSearchTermFromAPI(values, selection, selectionArgs); |
503 | break; |
504 | case URL_MATCH_API_HISTORY_CONTENT: |
505 | result = updateBookmarkFromAPI(values, buildHistoryWhereClause(selection), |
506 | selectionArgs); |
507 | break; |
508 | case URL_MATCH_API_HISTORY_CONTENT_ID: |
509 | result = updateBookmarkFromAPI(values, |
510 | buildHistoryWhereClause(bookmarkId, selection), selectionArgs); |
511 | break; |
512 | case URL_MATCH_API_BOOKMARK_CONTENT: |
513 | result = updateBookmarkFromAPI(values, buildBookmarkWhereClause(selection), |
514 | selectionArgs); |
515 | break; |
516 | case URL_MATCH_API_BOOKMARK_CONTENT_ID: |
517 | result = updateBookmarkFromAPI(values, |
518 | buildBookmarkWhereClause(bookmarkId, selection), selectionArgs); |
519 | break; |
520 | default: |
521 | throw new IllegalArgumentException(TAG + ": update - unknown URL " + uri); |
522 | } |
523 | if (result != 0) { |
524 | getContext().getContentResolver().notifyChange(uri, null); |
525 | } |
526 | return result; |
527 | } |
528 | |
529 | @Override |
530 | public String getType(Uri uri) { |
531 | ensureUriMatcherInitialized(); |
532 | int match = mUriMatcher.match(uri); |
533 | switch (match) { |
534 | case URI_MATCH_BOOKMARKS: |
535 | case URL_MATCH_API_BOOKMARK: |
536 | return "vnd.android.cursor.dir/bookmark"; |
537 | case URI_MATCH_BOOKMARKS_ID: |
538 | case URL_MATCH_API_BOOKMARK_ID: |
539 | return "vnd.android.cursor.item/bookmark"; |
540 | case URL_MATCH_API_SEARCHES: |
541 | return "vnd.android.cursor.dir/searches"; |
542 | case URL_MATCH_API_SEARCHES_ID: |
543 | return "vnd.android.cursor.item/searches"; |
544 | case URL_MATCH_API_HISTORY_CONTENT: |
545 | return BROWSER_CONTRACT_HISTORY_CONTENT_TYPE; |
546 | case URL_MATCH_API_HISTORY_CONTENT_ID: |
547 | return BROWSER_CONTRACT_HISTORY_CONTENT_ITEM_TYPE; |
548 | default: |
549 | throw new IllegalArgumentException(TAG + ": getType - unknown URL " + uri); |
550 | } |
551 | } |
552 | |
553 | private long addBookmark(ContentValues values) { |
554 | String url = values.getAsString(Browser.BookmarkColumns.URL); |
555 | String title = values.getAsString(Browser.BookmarkColumns.TITLE); |
556 | boolean isFolder = false; |
557 | if (values.containsKey(BOOKMARK_IS_FOLDER_PARAM)) { |
558 | isFolder = values.getAsBoolean(BOOKMARK_IS_FOLDER_PARAM); |
559 | } |
560 | long parentId = INVALID_BOOKMARK_ID; |
561 | if (values.containsKey(BOOKMARK_PARENT_ID_PARAM)) { |
562 | parentId = values.getAsLong(BOOKMARK_PARENT_ID_PARAM); |
563 | } |
564 | long id = nativeAddBookmark(mNativeChromeBrowserProvider, url, title, isFolder, parentId); |
565 | if (id == INVALID_BOOKMARK_ID) return id; |
566 | |
567 | if (isFolder) { |
568 | updateLastModifiedBookmarkFolder(id); |
569 | } else { |
570 | updateLastModifiedBookmarkFolder(parentId); |
571 | } |
572 | return id; |
573 | } |
574 | |
575 | private void updateLastModifiedBookmarkFolder(long id) { |
576 | if (getLastModifiedBookmarkFolderId() == id) return; |
577 | |
578 | mLastModifiedBookmarkFolderId = id; |
579 | SharedPreferences sharedPreferences = |
580 | PreferenceManager.getDefaultSharedPreferences(getContext()); |
581 | sharedPreferences.edit() |
582 | .putLong(LAST_MODIFIED_BOOKMARK_FOLDER_ID_KEY, mLastModifiedBookmarkFolderId) |
583 | .apply(); |
584 | } |
585 | |
586 | public static String getApiAuthority(Context context) { |
587 | return context.getPackageName() + API_AUTHORITY_SUFFIX; |
588 | } |
589 | |
590 | public static String getInternalAuthority(Context context) { |
591 | return context.getPackageName() + AUTHORITY_SUFFIX; |
592 | } |
593 | |
594 | public static Uri getBookmarksUri(Context context) { |
595 | return buildContentUri(getInternalAuthority(context), BOOKMARKS_PATH); |
596 | } |
597 | |
598 | public static Uri getBookmarkFolderUri(Context context) { |
599 | return buildContentUri(getInternalAuthority(context), BOOKMARK_FOLDER_PATH); |
600 | } |
601 | |
602 | public static Uri getBookmarksApiUri(Context context) { |
603 | return buildContentUri(getApiAuthority(context), BOOKMARKS_PATH); |
604 | } |
605 | |
606 | public static Uri getSearchesApiUri(Context context) { |
607 | return buildContentUri(getApiAuthority(context), SEARCHES_PATH); |
608 | } |
609 | |
610 | private boolean bookmarkNodeExists(long nodeId) { |
611 | if (nodeId < 0) return false; |
612 | return nativeBookmarkNodeExists(mNativeChromeBrowserProvider, nodeId); |
613 | } |
614 | |
615 | private long createBookmarksFolderOnce(String title, long parentId) { |
616 | return nativeCreateBookmarksFolderOnce(mNativeChromeBrowserProvider, title, parentId); |
617 | } |
618 | |
619 | private BookmarkNode getBookmarkFolderHierarchy() { |
620 | return nativeGetAllBookmarkFolders(mNativeChromeBrowserProvider); |
621 | } |
622 | |
623 | protected BookmarkNode getBookmarkNode(long nodeId, boolean getParent, boolean getChildren, |
624 | boolean getFavicons, boolean getThumbnails) { |
625 | // Don't allow going up the hierarchy if sync is disabled and the requested node |
626 | // is the Mobile Bookmarks folder. |
627 | if (getParent && nodeId == getMobileBookmarksFolderId() |
628 | && !SyncStatusHelper.get(getContext()).isSyncEnabled()) { |
629 | getParent = false; |
630 | } |
631 | |
632 | BookmarkNode node = nativeGetBookmarkNode(mNativeChromeBrowserProvider, nodeId, getParent, |
633 | getChildren); |
634 | if (!getFavicons && !getThumbnails) return node; |
635 | |
636 | // Favicons and thumbnails need to be populated separately as they are provided |
637 | // asynchronously by Chromium services other than the bookmark model. |
638 | if (node.parent() != null) populateNodeImages(node.parent(), getFavicons, getThumbnails); |
639 | for (BookmarkNode child : node.children()) { |
640 | populateNodeImages(child, getFavicons, getThumbnails); |
641 | } |
642 | |
643 | return node; |
644 | } |
645 | |
646 | private BookmarkNode getDefaultBookmarkFolder() { |
647 | // Try to access the bookmark folder last modified by us. If it doesn't exist anymore |
648 | // then use the synced node (Mobile Bookmarks). |
649 | BookmarkNode lastModified = getBookmarkNode(getLastModifiedBookmarkFolderId(), false, false, |
650 | false, false); |
651 | if (lastModified == null) { |
652 | lastModified = getMobileBookmarksFolder(); |
653 | mLastModifiedBookmarkFolderId = lastModified != null ? lastModified.id() : |
654 | INVALID_BOOKMARK_ID; |
655 | } |
656 | return lastModified; |
657 | } |
658 | |
659 | private void populateNodeImages(BookmarkNode node, boolean favicon, boolean thumbnail) { |
660 | if (node == null || node.type() != Type.URL) return; |
661 | |
662 | if (favicon) { |
663 | node.setFavicon(nativeGetFaviconOrTouchIcon(mNativeChromeBrowserProvider, node.url())); |
664 | } |
665 | |
666 | if (thumbnail) { |
667 | node.setThumbnail(nativeGetThumbnail(mNativeChromeBrowserProvider, node.url())); |
668 | } |
669 | } |
670 | |
671 | private BookmarkNode getMobileBookmarksFolder() { |
672 | if (mMobileBookmarksFolder == null) { |
673 | mMobileBookmarksFolder = nativeGetMobileBookmarksFolder(mNativeChromeBrowserProvider); |
674 | } |
675 | return mMobileBookmarksFolder; |
676 | } |
677 | |
678 | protected long getMobileBookmarksFolderId() { |
679 | BookmarkNode mobileBookmarks = getMobileBookmarksFolder(); |
680 | return mobileBookmarks != null ? mobileBookmarks.id() : INVALID_BOOKMARK_ID; |
681 | } |
682 | |
683 | private boolean isBookmarkInMobileBookmarksBranch(long nodeId) { |
684 | if (nodeId <= 0) return false; |
685 | return nativeIsBookmarkInMobileBookmarksBranch(mNativeChromeBrowserProvider, nodeId); |
686 | } |
687 | |
688 | static String argKey(int i) { |
689 | return "arg" + i; |
690 | } |
691 | |
692 | @Override |
693 | public Bundle call(String method, String arg, Bundle extras) { |
694 | // TODO(shashishekhar): Refactor this code into a separate class. |
695 | // Caller must have the READ_WRITE_BOOKMARK_FOLDERS permission. |
696 | getContext().enforcePermission(getReadWritePermissionNameForBookmarkFolders(), |
697 | Binder.getCallingPid(), Binder.getCallingUid(), TAG); |
698 | if (!canHandleContentProviderApiCall()) return null; |
699 | if (method == null || extras == null) return null; |
700 | |
701 | Bundle result = new Bundle(); |
702 | if (CLIENT_API_BOOKMARK_NODE_EXISTS.equals(method)) { |
703 | result.putBoolean(CLIENT_API_RESULT_KEY, |
704 | bookmarkNodeExists(extras.getLong(argKey(0)))); |
705 | } else if (CLIENT_API_CREATE_BOOKMARKS_FOLDER_ONCE.equals(method)) { |
706 | result.putLong(CLIENT_API_RESULT_KEY, |
707 | createBookmarksFolderOnce(extras.getString(argKey(0)), |
708 | extras.getLong(argKey(1)))); |
709 | } else if (CLIENT_API_GET_BOOKMARK_FOLDER_HIERARCHY.equals(method)) { |
710 | result.putParcelable(CLIENT_API_RESULT_KEY, getBookmarkFolderHierarchy()); |
711 | } else if (CLIENT_API_GET_BOOKMARK_NODE.equals(method)) { |
712 | result.putParcelable(CLIENT_API_RESULT_KEY, |
713 | getBookmarkNode(extras.getLong(argKey(0)), |
714 | extras.getBoolean(argKey(1)), |
715 | extras.getBoolean(argKey(2)), |
716 | extras.getBoolean(argKey(3)), |
717 | extras.getBoolean(argKey(4)))); |
718 | } else if (CLIENT_API_GET_DEFAULT_BOOKMARK_FOLDER.equals(method)) { |
719 | result.putParcelable(CLIENT_API_RESULT_KEY, getDefaultBookmarkFolder()); |
720 | } else if (method.equals(CLIENT_API_GET_MOBILE_BOOKMARKS_FOLDER_ID)) { |
721 | result.putLong(CLIENT_API_RESULT_KEY, getMobileBookmarksFolderId()); |
722 | } else if (CLIENT_API_IS_BOOKMARK_IN_MOBILE_BOOKMARKS_BRANCH.equals(method)) { |
723 | result.putBoolean(CLIENT_API_RESULT_KEY, |
724 | isBookmarkInMobileBookmarksBranch(extras.getLong(argKey(0)))); |
725 | } else if(CLIENT_API_DELETE_ALL_BOOKMARKS.equals(method)) { |
726 | nativeRemoveAllBookmarks(mNativeChromeBrowserProvider); |
727 | } else { |
728 | Log.w(TAG, "Received invalid method " + method); |
729 | return null; |
730 | } |
731 | |
732 | return result; |
733 | } |
734 | |
735 | /** |
736 | * Checks whether Chrome is sufficiently initialized to handle a call to the |
737 | * ChromeBrowserProvider. |
738 | */ |
739 | private boolean canHandleContentProviderApiCall() { |
740 | mContentProviderApiCalled = true; |
741 | |
742 | if (isInUiThread()) return false; |
743 | if (!ensureNativeChromeLoaded()) return false; |
744 | return true; |
745 | } |
746 | |
747 | /** |
748 | * The type of a BookmarkNode. |
749 | */ |
750 | public enum Type { |
751 | URL, |
752 | FOLDER, |
753 | BOOKMARK_BAR, |
754 | OTHER_NODE, |
755 | MOBILE |
756 | } |
757 | |
758 | /** |
759 | * Simple Data Object representing the chrome bookmark node. |
760 | */ |
761 | public static class BookmarkNode implements Parcelable { |
762 | private final long mId; |
763 | private final String mName; |
764 | private final String mUrl; |
765 | private final Type mType; |
766 | private final BookmarkNode mParent; |
767 | private final List<BookmarkNode> mChildren = new ArrayList<BookmarkNode>(); |
768 | |
769 | // Favicon and thumbnail optionally set in a 2-step procedure. |
770 | private byte[] mFavicon; |
771 | private byte[] mThumbnail; |
772 | |
773 | /** Used to pass structured data back from the native code. */ |
774 | @VisibleForTesting |
775 | public BookmarkNode(long id, Type type, String name, String url, BookmarkNode parent) { |
776 | mId = id; |
777 | mName = name; |
778 | mUrl = url; |
779 | mType = type; |
780 | mParent = parent; |
781 | } |
782 | |
783 | /** |
784 | * @return The id of this bookmark entry. |
785 | */ |
786 | public long id() { |
787 | return mId; |
788 | } |
789 | |
790 | /** |
791 | * @return The name of this bookmark entry. |
792 | */ |
793 | public String name() { |
794 | return mName; |
795 | } |
796 | |
797 | /** |
798 | * @return The URL of this bookmark entry. |
799 | */ |
800 | public String url() { |
801 | return mUrl; |
802 | } |
803 | |
804 | /** |
805 | * @return The type of this bookmark entry. |
806 | */ |
807 | public Type type() { |
808 | return mType; |
809 | } |
810 | |
811 | /** |
812 | * @return The bookmark favicon, if any. |
813 | */ |
814 | public byte[] favicon() { |
815 | return mFavicon; |
816 | } |
817 | |
818 | /** |
819 | * @return The bookmark thumbnail, if any. |
820 | */ |
821 | public byte[] thumbnail() { |
822 | return mThumbnail; |
823 | } |
824 | |
825 | /** |
826 | * @return The parent folder of this bookmark entry. |
827 | */ |
828 | public BookmarkNode parent() { |
829 | return mParent; |
830 | } |
831 | |
832 | /** |
833 | * Adds a child to this node. |
834 | * |
835 | * <p> |
836 | * Used solely by the native code. |
837 | */ |
838 | @VisibleForTesting |
839 | @CalledByNativeUnchecked("BookmarkNode") |
840 | public void addChild(BookmarkNode child) { |
841 | mChildren.add(child); |
842 | } |
843 | |
844 | /** |
845 | * @return The child bookmark nodes of this node. |
846 | */ |
847 | public List<BookmarkNode> children() { |
848 | return mChildren; |
849 | } |
850 | |
851 | /** |
852 | * @return Whether this node represents a bookmarked URL or not. |
853 | */ |
854 | public boolean isUrl() { |
855 | return mUrl != null; |
856 | } |
857 | |
858 | /** |
859 | * @return true if the two individual nodes contain the same information. |
860 | * The existence of parent and children nodes is checked, but their contents are not. |
861 | */ |
862 | public boolean equalContents(BookmarkNode node) { |
863 | return node != null && |
864 | mId == node.mId && |
865 | !(mName == null ^ node.mName == null) && |
866 | (mName == null || mName.equals(node.mName)) && |
867 | !(mUrl == null ^ node.mUrl == null) && |
868 | (mUrl == null || mUrl.equals(node.mUrl)) && |
869 | mType == node.mType && |
870 | byteArrayEqual(mFavicon, node.mFavicon) && |
871 | byteArrayEqual(mThumbnail, node.mThumbnail) && |
872 | !(mParent == null ^ node.mParent == null) && |
873 | children().size() == node.children().size(); |
874 | } |
875 | |
876 | private static boolean byteArrayEqual(byte[] byte1, byte[] byte2) { |
877 | if (byte1 == null && byte2 != null) return byte2.length == 0; |
878 | if (byte2 == null && byte1 != null) return byte1.length == 0; |
879 | return Arrays.equals(byte1, byte2); |
880 | } |
881 | |
882 | @CalledByNative("BookmarkNode") |
883 | private static BookmarkNode create( |
884 | long id, int type, String name, String url, BookmarkNode parent) { |
885 | return new BookmarkNode(id, Type.values()[type], name, url, parent); |
886 | } |
887 | |
888 | @VisibleForTesting |
889 | public void setFavicon(byte[] favicon) { |
890 | mFavicon = favicon; |
891 | } |
892 | |
893 | @VisibleForTesting |
894 | public void setThumbnail(byte[] thumbnail) { |
895 | mThumbnail = thumbnail; |
896 | } |
897 | |
898 | @Override |
899 | public int describeContents() { |
900 | return 0; |
901 | } |
902 | |
903 | @Override |
904 | public void writeToParcel(Parcel dest, int flags) { |
905 | // Write the current node id. |
906 | dest.writeLong(mId); |
907 | |
908 | // Serialize the full hierarchy from the root. |
909 | getHierarchyRoot().writeNodeContentsRecursive(dest); |
910 | } |
911 | |
912 | @VisibleForTesting |
913 | public BookmarkNode getHierarchyRoot() { |
914 | BookmarkNode root = this; |
915 | while (root.parent() != null) { |
916 | root = root.parent(); |
917 | } |
918 | return root; |
919 | } |
920 | |
921 | private void writeNodeContentsRecursive(Parcel dest) { |
922 | writeNodeContents(dest); |
923 | dest.writeInt(mChildren.size()); |
924 | for (BookmarkNode child : mChildren) { |
925 | child.writeNodeContentsRecursive(dest); |
926 | } |
927 | } |
928 | |
929 | private void writeNodeContents(Parcel dest) { |
930 | dest.writeLong(mId); |
931 | dest.writeString(mName); |
932 | dest.writeString(mUrl); |
933 | dest.writeInt(mType.ordinal()); |
934 | dest.writeByteArray(mFavicon); |
935 | dest.writeByteArray(mThumbnail); |
936 | dest.writeLong(mParent != null ? mParent.mId : INVALID_BOOKMARK_ID); |
937 | } |
938 | |
939 | public static final Creator<BookmarkNode> CREATOR = new Creator<BookmarkNode>() { |
940 | private HashMap<Long, BookmarkNode> mNodeMap; |
941 | |
942 | @Override |
943 | public BookmarkNode createFromParcel(Parcel source) { |
944 | mNodeMap = new HashMap<Long, BookmarkNode>(); |
945 | long currentNodeId = source.readLong(); |
946 | readNodeContentsRecursive(source); |
947 | BookmarkNode node = getNode(currentNodeId); |
948 | mNodeMap.clear(); |
949 | return node; |
950 | } |
951 | |
952 | @Override |
953 | public BookmarkNode[] newArray(int size) { |
954 | return new BookmarkNode[size]; |
955 | } |
956 | |
957 | private BookmarkNode getNode(long id) { |
958 | if (id == INVALID_BOOKMARK_ID) return null; |
959 | Long nodeId = Long.valueOf(id); |
960 | if (!mNodeMap.containsKey(nodeId)) { |
961 | Log.e(TAG, "Invalid BookmarkNode hierarchy. Unknown id " + id); |
962 | return null; |
963 | } |
964 | return mNodeMap.get(nodeId); |
965 | } |
966 | |
967 | private BookmarkNode readNodeContents(Parcel source) { |
968 | long id = source.readLong(); |
969 | String name = source.readString(); |
970 | String url = source.readString(); |
971 | int type = source.readInt(); |
972 | byte[] favicon = source.createByteArray(); |
973 | byte[] thumbnail = source.createByteArray(); |
974 | long parentId = source.readLong(); |
975 | if (type < 0 || type >= Type.values().length) { |
976 | Log.w(TAG, "Invalid node type ordinal value."); |
977 | return null; |
978 | } |
979 | |
980 | BookmarkNode node = new BookmarkNode(id, Type.values()[type], name, url, |
981 | getNode(parentId)); |
982 | node.setFavicon(favicon); |
983 | node.setThumbnail(thumbnail); |
984 | return node; |
985 | } |
986 | |
987 | private BookmarkNode readNodeContentsRecursive(Parcel source) { |
988 | BookmarkNode node = readNodeContents(source); |
989 | if (node == null) return null; |
990 | |
991 | Long nodeId = Long.valueOf(node.id()); |
992 | if (mNodeMap.containsKey(nodeId)) { |
993 | Log.e(TAG, "Invalid BookmarkNode hierarchy. Duplicate id " + node.id()); |
994 | return null; |
995 | } |
996 | mNodeMap.put(nodeId, node); |
997 | |
998 | int numChildren = source.readInt(); |
999 | for (int i = 0; i < numChildren; ++i) { |
1000 | node.addChild(readNodeContentsRecursive(source)); |
1001 | } |
1002 | |
1003 | return node; |
1004 | } |
1005 | }; |
1006 | } |
1007 | |
1008 | private long addBookmarkFromAPI(ContentValues values) { |
1009 | BookmarkRow row = BookmarkRow.fromContentValues(values); |
1010 | if (row.url == null) { |
1011 | throw new IllegalArgumentException("Must have a bookmark URL"); |
1012 | } |
1013 | return nativeAddBookmarkFromAPI(mNativeChromeBrowserProvider, |
1014 | row.url, row.created, row.isBookmark, row.date, row.favicon, |
1015 | row.title, row.visits, row.parentId); |
1016 | } |
1017 | |
1018 | private Cursor queryBookmarkFromAPI(String[] projectionIn, String selection, |
1019 | String[] selectionArgs, String sortOrder) { |
1020 | String[] projection = null; |
1021 | if (projectionIn == null || projectionIn.length == 0) { |
1022 | projection = BOOKMARK_DEFAULT_PROJECTION; |
1023 | } else { |
1024 | projection = projectionIn; |
1025 | } |
1026 | |
1027 | return nativeQueryBookmarkFromAPI(mNativeChromeBrowserProvider, projection, selection, |
1028 | selectionArgs, sortOrder); |
1029 | } |
1030 | |
1031 | private int updateBookmarkFromAPI(ContentValues values, String selection, |
1032 | String[] selectionArgs) { |
1033 | BookmarkRow row = BookmarkRow.fromContentValues(values); |
1034 | return nativeUpdateBookmarkFromAPI(mNativeChromeBrowserProvider, |
1035 | row.url, row.created, row.isBookmark, row.date, |
1036 | row.favicon, row.title, row.visits, row.parentId, selection, selectionArgs); |
1037 | } |
1038 | |
1039 | private int removeBookmarkFromAPI(String selection, String[] selectionArgs) { |
1040 | return nativeRemoveBookmarkFromAPI(mNativeChromeBrowserProvider, selection, selectionArgs); |
1041 | } |
1042 | |
1043 | private int removeHistoryFromAPI(String selection, String[] selectionArgs) { |
1044 | return nativeRemoveHistoryFromAPI(mNativeChromeBrowserProvider, selection, selectionArgs); |
1045 | } |
1046 | |
1047 | @CalledByNative |
1048 | private void onBookmarkChanged() { |
1049 | getContext().getContentResolver().notifyChange( |
1050 | buildAPIContentUri(getContext(), BOOKMARKS_PATH), null); |
1051 | } |
1052 | |
1053 | @CalledByNative |
1054 | private void onSearchTermChanged() { |
1055 | getContext().getContentResolver().notifyChange( |
1056 | buildAPIContentUri(getContext(), SEARCHES_PATH), null); |
1057 | } |
1058 | |
1059 | private long addSearchTermFromAPI(ContentValues values) { |
1060 | SearchRow row = SearchRow.fromContentValues(values); |
1061 | if (row.term == null) { |
1062 | throw new IllegalArgumentException("Must have a search term"); |
1063 | } |
1064 | return nativeAddSearchTermFromAPI(mNativeChromeBrowserProvider, row.term, row.date); |
1065 | } |
1066 | |
1067 | private int updateSearchTermFromAPI(ContentValues values, String selection, |
1068 | String[] selectionArgs) { |
1069 | SearchRow row = SearchRow.fromContentValues(values); |
1070 | return nativeUpdateSearchTermFromAPI(mNativeChromeBrowserProvider, |
1071 | row.term, row.date, selection, selectionArgs); |
1072 | } |
1073 | |
1074 | private Cursor querySearchTermFromAPI(String[] projectionIn, String selection, |
1075 | String[] selectionArgs, String sortOrder) { |
1076 | String[] projection = null; |
1077 | if (projectionIn == null || projectionIn.length == 0) { |
1078 | projection = android.provider.Browser.SEARCHES_PROJECTION; |
1079 | } else { |
1080 | projection = projectionIn; |
1081 | } |
1082 | return nativeQuerySearchTermFromAPI(mNativeChromeBrowserProvider, projection, selection, |
1083 | selectionArgs, sortOrder); |
1084 | } |
1085 | |
1086 | private int removeSearchFromAPI(String selection, String[] selectionArgs) { |
1087 | return nativeRemoveSearchTermFromAPI(mNativeChromeBrowserProvider, |
1088 | selection, selectionArgs); |
1089 | } |
1090 | |
1091 | private static boolean isInUiThread() { |
1092 | if (!ThreadUtils.runningOnUiThread()) return false; |
1093 | |
1094 | if (!"REL".equals(Build.VERSION.CODENAME)) { |
1095 | throw new IllegalStateException("Shouldn't run in the UI thread"); |
1096 | } |
1097 | |
1098 | Log.w(TAG, "ChromeBrowserProvider methods cannot be called from the UI thread."); |
1099 | return true; |
1100 | } |
1101 | |
1102 | private static Uri buildContentUri(String authority, String path) { |
1103 | return Uri.parse("content://" + authority + "/" + path); |
1104 | } |
1105 | |
1106 | private static Uri buildAPIContentUri(Context context, String path) { |
1107 | return buildContentUri(context.getPackageName() + API_AUTHORITY_SUFFIX, path); |
1108 | } |
1109 | |
1110 | private static String buildWhereClause(long id, String selection) { |
1111 | StringBuffer sb = new StringBuffer(); |
1112 | sb.append(BaseColumns._ID); |
1113 | sb.append(" = "); |
1114 | sb.append(id); |
1115 | if (!TextUtils.isEmpty(selection)) { |
1116 | sb.append(" AND ("); |
1117 | sb.append(selection); |
1118 | sb.append(")"); |
1119 | } |
1120 | return sb.toString(); |
1121 | } |
1122 | |
1123 | private static String buildHistoryWhereClause(long id, String selection) { |
1124 | return buildWhereClause(id, buildBookmarkWhereClause(selection, false)); |
1125 | } |
1126 | |
1127 | private static String buildHistoryWhereClause(String selection) { |
1128 | return buildBookmarkWhereClause(selection, false); |
1129 | } |
1130 | |
1131 | /** |
1132 | * @return a SQL where class which is inserted the bookmark condition. |
1133 | */ |
1134 | private static String buildBookmarkWhereClause(String selection, boolean is_bookmark) { |
1135 | StringBuffer sb = new StringBuffer(); |
1136 | sb.append(BookmarkColumns.BOOKMARK); |
1137 | sb.append(is_bookmark ? " = 1 " : " = 0"); |
1138 | if (!TextUtils.isEmpty(selection)) { |
1139 | sb.append(" AND ("); |
1140 | sb.append(selection); |
1141 | sb.append(")"); |
1142 | } |
1143 | return sb.toString(); |
1144 | } |
1145 | |
1146 | private static String buildBookmarkWhereClause(long id, String selection) { |
1147 | return buildWhereClause(id, buildBookmarkWhereClause(selection, true)); |
1148 | } |
1149 | |
1150 | private static String buildBookmarkWhereClause(String selection) { |
1151 | return buildBookmarkWhereClause(selection, true); |
1152 | } |
1153 | |
1154 | // Wrap the value of BookmarkColumn. |
1155 | private static class BookmarkRow { |
1156 | Boolean isBookmark; |
1157 | Long created; |
1158 | String url; |
1159 | Long date; |
1160 | byte[] favicon; |
1161 | String title; |
1162 | Integer visits; |
1163 | long parentId; |
1164 | |
1165 | static BookmarkRow fromContentValues(ContentValues values) { |
1166 | BookmarkRow row = new BookmarkRow(); |
1167 | if (values.containsKey(BookmarkColumns.URL)) { |
1168 | row.url = values.getAsString(BookmarkColumns.URL); |
1169 | } |
1170 | if (values.containsKey(BookmarkColumns.BOOKMARK)) { |
1171 | row.isBookmark = values.getAsInteger(BookmarkColumns.BOOKMARK) != 0; |
1172 | } |
1173 | if (values.containsKey(BookmarkColumns.CREATED)) { |
1174 | row.created = values.getAsLong(BookmarkColumns.CREATED); |
1175 | } |
1176 | if (values.containsKey(BookmarkColumns.DATE)) { |
1177 | row.date = values.getAsLong(BookmarkColumns.DATE); |
1178 | } |
1179 | if (values.containsKey(BookmarkColumns.FAVICON)) { |
1180 | row.favicon = values.getAsByteArray(BookmarkColumns.FAVICON); |
1181 | // We need to know that the caller set the favicon column. |
1182 | if (row.favicon == null) { |
1183 | row.favicon = new byte[0]; |
1184 | } |
1185 | } |
1186 | if (values.containsKey(BookmarkColumns.TITLE)) { |
1187 | row.title = values.getAsString(BookmarkColumns.TITLE); |
1188 | } |
1189 | if (values.containsKey(BookmarkColumns.VISITS)) { |
1190 | row.visits = values.getAsInteger(BookmarkColumns.VISITS); |
1191 | } |
1192 | if (values.containsKey(BOOKMARK_PARENT_ID_PARAM)) { |
1193 | row.parentId = values.getAsLong(BOOKMARK_PARENT_ID_PARAM); |
1194 | } |
1195 | return row; |
1196 | } |
1197 | } |
1198 | |
1199 | // Wrap the value of SearchColumn. |
1200 | private static class SearchRow { |
1201 | String term; |
1202 | Long date; |
1203 | |
1204 | static SearchRow fromContentValues(ContentValues values) { |
1205 | SearchRow row = new SearchRow(); |
1206 | if (values.containsKey(SearchColumns.SEARCH)) { |
1207 | row.term = values.getAsString(SearchColumns.SEARCH); |
1208 | } |
1209 | if (values.containsKey(SearchColumns.DATE)) { |
1210 | row.date = values.getAsLong(SearchColumns.DATE); |
1211 | } |
1212 | return row; |
1213 | } |
1214 | } |
1215 | |
1216 | /** |
1217 | * Returns true if the native side of the class is initialized. |
1218 | */ |
1219 | protected boolean isNativeSideInitialized() { |
1220 | return mNativeChromeBrowserProvider != 0; |
1221 | } |
1222 | |
1223 | /** |
1224 | * Make sure chrome is running. This method mustn't run on UI thread. |
1225 | * |
1226 | * @return Whether the native chrome process is running successfully once this has returned. |
1227 | */ |
1228 | private boolean ensureNativeChromeLoaded() { |
1229 | ensureUriMatcherInitialized(); |
1230 | |
1231 | synchronized(mLoadNativeLock) { |
1232 | if (mNativeChromeBrowserProvider != 0) return true; |
1233 | |
1234 | final AtomicBoolean retVal = new AtomicBoolean(true); |
1235 | ThreadUtils.runOnUiThreadBlocking(new Runnable() { |
1236 | @Override |
1237 | public void run() { |
1238 | retVal.set(ensureNativeChromeLoadedOnUIThread()); |
1239 | } |
1240 | }); |
1241 | return retVal.get(); |
1242 | } |
1243 | } |
1244 | |
1245 | /** |
1246 | * This method should only run on UI thread. |
1247 | */ |
1248 | protected boolean ensureNativeChromeLoadedOnUIThread() { |
1249 | if (isNativeSideInitialized()) return true; |
1250 | mNativeChromeBrowserProvider = nativeInit(); |
1251 | return isNativeSideInitialized(); |
1252 | } |
1253 | |
1254 | @Override |
1255 | protected void finalize() throws Throwable { |
1256 | try { |
1257 | // Tests might try to destroy this in the wrong thread. |
1258 | ThreadUtils.runOnUiThreadBlocking(new Runnable() { |
1259 | @Override |
1260 | public void run() { |
1261 | ensureNativeChromeDestroyedOnUIThread(); |
1262 | } |
1263 | }); |
1264 | } finally { |
1265 | super.finalize(); |
1266 | } |
1267 | } |
1268 | |
1269 | /** |
1270 | * This method should only run on UI thread. |
1271 | */ |
1272 | private void ensureNativeChromeDestroyedOnUIThread() { |
1273 | if (isNativeSideInitialized()) { |
1274 | nativeDestroy(mNativeChromeBrowserProvider); |
1275 | mNativeChromeBrowserProvider = 0; |
1276 | } |
1277 | } |
1278 | |
1279 | /** |
1280 | * Call to get the intent to create a bookmark shortcut on homescreen. |
1281 | */ |
1282 | public static Intent getShortcutToBookmark(String url, String title, Bitmap favicon, int rValue, |
1283 | int gValue, int bValue, Activity activity) { |
1284 | return BookmarkUtils.createAddToHomeIntent(activity, url, title, favicon, rValue, gValue, |
1285 | bValue); |
1286 | } |
1287 | |
1288 | private native int nativeInit(); |
1289 | private native void nativeDestroy(int nativeChromeBrowserProvider); |
1290 | |
1291 | // Public API native methods. |
1292 | private native long nativeAddBookmark(int nativeChromeBrowserProvider, |
1293 | String url, String title, boolean isFolder, long parentId); |
1294 | |
1295 | private native int nativeRemoveBookmark(int nativeChromeBrowserProvider, long id); |
1296 | |
1297 | private native int nativeUpdateBookmark(int nativeChromeBrowserProvider, |
1298 | long id, String url, String title, long parentId); |
1299 | |
1300 | private native long nativeAddBookmarkFromAPI(int nativeChromeBrowserProvider, |
1301 | String url, Long created, Boolean isBookmark, Long date, byte[] favicon, |
1302 | String title, Integer visits, long parentId); |
1303 | |
1304 | private native SQLiteCursor nativeQueryBookmarkFromAPI(int nativeChromeBrowserProvider, |
1305 | String[] projection, String selection, String[] selectionArgs, String sortOrder); |
1306 | |
1307 | private native int nativeUpdateBookmarkFromAPI(int nativeChromeBrowserProvider, |
1308 | String url, Long created, Boolean isBookmark, Long date, byte[] favicon, |
1309 | String title, Integer visits, long parentId, String selection, String[] selectionArgs); |
1310 | |
1311 | private native int nativeRemoveBookmarkFromAPI(int nativeChromeBrowserProvider, |
1312 | String selection, String[] selectionArgs); |
1313 | |
1314 | private native int nativeRemoveHistoryFromAPI(int nativeChromeBrowserProvider, |
1315 | String selection, String[] selectionArgs); |
1316 | |
1317 | private native long nativeAddSearchTermFromAPI(int nativeChromeBrowserProvider, |
1318 | String term, Long date); |
1319 | |
1320 | private native SQLiteCursor nativeQuerySearchTermFromAPI(int nativeChromeBrowserProvider, |
1321 | String[] projection, String selection, String[] selectionArgs, String sortOrder); |
1322 | |
1323 | private native int nativeUpdateSearchTermFromAPI(int nativeChromeBrowserProvider, |
1324 | String search, Long date, String selection, String[] selectionArgs); |
1325 | |
1326 | private native int nativeRemoveSearchTermFromAPI(int nativeChromeBrowserProvider, |
1327 | String selection, String[] selectionArgs); |
1328 | |
1329 | // Client API native methods. |
1330 | private native boolean nativeBookmarkNodeExists(int nativeChromeBrowserProvider, long id); |
1331 | |
1332 | private native long nativeCreateBookmarksFolderOnce(int nativeChromeBrowserProvider, |
1333 | String title, long parentId); |
1334 | |
1335 | private native BookmarkNode nativeGetAllBookmarkFolders(int nativeChromeBrowserProvider); |
1336 | |
1337 | private native void nativeRemoveAllBookmarks(int nativeChromeBrowserProvider); |
1338 | |
1339 | private native BookmarkNode nativeGetBookmarkNode(int nativeChromeBrowserProvider, |
1340 | long id, boolean getParent, boolean getChildren); |
1341 | |
1342 | private native BookmarkNode nativeGetMobileBookmarksFolder(int nativeChromeBrowserProvider); |
1343 | |
1344 | private native boolean nativeIsBookmarkInMobileBookmarksBranch(int nativeChromeBrowserProvider, |
1345 | long id); |
1346 | |
1347 | private native byte[] nativeGetFaviconOrTouchIcon(int nativeChromeBrowserProvider, String url); |
1348 | |
1349 | private native byte[] nativeGetThumbnail(int nativeChromeBrowserProvider, String url); |
1350 | } |