OSDN Git Service

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