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