OSDN Git Service

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