BrowserProvider.java revision bd359cc6925f8d06489c93327732251f1fecf425
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
17package com.android.browser;
18
19import com.google.android.providers.GoogleSettings.Partner;
20
21import android.app.SearchManager;
22import android.content.ComponentName;
23import android.content.ContentProvider;
24import android.content.ContentUris;
25import android.content.ContentValues;
26import android.content.Context;
27import android.content.Intent;
28import android.content.SharedPreferences;
29import android.content.UriMatcher;
30import android.content.SharedPreferences.Editor;
31import android.database.AbstractCursor;
32import android.database.Cursor;
33import android.database.sqlite.SQLiteDatabase;
34import android.database.sqlite.SQLiteOpenHelper;
35import android.net.Uri;
36import android.preference.PreferenceManager;
37import android.provider.Browser;
38import android.server.search.SearchableInfo;
39import android.text.TextUtils;
40import android.text.util.Regex;
41import android.util.Log;
42import android.util.TypedValue;
43
44import java.util.Date;
45import java.util.regex.Matcher;
46import java.util.regex.Pattern;
47
48
49public class BrowserProvider extends ContentProvider {
50
51    private SQLiteOpenHelper mOpenHelper;
52    private static final String sDatabaseName = "browser.db";
53    private static final String TAG = "BrowserProvider";
54    private static final String ORDER_BY = "visits DESC, date DESC";
55
56    private static final String PICASA_URL = "http://picasaweb.google.com/m/" +
57            "viewer?source=androidclient";
58
59    private static final String[] TABLE_NAMES = new String[] {
60        "bookmarks", "searches"
61    };
62    private static final String[] SUGGEST_PROJECTION = new String[] {
63            "_id", "url", "title", "bookmark"
64    };
65    private static final String SUGGEST_SELECTION =
66            "url LIKE ? OR url LIKE ? OR url LIKE ? OR url LIKE ?"
67                + " OR title LIKE ?";
68    private String[] SUGGEST_ARGS = new String[5];
69
70    // shared suggestion array index, make sure to match COLUMNS
71    private static final int SUGGEST_COLUMN_INTENT_ACTION_ID = 1;
72    private static final int SUGGEST_COLUMN_INTENT_DATA_ID = 2;
73    private static final int SUGGEST_COLUMN_TEXT_1_ID = 3;
74    private static final int SUGGEST_COLUMN_TEXT_2_ID = 4;
75    private static final int SUGGEST_COLUMN_ICON_1_ID = 5;
76    private static final int SUGGEST_COLUMN_ICON_2_ID = 6;
77    private static final int SUGGEST_COLUMN_QUERY_ID = 7;
78    private static final int SUGGEST_COLUMN_FORMAT = 8;
79
80    // shared suggestion columns
81    private static final String[] COLUMNS = new String[] {
82            "_id",
83            SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
84            SearchManager.SUGGEST_COLUMN_INTENT_DATA,
85            SearchManager.SUGGEST_COLUMN_TEXT_1,
86            SearchManager.SUGGEST_COLUMN_TEXT_2,
87            SearchManager.SUGGEST_COLUMN_ICON_1,
88            SearchManager.SUGGEST_COLUMN_ICON_2,
89            SearchManager.SUGGEST_COLUMN_QUERY,
90            SearchManager.SUGGEST_COLUMN_FORMAT};
91
92    private static final int MAX_SUGGESTION_SHORT_ENTRIES = 3;
93    private static final int MAX_SUGGESTION_LONG_ENTRIES = 6;
94    private static final String MAX_SUGGESTION_LONG_ENTRIES_STRING =
95            Integer.valueOf(MAX_SUGGESTION_LONG_ENTRIES).toString();
96
97    // make sure that these match the index of TABLE_NAMES
98    private static final int URI_MATCH_BOOKMARKS = 0;
99    private static final int URI_MATCH_SEARCHES = 1;
100    // (id % 10) should match the table name index
101    private static final int URI_MATCH_BOOKMARKS_ID = 10;
102    private static final int URI_MATCH_SEARCHES_ID = 11;
103    //
104    private static final int URI_MATCH_SUGGEST = 20;
105    private static final int URI_MATCH_BOOKMARKS_SUGGEST = 21;
106
107    private static final UriMatcher URI_MATCHER;
108
109    static {
110        URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
111        URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_BOOKMARKS],
112                URI_MATCH_BOOKMARKS);
113        URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_BOOKMARKS] + "/#",
114                URI_MATCH_BOOKMARKS_ID);
115        URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_SEARCHES],
116                URI_MATCH_SEARCHES);
117        URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_SEARCHES] + "/#",
118                URI_MATCH_SEARCHES_ID);
119        URI_MATCHER.addURI("browser", SearchManager.SUGGEST_URI_PATH_QUERY,
120                URI_MATCH_SUGGEST);
121        URI_MATCHER.addURI("browser",
122                TABLE_NAMES[URI_MATCH_BOOKMARKS] + "/" + SearchManager.SUGGEST_URI_PATH_QUERY,
123                URI_MATCH_BOOKMARKS_SUGGEST);
124    }
125
126    // 1 -> 2 add cache table
127    // 2 -> 3 update history table
128    // 3 -> 4 add passwords table
129    // 4 -> 5 add settings table
130    // 5 -> 6 ?
131    // 6 -> 7 ?
132    // 7 -> 8 drop proxy table
133    // 8 -> 9 drop settings table
134    // 9 -> 10 add form_urls and form_data
135    // 10 -> 11 add searches table
136    // 11 -> 12 modify cache table
137    // 12 -> 13 modify cache table
138    // 13 -> 14 correspond with Google Bookmarks schema
139    // 14 -> 15 move couple of tables to either browser private database or webview database
140    // 15 -> 17 Set it up for the SearchManager
141    // 17 -> 18 Added favicon in bookmarks table for Home shortcuts
142    // 18 -> 19 Remove labels table
143    private static final int DATABASE_VERSION = 19;
144
145    // Regular expression which matches http://, followed by some stuff, followed by
146    // optionally a trailing slash, all matched as separate groups.
147    private static final Pattern STRIP_URL_PATTERN = Pattern.compile("^(http://)(.*?)(/$)?");
148
149    // The hex color string to be applied to urls of website suggestions, as derived from
150    // the current theme. This is not set until/unless beautifyUrl is called, at which point
151    // this variable caches the color value.
152    private static String mSearchUrlColorHex;
153
154    public BrowserProvider() {
155    }
156
157
158    private static CharSequence replaceSystemPropertyInString(Context context, CharSequence srcString) {
159        StringBuffer sb = new StringBuffer();
160        int lastCharLoc = 0;
161
162        final String client_id = Partner.getString(context.getContentResolver(), Partner.CLIENT_ID);
163
164        for (int i = 0; i < srcString.length(); ++i) {
165            char c = srcString.charAt(i);
166            if (c == '{') {
167                sb.append(srcString.subSequence(lastCharLoc, i));
168                lastCharLoc = i;
169          inner:
170                for (int j = i; j < srcString.length(); ++j) {
171                    char k = srcString.charAt(j);
172                    if (k == '}') {
173                        String propertyKeyValue = srcString.subSequence(i + 1, j).toString();
174                        if (propertyKeyValue.equals("CLIENT_ID")) {
175                            sb.append(client_id);
176                        } else {
177                            sb.append("unknown");
178                        }
179                        lastCharLoc = j + 1;
180                        i = j;
181                        break inner;
182                    }
183                }
184            }
185        }
186        if (srcString.length() - lastCharLoc > 0) {
187            // Put on the tail, if there is one
188            sb.append(srcString.subSequence(lastCharLoc, srcString.length()));
189        }
190        return sb;
191    }
192
193    private static class DatabaseHelper extends SQLiteOpenHelper {
194        private Context mContext;
195
196        public DatabaseHelper(Context context) {
197            super(context, sDatabaseName, null, DATABASE_VERSION);
198            mContext = context;
199        }
200
201        @Override
202        public void onCreate(SQLiteDatabase db) {
203            db.execSQL("CREATE TABLE bookmarks (" +
204                    "_id INTEGER PRIMARY KEY," +
205                    "title TEXT," +
206                    "url TEXT," +
207                    "visits INTEGER," +
208                    "date LONG," +
209                    "created LONG," +
210                    "description TEXT," +
211                    "bookmark INTEGER," +
212                    "favicon BLOB DEFAULT NULL" +
213                    ");");
214
215            final CharSequence[] bookmarks = mContext.getResources()
216                    .getTextArray(R.array.bookmarks);
217            int size = bookmarks.length;
218            try {
219                for (int i = 0; i < size; i = i + 2) {
220                    CharSequence bookmarkDestination = replaceSystemPropertyInString(mContext, bookmarks[i + 1]);
221                    db.execSQL("INSERT INTO bookmarks (title, url, visits, " +
222                            "date, created, bookmark)" + " VALUES('" +
223                            bookmarks[i] + "', '" + bookmarkDestination +
224                            "', 0, 0, 0, 1);");
225                }
226            } catch (ArrayIndexOutOfBoundsException e) {
227            }
228
229            db.execSQL("CREATE TABLE searches (" +
230                    "_id INTEGER PRIMARY KEY," +
231                    "search TEXT," +
232                    "date LONG" +
233                    ");");
234        }
235
236        @Override
237        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
238            Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
239                    + newVersion + ", which will destroy all old data");
240            if (oldVersion == 18) {
241                db.execSQL("DROP TABLE IF EXISTS labels");
242            } else {
243                db.execSQL("DROP TABLE IF EXISTS bookmarks");
244                db.execSQL("DROP TABLE IF EXISTS searches");
245                onCreate(db);
246            }
247        }
248    }
249
250    @Override
251    public boolean onCreate() {
252        final Context context = getContext();
253        mOpenHelper = new DatabaseHelper(context);
254        // we added "picasa web album" into default bookmarks for version 19.
255        // To avoid erasing the bookmark table, we added it explicitly for
256        // version 18 and 19 as in the other cases, we will erase the table.
257        if (DATABASE_VERSION == 18 || DATABASE_VERSION == 19) {
258            SharedPreferences p = PreferenceManager
259                    .getDefaultSharedPreferences(context);
260            boolean fix = p.getBoolean("fix_picasa", true);
261            if (fix) {
262                fixPicasaBookmark();
263                Editor ed = p.edit();
264                ed.putBoolean("fix_picasa", false);
265                ed.commit();
266            }
267        }
268        return true;
269    }
270
271    private void fixPicasaBookmark() {
272        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
273        Cursor cursor = db.rawQuery("SELECT _id FROM bookmarks WHERE " +
274                "bookmark = 1 AND url = ?", new String[] { PICASA_URL });
275        try {
276            if (!cursor.moveToFirst()) {
277                // set "created" so that it will be on the top of the list
278                db.execSQL("INSERT INTO bookmarks (title, url, visits, " +
279                        "date, created, bookmark)" + " VALUES('" +
280                        getContext().getString(R.string.picasa) + "', '"
281                        + PICASA_URL + "', 0, 0, " + new Date().getTime()
282                        + ", 1);");
283            }
284        } finally {
285            if (cursor != null) {
286                cursor.close();
287            }
288        }
289    }
290
291    /*
292     * Subclass AbstractCursor so we can combine multiple Cursors and add
293     * "Google Search".
294     * Here are the rules.
295     * 1. We only have MAX_SUGGESTION_LONG_ENTRIES in the list plus
296     *      "Google Search";
297     * 2. If bookmark/history entries are less than
298     *      (MAX_SUGGESTION_SHORT_ENTRIES -1), we include Google suggest.
299     */
300    private class MySuggestionCursor extends AbstractCursor {
301        private Cursor  mHistoryCursor;
302        private Cursor  mSuggestCursor;
303        private int     mHistoryCount;
304        private int     mSuggestionCount;
305        private boolean mBeyondCursor;
306        private String  mString;
307
308        public MySuggestionCursor(Cursor hc, Cursor sc, String string) {
309            mHistoryCursor = hc;
310            mSuggestCursor = sc;
311            mHistoryCount = hc.getCount();
312            mSuggestionCount = sc != null ? sc.getCount() : 0;
313            if (mSuggestionCount > (MAX_SUGGESTION_LONG_ENTRIES - mHistoryCount)) {
314                mSuggestionCount = MAX_SUGGESTION_LONG_ENTRIES - mHistoryCount;
315            }
316            mString = string;
317            mBeyondCursor = false;
318        }
319
320        @Override
321        public boolean onMove(int oldPosition, int newPosition) {
322            if (mHistoryCursor == null) {
323                return false;
324            }
325            if (mHistoryCount > newPosition) {
326                mHistoryCursor.moveToPosition(newPosition);
327                mBeyondCursor = false;
328            } else if (mHistoryCount + mSuggestionCount > newPosition) {
329                mSuggestCursor.moveToPosition(newPosition - mHistoryCount);
330                mBeyondCursor = false;
331            } else {
332                mBeyondCursor = true;
333            }
334            return true;
335        }
336
337        @Override
338        public int getCount() {
339            if (mString.length() > 0) {
340                return mHistoryCount + mSuggestionCount + 1;
341            } else {
342                return mHistoryCount + mSuggestionCount;
343            }
344        }
345
346        @Override
347        public String[] getColumnNames() {
348            return COLUMNS;
349        }
350
351        @Override
352        public String getString(int columnIndex) {
353            if ((mPos != -1 && mHistoryCursor != null)) {
354                switch(columnIndex) {
355                    case SUGGEST_COLUMN_INTENT_ACTION_ID:
356                        if (mHistoryCount > mPos) {
357                            return Intent.ACTION_VIEW;
358                        } else {
359                            return Intent.ACTION_SEARCH;
360                        }
361
362                    case SUGGEST_COLUMN_INTENT_DATA_ID:
363                        if (mHistoryCount > mPos) {
364                            return mHistoryCursor.getString(1);
365                        } else {
366                            return null;
367                        }
368
369                    case SUGGEST_COLUMN_TEXT_1_ID:
370                        if (mHistoryCount > mPos) {
371                            return getHistoryTitle();
372                        } else if (!mBeyondCursor) {
373                            return mSuggestCursor.getString(1);
374                        } else {
375                            return mString;
376                        }
377
378                    case SUGGEST_COLUMN_TEXT_2_ID:
379                        if (mHistoryCount > mPos) {
380                            return getHistorySubtitle();
381                        } else if (!mBeyondCursor) {
382                            return mSuggestCursor.getString(2);
383                        } else {
384                            return getContext().getString(R.string.search_the_web);
385                        }
386
387                    case SUGGEST_COLUMN_ICON_1_ID:
388                        if (mHistoryCount > mPos) {
389                            if (mHistoryCursor.getInt(3) == 1) {
390                                return Integer.valueOf(
391                                        R.drawable.ic_search_category_bookmark)
392                                        .toString();
393                            } else {
394                                return Integer.valueOf(
395                                        R.drawable.ic_search_category_history)
396                                        .toString();
397                            }
398                        } else {
399                            return Integer.valueOf(
400                                    R.drawable.ic_search_category_suggest)
401                                    .toString();
402                        }
403
404                    case SUGGEST_COLUMN_ICON_2_ID:
405                        return "0";
406
407                    case SUGGEST_COLUMN_QUERY_ID:
408                        if (mHistoryCount > mPos) {
409                            return null;
410                        } else if (!mBeyondCursor) {
411                            return mSuggestCursor.getString(3);
412                        } else {
413                            return mString;
414                        }
415
416                    case SUGGEST_COLUMN_FORMAT:
417                        return "html";
418                }
419            }
420            return null;
421        }
422
423        @Override
424        public double getDouble(int column) {
425            throw new UnsupportedOperationException();
426        }
427
428        @Override
429        public float getFloat(int column) {
430            throw new UnsupportedOperationException();
431        }
432
433        @Override
434        public int getInt(int column) {
435            throw new UnsupportedOperationException();
436        }
437
438        @Override
439        public long getLong(int column) {
440            if ((mPos != -1) && column == 0) {
441                return mPos;        // use row# as the _Id
442            }
443            throw new UnsupportedOperationException();
444        }
445
446        @Override
447        public short getShort(int column) {
448            throw new UnsupportedOperationException();
449        }
450
451        @Override
452        public boolean isNull(int column) {
453            throw new UnsupportedOperationException();
454        }
455
456        // TODO Temporary change, finalize after jq's changes go in
457        public void deactivate() {
458            if (mHistoryCursor != null) {
459                mHistoryCursor.deactivate();
460            }
461            if (mSuggestCursor != null) {
462                mSuggestCursor.deactivate();
463            }
464            super.deactivate();
465        }
466
467        public boolean requery() {
468            return (mHistoryCursor != null ? mHistoryCursor.requery() : false) |
469                    (mSuggestCursor != null ? mSuggestCursor.requery() : false);
470        }
471
472        // TODO Temporary change, finalize after jq's changes go in
473        public void close() {
474            super.close();
475            if (mHistoryCursor != null) {
476                mHistoryCursor.close();
477                mHistoryCursor = null;
478            }
479            if (mSuggestCursor != null) {
480                mSuggestCursor.close();
481                mSuggestCursor = null;
482            }
483        }
484
485        /**
486         * Provides the title (text line 1) for a browser suggestion, which should be the
487         * webpage title. If the webpage title is empty, returns the stripped url instead.
488         *
489         * @return the title string to use
490         */
491        private String getHistoryTitle() {
492            String title = mHistoryCursor.getString(2 /* webpage title */);
493            if (TextUtils.isEmpty(title) || TextUtils.getTrimmedLength(title) == 0) {
494                title = beautifyUrl(mHistoryCursor.getString(1 /* url */));
495            }
496            return title;
497        }
498
499        /**
500         * Provides the subtitle (text line 2) for a browser suggestion, which should be the
501         * webpage url. If the webpage title is empty, then the url should go in the title
502         * instead, and the subtitle should be empty, so this would return null.
503         *
504         * @return the subtitle string to use, or null if none
505         */
506        private String getHistorySubtitle() {
507            String title = mHistoryCursor.getString(2 /* webpage title */);
508            if (TextUtils.isEmpty(title) || TextUtils.getTrimmedLength(title) == 0) {
509                return null;
510            } else {
511                return beautifyUrl(mHistoryCursor.getString(1 /* url */));
512            }
513        }
514
515        /**
516         * Strips "http://" from the beginning of a url and "/" from the end,
517         * and adds html formatting to make it green.
518         */
519        private String beautifyUrl(String url) {
520            if (mSearchUrlColorHex == null) {
521                // Get the color used for this purpose from the current theme.
522                TypedValue colorValue = new TypedValue();
523                getContext().getTheme().resolveAttribute(
524                        com.android.internal.R.attr.textColorSearchUrl, colorValue, true);
525                int color = getContext().getResources().getColor(colorValue.resourceId);
526
527                // Convert the int color value into a hex string, and strip the first two
528                // characters which will be the alpha transparency (html doesn't want this).
529                mSearchUrlColorHex = Integer.toHexString(color).substring(2);
530            }
531
532            return "<font color=\"#" + mSearchUrlColorHex + "\">" + stripUrl(url) + "</font>";
533        }
534    }
535
536    @Override
537    public Cursor query(Uri url, String[] projectionIn, String selection,
538            String[] selectionArgs, String sortOrder)
539            throws IllegalStateException {
540        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
541
542        int match = URI_MATCHER.match(url);
543        if (match == -1) {
544            throw new IllegalArgumentException("Unknown URL");
545        }
546
547        if (match == URI_MATCH_SUGGEST || match == URI_MATCH_BOOKMARKS_SUGGEST) {
548            String suggestSelection;
549            String [] myArgs;
550            if (selectionArgs[0] == null || selectionArgs[0].equals("")) {
551                suggestSelection = null;
552                myArgs = null;
553            } else {
554                String like = selectionArgs[0] + "%";
555                if (selectionArgs[0].startsWith("http")
556                        || selectionArgs[0].startsWith("file")) {
557                    myArgs = new String[1];
558                    myArgs[0] = like;
559                    suggestSelection = selection;
560                } else {
561                    SUGGEST_ARGS[0] = "http://" + like;
562                    SUGGEST_ARGS[1] = "http://www." + like;
563                    SUGGEST_ARGS[2] = "https://" + like;
564                    SUGGEST_ARGS[3] = "https://www." + like;
565                    // To match against titles.
566                    SUGGEST_ARGS[4] = like;
567                    myArgs = SUGGEST_ARGS;
568                    suggestSelection = SUGGEST_SELECTION;
569                }
570            }
571
572            Cursor c = db.query(TABLE_NAMES[URI_MATCH_BOOKMARKS],
573                    SUGGEST_PROJECTION, suggestSelection, myArgs, null, null,
574                    ORDER_BY, MAX_SUGGESTION_LONG_ENTRIES_STRING);
575
576            if (match == URI_MATCH_BOOKMARKS_SUGGEST
577                    || Regex.WEB_URL_PATTERN.matcher(selectionArgs[0]).matches()) {
578                return new MySuggestionCursor(c, null, "");
579            } else {
580                // get Google suggest if there is still space in the list
581                if (myArgs != null && myArgs.length > 1
582                        && c.getCount() < (MAX_SUGGESTION_SHORT_ENTRIES - 1)) {
583                    // TODO: This shouldn't be hard-coded. Instead, it should use the
584                    // default web search provider. But the API for that is not implemented yet.
585                    ComponentName googleSearchComponent =
586                            new ComponentName("com.android.googlesearch",
587                                    "com.android.googlesearch.GoogleSearch");
588                    SearchableInfo si =
589                            SearchManager.getSearchableInfo(googleSearchComponent, false);
590                    Cursor sc = SearchManager.getSuggestions(getContext(), si, selectionArgs[0]);
591                    return new MySuggestionCursor(c, sc, selectionArgs[0]);
592                }
593                return new MySuggestionCursor(c, null, selectionArgs[0]);
594            }
595        }
596
597        String[] projection = null;
598        if (projectionIn != null && projectionIn.length > 0) {
599            projection = new String[projectionIn.length + 1];
600            System.arraycopy(projectionIn, 0, projection, 0, projectionIn.length);
601            projection[projectionIn.length] = "_id AS _id";
602        }
603
604        StringBuilder whereClause = new StringBuilder(256);
605        if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) {
606            whereClause.append("(_id = ").append(url.getPathSegments().get(1))
607                    .append(")");
608        }
609
610        // Tack on the user's selection, if present
611        if (selection != null && selection.length() > 0) {
612            if (whereClause.length() > 0) {
613                whereClause.append(" AND ");
614            }
615
616            whereClause.append('(');
617            whereClause.append(selection);
618            whereClause.append(')');
619        }
620        Cursor c = db.query(TABLE_NAMES[match % 10], projection,
621                whereClause.toString(), selectionArgs, null, null, sortOrder,
622                null);
623        c.setNotificationUri(getContext().getContentResolver(), url);
624        return c;
625    }
626
627    @Override
628    public String getType(Uri url) {
629        int match = URI_MATCHER.match(url);
630        switch (match) {
631            case URI_MATCH_BOOKMARKS:
632                return "vnd.android.cursor.dir/bookmark";
633
634            case URI_MATCH_BOOKMARKS_ID:
635                return "vnd.android.cursor.item/bookmark";
636
637            case URI_MATCH_SEARCHES:
638                return "vnd.android.cursor.dir/searches";
639
640            case URI_MATCH_SEARCHES_ID:
641                return "vnd.android.cursor.item/searches";
642
643            case URI_MATCH_SUGGEST:
644                return SearchManager.SUGGEST_MIME_TYPE;
645
646            default:
647                throw new IllegalArgumentException("Unknown URL");
648        }
649    }
650
651    @Override
652    public Uri insert(Uri url, ContentValues initialValues) {
653        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
654
655        int match = URI_MATCHER.match(url);
656        Uri uri = null;
657        switch (match) {
658            case URI_MATCH_BOOKMARKS: {
659                // Insert into the bookmarks table
660                long rowID = db.insert(TABLE_NAMES[URI_MATCH_BOOKMARKS], "url",
661                        initialValues);
662                if (rowID > 0) {
663                    uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI,
664                            rowID);
665                }
666                break;
667            }
668
669            case URI_MATCH_SEARCHES: {
670                // Insert into the searches table
671                long rowID = db.insert(TABLE_NAMES[URI_MATCH_SEARCHES], "url",
672                        initialValues);
673                if (rowID > 0) {
674                    uri = ContentUris.withAppendedId(Browser.SEARCHES_URI,
675                            rowID);
676                }
677                break;
678            }
679
680            default:
681                throw new IllegalArgumentException("Unknown URL");
682        }
683
684        if (uri == null) {
685            throw new IllegalArgumentException("Unknown URL");
686        }
687        getContext().getContentResolver().notifyChange(uri, null);
688        return uri;
689    }
690
691    @Override
692    public int delete(Uri url, String where, String[] whereArgs) {
693        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
694
695        int match = URI_MATCHER.match(url);
696        if (match == -1 || match == URI_MATCH_SUGGEST) {
697            throw new IllegalArgumentException("Unknown URL");
698        }
699
700        if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) {
701            StringBuilder sb = new StringBuilder();
702            if (where != null && where.length() > 0) {
703                sb.append("( ");
704                sb.append(where);
705                sb.append(" ) AND ");
706            }
707            sb.append("_id = ");
708            sb.append(url.getPathSegments().get(1));
709            where = sb.toString();
710        }
711
712        int count = db.delete(TABLE_NAMES[match % 10], where, whereArgs);
713        getContext().getContentResolver().notifyChange(url, null);
714        return count;
715    }
716
717    @Override
718    public int update(Uri url, ContentValues values, String where,
719            String[] whereArgs) {
720        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
721
722        int match = URI_MATCHER.match(url);
723        if (match == -1 || match == URI_MATCH_SUGGEST) {
724            throw new IllegalArgumentException("Unknown URL");
725        }
726
727        if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) {
728            StringBuilder sb = new StringBuilder();
729            if (where != null && where.length() > 0) {
730                sb.append("( ");
731                sb.append(where);
732                sb.append(" ) AND ");
733            }
734            sb.append("_id = ");
735            sb.append(url.getPathSegments().get(1));
736            where = sb.toString();
737        }
738
739        int ret = db.update(TABLE_NAMES[match % 10], values, where, whereArgs);
740        getContext().getContentResolver().notifyChange(url, null);
741        return ret;
742    }
743
744    /**
745     * Strips the provided url of preceding "http://" and any trailing "/". Does not
746     * strip "https://". If the provided string cannot be stripped, the original string
747     * is returned.
748     *
749     * TODO: Put this in TextUtils to be used by other packages doing something similar.
750     *
751     * @param url a url to strip, like "http://www.google.com/"
752     * @return a stripped url like "www.google.com", or the original string if it could
753     *         not be stripped
754     */
755    private static String stripUrl(String url) {
756        if (url == null) return null;
757        Matcher m = STRIP_URL_PATTERN.matcher(url);
758        if (m.matches() && m.groupCount() == 3) {
759            return m.group(2);
760        } else {
761            return url;
762        }
763    }
764
765}
766