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