blob: 0c930c606768f8b6c4fac2d4c8535db49221a26c [file] [log] [blame]
The Android Open Source Project0c908882009-03-03 19:32:16 -08001/*
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}