OSDN Git Service

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