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