OSDN Git Service

Merge "Do not allow our resend/dontresend messages to be sent twice."
[android-x86/packages-apps-Browser.git] / src / com / android / browser / BrowserProvider.java
1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package com.android.browser;
18
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;
52
53
54 import java.io.File;
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;
60
61
62 public class BrowserProvider extends ContentProvider {
63
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";
69
70     private static final String PICASA_URL = "http://picasaweb.google.com/m/" +
71             "viewer?source=androidclient";
72
73     private static final String[] TABLE_NAMES = new String[] {
74         "bookmarks", "searches", "geolocation"
75     };
76     private static final String[] SUGGEST_PROJECTION = new String[] {
77             "_id", "url", "title", "bookmark", "user_entered"
78     };
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];
83
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;
94
95     // shared suggestion columns
96     private static final String[] COLUMNS = new String[] {
97             "_id",
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};
107
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();
112
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;
120     //
121     private static final int URI_MATCH_SUGGEST = 20;
122     private static final int URI_MATCH_BOOKMARKS_SUGGEST = 21;
123
124     private static final UriMatcher URI_MATCHER;
125
126     static {
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],
133                 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,
137                 URI_MATCH_SUGGEST);
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);
143     }
144
145     // 1 -> 2 add cache table
146     // 2 -> 3 update history table
147     // 3 -> 4 add passwords table
148     // 4 -> 5 add settings table
149     // 5 -> 6 ?
150     // 6 -> 7 ?
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;
167
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://)(.*?)(/$)?");
171
172     private SearchManager mSearchManager;
173
174     public BrowserProvider() {
175     }
176
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";
181         Cursor c = null;
182         try {
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);
187             }
188         } catch (RuntimeException ex) {
189             // fall through to return the default
190         } finally {
191             if (c != null) {
192                 c.close();
193             }
194         }
195         return ret;
196     }
197
198     private static CharSequence replaceSystemPropertyInString(Context context, CharSequence srcString) {
199         StringBuffer sb = new StringBuffer();
200         int lastCharLoc = 0;
201
202         final String client_id = getClientId(context.getContentResolver());
203
204         for (int i = 0; i < srcString.length(); ++i) {
205             char c = srcString.charAt(i);
206             if (c == '{') {
207                 sb.append(srcString.subSequence(lastCharLoc, i));
208                 lastCharLoc = i;
209           inner:
210                 for (int j = i; j < srcString.length(); ++j) {
211                     char k = srcString.charAt(j);
212                     if (k == '}') {
213                         String propertyKeyValue = srcString.subSequence(i + 1, j).toString();
214                         if (propertyKeyValue.equals("CLIENT_ID")) {
215                             sb.append(client_id);
216                         } else {
217                             sb.append("unknown");
218                         }
219                         lastCharLoc = j + 1;
220                         i = j;
221                         break inner;
222                     }
223                 }
224             }
225         }
226         if (srcString.length() - lastCharLoc > 0) {
227             // Put on the tail, if there is one
228             sb.append(srcString.subSequence(lastCharLoc, srcString.length()));
229         }
230         return sb;
231     }
232
233     private static class DatabaseHelper extends SQLiteOpenHelper {
234         private Context mContext;
235
236         public DatabaseHelper(Context context) {
237             super(context, sDatabaseName, null, DATABASE_VERSION);
238             mContext = context;
239         }
240
241         @Override
242         public void onCreate(SQLiteDatabase db) {
243             db.execSQL("CREATE TABLE bookmarks (" +
244                     "_id INTEGER PRIMARY KEY," +
245                     "title TEXT," +
246                     "url TEXT," +
247                     "visits INTEGER," +
248                     "date LONG," +
249                     "created LONG," +
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" +
256                     ");");
257
258             final CharSequence[] bookmarks = mContext.getResources()
259                     .getTextArray(R.array.bookmarks);
260             int size = bookmarks.length;
261             try {
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 +
267                             "', 0, 0, 0, 1);");
268                 }
269             } catch (ArrayIndexOutOfBoundsException e) {
270             }
271
272             db.execSQL("CREATE TABLE searches (" +
273                     "_id INTEGER PRIMARY KEY," +
274                     "search TEXT," +
275                     "date LONG" +
276                     ");");
277         }
278
279         @Override
280         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
281             Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
282                     + newVersion);
283             if (oldVersion == 18) {
284                 db.execSQL("DROP TABLE IF EXISTS labels");
285             }
286             if (oldVersion <= 19) {
287                 db.execSQL("ALTER TABLE bookmarks ADD COLUMN thumbnail BLOB DEFAULT NULL;");
288             }
289             if (oldVersion < 21) {
290                 db.execSQL("ALTER TABLE bookmarks ADD COLUMN touch_icon BLOB DEFAULT NULL;");
291             }
292             if (oldVersion < 22) {
293                 db.execSQL("DELETE FROM bookmarks WHERE (bookmark = 0 AND url LIKE \"%.google.%client=ms-%\")");
294                 removeGears();
295             }
296             if (oldVersion < 23) {
297                 db.execSQL("ALTER TABLE bookmarks ADD COLUMN user_entered INTEGER;");
298             } else {
299                 db.execSQL("DROP TABLE IF EXISTS bookmarks");
300                 db.execSQL("DROP TABLE IF EXISTS searches");
301                 onCreate(db);
302             }
303         }
304
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()) {
314                         return null;
315                     }
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);
320                         }
321                     });
322                     for (int i = 0; i < gearsFiles.length; ++i) {
323                         if (gearsFiles[i].isDirectory()) {
324                             deleteDirectory(gearsFiles[i]);
325                         } else {
326                             gearsFiles[i].delete();
327                         }
328                     }
329                     // Delete the Gears data files
330                     File gearsDataDir = new File(browserDataDirString + File.separator
331                             + gearsPrefix);
332                     if (!gearsDataDir.exists()) {
333                         return null;
334                     }
335                     deleteDirectory(gearsDataDir);
336                     return null;
337                 }
338
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]);
344                         }
345                         files[i].delete();
346                     }
347                     currentDir.delete();
348                 }
349             };
350
351             task.execute();
352         }
353     }
354
355     @Override
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);
367             if (fix) {
368                 fixPicasaBookmark();
369                 Editor ed = p.edit();
370                 ed.putBoolean("fix_picasa", false);
371                 ed.commit();
372             }
373         }
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();
382         return true;
383     }
384
385     /**
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.
389      */
390     /* package */ class ShowWebSuggestionsSettingChangeObserver
391             extends ContentObserver {
392         public ShowWebSuggestionsSettingChangeObserver() {
393             super(new Handler());
394         }
395
396         @Override
397         public void onChange(boolean selfChange) {
398             updateShowWebSuggestions();
399         }
400     }
401
402     private ShowWebSuggestionsSettingChangeObserver
403             mShowWebSuggestionsSettingChangeObserver;
404
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;
408
409     /**
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.
413      */
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);
423             }
424         }
425     }
426
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 });
431         try {
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()
438                         + ", 1);");
439             }
440         } finally {
441             if (cursor != null) {
442                 cursor.close();
443             }
444         }
445     }
446
447     /*
448      * Subclass AbstractCursor so we can combine multiple Cursors and add
449      * "Search the web".
450      * Here are the rules.
451      * 1. We only have MAX_SUGGESTION_LONG_ENTRIES in the list plus
452      *      "Search the web";
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
455      *      place.
456      */
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;
469
470         public MySuggestionCursor(Cursor hc, Cursor sc, String string) {
471             mHistoryCursor = hc;
472             mSuggestCursor = sc;
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;
477             }
478             mString = string;
479             mIncludeWebSearch = string.length() > 0;
480
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;
490             } else {
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);
501             }
502         }
503
504         @Override
505         public boolean onMove(int oldPosition, int newPosition) {
506             if (mHistoryCursor == null) {
507                 return false;
508             }
509             if (mIncludeWebSearch) {
510                 if (mHistoryCount == 0 && newPosition == 0) {
511                     return true;
512                 } else if (mHistoryCount > 0) {
513                     if (newPosition == 0) {
514                         mHistoryCursor.moveToPosition(0);
515                         return true;
516                     } else if (newPosition == 1) {
517                         return true;
518                     }
519                 }
520                 newPosition--;
521             }
522             if (mHistoryCount > newPosition) {
523                 mHistoryCursor.moveToPosition(newPosition);
524             } else {
525                 mSuggestCursor.moveToPosition(newPosition - mHistoryCount);
526             }
527             return true;
528         }
529
530         @Override
531         public int getCount() {
532             if (mIncludeWebSearch) {
533                 return mHistoryCount + mSuggestionCount + 1;
534             } else {
535                 return mHistoryCount + mSuggestionCount;
536             }
537         }
538
539         @Override
540         public String[] getColumnNames() {
541             return COLUMNS;
542         }
543
544         @Override
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) {
550                         type = 0;
551                     } else if (mHistoryCount > 0) {
552                         if (mPos == 0) {
553                             type = 1;
554                         } else if (mPos == 1) {
555                             type = 0;
556                         }
557                     }
558                     if (type == -1) type = (mPos - 1) < mHistoryCount ? 1 : 2;
559                 } else {
560                     type = mPos < mHistoryCount ? 1 : 2;
561                 }
562
563                 switch(columnIndex) {
564                     case SUGGEST_COLUMN_INTENT_ACTION_ID:
565                         if (type == 1) {
566                             return Intent.ACTION_VIEW;
567                         } else {
568                             return Intent.ACTION_SEARCH;
569                         }
570
571                     case SUGGEST_COLUMN_INTENT_DATA_ID:
572                         if (type == 1) {
573                             return mHistoryCursor.getString(1);
574                         } else {
575                             return null;
576                         }
577
578                     case SUGGEST_COLUMN_TEXT_1_ID:
579                         if (type == 0) {
580                             return mString;
581                         } else if (type == 1) {
582                             return getHistoryTitle();
583                         } else {
584                             if (mSuggestText1Id == -1) return null;
585                             return mSuggestCursor.getString(mSuggestText1Id);
586                         }
587
588                     case SUGGEST_COLUMN_TEXT_2_ID:
589                         if (type == 0) {
590                             return getContext().getString(R.string.search_the_web);
591                         } else if (type == 1) {
592                             return null;  // Use TEXT_2_URL instead
593                         } else {
594                             if (mSuggestText2Id == -1) return null;
595                             return mSuggestCursor.getString(mSuggestText2Id);
596                         }
597
598                     case SUGGEST_COLUMN_TEXT_2_URL_ID:
599                         if (type == 0) {
600                             return null;
601                         } else if (type == 1) {
602                             return getHistoryUrl();
603                         } else {
604                             if (mSuggestText2UrlId == -1) return null;
605                             return mSuggestCursor.getString(mSuggestText2UrlId);
606                         }
607
608                     case SUGGEST_COLUMN_ICON_1_ID:
609                         if (type == 1) {
610                             if (mHistoryCursor.getInt(3) == 1) {
611                                 return Integer.valueOf(
612                                         R.drawable.ic_search_category_bookmark)
613                                         .toString();
614                             } else {
615                                 return Integer.valueOf(
616                                         R.drawable.ic_search_category_history)
617                                         .toString();
618                             }
619                         } else {
620                             return Integer.valueOf(
621                                     R.drawable.ic_search_category_suggest)
622                                     .toString();
623                         }
624
625                     case SUGGEST_COLUMN_ICON_2_ID:
626                         return "0";
627
628                     case SUGGEST_COLUMN_QUERY_ID:
629                         if (type == 0) {
630                             return mString;
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);
637                         } else {
638                             if (mSuggestQueryId == -1) return null;
639                             return mSuggestCursor.getString(mSuggestQueryId);
640                         }
641
642                     case SUGGEST_COLUMN_INTENT_EXTRA_DATA:
643                         if (type == 0) {
644                             return null;
645                         } else if (type == 1) {
646                             return null;
647                         } else {
648                             if (mSuggestIntentExtraDataId == -1) return null;
649                             return mSuggestCursor.getString(mSuggestIntentExtraDataId);
650                         }
651                 }
652             }
653             return null;
654         }
655
656         @Override
657         public double getDouble(int column) {
658             throw new UnsupportedOperationException();
659         }
660
661         @Override
662         public float getFloat(int column) {
663             throw new UnsupportedOperationException();
664         }
665
666         @Override
667         public int getInt(int column) {
668             throw new UnsupportedOperationException();
669         }
670
671         @Override
672         public long getLong(int column) {
673             if ((mPos != -1) && column == 0) {
674                 return mPos;        // use row# as the _Id
675             }
676             throw new UnsupportedOperationException();
677         }
678
679         @Override
680         public short getShort(int column) {
681             throw new UnsupportedOperationException();
682         }
683
684         @Override
685         public boolean isNull(int column) {
686             throw new UnsupportedOperationException();
687         }
688
689         // TODO Temporary change, finalize after jq's changes go in
690         public void deactivate() {
691             if (mHistoryCursor != null) {
692                 mHistoryCursor.deactivate();
693             }
694             if (mSuggestCursor != null) {
695                 mSuggestCursor.deactivate();
696             }
697             super.deactivate();
698         }
699
700         public boolean requery() {
701             return (mHistoryCursor != null ? mHistoryCursor.requery() : false) |
702                     (mSuggestCursor != null ? mSuggestCursor.requery() : false);
703         }
704
705         // TODO Temporary change, finalize after jq's changes go in
706         public void close() {
707             super.close();
708             if (mHistoryCursor != null) {
709                 mHistoryCursor.close();
710                 mHistoryCursor = null;
711             }
712             if (mSuggestCursor != null) {
713                 mSuggestCursor.close();
714                 mSuggestCursor = null;
715             }
716         }
717
718         /**
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.
721          *
722          * @return the title string to use
723          */
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 */));
728             }
729             return title;
730         }
731
732         /**
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.
736          *
737          * @return the subtitle string to use, or null if none
738          */
739         private String getHistoryUrl() {
740             String title = mHistoryCursor.getString(2 /* webpage title */);
741             if (TextUtils.isEmpty(title) || TextUtils.getTrimmedLength(title) == 0) {
742                 return null;
743             } else {
744                 return stripUrl(mHistoryCursor.getString(1 /* url */));
745             }
746         }
747
748     }
749
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;
757
758         private static final String[] RESULTS_COLUMNS = new String[] {
759                 "_id",
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
765         };
766         private final ArrayList<String> mResults;
767         public ResultsCursor(ArrayList<String> results) {
768             mResults = results;
769         }
770         public int getCount() { return mResults.size(); }
771
772         public String[] getColumnNames() {
773             return RESULTS_COLUMNS;
774         }
775
776         public String getString(int column) {
777             switch (column) {
778                 case RESULT_ACTION_ID:
779                     return RecognizerResultsIntent.ACTION_VOICE_SEARCH_RESULTS;
780                 case RESULT_TEXT_ID:
781                 // The data is used when the phone is in landscape mode.  We
782                 // still want to show the result string.
783                 case RESULT_DATA_ID:
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
788                     // use.
789                     return Integer.toString(mPos);
790                 case RESULT_ICON_ID:
791                     return Integer.valueOf(R.drawable.magnifying_glass)
792                             .toString();
793                 default:
794                     return null;
795             }
796         }
797         public short getShort(int column) {
798             throw new UnsupportedOperationException();
799         }
800         public int getInt(int column) {
801             throw new UnsupportedOperationException();
802         }
803         public long getLong(int column) {
804             if ((mPos != -1) && column == 0) {
805                 return mPos;        // use row# as the _id
806             }
807             throw new UnsupportedOperationException();
808         }
809         public float getFloat(int column) {
810             throw new UnsupportedOperationException();
811         }
812         public double getDouble(int column) {
813             throw new UnsupportedOperationException();
814         }
815         public boolean isNull(int column) {
816             throw new UnsupportedOperationException();
817         }
818     }
819
820     private ResultsCursor mResultsCursor;
821
822     /**
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
826      */
827     /* package */ void setQueryResults(ArrayList<String> results) {
828         if (results == null) {
829             mResultsCursor = null;
830         } else {
831             mResultsCursor = new ResultsCursor(results);
832         }
833     }
834
835     @Override
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);
840         if (match == -1) {
841             throw new IllegalArgumentException("Unknown URL");
842         }
843         if (match == URI_MATCH_GEOLOCATION) {
844             throw new UnsupportedOperationException("query() not supported for geolocation");
845         }
846         if (match == URI_MATCH_SUGGEST && mResultsCursor != null) {
847             Cursor results = mResultsCursor;
848             mResultsCursor = null;
849             return results;
850         }
851         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
852
853         if (match == URI_MATCH_SUGGEST || match == URI_MATCH_BOOKMARKS_SUGGEST) {
854             String suggestSelection;
855             String [] myArgs;
856             if (selectionArgs[0] == null || selectionArgs[0].equals("")) {
857                 suggestSelection = null;
858                 myArgs = null;
859             } else {
860                 String like = selectionArgs[0] + "%";
861                 if (selectionArgs[0].startsWith("http")
862                         || selectionArgs[0].startsWith("file")) {
863                     myArgs = new String[1];
864                     myArgs[0] = like;
865                     suggestSelection = selection;
866                 } else {
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;
875                 }
876             }
877
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);
881
882             if (match == URI_MATCH_BOOKMARKS_SUGGEST
883                     || Patterns.WEB_URL.matcher(selectionArgs[0]).matches()) {
884                 return new MySuggestionCursor(c, null, "");
885             } else {
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]);
892                 }
893                 return new MySuggestionCursor(c, null, selectionArgs[0]);
894             }
895         }
896
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";
902         }
903
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))
907                     .append(")");
908         }
909
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 ");
914             }
915
916             whereClause.append('(');
917             whereClause.append(selection);
918             whereClause.append(')');
919         }
920         Cursor c = db.query(TABLE_NAMES[match % 10], projection,
921                 whereClause.toString(), selectionArgs, null, null, sortOrder,
922                 null);
923         c.setNotificationUri(getContext().getContentResolver(), url);
924         return c;
925     }
926
927     @Override
928     public String getType(Uri url) {
929         int match = URI_MATCHER.match(url);
930         switch (match) {
931             case URI_MATCH_BOOKMARKS:
932                 return "vnd.android.cursor.dir/bookmark";
933
934             case URI_MATCH_BOOKMARKS_ID:
935                 return "vnd.android.cursor.item/bookmark";
936
937             case URI_MATCH_SEARCHES:
938                 return "vnd.android.cursor.dir/searches";
939
940             case URI_MATCH_SEARCHES_ID:
941                 return "vnd.android.cursor.item/searches";
942
943             case URI_MATCH_SUGGEST:
944                 return SearchManager.SUGGEST_MIME_TYPE;
945
946             case URI_MATCH_GEOLOCATION:
947                 return "vnd.android.cursor.dir/geolocation";
948
949             default:
950                 throw new IllegalArgumentException("Unknown URL");
951         }
952     }
953
954     @Override
955     public Uri insert(Uri url, ContentValues initialValues) {
956         boolean isBookmarkTable = false;
957         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
958
959         int match = URI_MATCHER.match(url);
960         Uri uri = null;
961         switch (match) {
962             case URI_MATCH_BOOKMARKS: {
963                 // Insert into the bookmarks table
964                 long rowID = db.insert(TABLE_NAMES[URI_MATCH_BOOKMARKS], "url",
965                         initialValues);
966                 if (rowID > 0) {
967                     uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI,
968                             rowID);
969                 }
970                 isBookmarkTable = true;
971                 break;
972             }
973
974             case URI_MATCH_SEARCHES: {
975                 // Insert into the searches table
976                 long rowID = db.insert(TABLE_NAMES[URI_MATCH_SEARCHES], "url",
977                         initialValues);
978                 if (rowID > 0) {
979                     uri = ContentUris.withAppendedId(Browser.SEARCHES_URI,
980                             rowID);
981                 }
982                 break;
983             }
984
985             case URI_MATCH_GEOLOCATION:
986                 String origin = initialValues.getAsString(Browser.GeolocationColumns.ORIGIN);
987                 if (TextUtils.isEmpty(origin)) {
988                     throw new IllegalArgumentException("Empty origin");
989                 }
990                 GeolocationPermissions.getInstance().allow(origin);
991                 // TODO: Should we have one URI per permission?
992                 uri = Browser.GEOLOCATION_URI;
993                 break;
994
995             default:
996                 throw new IllegalArgumentException("Unknown URL");
997         }
998
999         if (uri == null) {
1000             throw new IllegalArgumentException("Unknown URL");
1001         }
1002         getContext().getContentResolver().notifyChange(uri, null);
1003
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.
1007         if (isBookmarkTable
1008                 && initialValues.containsKey(BookmarkColumns.BOOKMARK)
1009                 && initialValues.getAsInteger(BookmarkColumns.BOOKMARK) != 0) {
1010             mBackupManager.dataChanged();
1011         }
1012         return uri;
1013     }
1014
1015     @Override
1016     public int delete(Uri url, String where, String[] whereArgs) {
1017         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1018
1019         int match = URI_MATCHER.match(url);
1020         if (match == -1 || match == URI_MATCH_SUGGEST) {
1021             throw new IllegalArgumentException("Unknown URL");
1022         }
1023
1024         if (match == URI_MATCH_GEOLOCATION) {
1025             return deleteGeolocation(url, where, whereArgs);
1026         }
1027
1028         // need to know whether it's the bookmarks table for a couple of reasons
1029         boolean isBookmarkTable = (match == URI_MATCH_BOOKMARKS_ID);
1030         String id = null;
1031
1032         if (isBookmarkTable || match == URI_MATCH_SEARCHES_ID) {
1033             StringBuilder sb = new StringBuilder();
1034             if (where != null && where.length() > 0) {
1035                 sb.append("( ");
1036                 sb.append(where);
1037                 sb.append(" ) AND ");
1038             }
1039             id = url.getPathSegments().get(1);
1040             sb.append("_id = ");
1041             sb.append(id);
1042             where = sb.toString();
1043         }
1044
1045         ContentResolver cr = getContext().getContentResolver();
1046
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();
1056                 }
1057             }
1058             cursor.close();
1059         }
1060
1061         int count = db.delete(TABLE_NAMES[match % 10], where, whereArgs);
1062         cr.notifyChange(url, null);
1063         return count;
1064     }
1065
1066     private int deleteGeolocation(Uri uri, String where, String[] whereArgs) {
1067         if (whereArgs.length != 1) {
1068             throw new IllegalArgumentException("Bad where arguments");
1069         }
1070         String origin = whereArgs[0];
1071         if (TextUtils.isEmpty(origin)) {
1072             throw new IllegalArgumentException("Empty origin");
1073         }
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
1077     }
1078
1079     @Override
1080     public int update(Uri url, ContentValues values, String where,
1081             String[] whereArgs) {
1082         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1083
1084         int match = URI_MATCHER.match(url);
1085         if (match == -1 || match == URI_MATCH_SUGGEST) {
1086             throw new IllegalArgumentException("Unknown URL");
1087         }
1088
1089         if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) {
1090             StringBuilder sb = new StringBuilder();
1091             if (where != null && where.length() > 0) {
1092                 sb.append("( ");
1093                 sb.append(where);
1094                 sb.append(" ) AND ");
1095             }
1096             String id = url.getPathSegments().get(1);
1097             sb.append("_id = ");
1098             sb.append(id);
1099             where = sb.toString();
1100         }
1101
1102         ContentResolver cr = getContext().getContentResolver();
1103
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,
1119                 // so use it.
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);
1126                 }
1127                 cursor.close();
1128             }
1129
1130             // if this *is* a bookmark row we're altering, we need to back it up.
1131             if (changingBookmarks) {
1132                 mBackupManager.dataChanged();
1133             }
1134         }
1135
1136         int ret = db.update(TABLE_NAMES[match % 10], values, where, whereArgs);
1137         cr.notifyChange(url, null);
1138         return ret;
1139     }
1140
1141     /**
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
1144      * is returned.
1145      *
1146      * TODO: Put this in TextUtils to be used by other packages doing something similar.
1147      *
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
1150      *         not be stripped
1151      */
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) {
1156             return m.group(2);
1157         } else {
1158             return url;
1159         }
1160     }
1161
1162 }