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