2 * Copyright (C) 2006 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.browser;
19 import android.app.SearchManager;
20 import android.app.SearchableInfo;
21 import android.backup.BackupManager;
22 import android.content.ComponentName;
23 import android.content.ContentProvider;
24 import android.content.ContentResolver;
25 import android.content.ContentUris;
26 import android.content.ContentValues;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.SharedPreferences;
30 import android.content.UriMatcher;
31 import android.content.SharedPreferences.Editor;
32 import android.content.pm.PackageManager;
33 import android.content.pm.ResolveInfo;
34 import android.database.AbstractCursor;
35 import android.database.ContentObserver;
36 import android.database.Cursor;
37 import android.database.sqlite.SQLiteDatabase;
38 import android.database.sqlite.SQLiteOpenHelper;
39 import android.net.Uri;
40 import android.os.AsyncTask;
41 import android.os.Handler;
42 import android.preference.PreferenceManager;
43 import android.provider.Browser;
44 import android.provider.Settings;
45 import android.provider.Browser.BookmarkColumns;
46 import android.speech.RecognizerResultsIntent;
47 import android.text.TextUtils;
48 import android.util.Log;
49 import android.util.Patterns;
50 import android.util.TypedValue;
51 import android.webkit.GeolocationPermissions;
55 import java.io.FilenameFilter;
56 import java.util.ArrayList;
57 import java.util.Date;
58 import java.util.regex.Matcher;
59 import java.util.regex.Pattern;
62 public class BrowserProvider extends ContentProvider {
64 private SQLiteOpenHelper mOpenHelper;
65 private BackupManager mBackupManager;
66 private static final String sDatabaseName = "browser.db";
67 private static final String TAG = "BrowserProvider";
68 private static final String ORDER_BY = "visits DESC, date DESC";
70 private static final String PICASA_URL = "http://picasaweb.google.com/m/" +
71 "viewer?source=androidclient";
73 private static final String[] TABLE_NAMES = new String[] {
74 "bookmarks", "searches", "geolocation"
76 private static final String[] SUGGEST_PROJECTION = new String[] {
77 "_id", "url", "title", "bookmark", "user_entered"
79 private static final String SUGGEST_SELECTION =
80 "(url LIKE ? OR url LIKE ? OR url LIKE ? OR url LIKE ?"
81 + " OR title LIKE ?) AND (bookmark = 1 OR user_entered = 1)";
82 private String[] SUGGEST_ARGS = new String[5];
84 // shared suggestion array index, make sure to match COLUMNS
85 private static final int SUGGEST_COLUMN_INTENT_ACTION_ID = 1;
86 private static final int SUGGEST_COLUMN_INTENT_DATA_ID = 2;
87 private static final int SUGGEST_COLUMN_TEXT_1_ID = 3;
88 private static final int SUGGEST_COLUMN_TEXT_2_ID = 4;
89 private static final int SUGGEST_COLUMN_TEXT_2_URL_ID = 5;
90 private static final int SUGGEST_COLUMN_ICON_1_ID = 6;
91 private static final int SUGGEST_COLUMN_ICON_2_ID = 7;
92 private static final int SUGGEST_COLUMN_QUERY_ID = 8;
93 private static final int SUGGEST_COLUMN_INTENT_EXTRA_DATA = 9;
95 // shared suggestion columns
96 private static final String[] COLUMNS = new String[] {
98 SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
99 SearchManager.SUGGEST_COLUMN_INTENT_DATA,
100 SearchManager.SUGGEST_COLUMN_TEXT_1,
101 SearchManager.SUGGEST_COLUMN_TEXT_2,
102 SearchManager.SUGGEST_COLUMN_TEXT_2_URL,
103 SearchManager.SUGGEST_COLUMN_ICON_1,
104 SearchManager.SUGGEST_COLUMN_ICON_2,
105 SearchManager.SUGGEST_COLUMN_QUERY,
106 SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA};
108 private static final int MAX_SUGGESTION_SHORT_ENTRIES = 3;
109 private static final int MAX_SUGGESTION_LONG_ENTRIES = 6;
110 private static final String MAX_SUGGESTION_LONG_ENTRIES_STRING =
111 Integer.valueOf(MAX_SUGGESTION_LONG_ENTRIES).toString();
113 // make sure that these match the index of TABLE_NAMES
114 private static final int URI_MATCH_BOOKMARKS = 0;
115 private static final int URI_MATCH_SEARCHES = 1;
116 private static final int URI_MATCH_GEOLOCATION = 2;
117 // (id % 10) should match the table name index
118 private static final int URI_MATCH_BOOKMARKS_ID = 10;
119 private static final int URI_MATCH_SEARCHES_ID = 11;
121 private static final int URI_MATCH_SUGGEST = 20;
122 private static final int URI_MATCH_BOOKMARKS_SUGGEST = 21;
124 private static final UriMatcher URI_MATCHER;
127 URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
128 URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_BOOKMARKS],
129 URI_MATCH_BOOKMARKS);
130 URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_BOOKMARKS] + "/#",
131 URI_MATCH_BOOKMARKS_ID);
132 URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_SEARCHES],
134 URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_SEARCHES] + "/#",
135 URI_MATCH_SEARCHES_ID);
136 URI_MATCHER.addURI("browser", SearchManager.SUGGEST_URI_PATH_QUERY,
138 URI_MATCHER.addURI("browser",
139 TABLE_NAMES[URI_MATCH_BOOKMARKS] + "/" + SearchManager.SUGGEST_URI_PATH_QUERY,
140 URI_MATCH_BOOKMARKS_SUGGEST);
141 URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_GEOLOCATION],
142 URI_MATCH_GEOLOCATION);
145 // 1 -> 2 add cache table
146 // 2 -> 3 update history table
147 // 3 -> 4 add passwords table
148 // 4 -> 5 add settings table
151 // 7 -> 8 drop proxy table
152 // 8 -> 9 drop settings table
153 // 9 -> 10 add form_urls and form_data
154 // 10 -> 11 add searches table
155 // 11 -> 12 modify cache table
156 // 12 -> 13 modify cache table
157 // 13 -> 14 correspond with Google Bookmarks schema
158 // 14 -> 15 move couple of tables to either browser private database or webview database
159 // 15 -> 17 Set it up for the SearchManager
160 // 17 -> 18 Added favicon in bookmarks table for Home shortcuts
161 // 18 -> 19 Remove labels table
162 // 19 -> 20 Added thumbnail
163 // 20 -> 21 Added touch_icon
164 // 21 -> 22 Remove "clientid"
165 // 22 -> 23 Added user_entered
166 private static final int DATABASE_VERSION = 23;
168 // Regular expression which matches http://, followed by some stuff, followed by
169 // optionally a trailing slash, all matched as separate groups.
170 private static final Pattern STRIP_URL_PATTERN = Pattern.compile("^(http://)(.*?)(/$)?");
172 private SearchManager mSearchManager;
174 public BrowserProvider() {
177 // XXX: This is a major hack to remove our dependency on gsf constants and
178 // its content provider. http://b/issue?id=2425179
179 static String getClientId(ContentResolver cr) {
180 String ret = "android-google";
183 c = cr.query(Uri.parse("content://com.google.settings/partner"),
184 new String[] { "value" }, "name='client_id'", null, null);
185 if (c != null && c.moveToNext()) {
186 ret = c.getString(0);
188 } catch (RuntimeException ex) {
189 // fall through to return the default
198 private static CharSequence replaceSystemPropertyInString(Context context, CharSequence srcString) {
199 StringBuffer sb = new StringBuffer();
202 final String client_id = getClientId(context.getContentResolver());
204 for (int i = 0; i < srcString.length(); ++i) {
205 char c = srcString.charAt(i);
207 sb.append(srcString.subSequence(lastCharLoc, i));
210 for (int j = i; j < srcString.length(); ++j) {
211 char k = srcString.charAt(j);
213 String propertyKeyValue = srcString.subSequence(i + 1, j).toString();
214 if (propertyKeyValue.equals("CLIENT_ID")) {
215 sb.append(client_id);
217 sb.append("unknown");
226 if (srcString.length() - lastCharLoc > 0) {
227 // Put on the tail, if there is one
228 sb.append(srcString.subSequence(lastCharLoc, srcString.length()));
233 private static class DatabaseHelper extends SQLiteOpenHelper {
234 private Context mContext;
236 public DatabaseHelper(Context context) {
237 super(context, sDatabaseName, null, DATABASE_VERSION);
242 public void onCreate(SQLiteDatabase db) {
243 db.execSQL("CREATE TABLE bookmarks (" +
244 "_id INTEGER PRIMARY KEY," +
250 "description TEXT," +
251 "bookmark INTEGER," +
252 "favicon BLOB DEFAULT NULL," +
253 "thumbnail BLOB DEFAULT NULL," +
254 "touch_icon BLOB DEFAULT NULL," +
255 "user_entered INTEGER" +
258 final CharSequence[] bookmarks = mContext.getResources()
259 .getTextArray(R.array.bookmarks);
260 int size = bookmarks.length;
262 for (int i = 0; i < size; i = i + 2) {
263 CharSequence bookmarkDestination = replaceSystemPropertyInString(mContext, bookmarks[i + 1]);
264 db.execSQL("INSERT INTO bookmarks (title, url, visits, " +
265 "date, created, bookmark)" + " VALUES('" +
266 bookmarks[i] + "', '" + bookmarkDestination +
269 } catch (ArrayIndexOutOfBoundsException e) {
272 db.execSQL("CREATE TABLE searches (" +
273 "_id INTEGER PRIMARY KEY," +
280 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
281 Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
283 if (oldVersion == 18) {
284 db.execSQL("DROP TABLE IF EXISTS labels");
286 if (oldVersion <= 19) {
287 db.execSQL("ALTER TABLE bookmarks ADD COLUMN thumbnail BLOB DEFAULT NULL;");
289 if (oldVersion < 21) {
290 db.execSQL("ALTER TABLE bookmarks ADD COLUMN touch_icon BLOB DEFAULT NULL;");
292 if (oldVersion < 22) {
293 db.execSQL("DELETE FROM bookmarks WHERE (bookmark = 0 AND url LIKE \"%.google.%client=ms-%\")");
296 if (oldVersion < 23) {
297 db.execSQL("ALTER TABLE bookmarks ADD COLUMN user_entered INTEGER;");
299 db.execSQL("DROP TABLE IF EXISTS bookmarks");
300 db.execSQL("DROP TABLE IF EXISTS searches");
305 private void removeGears() {
306 AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
307 public Void doInBackground(Void... unused) {
308 String browserDataDirString = mContext.getApplicationInfo().dataDir;
309 final String appPluginsDirString = "app_plugins";
310 final String gearsPrefix = "gears";
311 File appPluginsDir = new File(browserDataDirString + File.separator
312 + appPluginsDirString);
313 if (!appPluginsDir.exists()) {
316 // Delete the Gears plugin files
317 File[] gearsFiles = appPluginsDir.listFiles(new FilenameFilter() {
318 public boolean accept(File dir, String filename) {
319 return filename.startsWith(gearsPrefix);
322 for (int i = 0; i < gearsFiles.length; ++i) {
323 if (gearsFiles[i].isDirectory()) {
324 deleteDirectory(gearsFiles[i]);
326 gearsFiles[i].delete();
329 // Delete the Gears data files
330 File gearsDataDir = new File(browserDataDirString + File.separator
332 if (!gearsDataDir.exists()) {
335 deleteDirectory(gearsDataDir);
339 private void deleteDirectory(File currentDir) {
340 File[] files = currentDir.listFiles();
341 for (int i = 0; i < files.length; ++i) {
342 if (files[i].isDirectory()) {
343 deleteDirectory(files[i]);
356 public boolean onCreate() {
357 final Context context = getContext();
358 mOpenHelper = new DatabaseHelper(context);
359 mBackupManager = new BackupManager(context);
360 // we added "picasa web album" into default bookmarks for version 19.
361 // To avoid erasing the bookmark table, we added it explicitly for
362 // version 18 and 19 as in the other cases, we will erase the table.
363 if (DATABASE_VERSION == 18 || DATABASE_VERSION == 19) {
364 SharedPreferences p = PreferenceManager
365 .getDefaultSharedPreferences(context);
366 boolean fix = p.getBoolean("fix_picasa", true);
369 Editor ed = p.edit();
370 ed.putBoolean("fix_picasa", false);
374 mSearchManager = (SearchManager) context.getSystemService(Context.SEARCH_SERVICE);
375 mShowWebSuggestionsSettingChangeObserver
376 = new ShowWebSuggestionsSettingChangeObserver();
377 context.getContentResolver().registerContentObserver(
378 Settings.System.getUriFor(
379 Settings.System.SHOW_WEB_SUGGESTIONS),
380 true, mShowWebSuggestionsSettingChangeObserver);
381 updateShowWebSuggestions();
386 * This Observer will ensure that if the user changes the system
387 * setting of whether to display web suggestions, we will
388 * change accordingly.
390 /* package */ class ShowWebSuggestionsSettingChangeObserver
391 extends ContentObserver {
392 public ShowWebSuggestionsSettingChangeObserver() {
393 super(new Handler());
397 public void onChange(boolean selfChange) {
398 updateShowWebSuggestions();
402 private ShowWebSuggestionsSettingChangeObserver
403 mShowWebSuggestionsSettingChangeObserver;
405 // If non-null, then the system is set to show web suggestions,
406 // and this is the SearchableInfo to use to get them.
407 private SearchableInfo mSearchableInfo;
410 * Check the system settings to see whether web suggestions are
411 * allowed. If so, store the SearchableInfo to grab suggestions
412 * while the user is typing.
414 private void updateShowWebSuggestions() {
415 mSearchableInfo = null;
416 Context context = getContext();
417 if (Settings.System.getInt(context.getContentResolver(),
418 Settings.System.SHOW_WEB_SUGGESTIONS,
419 1 /* default on */) == 1) {
420 ComponentName webSearchComponent = mSearchManager.getWebSearchActivity();
421 if (webSearchComponent != null) {
422 mSearchableInfo = mSearchManager.getSearchableInfo(webSearchComponent);
427 private void fixPicasaBookmark() {
428 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
429 Cursor cursor = db.rawQuery("SELECT _id FROM bookmarks WHERE " +
430 "bookmark = 1 AND url = ?", new String[] { PICASA_URL });
432 if (!cursor.moveToFirst()) {
433 // set "created" so that it will be on the top of the list
434 db.execSQL("INSERT INTO bookmarks (title, url, visits, " +
435 "date, created, bookmark)" + " VALUES('" +
436 getContext().getString(R.string.picasa) + "', '"
437 + PICASA_URL + "', 0, 0, " + new Date().getTime()
441 if (cursor != null) {
448 * Subclass AbstractCursor so we can combine multiple Cursors and add
450 * Here are the rules.
451 * 1. We only have MAX_SUGGESTION_LONG_ENTRIES in the list plus
453 * 2. If bookmark/history entries has a match, "Search the web" shows up at
454 * the second place. Otherwise, "Search the web" shows up at the first
457 private class MySuggestionCursor extends AbstractCursor {
458 private Cursor mHistoryCursor;
459 private Cursor mSuggestCursor;
460 private int mHistoryCount;
461 private int mSuggestionCount;
462 private boolean mIncludeWebSearch;
463 private String mString;
464 private int mSuggestText1Id;
465 private int mSuggestText2Id;
466 private int mSuggestText2UrlId;
467 private int mSuggestQueryId;
468 private int mSuggestIntentExtraDataId;
470 public MySuggestionCursor(Cursor hc, Cursor sc, String string) {
473 mHistoryCount = hc.getCount();
474 mSuggestionCount = sc != null ? sc.getCount() : 0;
475 if (mSuggestionCount > (MAX_SUGGESTION_LONG_ENTRIES - mHistoryCount)) {
476 mSuggestionCount = MAX_SUGGESTION_LONG_ENTRIES - mHistoryCount;
479 mIncludeWebSearch = string.length() > 0;
481 // Some web suggest providers only give suggestions and have no description string for
482 // items. The order of the result columns may be different as well. So retrieve the
483 // column indices for the fields we need now and check before using below.
484 if (mSuggestCursor == null) {
485 mSuggestText1Id = -1;
486 mSuggestText2Id = -1;
487 mSuggestText2UrlId = -1;
488 mSuggestQueryId = -1;
489 mSuggestIntentExtraDataId = -1;
491 mSuggestText1Id = mSuggestCursor.getColumnIndex(
492 SearchManager.SUGGEST_COLUMN_TEXT_1);
493 mSuggestText2Id = mSuggestCursor.getColumnIndex(
494 SearchManager.SUGGEST_COLUMN_TEXT_2);
495 mSuggestText2UrlId = mSuggestCursor.getColumnIndex(
496 SearchManager.SUGGEST_COLUMN_TEXT_2_URL);
497 mSuggestQueryId = mSuggestCursor.getColumnIndex(
498 SearchManager.SUGGEST_COLUMN_QUERY);
499 mSuggestIntentExtraDataId = mSuggestCursor.getColumnIndex(
500 SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
505 public boolean onMove(int oldPosition, int newPosition) {
506 if (mHistoryCursor == null) {
509 if (mIncludeWebSearch) {
510 if (mHistoryCount == 0 && newPosition == 0) {
512 } else if (mHistoryCount > 0) {
513 if (newPosition == 0) {
514 mHistoryCursor.moveToPosition(0);
516 } else if (newPosition == 1) {
522 if (mHistoryCount > newPosition) {
523 mHistoryCursor.moveToPosition(newPosition);
525 mSuggestCursor.moveToPosition(newPosition - mHistoryCount);
531 public int getCount() {
532 if (mIncludeWebSearch) {
533 return mHistoryCount + mSuggestionCount + 1;
535 return mHistoryCount + mSuggestionCount;
540 public String[] getColumnNames() {
545 public String getString(int columnIndex) {
546 if ((mPos != -1 && mHistoryCursor != null)) {
547 int type = -1; // 0: web search; 1: history; 2: suggestion
548 if (mIncludeWebSearch) {
549 if (mHistoryCount == 0 && mPos == 0) {
551 } else if (mHistoryCount > 0) {
554 } else if (mPos == 1) {
558 if (type == -1) type = (mPos - 1) < mHistoryCount ? 1 : 2;
560 type = mPos < mHistoryCount ? 1 : 2;
563 switch(columnIndex) {
564 case SUGGEST_COLUMN_INTENT_ACTION_ID:
566 return Intent.ACTION_VIEW;
568 return Intent.ACTION_SEARCH;
571 case SUGGEST_COLUMN_INTENT_DATA_ID:
573 return mHistoryCursor.getString(1);
578 case SUGGEST_COLUMN_TEXT_1_ID:
581 } else if (type == 1) {
582 return getHistoryTitle();
584 if (mSuggestText1Id == -1) return null;
585 return mSuggestCursor.getString(mSuggestText1Id);
588 case SUGGEST_COLUMN_TEXT_2_ID:
590 return getContext().getString(R.string.search_the_web);
591 } else if (type == 1) {
592 return null; // Use TEXT_2_URL instead
594 if (mSuggestText2Id == -1) return null;
595 return mSuggestCursor.getString(mSuggestText2Id);
598 case SUGGEST_COLUMN_TEXT_2_URL_ID:
601 } else if (type == 1) {
602 return getHistoryUrl();
604 if (mSuggestText2UrlId == -1) return null;
605 return mSuggestCursor.getString(mSuggestText2UrlId);
608 case SUGGEST_COLUMN_ICON_1_ID:
610 if (mHistoryCursor.getInt(3) == 1) {
611 return Integer.valueOf(
612 R.drawable.ic_search_category_bookmark)
615 return Integer.valueOf(
616 R.drawable.ic_search_category_history)
620 return Integer.valueOf(
621 R.drawable.ic_search_category_suggest)
625 case SUGGEST_COLUMN_ICON_2_ID:
628 case SUGGEST_COLUMN_QUERY_ID:
631 } else if (type == 1) {
632 // Return the url in the intent query column. This is ignored
633 // within the browser because our searchable is set to
634 // android:searchMode="queryRewriteFromData", but it is used by
635 // global search for query rewriting.
636 return mHistoryCursor.getString(1);
638 if (mSuggestQueryId == -1) return null;
639 return mSuggestCursor.getString(mSuggestQueryId);
642 case SUGGEST_COLUMN_INTENT_EXTRA_DATA:
645 } else if (type == 1) {
648 if (mSuggestIntentExtraDataId == -1) return null;
649 return mSuggestCursor.getString(mSuggestIntentExtraDataId);
657 public double getDouble(int column) {
658 throw new UnsupportedOperationException();
662 public float getFloat(int column) {
663 throw new UnsupportedOperationException();
667 public int getInt(int column) {
668 throw new UnsupportedOperationException();
672 public long getLong(int column) {
673 if ((mPos != -1) && column == 0) {
674 return mPos; // use row# as the _Id
676 throw new UnsupportedOperationException();
680 public short getShort(int column) {
681 throw new UnsupportedOperationException();
685 public boolean isNull(int column) {
686 throw new UnsupportedOperationException();
689 // TODO Temporary change, finalize after jq's changes go in
690 public void deactivate() {
691 if (mHistoryCursor != null) {
692 mHistoryCursor.deactivate();
694 if (mSuggestCursor != null) {
695 mSuggestCursor.deactivate();
700 public boolean requery() {
701 return (mHistoryCursor != null ? mHistoryCursor.requery() : false) |
702 (mSuggestCursor != null ? mSuggestCursor.requery() : false);
705 // TODO Temporary change, finalize after jq's changes go in
706 public void close() {
708 if (mHistoryCursor != null) {
709 mHistoryCursor.close();
710 mHistoryCursor = null;
712 if (mSuggestCursor != null) {
713 mSuggestCursor.close();
714 mSuggestCursor = null;
719 * Provides the title (text line 1) for a browser suggestion, which should be the
720 * webpage title. If the webpage title is empty, returns the stripped url instead.
722 * @return the title string to use
724 private String getHistoryTitle() {
725 String title = mHistoryCursor.getString(2 /* webpage title */);
726 if (TextUtils.isEmpty(title) || TextUtils.getTrimmedLength(title) == 0) {
727 title = stripUrl(mHistoryCursor.getString(1 /* url */));
733 * Provides the subtitle (text line 2) for a browser suggestion, which should be the
734 * webpage url. If the webpage title is empty, then the url should go in the title
735 * instead, and the subtitle should be empty, so this would return null.
737 * @return the subtitle string to use, or null if none
739 private String getHistoryUrl() {
740 String title = mHistoryCursor.getString(2 /* webpage title */);
741 if (TextUtils.isEmpty(title) || TextUtils.getTrimmedLength(title) == 0) {
744 return stripUrl(mHistoryCursor.getString(1 /* url */));
750 private static class ResultsCursor extends AbstractCursor {
751 // Array indices for RESULTS_COLUMNS
752 private static final int RESULT_ACTION_ID = 1;
753 private static final int RESULT_DATA_ID = 2;
754 private static final int RESULT_TEXT_ID = 3;
755 private static final int RESULT_ICON_ID = 4;
756 private static final int RESULT_EXTRA_ID = 5;
758 private static final String[] RESULTS_COLUMNS = new String[] {
760 SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
761 SearchManager.SUGGEST_COLUMN_INTENT_DATA,
762 SearchManager.SUGGEST_COLUMN_TEXT_1,
763 SearchManager.SUGGEST_COLUMN_ICON_1,
764 SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA
766 private final ArrayList<String> mResults;
767 public ResultsCursor(ArrayList<String> results) {
770 public int getCount() { return mResults.size(); }
772 public String[] getColumnNames() {
773 return RESULTS_COLUMNS;
776 public String getString(int column) {
778 case RESULT_ACTION_ID:
779 return RecognizerResultsIntent.ACTION_VOICE_SEARCH_RESULTS;
781 // The data is used when the phone is in landscape mode. We
782 // still want to show the result string.
784 return mResults.get(mPos);
785 case RESULT_EXTRA_ID:
786 // The Intent's extra data will store the index into
787 // mResults so the BrowserActivity will know which result to
789 return Integer.toString(mPos);
791 return Integer.valueOf(R.drawable.magnifying_glass)
797 public short getShort(int column) {
798 throw new UnsupportedOperationException();
800 public int getInt(int column) {
801 throw new UnsupportedOperationException();
803 public long getLong(int column) {
804 if ((mPos != -1) && column == 0) {
805 return mPos; // use row# as the _id
807 throw new UnsupportedOperationException();
809 public float getFloat(int column) {
810 throw new UnsupportedOperationException();
812 public double getDouble(int column) {
813 throw new UnsupportedOperationException();
815 public boolean isNull(int column) {
816 throw new UnsupportedOperationException();
820 private ResultsCursor mResultsCursor;
823 * Provide a set of results to be returned to query, intended to be used
824 * by the SearchDialog when the BrowserActivity is in voice search mode.
825 * @param results Strings to display in the dropdown from the SearchDialog
827 /* package */ void setQueryResults(ArrayList<String> results) {
828 if (results == null) {
829 mResultsCursor = null;
831 mResultsCursor = new ResultsCursor(results);
836 public Cursor query(Uri url, String[] projectionIn, String selection,
837 String[] selectionArgs, String sortOrder)
838 throws IllegalStateException {
839 int match = URI_MATCHER.match(url);
841 throw new IllegalArgumentException("Unknown URL");
843 if (match == URI_MATCH_GEOLOCATION) {
844 throw new UnsupportedOperationException("query() not supported for geolocation");
846 if (match == URI_MATCH_SUGGEST && mResultsCursor != null) {
847 Cursor results = mResultsCursor;
848 mResultsCursor = null;
851 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
853 if (match == URI_MATCH_SUGGEST || match == URI_MATCH_BOOKMARKS_SUGGEST) {
854 String suggestSelection;
856 if (selectionArgs[0] == null || selectionArgs[0].equals("")) {
857 suggestSelection = null;
860 String like = selectionArgs[0] + "%";
861 if (selectionArgs[0].startsWith("http")
862 || selectionArgs[0].startsWith("file")) {
863 myArgs = new String[1];
865 suggestSelection = selection;
867 SUGGEST_ARGS[0] = "http://" + like;
868 SUGGEST_ARGS[1] = "http://www." + like;
869 SUGGEST_ARGS[2] = "https://" + like;
870 SUGGEST_ARGS[3] = "https://www." + like;
871 // To match against titles.
872 SUGGEST_ARGS[4] = like;
873 myArgs = SUGGEST_ARGS;
874 suggestSelection = SUGGEST_SELECTION;
878 Cursor c = db.query(TABLE_NAMES[URI_MATCH_BOOKMARKS],
879 SUGGEST_PROJECTION, suggestSelection, myArgs, null, null,
880 ORDER_BY, MAX_SUGGESTION_LONG_ENTRIES_STRING);
882 if (match == URI_MATCH_BOOKMARKS_SUGGEST
883 || Patterns.WEB_URL.matcher(selectionArgs[0]).matches()) {
884 return new MySuggestionCursor(c, null, "");
886 // get Google suggest if there is still space in the list
887 if (myArgs != null && myArgs.length > 1
888 && mSearchableInfo != null
889 && c.getCount() < (MAX_SUGGESTION_SHORT_ENTRIES - 1)) {
890 Cursor sc = mSearchManager.getSuggestions(mSearchableInfo, selectionArgs[0]);
891 return new MySuggestionCursor(c, sc, selectionArgs[0]);
893 return new MySuggestionCursor(c, null, selectionArgs[0]);
897 String[] projection = null;
898 if (projectionIn != null && projectionIn.length > 0) {
899 projection = new String[projectionIn.length + 1];
900 System.arraycopy(projectionIn, 0, projection, 0, projectionIn.length);
901 projection[projectionIn.length] = "_id AS _id";
904 StringBuilder whereClause = new StringBuilder(256);
905 if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) {
906 whereClause.append("(_id = ").append(url.getPathSegments().get(1))
910 // Tack on the user's selection, if present
911 if (selection != null && selection.length() > 0) {
912 if (whereClause.length() > 0) {
913 whereClause.append(" AND ");
916 whereClause.append('(');
917 whereClause.append(selection);
918 whereClause.append(')');
920 Cursor c = db.query(TABLE_NAMES[match % 10], projection,
921 whereClause.toString(), selectionArgs, null, null, sortOrder,
923 c.setNotificationUri(getContext().getContentResolver(), url);
928 public String getType(Uri url) {
929 int match = URI_MATCHER.match(url);
931 case URI_MATCH_BOOKMARKS:
932 return "vnd.android.cursor.dir/bookmark";
934 case URI_MATCH_BOOKMARKS_ID:
935 return "vnd.android.cursor.item/bookmark";
937 case URI_MATCH_SEARCHES:
938 return "vnd.android.cursor.dir/searches";
940 case URI_MATCH_SEARCHES_ID:
941 return "vnd.android.cursor.item/searches";
943 case URI_MATCH_SUGGEST:
944 return SearchManager.SUGGEST_MIME_TYPE;
946 case URI_MATCH_GEOLOCATION:
947 return "vnd.android.cursor.dir/geolocation";
950 throw new IllegalArgumentException("Unknown URL");
955 public Uri insert(Uri url, ContentValues initialValues) {
956 boolean isBookmarkTable = false;
957 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
959 int match = URI_MATCHER.match(url);
962 case URI_MATCH_BOOKMARKS: {
963 // Insert into the bookmarks table
964 long rowID = db.insert(TABLE_NAMES[URI_MATCH_BOOKMARKS], "url",
967 uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI,
970 isBookmarkTable = true;
974 case URI_MATCH_SEARCHES: {
975 // Insert into the searches table
976 long rowID = db.insert(TABLE_NAMES[URI_MATCH_SEARCHES], "url",
979 uri = ContentUris.withAppendedId(Browser.SEARCHES_URI,
985 case URI_MATCH_GEOLOCATION:
986 String origin = initialValues.getAsString(Browser.GeolocationColumns.ORIGIN);
987 if (TextUtils.isEmpty(origin)) {
988 throw new IllegalArgumentException("Empty origin");
990 GeolocationPermissions.getInstance().allow(origin);
991 // TODO: Should we have one URI per permission?
992 uri = Browser.GEOLOCATION_URI;
996 throw new IllegalArgumentException("Unknown URL");
1000 throw new IllegalArgumentException("Unknown URL");
1002 getContext().getContentResolver().notifyChange(uri, null);
1004 // Back up the new bookmark set if we just inserted one.
1005 // A row created when bookmarks are added from scratch will have
1006 // bookmark=1 in the initial value set.
1008 && initialValues.containsKey(BookmarkColumns.BOOKMARK)
1009 && initialValues.getAsInteger(BookmarkColumns.BOOKMARK) != 0) {
1010 mBackupManager.dataChanged();
1016 public int delete(Uri url, String where, String[] whereArgs) {
1017 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1019 int match = URI_MATCHER.match(url);
1020 if (match == -1 || match == URI_MATCH_SUGGEST) {
1021 throw new IllegalArgumentException("Unknown URL");
1024 if (match == URI_MATCH_GEOLOCATION) {
1025 return deleteGeolocation(url, where, whereArgs);
1028 // need to know whether it's the bookmarks table for a couple of reasons
1029 boolean isBookmarkTable = (match == URI_MATCH_BOOKMARKS_ID);
1032 if (isBookmarkTable || match == URI_MATCH_SEARCHES_ID) {
1033 StringBuilder sb = new StringBuilder();
1034 if (where != null && where.length() > 0) {
1037 sb.append(" ) AND ");
1039 id = url.getPathSegments().get(1);
1040 sb.append("_id = ");
1042 where = sb.toString();
1045 ContentResolver cr = getContext().getContentResolver();
1047 // we'lll need to back up the bookmark set if we are about to delete one
1048 if (isBookmarkTable) {
1049 Cursor cursor = cr.query(Browser.BOOKMARKS_URI,
1050 new String[] { BookmarkColumns.BOOKMARK },
1051 "_id = " + id, null, null);
1052 if (cursor.moveToNext()) {
1053 if (cursor.getInt(0) != 0) {
1054 // yep, this record is a bookmark
1055 mBackupManager.dataChanged();
1061 int count = db.delete(TABLE_NAMES[match % 10], where, whereArgs);
1062 cr.notifyChange(url, null);
1066 private int deleteGeolocation(Uri uri, String where, String[] whereArgs) {
1067 if (whereArgs.length != 1) {
1068 throw new IllegalArgumentException("Bad where arguments");
1070 String origin = whereArgs[0];
1071 if (TextUtils.isEmpty(origin)) {
1072 throw new IllegalArgumentException("Empty origin");
1074 GeolocationPermissions.getInstance().clear(origin);
1075 getContext().getContentResolver().notifyChange(Browser.GEOLOCATION_URI, null);
1076 return 1; // We always return 1, to avoid having to check whether anything was actually removed
1080 public int update(Uri url, ContentValues values, String where,
1081 String[] whereArgs) {
1082 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1084 int match = URI_MATCHER.match(url);
1085 if (match == -1 || match == URI_MATCH_SUGGEST) {
1086 throw new IllegalArgumentException("Unknown URL");
1089 if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) {
1090 StringBuilder sb = new StringBuilder();
1091 if (where != null && where.length() > 0) {
1094 sb.append(" ) AND ");
1096 String id = url.getPathSegments().get(1);
1097 sb.append("_id = ");
1099 where = sb.toString();
1102 ContentResolver cr = getContext().getContentResolver();
1104 // Not all bookmark-table updates should be backed up. Look to see
1105 // whether we changed the title, url, or "is a bookmark" state, and
1106 // request a backup if so.
1107 if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_BOOKMARKS) {
1108 boolean changingBookmarks = false;
1109 // Alterations to the bookmark field inherently change the bookmark
1110 // set, so we don't need to query the record; we know a priori that
1111 // we will need to back up this change.
1112 if (values.containsKey(BookmarkColumns.BOOKMARK)) {
1113 changingBookmarks = true;
1114 } else if ((values.containsKey(BookmarkColumns.TITLE)
1115 || values.containsKey(BookmarkColumns.URL))
1116 && values.containsKey(BookmarkColumns._ID)) {
1117 // If a title or URL has been changed, check to see if it is to
1118 // a bookmark. The ID should have been included in the update,
1120 Cursor cursor = cr.query(Browser.BOOKMARKS_URI,
1121 new String[] { BookmarkColumns.BOOKMARK },
1122 BookmarkColumns._ID + " = "
1123 + values.getAsString(BookmarkColumns._ID), null, null);
1124 if (cursor.moveToNext()) {
1125 changingBookmarks = (cursor.getInt(0) != 0);
1130 // if this *is* a bookmark row we're altering, we need to back it up.
1131 if (changingBookmarks) {
1132 mBackupManager.dataChanged();
1136 int ret = db.update(TABLE_NAMES[match % 10], values, where, whereArgs);
1137 cr.notifyChange(url, null);
1142 * Strips the provided url of preceding "http://" and any trailing "/". Does not
1143 * strip "https://". If the provided string cannot be stripped, the original string
1146 * TODO: Put this in TextUtils to be used by other packages doing something similar.
1148 * @param url a url to strip, like "http://www.google.com/"
1149 * @return a stripped url like "www.google.com", or the original string if it could
1152 private static String stripUrl(String url) {
1153 if (url == null) return null;
1154 Matcher m = STRIP_URL_PATTERN.matcher(url);
1155 if (m.matches() && m.groupCount() == 3) {