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