BrowserProvider.java revision 0c90888c75eed12f6e2e14a9044faf50bd4af8ed
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.ISearchManager;
20import android.app.SearchManager;
21import android.content.ComponentName;
22import android.content.ContentProvider;
23import android.content.ContentUris;
24import android.content.ContentValues;
25import android.content.Context;
26import android.content.Intent;
27import android.content.UriMatcher;
28import android.database.AbstractCursor;
29import android.database.Cursor;
30import android.database.sqlite.SQLiteOpenHelper;
31import android.database.sqlite.SQLiteDatabase;
32import android.net.Uri;
33import android.os.RemoteException;
34import android.os.ServiceManager;
35import android.os.SystemProperties;
36import android.provider.Browser;
37import android.util.Log;
38import android.server.search.SearchableInfo;
39import android.text.util.Regex;
40
41public class BrowserProvider extends ContentProvider {
42
43    private SQLiteOpenHelper mOpenHelper;
44    private static final String sDatabaseName = "browser.db";
45    private static final String TAG = "BrowserProvider";
46    private static final String ORDER_BY = "visits DESC, date DESC";
47
48    private static final String[] TABLE_NAMES = new String[] {
49        "bookmarks", "searches"
50    };
51    private static final String[] SUGGEST_PROJECTION = new String[] {
52            "_id", "url", "title", "bookmark"
53    };
54    private static final String SUGGEST_SELECTION =
55            "url LIKE ? OR url LIKE ? OR url LIKE ? OR url LIKE ?";
56    private String[] SUGGEST_ARGS = new String[4];
57
58    // shared suggestion array index, make sure to match COLUMNS
59    private static final int SUGGEST_COLUMN_INTENT_ACTION_ID = 1;
60    private static final int SUGGEST_COLUMN_INTENT_DATA_ID = 2;
61    private static final int SUGGEST_COLUMN_TEXT_1_ID = 3;
62    private static final int SUGGEST_COLUMN_TEXT_2_ID = 4;
63    private static final int SUGGEST_COLUMN_ICON_1_ID = 5;
64    private static final int SUGGEST_COLUMN_ICON_2_ID = 6;
65    private static final int SUGGEST_COLUMN_QUERY_ID = 7;
66
67    // shared suggestion columns
68    private static final String[] COLUMNS = new String[] {
69            "_id",
70            SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
71            SearchManager.SUGGEST_COLUMN_INTENT_DATA,
72            SearchManager.SUGGEST_COLUMN_TEXT_1,
73            SearchManager.SUGGEST_COLUMN_TEXT_2,
74            SearchManager.SUGGEST_COLUMN_ICON_1,
75            SearchManager.SUGGEST_COLUMN_ICON_2,
76            SearchManager.SUGGEST_COLUMN_QUERY};
77
78    private static final int MAX_SUGGESTION_SHORT_ENTRIES = 3;
79    private static final int MAX_SUGGESTION_LONG_ENTRIES = 6;
80
81    // make sure that these match the index of TABLE_NAMES
82    private static final int URI_MATCH_BOOKMARKS = 0;
83    private static final int URI_MATCH_SEARCHES = 1;
84    // (id % 10) should match the table name index
85    private static final int URI_MATCH_BOOKMARKS_ID = 10;
86    private static final int URI_MATCH_SEARCHES_ID = 11;
87    //
88    private static final int URI_MATCH_SUGGEST = 20;
89
90    private static final UriMatcher URI_MATCHER;
91
92    static {
93        URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
94        URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_BOOKMARKS],
95                URI_MATCH_BOOKMARKS);
96        URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_BOOKMARKS] + "/#",
97                URI_MATCH_BOOKMARKS_ID);
98        URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_SEARCHES],
99                URI_MATCH_SEARCHES);
100        URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_SEARCHES] + "/#",
101                URI_MATCH_SEARCHES_ID);
102        URI_MATCHER.addURI("browser", SearchManager.SUGGEST_URI_PATH_QUERY,
103                URI_MATCH_SUGGEST);
104    }
105
106    // 1 -> 2 add cache table
107    // 2 -> 3 update history table
108    // 3 -> 4 add passwords table
109    // 4 -> 5 add settings table
110    // 5 -> 6 ?
111    // 6 -> 7 ?
112    // 7 -> 8 drop proxy table
113    // 8 -> 9 drop settings table
114    // 9 -> 10 add form_urls and form_data
115    // 10 -> 11 add searches table
116    // 11 -> 12 modify cache table
117    // 12 -> 13 modify cache table
118    // 13 -> 14 correspond with Google Bookmarks schema
119    // 14 -> 15 move couple of tables to either browser private database or webview database
120    // 15 -> 17 Set it up for the SearchManager
121    // 17 -> 18 Added favicon in bookmarks table for Home shortcuts
122    // 18 -> 19 Remove labels table
123    private static final int DATABASE_VERSION = 19;
124
125    public BrowserProvider() {
126    }
127
128
129    private static CharSequence replaceSystemPropertyInString(CharSequence srcString) {
130        StringBuffer sb = new StringBuffer();
131        int lastCharLoc = 0;
132        for (int i = 0; i < srcString.length(); ++i) {
133            char c = srcString.charAt(i);
134            if (c == '{') {
135                sb.append(srcString.subSequence(lastCharLoc, i));
136                lastCharLoc = i;
137          inner:
138                for (int j = i; j < srcString.length(); ++j) {
139                    char k = srcString.charAt(j);
140                    if (k == '}') {
141                        String propertyKeyValue = srcString.subSequence(i + 1, j).toString();
142                        // See if the propertyKeyValue specifies a default value
143                        int defaultOffset = propertyKeyValue.indexOf(':');
144                        if (defaultOffset == -1) {
145                            sb.append(SystemProperties.get(propertyKeyValue));
146                        } else {
147                            String propertyKey = propertyKeyValue.substring(0, defaultOffset);
148                            String defaultValue =
149                                    propertyKeyValue.substring(defaultOffset + 1,
150                                                               propertyKeyValue.length());
151                            sb.append(SystemProperties.get(propertyKey, defaultValue));
152                        }
153                        lastCharLoc = j + 1;
154                        i = j;
155                        break inner;
156                    }
157                }
158            }
159        }
160        if (srcString.length() - lastCharLoc > 0) {
161            // Put on the tail, if there is one
162            sb.append(srcString.subSequence(lastCharLoc, srcString.length()));
163        }
164        return sb;
165    }
166
167    private static class DatabaseHelper extends SQLiteOpenHelper {
168        private Context mContext;
169
170        public DatabaseHelper(Context context) {
171            super(context, sDatabaseName, null, DATABASE_VERSION);
172            mContext = context;
173        }
174
175        @Override
176        public void onCreate(SQLiteDatabase db) {
177            db.execSQL("CREATE TABLE bookmarks (" +
178                    "_id INTEGER PRIMARY KEY," +
179                    "title TEXT," +
180                    "url TEXT," +
181                    "visits INTEGER," +
182                    "date LONG," +
183                    "created LONG," +
184                    "description TEXT," +
185                    "bookmark INTEGER," +
186                    "favicon BLOB DEFAULT NULL" +
187                    ");");
188
189            final CharSequence[] bookmarks = mContext.getResources()
190                    .getTextArray(R.array.bookmarks);
191            int size = bookmarks.length;
192            try {
193                for (int i = 0; i < size; i = i + 2) {
194                    CharSequence bookmarkDestination = replaceSystemPropertyInString(bookmarks[i + 1]);
195                    db.execSQL("INSERT INTO bookmarks (title, url, visits, " +
196                            "date, created, bookmark)" + " VALUES('" +
197                            bookmarks[i] + "', '" + bookmarkDestination +
198                            "', 0, 0, 0, 1);");
199                }
200            } catch (ArrayIndexOutOfBoundsException e) {
201            }
202
203            db.execSQL("CREATE TABLE searches (" +
204                    "_id INTEGER PRIMARY KEY," +
205                    "search TEXT," +
206                    "date LONG" +
207                    ");");
208        }
209
210        @Override
211        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
212            Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
213                    + newVersion + ", which will destroy all old data");
214            if (oldVersion == 18) {
215                db.execSQL("DROP TABLE IF EXISTS labels");
216            } else {
217                db.execSQL("DROP TABLE IF EXISTS bookmarks");
218                db.execSQL("DROP TABLE IF EXISTS searches");
219                onCreate(db);
220            }
221        }
222    }
223
224    @Override
225    public boolean onCreate() {
226        mOpenHelper = new DatabaseHelper(getContext());
227        return true;
228    }
229
230    /*
231     * Subclass AbstractCursor so we can combine multiple Cursors and add
232     * "Google Search".
233     * Here are the rules.
234     * 1. We only have MAX_SUGGESTION_LONG_ENTRIES in the list plus
235     *      "Google Search";
236     * 2. If bookmark/history entries are less than
237     *      (MAX_SUGGESTION_SHORT_ENTRIES -1), we include Google suggest.
238     */
239    private class MySuggestionCursor extends AbstractCursor {
240        private Cursor  mHistoryCursor;
241        private Cursor  mSuggestCursor;
242        private int     mHistoryCount;
243        private int     mSuggestionCount;
244        private boolean mBeyondCursor;
245        private String  mString;
246
247        public MySuggestionCursor(Cursor hc, Cursor sc, String string) {
248            mHistoryCursor = hc;
249            mSuggestCursor = sc;
250            mHistoryCount = hc.getCount();
251            mSuggestionCount = sc != null ? sc.getCount() : 0;
252            if (mSuggestionCount > (MAX_SUGGESTION_LONG_ENTRIES - mHistoryCount)) {
253                mSuggestionCount = MAX_SUGGESTION_LONG_ENTRIES - mHistoryCount;
254            }
255            mString = string;
256            mBeyondCursor = false;
257        }
258
259        @Override
260        public boolean onMove(int oldPosition, int newPosition) {
261            if (mHistoryCursor == null) {
262                return false;
263            }
264            if (mHistoryCount > newPosition) {
265                mHistoryCursor.moveToPosition(newPosition);
266                mBeyondCursor = false;
267            } else if (mHistoryCount + mSuggestionCount > newPosition) {
268                mSuggestCursor.moveToPosition(newPosition - mHistoryCount);
269                mBeyondCursor = false;
270            } else {
271                mBeyondCursor = true;
272            }
273            return true;
274        }
275
276        @Override
277        public int getCount() {
278            if (mString.length() > 0) {
279                return mHistoryCount + mSuggestionCount + 1;
280            } else {
281                return mHistoryCount + mSuggestionCount;
282            }
283        }
284
285        @Override
286        public String[] getColumnNames() {
287            return COLUMNS;
288        }
289
290        @Override
291        public String getString(int columnIndex) {
292            if ((mPos != -1 && mHistoryCursor != null)) {
293                switch(columnIndex) {
294                    case SUGGEST_COLUMN_INTENT_ACTION_ID:
295                        if (mHistoryCount > mPos) {
296                            return Intent.ACTION_VIEW;
297                        } else {
298                            return Intent.ACTION_SEARCH;
299                        }
300
301                    case SUGGEST_COLUMN_INTENT_DATA_ID:
302                        if (mHistoryCount > mPos) {
303                            return mHistoryCursor.getString(1);
304                        } else {
305                            return null;
306                        }
307
308                    case SUGGEST_COLUMN_TEXT_1_ID:
309                        if (mHistoryCount > mPos) {
310                            return mHistoryCursor.getString(1);
311                        } else if (!mBeyondCursor) {
312                            return mSuggestCursor.getString(1);
313                        } else {
314                            return mString;
315                        }
316
317                    case SUGGEST_COLUMN_TEXT_2_ID:
318                        if (mHistoryCount > mPos) {
319                            return mHistoryCursor.getString(2);
320                        } else if (!mBeyondCursor) {
321                            return mSuggestCursor.getString(2);
322                        } else {
323                            return getContext().getString(R.string.search_google);
324                        }
325
326                    case SUGGEST_COLUMN_ICON_1_ID:
327                        if (mHistoryCount > mPos) {
328                            if (mHistoryCursor.getInt(3) == 1) {
329                                return new Integer(
330                                        R.drawable.ic_search_category_bookmark)
331                                        .toString();
332                            } else {
333                                return new Integer(
334                                        R.drawable.ic_search_category_history)
335                                        .toString();
336                            }
337                        } else {
338                            return new Integer(
339                                    R.drawable.ic_search_category_suggest)
340                                    .toString();
341                        }
342
343                    case SUGGEST_COLUMN_ICON_2_ID:
344                        return new String("0");
345
346                    case SUGGEST_COLUMN_QUERY_ID:
347                        if (mHistoryCount > mPos) {
348                            return null;
349                        } else if (!mBeyondCursor) {
350                            return mSuggestCursor.getString(3);
351                        } else {
352                            return mString;
353                        }
354                }
355            }
356            return null;
357        }
358
359        @Override
360        public double getDouble(int column) {
361            throw new UnsupportedOperationException();
362        }
363
364        @Override
365        public float getFloat(int column) {
366            throw new UnsupportedOperationException();
367        }
368
369        @Override
370        public int getInt(int column) {
371            throw new UnsupportedOperationException();
372        }
373
374        @Override
375        public long getLong(int column) {
376            if ((mPos != -1) && column == 0) {
377                return mPos;        // use row# as the _Id
378            }
379            throw new UnsupportedOperationException();
380        }
381
382        @Override
383        public short getShort(int column) {
384            throw new UnsupportedOperationException();
385        }
386
387        @Override
388        public boolean isNull(int column) {
389            throw new UnsupportedOperationException();
390        }
391
392        // TODO Temporary change, finalize after jq's changes go in
393        public void deactivate() {
394            if (mHistoryCursor != null) {
395                mHistoryCursor.deactivate();
396            }
397            if (mSuggestCursor != null) {
398                mSuggestCursor.deactivate();
399            }
400            super.deactivate();
401        }
402
403        public boolean requery() {
404            return (mHistoryCursor != null ? mHistoryCursor.requery() : false) |
405                    (mSuggestCursor != null ? mSuggestCursor.requery() : false);
406        }
407
408        // TODO Temporary change, finalize after jq's changes go in
409        public void close() {
410            super.close();
411            if (mHistoryCursor != null) {
412                mHistoryCursor.close();
413                mHistoryCursor = null;
414            }
415            if (mSuggestCursor != null) {
416                mSuggestCursor.close();
417                mSuggestCursor = null;
418            }
419        }
420    }
421
422    @Override
423    public Cursor query(Uri url, String[] projectionIn, String selection,
424            String[] selectionArgs, String sortOrder)
425            throws IllegalStateException {
426        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
427
428        int match = URI_MATCHER.match(url);
429        if (match == -1) {
430            throw new IllegalArgumentException("Unknown URL");
431        }
432
433        if (match == URI_MATCH_SUGGEST) {
434            String suggestSelection;
435            String [] myArgs;
436            if (selectionArgs[0] == null || selectionArgs[0].equals("")) {
437                suggestSelection = null;
438                myArgs = null;
439            } else {
440                String like = selectionArgs[0] + "%";
441                if (selectionArgs[0].startsWith("http")) {
442                    myArgs = new String[1];
443                    myArgs[0] = like;
444                    suggestSelection = selection;
445                } else {
446                    SUGGEST_ARGS[0] = "http://" + like;
447                    SUGGEST_ARGS[1] = "http://www." + like;
448                    SUGGEST_ARGS[2] = "https://" + like;
449                    SUGGEST_ARGS[3] = "https://www." + like;
450                    myArgs = SUGGEST_ARGS;
451                    suggestSelection = SUGGEST_SELECTION;
452                }
453            }
454
455            Cursor c = db.query(TABLE_NAMES[URI_MATCH_BOOKMARKS],
456                    SUGGEST_PROJECTION, suggestSelection, myArgs, null, null,
457                    ORDER_BY,
458                    (new Integer(MAX_SUGGESTION_LONG_ENTRIES)).toString());
459
460            if (Regex.WEB_URL_PATTERN.matcher(selectionArgs[0]).matches()) {
461                return new MySuggestionCursor(c, null, "");
462            } else {
463                // get Google suggest if there is still space in the list
464                if (myArgs != null && myArgs.length > 1
465                        && c.getCount() < (MAX_SUGGESTION_SHORT_ENTRIES - 1)) {
466                    ISearchManager sm = ISearchManager.Stub
467                            .asInterface(ServiceManager
468                                    .getService(Context.SEARCH_SERVICE));
469                    SearchableInfo si = null;
470                    try {
471                        // use the global search to get Google suggest provider
472                        si = sm.getSearchableInfo(new ComponentName(
473                                getContext(), "com.android.browser"), true);
474
475                        // similar to the getSuggestions() in SearchDialog.java
476                        StringBuilder uriStr = new StringBuilder("content://");
477                        uriStr.append(si.getSuggestAuthority());
478                        // if content path provided, insert it now
479                        final String contentPath = si.getSuggestPath();
480                        if (contentPath != null) {
481                            uriStr.append('/');
482                            uriStr.append(contentPath);
483                        }
484                        // append standard suggestion query path
485                        uriStr.append('/' + SearchManager.SUGGEST_URI_PATH_QUERY);
486                        // inject query, either as selection args or inline
487                        String[] selArgs = null;
488                        if (si.getSuggestSelection() != null) {
489                            selArgs = new String[] {selectionArgs[0]};
490                        } else {
491                            uriStr.append('/');
492                            uriStr.append(Uri.encode(selectionArgs[0]));
493                        }
494
495                        // finally, make the query
496                        Cursor sc = getContext().getContentResolver().query(
497                                Uri.parse(uriStr.toString()), null,
498                                si.getSuggestSelection(), selArgs, null);
499
500                        return new MySuggestionCursor(c, sc, selectionArgs[0]);
501                    } catch (RemoteException e) {
502                    }
503                }
504                return new MySuggestionCursor(c, null, selectionArgs[0]);
505            }
506        }
507
508        String[] projection = null;
509        if (projectionIn != null && projectionIn.length > 0) {
510            projection = new String[projectionIn.length + 1];
511            System.arraycopy(projectionIn, 0, projection, 0, projectionIn.length);
512            projection[projectionIn.length] = "_id AS _id";
513        }
514
515        StringBuilder whereClause = new StringBuilder(256);
516        if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) {
517            whereClause.append("(_id = ").append(url.getPathSegments().get(1))
518                    .append(")");
519        }
520
521        // Tack on the user's selection, if present
522        if (selection != null && selection.length() > 0) {
523            if (whereClause.length() > 0) {
524                whereClause.append(" AND ");
525            }
526
527            whereClause.append('(');
528            whereClause.append(selection);
529            whereClause.append(')');
530        }
531        Cursor c = db.query(TABLE_NAMES[match % 10], projection,
532                whereClause.toString(), selectionArgs, null, null, sortOrder,
533                null);
534        c.setNotificationUri(getContext().getContentResolver(), url);
535        return c;
536    }
537
538    @Override
539    public String getType(Uri url) {
540        int match = URI_MATCHER.match(url);
541        switch (match) {
542            case URI_MATCH_BOOKMARKS:
543                return "vnd.android.cursor.dir/bookmark";
544
545            case URI_MATCH_BOOKMARKS_ID:
546                return "vnd.android.cursor.item/bookmark";
547
548            case URI_MATCH_SEARCHES:
549                return "vnd.android.cursor.dir/searches";
550
551            case URI_MATCH_SEARCHES_ID:
552                return "vnd.android.cursor.item/searches";
553
554            case URI_MATCH_SUGGEST:
555                return SearchManager.SUGGEST_MIME_TYPE;
556
557            default:
558                throw new IllegalArgumentException("Unknown URL");
559        }
560    }
561
562    @Override
563    public Uri insert(Uri url, ContentValues initialValues) {
564        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
565
566        int match = URI_MATCHER.match(url);
567        Uri uri = null;
568        switch (match) {
569            case URI_MATCH_BOOKMARKS: {
570                // Insert into the bookmarks table
571                long rowID = db.insert(TABLE_NAMES[URI_MATCH_BOOKMARKS], "url",
572                        initialValues);
573                if (rowID > 0) {
574                    uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI,
575                            rowID);
576                }
577                break;
578            }
579
580            case URI_MATCH_SEARCHES: {
581                // Insert into the searches table
582                long rowID = db.insert(TABLE_NAMES[URI_MATCH_SEARCHES], "url",
583                        initialValues);
584                if (rowID > 0) {
585                    uri = ContentUris.withAppendedId(Browser.SEARCHES_URI,
586                            rowID);
587                }
588                break;
589            }
590
591            default:
592                throw new IllegalArgumentException("Unknown URL");
593        }
594
595        if (uri == null) {
596            throw new IllegalArgumentException("Unknown URL");
597        }
598        getContext().getContentResolver().notifyChange(uri, null);
599        return uri;
600    }
601
602    @Override
603    public int delete(Uri url, String where, String[] whereArgs) {
604        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
605
606        int match = URI_MATCHER.match(url);
607        if (match == -1 || match == URI_MATCH_SUGGEST) {
608            throw new IllegalArgumentException("Unknown URL");
609        }
610
611        if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) {
612            StringBuilder sb = new StringBuilder();
613            if (where != null && where.length() > 0) {
614                sb.append("( ");
615                sb.append(where);
616                sb.append(" ) AND ");
617            }
618            sb.append("_id = ");
619            sb.append(url.getPathSegments().get(1));
620            where = sb.toString();
621        }
622
623        int count = db.delete(TABLE_NAMES[match % 10], where, whereArgs);
624        getContext().getContentResolver().notifyChange(url, null);
625        return count;
626    }
627
628    @Override
629    public int update(Uri url, ContentValues values, String where,
630            String[] whereArgs) {
631        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
632
633        int match = URI_MATCHER.match(url);
634        if (match == -1 || match == URI_MATCH_SUGGEST) {
635            throw new IllegalArgumentException("Unknown URL");
636        }
637
638        if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) {
639            StringBuilder sb = new StringBuilder();
640            if (where != null && where.length() > 0) {
641                sb.append("( ");
642                sb.append(where);
643                sb.append(" ) AND ");
644            }
645            sb.append("_id = ");
646            sb.append(url.getPathSegments().get(1));
647            where = sb.toString();
648        }
649
650        int ret = db.update(TABLE_NAMES[match % 10], values, where, whereArgs);
651        getContext().getContentResolver().notifyChange(url, null);
652        return ret;
653    }
654}
655