OSDN Git Service

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