• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.provider;
18 
19 import com.android.browser.BrowserSettings;
20 import com.android.browser.R;
21 import com.android.browser.search.SearchEngine;
22 
23 import android.app.SearchManager;
24 import android.app.backup.BackupManager;
25 import android.content.ContentProvider;
26 import android.content.ContentResolver;
27 import android.content.ContentUris;
28 import android.content.ContentValues;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.SharedPreferences;
32 import android.content.SharedPreferences.Editor;
33 import android.content.UriMatcher;
34 import android.content.res.Configuration;
35 import android.database.AbstractCursor;
36 import android.database.Cursor;
37 import android.database.DatabaseUtils;
38 import android.database.sqlite.SQLiteDatabase;
39 import android.database.sqlite.SQLiteOpenHelper;
40 import android.net.Uri;
41 import android.os.Process;
42 import android.preference.PreferenceManager;
43 import android.provider.Browser;
44 import android.provider.Browser.BookmarkColumns;
45 import android.speech.RecognizerResultsIntent;
46 import android.text.TextUtils;
47 import android.util.Log;
48 import android.util.Patterns;
49 
50 import java.io.File;
51 import java.io.FilenameFilter;
52 import java.util.ArrayList;
53 import java.util.Date;
54 import java.util.regex.Matcher;
55 import java.util.regex.Pattern;
56 
57 
58 public class BrowserProvider extends ContentProvider {
59 
60     private SQLiteOpenHelper mOpenHelper;
61     private BackupManager mBackupManager;
62     static final String sDatabaseName = "browser.db";
63     private static final String TAG = "BrowserProvider";
64     private static final String ORDER_BY = "visits DESC, date DESC";
65 
66     private static final String PICASA_URL = "http://picasaweb.google.com/m/" +
67             "viewer?source=androidclient";
68 
69     static final String[] TABLE_NAMES = new String[] {
70         "bookmarks", "searches"
71     };
72     private static final String[] SUGGEST_PROJECTION = new String[] {
73             "_id", "url", "title", "bookmark", "user_entered"
74     };
75     private static final String SUGGEST_SELECTION =
76             "(url LIKE ? OR url LIKE ? OR url LIKE ? OR url LIKE ?"
77                 + " OR title LIKE ?) AND (bookmark = 1 OR user_entered = 1)";
78     private String[] SUGGEST_ARGS = new String[5];
79 
80     // shared suggestion array index, make sure to match COLUMNS
81     private static final int SUGGEST_COLUMN_INTENT_ACTION_ID = 1;
82     private static final int SUGGEST_COLUMN_INTENT_DATA_ID = 2;
83     private static final int SUGGEST_COLUMN_TEXT_1_ID = 3;
84     private static final int SUGGEST_COLUMN_TEXT_2_ID = 4;
85     private static final int SUGGEST_COLUMN_TEXT_2_URL_ID = 5;
86     private static final int SUGGEST_COLUMN_ICON_1_ID = 6;
87     private static final int SUGGEST_COLUMN_ICON_2_ID = 7;
88     private static final int SUGGEST_COLUMN_QUERY_ID = 8;
89     private static final int SUGGEST_COLUMN_INTENT_EXTRA_DATA = 9;
90 
91     // how many suggestions will be shown in dropdown
92     // 0..SHORT: filled by browser db
93     private static final int MAX_SUGGEST_SHORT_SMALL = 3;
94     // SHORT..LONG: filled by search suggestions
95     private static final int MAX_SUGGEST_LONG_SMALL = 6;
96 
97     // large screen size shows more
98     private static final int MAX_SUGGEST_SHORT_LARGE = 6;
99     private static final int MAX_SUGGEST_LONG_LARGE = 9;
100 
101 
102     // shared suggestion columns
103     private static final String[] COLUMNS = new String[] {
104             "_id",
105             SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
106             SearchManager.SUGGEST_COLUMN_INTENT_DATA,
107             SearchManager.SUGGEST_COLUMN_TEXT_1,
108             SearchManager.SUGGEST_COLUMN_TEXT_2,
109             SearchManager.SUGGEST_COLUMN_TEXT_2_URL,
110             SearchManager.SUGGEST_COLUMN_ICON_1,
111             SearchManager.SUGGEST_COLUMN_ICON_2,
112             SearchManager.SUGGEST_COLUMN_QUERY,
113             SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA};
114 
115 
116     // make sure that these match the index of TABLE_NAMES
117     static final int URI_MATCH_BOOKMARKS = 0;
118     private static final int URI_MATCH_SEARCHES = 1;
119     // (id % 10) should match the table name index
120     private static final int URI_MATCH_BOOKMARKS_ID = 10;
121     private static final int URI_MATCH_SEARCHES_ID = 11;
122     //
123     private static final int URI_MATCH_SUGGEST = 20;
124     private static final int URI_MATCH_BOOKMARKS_SUGGEST = 21;
125 
126     private static final UriMatcher URI_MATCHER;
127 
128     static {
129         URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
130         URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_BOOKMARKS],
131                 URI_MATCH_BOOKMARKS);
132         URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_BOOKMARKS] + "/#",
133                 URI_MATCH_BOOKMARKS_ID);
134         URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_SEARCHES],
135                 URI_MATCH_SEARCHES);
136         URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_SEARCHES] + "/#",
137                 URI_MATCH_SEARCHES_ID);
138         URI_MATCHER.addURI("browser", SearchManager.SUGGEST_URI_PATH_QUERY,
139                 URI_MATCH_SUGGEST);
140         URI_MATCHER.addURI("browser",
141                 TABLE_NAMES[URI_MATCH_BOOKMARKS] + "/" + SearchManager.SUGGEST_URI_PATH_QUERY,
142                 URI_MATCH_BOOKMARKS_SUGGEST);
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     // 23 -> 24 Url not allowed to be null anymore.
167     private static final int DATABASE_VERSION = 24;
168 
169     // Regular expression which matches http://, followed by some stuff, followed by
170     // optionally a trailing slash, all matched as separate groups.
171     private static final Pattern STRIP_URL_PATTERN = Pattern.compile("^(http://)(.*?)(/$)?");
172 
173     private BrowserSettings mSettings;
174 
175     private int mMaxSuggestionShortSize;
176     private int mMaxSuggestionLongSize;
177 
BrowserProvider()178     public BrowserProvider() {
179     }
180 
181     // XXX: This is a major hack to remove our dependency on gsf constants and
182     // its content provider. http://b/issue?id=2425179
getClientId(ContentResolver cr)183     public static String getClientId(ContentResolver cr) {
184         String ret = "android-google";
185         Cursor legacyClientIdCursor = null;
186         Cursor searchClientIdCursor = null;
187 
188         // search_client_id includes search prefix, legacy client_id does not include prefix
189         try {
190             searchClientIdCursor = cr.query(Uri.parse("content://com.google.settings/partner"),
191                new String[] { "value" }, "name='search_client_id'", null, null);
192             if (searchClientIdCursor != null && searchClientIdCursor.moveToNext()) {
193                 ret = searchClientIdCursor.getString(0);
194             } else {
195                 legacyClientIdCursor = cr.query(Uri.parse("content://com.google.settings/partner"),
196                     new String[] { "value" }, "name='client_id'", null, null);
197                 if (legacyClientIdCursor != null && legacyClientIdCursor.moveToNext()) {
198                     ret = "ms-" + legacyClientIdCursor.getString(0);
199                 }
200             }
201         } catch (RuntimeException ex) {
202             // fall through to return the default
203         } finally {
204             if (legacyClientIdCursor != null) {
205                 legacyClientIdCursor.close();
206             }
207             if (searchClientIdCursor != null) {
208                 searchClientIdCursor.close();
209             }
210         }
211         return ret;
212     }
213 
replaceSystemPropertyInString(Context context, CharSequence srcString)214     private static CharSequence replaceSystemPropertyInString(Context context, CharSequence srcString) {
215         StringBuffer sb = new StringBuffer();
216         int lastCharLoc = 0;
217 
218         final String client_id = getClientId(context.getContentResolver());
219 
220         for (int i = 0; i < srcString.length(); ++i) {
221             char c = srcString.charAt(i);
222             if (c == '{') {
223                 sb.append(srcString.subSequence(lastCharLoc, i));
224                 lastCharLoc = i;
225           inner:
226                 for (int j = i; j < srcString.length(); ++j) {
227                     char k = srcString.charAt(j);
228                     if (k == '}') {
229                         String propertyKeyValue = srcString.subSequence(i + 1, j).toString();
230                         if (propertyKeyValue.equals("CLIENT_ID")) {
231                             sb.append(client_id);
232                         } else {
233                             sb.append("unknown");
234                         }
235                         lastCharLoc = j + 1;
236                         i = j;
237                         break inner;
238                     }
239                 }
240             }
241         }
242         if (srcString.length() - lastCharLoc > 0) {
243             // Put on the tail, if there is one
244             sb.append(srcString.subSequence(lastCharLoc, srcString.length()));
245         }
246         return sb;
247     }
248 
249     static class DatabaseHelper extends SQLiteOpenHelper {
250         private Context mContext;
251 
DatabaseHelper(Context context)252         public DatabaseHelper(Context context) {
253             super(context, sDatabaseName, null, DATABASE_VERSION);
254             mContext = context;
255         }
256 
257         @Override
onCreate(SQLiteDatabase db)258         public void onCreate(SQLiteDatabase db) {
259             db.execSQL("CREATE TABLE bookmarks (" +
260                     "_id INTEGER PRIMARY KEY," +
261                     "title TEXT," +
262                     "url TEXT NOT NULL," +
263                     "visits INTEGER," +
264                     "date LONG," +
265                     "created LONG," +
266                     "description TEXT," +
267                     "bookmark INTEGER," +
268                     "favicon BLOB DEFAULT NULL," +
269                     "thumbnail BLOB DEFAULT NULL," +
270                     "touch_icon BLOB DEFAULT NULL," +
271                     "user_entered INTEGER" +
272                     ");");
273 
274             final CharSequence[] bookmarks = mContext.getResources()
275                     .getTextArray(R.array.bookmarks);
276             int size = bookmarks.length;
277             try {
278                 for (int i = 0; i < size; i = i + 2) {
279                     CharSequence bookmarkDestination = replaceSystemPropertyInString(mContext, bookmarks[i + 1]);
280                     db.execSQL("INSERT INTO bookmarks (title, url, visits, " +
281                             "date, created, bookmark)" + " VALUES('" +
282                             bookmarks[i] + "', '" + bookmarkDestination +
283                             "', 0, 0, 0, 1);");
284                 }
285             } catch (ArrayIndexOutOfBoundsException e) {
286             }
287 
288             db.execSQL("CREATE TABLE searches (" +
289                     "_id INTEGER PRIMARY KEY," +
290                     "search TEXT," +
291                     "date LONG" +
292                     ");");
293         }
294 
295         @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)296         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
297             Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
298                     + newVersion);
299             if (oldVersion == 18) {
300                 db.execSQL("DROP TABLE IF EXISTS labels");
301             }
302             if (oldVersion <= 19) {
303                 db.execSQL("ALTER TABLE bookmarks ADD COLUMN thumbnail BLOB DEFAULT NULL;");
304             }
305             if (oldVersion < 21) {
306                 db.execSQL("ALTER TABLE bookmarks ADD COLUMN touch_icon BLOB DEFAULT NULL;");
307             }
308             if (oldVersion < 22) {
309                 db.execSQL("DELETE FROM bookmarks WHERE (bookmark = 0 AND url LIKE \"%.google.%client=ms-%\")");
310                 removeGears();
311             }
312             if (oldVersion < 23) {
313                 db.execSQL("ALTER TABLE bookmarks ADD COLUMN user_entered INTEGER;");
314             }
315             if (oldVersion < 24) {
316                 /* SQLite does not support ALTER COLUMN, hence the lengthy code. */
317                 db.execSQL("DELETE FROM bookmarks WHERE url IS NULL;");
318                 db.execSQL("ALTER TABLE bookmarks RENAME TO bookmarks_temp;");
319                 db.execSQL("CREATE TABLE bookmarks (" +
320                         "_id INTEGER PRIMARY KEY," +
321                         "title TEXT," +
322                         "url TEXT NOT NULL," +
323                         "visits INTEGER," +
324                         "date LONG," +
325                         "created LONG," +
326                         "description TEXT," +
327                         "bookmark INTEGER," +
328                         "favicon BLOB DEFAULT NULL," +
329                         "thumbnail BLOB DEFAULT NULL," +
330                         "touch_icon BLOB DEFAULT NULL," +
331                         "user_entered INTEGER" +
332                         ");");
333                 db.execSQL("INSERT INTO bookmarks SELECT * FROM bookmarks_temp;");
334                 db.execSQL("DROP TABLE bookmarks_temp;");
335             } else {
336                 db.execSQL("DROP TABLE IF EXISTS bookmarks");
337                 db.execSQL("DROP TABLE IF EXISTS searches");
338                 onCreate(db);
339             }
340         }
341 
removeGears()342         private void removeGears() {
343             new Thread() {
344                 @Override
345                 public void run() {
346                     Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
347                     String browserDataDirString = mContext.getApplicationInfo().dataDir;
348                     final String appPluginsDirString = "app_plugins";
349                     final String gearsPrefix = "gears";
350                     File appPluginsDir = new File(browserDataDirString + File.separator
351                             + appPluginsDirString);
352                     if (!appPluginsDir.exists()) {
353                         return;
354                     }
355                     // Delete the Gears plugin files
356                     File[] gearsFiles = appPluginsDir.listFiles(new FilenameFilter() {
357                         public boolean accept(File dir, String filename) {
358                             return filename.startsWith(gearsPrefix);
359                         }
360                     });
361                     for (int i = 0; i < gearsFiles.length; ++i) {
362                         if (gearsFiles[i].isDirectory()) {
363                             deleteDirectory(gearsFiles[i]);
364                         } else {
365                             gearsFiles[i].delete();
366                         }
367                     }
368                     // Delete the Gears data files
369                     File gearsDataDir = new File(browserDataDirString + File.separator
370                             + gearsPrefix);
371                     if (!gearsDataDir.exists()) {
372                         return;
373                     }
374                     deleteDirectory(gearsDataDir);
375                 }
376 
377                 private void deleteDirectory(File currentDir) {
378                     File[] files = currentDir.listFiles();
379                     for (int i = 0; i < files.length; ++i) {
380                         if (files[i].isDirectory()) {
381                             deleteDirectory(files[i]);
382                         }
383                         files[i].delete();
384                     }
385                     currentDir.delete();
386                 }
387             }.start();
388         }
389     }
390 
391     @Override
onCreate()392     public boolean onCreate() {
393         final Context context = getContext();
394         boolean xlargeScreenSize = (context.getResources().getConfiguration().screenLayout
395                 & Configuration.SCREENLAYOUT_SIZE_MASK)
396                 == Configuration.SCREENLAYOUT_SIZE_XLARGE;
397         boolean isPortrait = (context.getResources().getConfiguration().orientation
398                 == Configuration.ORIENTATION_PORTRAIT);
399 
400 
401         if (xlargeScreenSize && isPortrait) {
402             mMaxSuggestionLongSize = MAX_SUGGEST_LONG_LARGE;
403             mMaxSuggestionShortSize = MAX_SUGGEST_SHORT_LARGE;
404         } else {
405             mMaxSuggestionLongSize = MAX_SUGGEST_LONG_SMALL;
406             mMaxSuggestionShortSize = MAX_SUGGEST_SHORT_SMALL;
407         }
408         mOpenHelper = new DatabaseHelper(context);
409         mBackupManager = new BackupManager(context);
410         // we added "picasa web album" into default bookmarks for version 19.
411         // To avoid erasing the bookmark table, we added it explicitly for
412         // version 18 and 19 as in the other cases, we will erase the table.
413         if (DATABASE_VERSION == 18 || DATABASE_VERSION == 19) {
414             SharedPreferences p = PreferenceManager
415                     .getDefaultSharedPreferences(context);
416             boolean fix = p.getBoolean("fix_picasa", true);
417             if (fix) {
418                 fixPicasaBookmark();
419                 Editor ed = p.edit();
420                 ed.putBoolean("fix_picasa", false);
421                 ed.apply();
422             }
423         }
424         mSettings = BrowserSettings.getInstance();
425         return true;
426     }
427 
fixPicasaBookmark()428     private void fixPicasaBookmark() {
429         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
430         Cursor cursor = db.rawQuery("SELECT _id FROM bookmarks WHERE " +
431                 "bookmark = 1 AND url = ?", new String[] { PICASA_URL });
432         try {
433             if (!cursor.moveToFirst()) {
434                 // set "created" so that it will be on the top of the list
435                 db.execSQL("INSERT INTO bookmarks (title, url, visits, " +
436                         "date, created, bookmark)" + " VALUES('" +
437                         getContext().getString(R.string.picasa) + "', '"
438                         + PICASA_URL + "', 0, 0, " + new Date().getTime()
439                         + ", 1);");
440             }
441         } finally {
442             if (cursor != null) {
443                 cursor.close();
444             }
445         }
446     }
447 
448     /*
449      * Subclass AbstractCursor so we can combine multiple Cursors and add
450      * "Search the web".
451      * Here are the rules.
452      * 1. We only have MAX_SUGGESTION_LONG_ENTRIES in the list plus
453      *      "Search the web";
454      * 2. If bookmark/history entries has a match, "Search the web" shows up at
455      *      the second place. Otherwise, "Search the web" shows up at the first
456      *      place.
457      */
458     private class MySuggestionCursor extends AbstractCursor {
459         private Cursor  mHistoryCursor;
460         private Cursor  mSuggestCursor;
461         private int     mHistoryCount;
462         private int     mSuggestionCount;
463         private boolean mIncludeWebSearch;
464         private String  mString;
465         private int     mSuggestText1Id;
466         private int     mSuggestText2Id;
467         private int     mSuggestText2UrlId;
468         private int     mSuggestQueryId;
469         private int     mSuggestIntentExtraDataId;
470 
MySuggestionCursor(Cursor hc, Cursor sc, String string)471         public MySuggestionCursor(Cursor hc, Cursor sc, String string) {
472             mHistoryCursor = hc;
473             mSuggestCursor = sc;
474             mHistoryCount = hc != null ? hc.getCount() : 0;
475             mSuggestionCount = sc != null ? sc.getCount() : 0;
476             if (mSuggestionCount > (mMaxSuggestionLongSize - mHistoryCount)) {
477                 mSuggestionCount = mMaxSuggestionLongSize - mHistoryCount;
478             }
479             mString = string;
480             mIncludeWebSearch = string.length() > 0;
481 
482             // Some web suggest providers only give suggestions and have no description string for
483             // items. The order of the result columns may be different as well. So retrieve the
484             // column indices for the fields we need now and check before using below.
485             if (mSuggestCursor == null) {
486                 mSuggestText1Id = -1;
487                 mSuggestText2Id = -1;
488                 mSuggestText2UrlId = -1;
489                 mSuggestQueryId = -1;
490                 mSuggestIntentExtraDataId = -1;
491             } else {
492                 mSuggestText1Id = mSuggestCursor.getColumnIndex(
493                                 SearchManager.SUGGEST_COLUMN_TEXT_1);
494                 mSuggestText2Id = mSuggestCursor.getColumnIndex(
495                                 SearchManager.SUGGEST_COLUMN_TEXT_2);
496                 mSuggestText2UrlId = mSuggestCursor.getColumnIndex(
497                         SearchManager.SUGGEST_COLUMN_TEXT_2_URL);
498                 mSuggestQueryId = mSuggestCursor.getColumnIndex(
499                                 SearchManager.SUGGEST_COLUMN_QUERY);
500                 mSuggestIntentExtraDataId = mSuggestCursor.getColumnIndex(
501                                 SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
502             }
503         }
504 
505         @Override
onMove(int oldPosition, int newPosition)506         public boolean onMove(int oldPosition, int newPosition) {
507             if (mHistoryCursor == null) {
508                 return false;
509             }
510             if (mIncludeWebSearch) {
511                 if (mHistoryCount == 0 && newPosition == 0) {
512                     return true;
513                 } else if (mHistoryCount > 0) {
514                     if (newPosition == 0) {
515                         mHistoryCursor.moveToPosition(0);
516                         return true;
517                     } else if (newPosition == 1) {
518                         return true;
519                     }
520                 }
521                 newPosition--;
522             }
523             if (mHistoryCount > newPosition) {
524                 mHistoryCursor.moveToPosition(newPosition);
525             } else {
526                 mSuggestCursor.moveToPosition(newPosition - mHistoryCount);
527             }
528             return true;
529         }
530 
531         @Override
getCount()532         public int getCount() {
533             if (mIncludeWebSearch) {
534                 return mHistoryCount + mSuggestionCount + 1;
535             } else {
536                 return mHistoryCount + mSuggestionCount;
537             }
538         }
539 
540         @Override
getColumnNames()541         public String[] getColumnNames() {
542             return COLUMNS;
543         }
544 
545         @Override
getString(int columnIndex)546         public String getString(int columnIndex) {
547             if ((mPos != -1 && mHistoryCursor != null)) {
548                 int type = -1; // 0: web search; 1: history; 2: suggestion
549                 if (mIncludeWebSearch) {
550                     if (mHistoryCount == 0 && mPos == 0) {
551                         type = 0;
552                     } else if (mHistoryCount > 0) {
553                         if (mPos == 0) {
554                             type = 1;
555                         } else if (mPos == 1) {
556                             type = 0;
557                         }
558                     }
559                     if (type == -1) type = (mPos - 1) < mHistoryCount ? 1 : 2;
560                 } else {
561                     type = mPos < mHistoryCount ? 1 : 2;
562                 }
563 
564                 switch(columnIndex) {
565                     case SUGGEST_COLUMN_INTENT_ACTION_ID:
566                         if (type == 1) {
567                             return Intent.ACTION_VIEW;
568                         } else {
569                             return Intent.ACTION_SEARCH;
570                         }
571 
572                     case SUGGEST_COLUMN_INTENT_DATA_ID:
573                         if (type == 1) {
574                             return mHistoryCursor.getString(1);
575                         } else {
576                             return null;
577                         }
578 
579                     case SUGGEST_COLUMN_TEXT_1_ID:
580                         if (type == 0) {
581                             return mString;
582                         } else if (type == 1) {
583                             return getHistoryTitle();
584                         } else {
585                             if (mSuggestText1Id == -1) return null;
586                             return mSuggestCursor.getString(mSuggestText1Id);
587                         }
588 
589                     case SUGGEST_COLUMN_TEXT_2_ID:
590                         if (type == 0) {
591                             return getContext().getString(R.string.search_the_web);
592                         } else if (type == 1) {
593                             return null;  // Use TEXT_2_URL instead
594                         } else {
595                             if (mSuggestText2Id == -1) return null;
596                             return mSuggestCursor.getString(mSuggestText2Id);
597                         }
598 
599                     case SUGGEST_COLUMN_TEXT_2_URL_ID:
600                         if (type == 0) {
601                             return null;
602                         } else if (type == 1) {
603                             return getHistoryUrl();
604                         } else {
605                             if (mSuggestText2UrlId == -1) return null;
606                             return mSuggestCursor.getString(mSuggestText2UrlId);
607                         }
608 
609                     case SUGGEST_COLUMN_ICON_1_ID:
610                         if (type == 1) {
611                             if (mHistoryCursor.getInt(3) == 1) {
612                                 return Integer.valueOf(
613                                         R.drawable.ic_search_category_bookmark)
614                                         .toString();
615                             } else {
616                                 return Integer.valueOf(
617                                         R.drawable.ic_search_category_history)
618                                         .toString();
619                             }
620                         } else {
621                             return Integer.valueOf(
622                                     R.drawable.ic_search_category_suggest)
623                                     .toString();
624                         }
625 
626                     case SUGGEST_COLUMN_ICON_2_ID:
627                         return "0";
628 
629                     case SUGGEST_COLUMN_QUERY_ID:
630                         if (type == 0) {
631                             return mString;
632                         } else if (type == 1) {
633                             // Return the url in the intent query column. This is ignored
634                             // within the browser because our searchable is set to
635                             // android:searchMode="queryRewriteFromData", but it is used by
636                             // global search for query rewriting.
637                             return mHistoryCursor.getString(1);
638                         } else {
639                             if (mSuggestQueryId == -1) return null;
640                             return mSuggestCursor.getString(mSuggestQueryId);
641                         }
642 
643                     case SUGGEST_COLUMN_INTENT_EXTRA_DATA:
644                         if (type == 0) {
645                             return null;
646                         } else if (type == 1) {
647                             return null;
648                         } else {
649                             if (mSuggestIntentExtraDataId == -1) return null;
650                             return mSuggestCursor.getString(mSuggestIntentExtraDataId);
651                         }
652                 }
653             }
654             return null;
655         }
656 
657         @Override
658         public double getDouble(int column) {
659             throw new UnsupportedOperationException();
660         }
661 
662         @Override
663         public float getFloat(int column) {
664             throw new UnsupportedOperationException();
665         }
666 
667         @Override
668         public int getInt(int column) {
669             throw new UnsupportedOperationException();
670         }
671 
672         @Override
673         public long getLong(int column) {
674             if ((mPos != -1) && column == 0) {
675                 return mPos;        // use row# as the _Id
676             }
677             throw new UnsupportedOperationException();
678         }
679 
680         @Override
681         public short getShort(int column) {
682             throw new UnsupportedOperationException();
683         }
684 
685         @Override
686         public boolean isNull(int column) {
687             throw new UnsupportedOperationException();
688         }
689 
690         // TODO Temporary change, finalize after jq's changes go in
691         @Override
692         public void deactivate() {
693             if (mHistoryCursor != null) {
694                 mHistoryCursor.deactivate();
695             }
696             if (mSuggestCursor != null) {
697                 mSuggestCursor.deactivate();
698             }
699             super.deactivate();
700         }
701 
702         @Override
703         public boolean requery() {
704             return (mHistoryCursor != null ? mHistoryCursor.requery() : false) |
705                     (mSuggestCursor != null ? mSuggestCursor.requery() : false);
706         }
707 
708         // TODO Temporary change, finalize after jq's changes go in
709         @Override
710         public void close() {
711             super.close();
712             if (mHistoryCursor != null) {
713                 mHistoryCursor.close();
714                 mHistoryCursor = null;
715             }
716             if (mSuggestCursor != null) {
717                 mSuggestCursor.close();
718                 mSuggestCursor = null;
719             }
720         }
721 
722         /**
723          * Provides the title (text line 1) for a browser suggestion, which should be the
724          * webpage title. If the webpage title is empty, returns the stripped url instead.
725          *
726          * @return the title string to use
727          */
728         private String getHistoryTitle() {
729             String title = mHistoryCursor.getString(2 /* webpage title */);
730             if (TextUtils.isEmpty(title) || TextUtils.getTrimmedLength(title) == 0) {
731                 title = stripUrl(mHistoryCursor.getString(1 /* url */));
732             }
733             return title;
734         }
735 
736         /**
737          * Provides the subtitle (text line 2) for a browser suggestion, which should be the
738          * webpage url. If the webpage title is empty, then the url should go in the title
739          * instead, and the subtitle should be empty, so this would return null.
740          *
741          * @return the subtitle string to use, or null if none
742          */
743         private String getHistoryUrl() {
744             String title = mHistoryCursor.getString(2 /* webpage title */);
745             if (TextUtils.isEmpty(title) || TextUtils.getTrimmedLength(title) == 0) {
746                 return null;
747             } else {
748                 return stripUrl(mHistoryCursor.getString(1 /* url */));
749             }
750         }
751 
752     }
753 
754     private static class ResultsCursor extends AbstractCursor {
755         // Array indices for RESULTS_COLUMNS
756         private static final int RESULT_ACTION_ID = 1;
757         private static final int RESULT_DATA_ID = 2;
758         private static final int RESULT_TEXT_ID = 3;
759         private static final int RESULT_ICON_ID = 4;
760         private static final int RESULT_EXTRA_ID = 5;
761 
762         private static final String[] RESULTS_COLUMNS = new String[] {
763                 "_id",
764                 SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
765                 SearchManager.SUGGEST_COLUMN_INTENT_DATA,
766                 SearchManager.SUGGEST_COLUMN_TEXT_1,
767                 SearchManager.SUGGEST_COLUMN_ICON_1,
768                 SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA
769         };
770         private final ArrayList<String> mResults;
771         public ResultsCursor(ArrayList<String> results) {
772             mResults = results;
773         }
774         @Override
775         public int getCount() { return mResults.size(); }
776 
777         @Override
778         public String[] getColumnNames() {
779             return RESULTS_COLUMNS;
780         }
781 
782         @Override
783         public String getString(int column) {
784             switch (column) {
785                 case RESULT_ACTION_ID:
786                     return RecognizerResultsIntent.ACTION_VOICE_SEARCH_RESULTS;
787                 case RESULT_TEXT_ID:
788                 // The data is used when the phone is in landscape mode.  We
789                 // still want to show the result string.
790                 case RESULT_DATA_ID:
791                     return mResults.get(mPos);
792                 case RESULT_EXTRA_ID:
793                     // The Intent's extra data will store the index into
794                     // mResults so the BrowserActivity will know which result to
795                     // use.
796                     return Integer.toString(mPos);
797                 case RESULT_ICON_ID:
798                     return Integer.valueOf(R.drawable.magnifying_glass)
799                             .toString();
800                 default:
801                     return null;
802             }
803         }
804         @Override
805         public short getShort(int column) {
806             throw new UnsupportedOperationException();
807         }
808         @Override
809         public int getInt(int column) {
810             throw new UnsupportedOperationException();
811         }
812         @Override
813         public long getLong(int column) {
814             if ((mPos != -1) && column == 0) {
815                 return mPos;        // use row# as the _id
816             }
817             throw new UnsupportedOperationException();
818         }
819         @Override
820         public float getFloat(int column) {
821             throw new UnsupportedOperationException();
822         }
823         @Override
824         public double getDouble(int column) {
825             throw new UnsupportedOperationException();
826         }
827         @Override
828         public boolean isNull(int column) {
829             throw new UnsupportedOperationException();
830         }
831     }
832 
833     /** Contains custom suggestions results set by the UI */
834     private ResultsCursor mResultsCursor;
835     /** Locks access to {@link #mResultsCursor} */
836     private Object mResultsCursorLock = new Object();
837 
838     /**
839      * Provide a set of results to be returned to query, intended to be used
840      * by the SearchDialog when the BrowserActivity is in voice search mode.
841      * @param results Strings to display in the dropdown from the SearchDialog
842      */
843     public /* package */ void setQueryResults(ArrayList<String> results) {
844         synchronized (mResultsCursorLock) {
845             if (results == null) {
846                 mResultsCursor = null;
847             } else {
848                 mResultsCursor = new ResultsCursor(results);
849             }
850         }
851     }
852 
853     @Override
854     public Cursor query(Uri url, String[] projectionIn, String selection,
855             String[] selectionArgs, String sortOrder)
856             throws IllegalStateException {
857         int match = URI_MATCHER.match(url);
858         if (match == -1) {
859             throw new IllegalArgumentException("Unknown URL");
860         }
861 
862         // If results for the suggestion are already ready just return them directly
863         synchronized (mResultsCursorLock) {
864             if (match == URI_MATCH_SUGGEST && mResultsCursor != null) {
865                 Cursor results = mResultsCursor;
866                 mResultsCursor = null;
867                 return results;
868             }
869         }
870 
871         if (match == URI_MATCH_SUGGEST || match == URI_MATCH_BOOKMARKS_SUGGEST) {
872             // Handle suggestions
873             return doSuggestQuery(selection, selectionArgs, match == URI_MATCH_BOOKMARKS_SUGGEST);
874         }
875 
876         String[] projection = null;
877         if (projectionIn != null && projectionIn.length > 0) {
878             projection = new String[projectionIn.length + 1];
879             System.arraycopy(projectionIn, 0, projection, 0, projectionIn.length);
880             projection[projectionIn.length] = "_id AS _id";
881         }
882 
883         String whereClause = null;
884         if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) {
885             whereClause = "_id = " + url.getPathSegments().get(1);
886         }
887 
888         Cursor c = mOpenHelper.getReadableDatabase().query(TABLE_NAMES[match % 10], projection,
889                 DatabaseUtils.concatenateWhere(whereClause, selection), selectionArgs,
890                 null, null, sortOrder, null);
891         c.setNotificationUri(getContext().getContentResolver(), url);
892         return c;
893     }
894 
895     private Cursor doSuggestQuery(String selection, String[] selectionArgs, boolean bookmarksOnly) {
896         String suggestSelection;
897         String [] myArgs;
898         if (selectionArgs[0] == null || selectionArgs[0].equals("")) {
899             return new MySuggestionCursor(null, null, "");
900         } else {
901             String like = selectionArgs[0] + "%";
902             if (selectionArgs[0].startsWith("http")
903                     || selectionArgs[0].startsWith("file")) {
904                 myArgs = new String[1];
905                 myArgs[0] = like;
906                 suggestSelection = selection;
907             } else {
908                 SUGGEST_ARGS[0] = "http://" + like;
909                 SUGGEST_ARGS[1] = "http://www." + like;
910                 SUGGEST_ARGS[2] = "https://" + like;
911                 SUGGEST_ARGS[3] = "https://www." + like;
912                 // To match against titles.
913                 SUGGEST_ARGS[4] = like;
914                 myArgs = SUGGEST_ARGS;
915                 suggestSelection = SUGGEST_SELECTION;
916             }
917         }
918 
919         Cursor c = mOpenHelper.getReadableDatabase().query(TABLE_NAMES[URI_MATCH_BOOKMARKS],
920                 SUGGEST_PROJECTION, suggestSelection, myArgs, null, null,
921                 ORDER_BY, Integer.toString(mMaxSuggestionLongSize));
922 
923         if (bookmarksOnly || Patterns.WEB_URL.matcher(selectionArgs[0]).matches()) {
924             return new MySuggestionCursor(c, null, "");
925         } else {
926             // get search suggestions if there is still space in the list
927             if (myArgs != null && myArgs.length > 1
928                     && c.getCount() < (MAX_SUGGEST_SHORT_SMALL - 1)) {
929                 SearchEngine searchEngine = mSettings.getSearchEngine();
930                 if (searchEngine != null && searchEngine.supportsSuggestions()) {
931                     Cursor sc = searchEngine.getSuggestions(getContext(), selectionArgs[0]);
932                     return new MySuggestionCursor(c, sc, selectionArgs[0]);
933                 }
934             }
935             return new MySuggestionCursor(c, null, selectionArgs[0]);
936         }
937     }
938 
939     @Override
940     public String getType(Uri url) {
941         int match = URI_MATCHER.match(url);
942         switch (match) {
943             case URI_MATCH_BOOKMARKS:
944                 return "vnd.android.cursor.dir/bookmark";
945 
946             case URI_MATCH_BOOKMARKS_ID:
947                 return "vnd.android.cursor.item/bookmark";
948 
949             case URI_MATCH_SEARCHES:
950                 return "vnd.android.cursor.dir/searches";
951 
952             case URI_MATCH_SEARCHES_ID:
953                 return "vnd.android.cursor.item/searches";
954 
955             case URI_MATCH_SUGGEST:
956                 return SearchManager.SUGGEST_MIME_TYPE;
957 
958             default:
959                 throw new IllegalArgumentException("Unknown URL");
960         }
961     }
962 
963     @Override
964     public Uri insert(Uri url, ContentValues initialValues) {
965         boolean isBookmarkTable = false;
966         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
967 
968         int match = URI_MATCHER.match(url);
969         Uri uri = null;
970         switch (match) {
971             case URI_MATCH_BOOKMARKS: {
972                 // Insert into the bookmarks table
973                 long rowID = db.insert(TABLE_NAMES[URI_MATCH_BOOKMARKS], "url",
974                         initialValues);
975                 if (rowID > 0) {
976                     uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI,
977                             rowID);
978                 }
979                 isBookmarkTable = true;
980                 break;
981             }
982 
983             case URI_MATCH_SEARCHES: {
984                 // Insert into the searches table
985                 long rowID = db.insert(TABLE_NAMES[URI_MATCH_SEARCHES], "url",
986                         initialValues);
987                 if (rowID > 0) {
988                     uri = ContentUris.withAppendedId(Browser.SEARCHES_URI,
989                             rowID);
990                 }
991                 break;
992             }
993 
994             default:
995                 throw new IllegalArgumentException("Unknown URL");
996         }
997 
998         if (uri == null) {
999             throw new IllegalArgumentException("Unknown URL");
1000         }
1001         getContext().getContentResolver().notifyChange(uri, null);
1002 
1003         // Back up the new bookmark set if we just inserted one.
1004         // A row created when bookmarks are added from scratch will have
1005         // bookmark=1 in the initial value set.
1006         if (isBookmarkTable
1007                 && initialValues.containsKey(BookmarkColumns.BOOKMARK)
1008                 && initialValues.getAsInteger(BookmarkColumns.BOOKMARK) != 0) {
1009             mBackupManager.dataChanged();
1010         }
1011         return uri;
1012     }
1013 
1014     @Override
delete(Uri url, String where, String[] whereArgs)1015     public int delete(Uri url, String where, String[] whereArgs) {
1016         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1017 
1018         int match = URI_MATCHER.match(url);
1019         if (match == -1 || match == URI_MATCH_SUGGEST) {
1020             throw new IllegalArgumentException("Unknown URL");
1021         }
1022 
1023         // need to know whether it's the bookmarks table for a couple of reasons
1024         boolean isBookmarkTable = (match == URI_MATCH_BOOKMARKS_ID);
1025         String id = null;
1026 
1027         if (isBookmarkTable || match == URI_MATCH_SEARCHES_ID) {
1028             StringBuilder sb = new StringBuilder();
1029             if (where != null && where.length() > 0) {
1030                 sb.append("( ");
1031                 sb.append(where);
1032                 sb.append(" ) AND ");
1033             }
1034             id = url.getPathSegments().get(1);
1035             sb.append("_id = ");
1036             sb.append(id);
1037             where = sb.toString();
1038         }
1039 
1040         ContentResolver cr = getContext().getContentResolver();
1041 
1042         // we'lll need to back up the bookmark set if we are about to delete one
1043         if (isBookmarkTable) {
1044             Cursor cursor = cr.query(Browser.BOOKMARKS_URI,
1045                     new String[] { BookmarkColumns.BOOKMARK },
1046                     "_id = " + id, null, null);
1047             if (cursor.moveToNext()) {
1048                 if (cursor.getInt(0) != 0) {
1049                     // yep, this record is a bookmark
1050                     mBackupManager.dataChanged();
1051                 }
1052             }
1053             cursor.close();
1054         }
1055 
1056         int count = db.delete(TABLE_NAMES[match % 10], where, whereArgs);
1057         cr.notifyChange(url, null);
1058         return count;
1059     }
1060 
1061     @Override
update(Uri url, ContentValues values, String where, String[] whereArgs)1062     public int update(Uri url, ContentValues values, String where,
1063             String[] whereArgs) {
1064         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1065 
1066         int match = URI_MATCHER.match(url);
1067         if (match == -1 || match == URI_MATCH_SUGGEST) {
1068             throw new IllegalArgumentException("Unknown URL");
1069         }
1070 
1071         if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) {
1072             StringBuilder sb = new StringBuilder();
1073             if (where != null && where.length() > 0) {
1074                 sb.append("( ");
1075                 sb.append(where);
1076                 sb.append(" ) AND ");
1077             }
1078             String id = url.getPathSegments().get(1);
1079             sb.append("_id = ");
1080             sb.append(id);
1081             where = sb.toString();
1082         }
1083 
1084         ContentResolver cr = getContext().getContentResolver();
1085 
1086         // Not all bookmark-table updates should be backed up.  Look to see
1087         // whether we changed the title, url, or "is a bookmark" state, and
1088         // request a backup if so.
1089         if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_BOOKMARKS) {
1090             boolean changingBookmarks = false;
1091             // Alterations to the bookmark field inherently change the bookmark
1092             // set, so we don't need to query the record; we know a priori that
1093             // we will need to back up this change.
1094             if (values.containsKey(BookmarkColumns.BOOKMARK)) {
1095                 changingBookmarks = true;
1096             } else if ((values.containsKey(BookmarkColumns.TITLE)
1097                      || values.containsKey(BookmarkColumns.URL))
1098                      && values.containsKey(BookmarkColumns._ID)) {
1099                 // If a title or URL has been changed, check to see if it is to
1100                 // a bookmark.  The ID should have been included in the update,
1101                 // so use it.
1102                 Cursor cursor = cr.query(Browser.BOOKMARKS_URI,
1103                         new String[] { BookmarkColumns.BOOKMARK },
1104                         BookmarkColumns._ID + " = "
1105                         + values.getAsString(BookmarkColumns._ID), null, null);
1106                 if (cursor.moveToNext()) {
1107                     changingBookmarks = (cursor.getInt(0) != 0);
1108                 }
1109                 cursor.close();
1110             }
1111 
1112             // if this *is* a bookmark row we're altering, we need to back it up.
1113             if (changingBookmarks) {
1114                 mBackupManager.dataChanged();
1115             }
1116         }
1117 
1118         int ret = db.update(TABLE_NAMES[match % 10], values, where, whereArgs);
1119         cr.notifyChange(url, null);
1120         return ret;
1121     }
1122 
1123     /**
1124      * Strips the provided url of preceding "http://" and any trailing "/". Does not
1125      * strip "https://". If the provided string cannot be stripped, the original string
1126      * is returned.
1127      *
1128      * TODO: Put this in TextUtils to be used by other packages doing something similar.
1129      *
1130      * @param url a url to strip, like "http://www.google.com/"
1131      * @return a stripped url like "www.google.com", or the original string if it could
1132      *         not be stripped
1133      */
stripUrl(String url)1134     private static String stripUrl(String url) {
1135         if (url == null) return null;
1136         Matcher m = STRIP_URL_PATTERN.matcher(url);
1137         if (m.matches() && m.groupCount() == 3) {
1138             return m.group(2);
1139         } else {
1140             return url;
1141         }
1142     }
1143 
getBookmarksSuggestions(ContentResolver cr, String constraint)1144     public static Cursor getBookmarksSuggestions(ContentResolver cr, String constraint) {
1145         Uri uri = Uri.parse("content://browser/" + SearchManager.SUGGEST_URI_PATH_QUERY);
1146         return cr.query(uri, SUGGEST_PROJECTION, SUGGEST_SELECTION,
1147             new String[] { constraint }, ORDER_BY);
1148     }
1149 
1150 }
1151