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