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