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