Browser.java revision 679091849754c60bdde5670495c38493065fd308
1/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.provider;
18
19import android.content.ContentResolver;
20import android.content.ContentValues;
21import android.content.Context;
22import android.content.Intent;
23import android.database.Cursor;
24import android.database.DatabaseUtils;
25import android.net.Uri;
26import android.util.Log;
27import android.webkit.WebIconDatabase;
28
29import java.util.Date;
30
31public class Browser {
32    private static final String LOGTAG = "browser";
33    public static final Uri BOOKMARKS_URI =
34        Uri.parse("content://browser/bookmarks");
35
36    /**
37     * The name of extra data when starting Browser with ACTION_VIEW or
38     * ACTION_SEARCH intent.
39     * <p>
40     * The value should be an integer between 0 and 1000. If not set or set to
41     * 0, the Browser will use default. If set to 100, the Browser will start
42     * with 100%.
43     */
44    public static final String INITIAL_ZOOM_LEVEL = "browser.initialZoomLevel";
45
46    /**
47     * The name of the extra data when starting the Browser from another
48     * application.
49     * <p>
50     * The value is a unique identification string that will be used to
51     * indentify the calling application. The Browser will attempt to reuse the
52     * same window each time the application launches the Browser with the same
53     * identifier.
54     */
55    public static final String EXTRA_APPLICATION_ID =
56        "com.android.browser.application_id";
57
58    /**
59     * The name of the extra data in the VIEW intent. The data is in boolean.
60     * <p>
61     * If the Browser is handling the intent and the setting for
62     * USE_LOCATION_FOR_SERVICES is allow, the Browser will send the location in
63     * the POST data if this extra data is presented and it is true.
64     * <p>
65     * pending api approval
66     * @hide
67     */
68    public static final String EXTRA_APPEND_LOCATION = "com.android.browser.append_location";
69
70    /**
71     * The name of the extra data in the VIEW intent. The data is in the format of
72     * a byte array.
73     * <p>
74     * Any value sent here will be passed in the http request to the provided url as post data.
75     * <p>
76     * pending api approval
77     * @hide
78     */
79    public static final String EXTRA_POST_DATA = "com.android.browser.post_data";
80
81    /**
82     * The name of the extra data in the VIEW intent. The data are key/value
83     * pairs in the format of Bundle. They will be sent in the HTTP request
84     * headers for the provided url. The keys can't be the standard HTTP headers
85     * as they are set by the WebView. The url's schema must be http(s).
86     * <p>
87     */
88    public static final String EXTRA_HEADERS = "com.android.browser.headers";
89
90    /* if you change column order you must also change indices
91       below */
92    public static final String[] HISTORY_PROJECTION = new String[] {
93        BookmarkColumns._ID, BookmarkColumns.URL, BookmarkColumns.VISITS,
94        BookmarkColumns.DATE, BookmarkColumns.BOOKMARK, BookmarkColumns.TITLE,
95        BookmarkColumns.FAVICON, BookmarkColumns.THUMBNAIL,
96        BookmarkColumns.TOUCH_ICON, BookmarkColumns.USER_ENTERED };
97
98    /* these indices dependent on HISTORY_PROJECTION */
99    public static final int HISTORY_PROJECTION_ID_INDEX = 0;
100    public static final int HISTORY_PROJECTION_URL_INDEX = 1;
101    public static final int HISTORY_PROJECTION_VISITS_INDEX = 2;
102    public static final int HISTORY_PROJECTION_DATE_INDEX = 3;
103    public static final int HISTORY_PROJECTION_BOOKMARK_INDEX = 4;
104    public static final int HISTORY_PROJECTION_TITLE_INDEX = 5;
105    public static final int HISTORY_PROJECTION_FAVICON_INDEX = 6;
106    /**
107     * @hide
108     */
109    public static final int HISTORY_PROJECTION_THUMBNAIL_INDEX = 7;
110    /**
111     * @hide
112     */
113    public static final int HISTORY_PROJECTION_TOUCH_ICON_INDEX = 8;
114
115    /* columns needed to determine whether to truncate history */
116    public static final String[] TRUNCATE_HISTORY_PROJECTION = new String[] {
117        BookmarkColumns._ID, BookmarkColumns.DATE, };
118    public static final int TRUNCATE_HISTORY_PROJECTION_ID_INDEX = 0;
119
120    /* truncate this many history items at a time */
121    public static final int TRUNCATE_N_OLDEST = 5;
122
123    public static final Uri SEARCHES_URI =
124        Uri.parse("content://browser/searches");
125
126    /* if you change column order you must also change indices
127       below */
128    public static final String[] SEARCHES_PROJECTION = new String[] {
129        SearchColumns._ID, SearchColumns.SEARCH, SearchColumns.DATE };
130
131    /* these indices dependent on SEARCHES_PROJECTION */
132    public static final int SEARCHES_PROJECTION_SEARCH_INDEX = 1;
133    public static final int SEARCHES_PROJECTION_DATE_INDEX = 2;
134
135    private static final String SEARCHES_WHERE_CLAUSE = "search = ?";
136
137    /* Set a cap on the count of history items in the history/bookmark
138       table, to prevent db and layout operations from dragging to a
139       crawl.  Revisit this cap when/if db/layout performance
140       improvements are made.  Note: this does not affect bookmark
141       entries -- if the user wants more bookmarks than the cap, they
142       get them. */
143    private static final int MAX_HISTORY_COUNT = 250;
144
145    /**
146     *  Open the AddBookmark activity to save a bookmark.  Launch with
147     *  and/or url, which can be edited by the user before saving.
148     *  @param c        Context used to launch the AddBookmark activity.
149     *  @param title    Title for the bookmark. Can be null or empty string.
150     *  @param url      Url for the bookmark. Can be null or empty string.
151     */
152    public static final void saveBookmark(Context c,
153                                          String title,
154                                          String url) {
155        Intent i = new Intent(Intent.ACTION_INSERT, Browser.BOOKMARKS_URI);
156        i.putExtra("title", title);
157        i.putExtra("url", url);
158        c.startActivity(i);
159    }
160
161    /**
162     * Stores a String extra in an {@link Intent} representing the title of a
163     * page to share.  When receiving an {@link Intent#ACTION_SEND} from the
164     * Browser, use this to access the title.
165     * @hide
166     */
167    public final static String EXTRA_SHARE_TITLE = "share_title";
168
169    /**
170     * Stores a Bitmap extra in an {@link Intent} representing the screenshot of
171     * a page to share.  When receiving an {@link Intent#ACTION_SEND} from the
172     * Browser, use this to access the screenshot.
173     * @hide
174     */
175    public final static String EXTRA_SHARE_SCREENSHOT = "share_screenshot";
176
177    /**
178     * Stores a Bitmap extra in an {@link Intent} representing the favicon of a
179     * page to share.  When receiving an {@link Intent#ACTION_SEND} from the
180     * Browser, use this to access the favicon.
181     * @hide
182     */
183    public final static String EXTRA_SHARE_FAVICON = "share_favicon";
184
185    public static final void sendString(Context c, String s) {
186        sendString(c, s, c.getString(com.android.internal.R.string.sendText));
187    }
188
189    /**
190     *  Find an application to handle the given string and, if found, invoke
191     *  it with the given string as a parameter.
192     *  @param c Context used to launch the new activity.
193     *  @param stringToSend The string to be handled.
194     *  @param chooserDialogTitle The title of the dialog that allows the user
195     *  to select between multiple applications that are all capable of handling
196     *  the string.
197     *  @hide pending API council approval
198     */
199    public static final void sendString(Context c,
200                                        String stringToSend,
201                                        String chooserDialogTitle) {
202        Intent send = new Intent(Intent.ACTION_SEND);
203        send.setType("text/plain");
204        send.putExtra(Intent.EXTRA_TEXT, stringToSend);
205
206        try {
207            c.startActivity(Intent.createChooser(send, chooserDialogTitle));
208        } catch(android.content.ActivityNotFoundException ex) {
209            // if no app handles it, do nothing
210        }
211    }
212
213    /**
214     *  Return a cursor pointing to a list of all the bookmarks.
215     *  Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS}
216     *  @param cr   The ContentResolver used to access the database.
217     */
218    public static final Cursor getAllBookmarks(ContentResolver cr) throws
219            IllegalStateException {
220        return cr.query(BOOKMARKS_URI,
221                new String[] { BookmarkColumns.URL },
222                "bookmark = 1", null, null);
223    }
224
225    /**
226     *  Return a cursor pointing to a list of all visited site urls.
227     *  Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS}
228     *  @param cr   The ContentResolver used to access the database.
229     */
230    public static final Cursor getAllVisitedUrls(ContentResolver cr) throws
231            IllegalStateException {
232        return cr.query(BOOKMARKS_URI,
233                new String[] { BookmarkColumns.URL }, null, null, null);
234    }
235
236    /**
237     *  Update the visited history to acknowledge that a site has been
238     *  visited.
239     *  Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS}
240     *  Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS}
241     *  @param cr   The ContentResolver used to access the database.
242     *  @param url  The site being visited.
243     *  @param real If true, this is an actual visit, and should add to the
244     *              number of visits.  If false, the user entered it manually.
245     */
246    public static final void updateVisitedHistory(ContentResolver cr,
247                                                  String url, boolean real) {
248        long now = new Date().getTime();
249        try {
250            StringBuilder sb = new StringBuilder(BookmarkColumns.URL + " = ");
251            DatabaseUtils.appendEscapedSQLString(sb, url);
252            Cursor c = cr.query(
253                    BOOKMARKS_URI,
254                    HISTORY_PROJECTION,
255                    sb.toString(),
256                    null,
257                    null);
258            /* We should only get one answer that is exactly the same. */
259            if (c.moveToFirst()) {
260                ContentValues map = new ContentValues();
261                if (real) {
262                    map.put(BookmarkColumns.VISITS, c
263                            .getInt(HISTORY_PROJECTION_VISITS_INDEX) + 1);
264                } else {
265                    map.put(BookmarkColumns.USER_ENTERED, 1);
266                }
267                map.put(BookmarkColumns.DATE, now);
268                cr.update(BOOKMARKS_URI, map, "_id = " + c.getInt(0), null);
269            } else {
270                truncateHistory(cr);
271                ContentValues map = new ContentValues();
272                int visits;
273                int user_entered;
274                if (real) {
275                    visits = 1;
276                    user_entered = 0;
277                } else {
278                    visits = 0;
279                    user_entered = 1;
280                }
281                map.put(BookmarkColumns.URL, url);
282                map.put(BookmarkColumns.VISITS, visits);
283                map.put(BookmarkColumns.DATE, now);
284                map.put(BookmarkColumns.BOOKMARK, 0);
285                map.put(BookmarkColumns.TITLE, url);
286                map.put(BookmarkColumns.CREATED, 0);
287                map.put(BookmarkColumns.USER_ENTERED, user_entered);
288                cr.insert(BOOKMARKS_URI, map);
289            }
290            c.deactivate();
291        } catch (IllegalStateException e) {
292            return;
293        }
294    }
295
296    /**
297     *  Returns all the URLs in the history.
298     *  Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS}
299     *  @param cr   The ContentResolver used to access the database.
300     *  @hide pending API council approval
301     */
302    public static final String[] getVisitedHistory(ContentResolver cr) {
303        try {
304            String[] projection = new String[] {
305                "url"
306            };
307            Cursor c = cr.query(BOOKMARKS_URI, projection, "visits > 0", null,
308                    null);
309            String[] str = new String[c.getCount()];
310            int i = 0;
311            while (c.moveToNext()) {
312                str[i] = c.getString(0);
313                i++;
314            }
315            c.deactivate();
316            return str;
317        } catch (IllegalStateException e) {
318            return new String[0];
319        }
320    }
321
322    /**
323     * If there are more than MAX_HISTORY_COUNT non-bookmark history
324     * items in the bookmark/history table, delete TRUNCATE_N_OLDEST
325     * of them.  This is used to keep our history table to a
326     * reasonable size.  Note: it does not prune bookmarks.  If the
327     * user wants 1000 bookmarks, the user gets 1000 bookmarks.
328     *  Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS}
329     *  Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS}
330     *
331     * @param cr The ContentResolver used to access the database.
332     */
333    public static final void truncateHistory(ContentResolver cr) {
334        try {
335            // Select non-bookmark history, ordered by date
336            Cursor c = cr.query(
337                    BOOKMARKS_URI,
338                    TRUNCATE_HISTORY_PROJECTION,
339                    "bookmark = 0",
340                    null,
341                    BookmarkColumns.DATE);
342            // Log.v(LOGTAG, "history count " + c.count());
343            if (c.moveToFirst() && c.getCount() >= MAX_HISTORY_COUNT) {
344                /* eliminate oldest history items */
345                for (int i = 0; i < TRUNCATE_N_OLDEST; i++) {
346                    // Log.v(LOGTAG, "truncate history " +
347                    // c.getInt(TRUNCATE_HISTORY_PROJECTION_ID_INDEX));
348                    deleteHistoryWhere(
349                            cr, "_id = " +
350                            c.getInt(TRUNCATE_HISTORY_PROJECTION_ID_INDEX));
351                    if (!c.moveToNext()) break;
352                }
353            }
354            c.deactivate();
355        } catch (IllegalStateException e) {
356            Log.e(LOGTAG, "truncateHistory", e);
357            return;
358        }
359    }
360
361    /**
362     * Returns whether there is any history to clear.
363     *  Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS}
364     * @param cr   The ContentResolver used to access the database.
365     * @return boolean  True if the history can be cleared.
366     */
367    public static final boolean canClearHistory(ContentResolver cr) {
368        try {
369            Cursor c = cr.query(
370                BOOKMARKS_URI,
371                new String [] { BookmarkColumns._ID,
372                                BookmarkColumns.BOOKMARK,
373                                BookmarkColumns.VISITS },
374                "bookmark = 0 OR visits > 0",
375                null,
376                null
377                );
378            boolean ret = c.moveToFirst();
379            c.deactivate();
380            return ret;
381        } catch (IllegalStateException e) {
382            return false;
383        }
384    }
385
386    /**
387     *  Delete all entries from the bookmarks/history table which are
388     *  not bookmarks.  Also set all visited bookmarks to unvisited.
389     *  Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS}
390     *  @param cr   The ContentResolver used to access the database.
391     */
392    public static final void clearHistory(ContentResolver cr) {
393        deleteHistoryWhere(cr, null);
394    }
395
396    /**
397     * Helper function to delete all history items and revert all
398     * bookmarks to zero visits which meet the criteria provided.
399     *  Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS}
400     *  Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS}
401     * @param cr   The ContentResolver used to access the database.
402     * @param whereClause   String to limit the items affected.
403     *                      null means all items.
404     */
405    private static final void deleteHistoryWhere(ContentResolver cr,
406            String whereClause) {
407        try {
408            Cursor c = cr.query(BOOKMARKS_URI,
409                HISTORY_PROJECTION,
410                whereClause,
411                null,
412                null);
413            if (!c.moveToFirst()) {
414                c.deactivate();
415                return;
416            }
417
418            final WebIconDatabase iconDb = WebIconDatabase.getInstance();
419            /* Delete favicons, and revert bookmarks which have been visited
420             * to simply bookmarks.
421             */
422            StringBuffer sb = new StringBuffer();
423            boolean firstTime = true;
424            do {
425                String url = c.getString(HISTORY_PROJECTION_URL_INDEX);
426                boolean isBookmark =
427                    c.getInt(HISTORY_PROJECTION_BOOKMARK_INDEX) == 1;
428                if (isBookmark) {
429                    if (firstTime) {
430                        firstTime = false;
431                    } else {
432                        sb.append(" OR ");
433                    }
434                    sb.append("( _id = ");
435                    sb.append(c.getInt(0));
436                    sb.append(" )");
437                } else {
438                    iconDb.releaseIconForPageUrl(url);
439                }
440            } while (c.moveToNext());
441            c.deactivate();
442
443            if (!firstTime) {
444                ContentValues map = new ContentValues();
445                map.put(BookmarkColumns.VISITS, 0);
446                map.put(BookmarkColumns.DATE, 0);
447                /* FIXME: Should I also remove the title? */
448                cr.update(BOOKMARKS_URI, map, sb.toString(), null);
449            }
450
451            String deleteWhereClause = BookmarkColumns.BOOKMARK + " = 0";
452            if (whereClause != null) {
453                deleteWhereClause += " AND " + whereClause;
454            }
455            cr.delete(BOOKMARKS_URI, deleteWhereClause, null);
456        } catch (IllegalStateException e) {
457            return;
458        }
459    }
460
461    /**
462     * Delete all history items from begin to end.
463     *  Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS}
464     * @param cr    The ContentResolver used to access the database.
465     * @param begin First date to remove.  If -1, all dates before end.
466     *              Inclusive.
467     * @param end   Last date to remove. If -1, all dates after begin.
468     *              Non-inclusive.
469     */
470    public static final void deleteHistoryTimeFrame(ContentResolver cr,
471            long begin, long end) {
472        String whereClause;
473        String date = BookmarkColumns.DATE;
474        if (-1 == begin) {
475            if (-1 == end) {
476                clearHistory(cr);
477                return;
478            }
479            whereClause = date + " < " + Long.toString(end);
480        } else if (-1 == end) {
481            whereClause = date + " >= " + Long.toString(begin);
482        } else {
483            whereClause = date + " >= " + Long.toString(begin) + " AND " + date
484                    + " < " + Long.toString(end);
485        }
486        deleteHistoryWhere(cr, whereClause);
487    }
488
489    /**
490     * Remove a specific url from the history database.
491     *  Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS}
492     * @param cr    The ContentResolver used to access the database.
493     * @param url   url to remove.
494     */
495    public static final void deleteFromHistory(ContentResolver cr,
496                                               String url) {
497        StringBuilder sb = new StringBuilder(BookmarkColumns.URL + " = ");
498        DatabaseUtils.appendEscapedSQLString(sb, url);
499        String matchesUrl = sb.toString();
500        deleteHistoryWhere(cr, matchesUrl);
501    }
502
503    /**
504     * Add a search string to the searches database.
505     *  Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS}
506     *  Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS}
507     * @param cr   The ContentResolver used to access the database.
508     * @param search    The string to add to the searches database.
509     */
510    public static final void addSearchUrl(ContentResolver cr, String search) {
511        long now = new Date().getTime();
512        try {
513            Cursor c = cr.query(
514                SEARCHES_URI,
515                SEARCHES_PROJECTION,
516                SEARCHES_WHERE_CLAUSE,
517                new String [] { search },
518                null);
519            ContentValues map = new ContentValues();
520            map.put(SearchColumns.SEARCH, search);
521            map.put(SearchColumns.DATE, now);
522            /* We should only get one answer that is exactly the same. */
523            if (c.moveToFirst()) {
524                cr.update(SEARCHES_URI, map, "_id = " + c.getInt(0), null);
525            } else {
526                cr.insert(SEARCHES_URI, map);
527            }
528            c.deactivate();
529        } catch (IllegalStateException e) {
530            Log.e(LOGTAG, "addSearchUrl", e);
531            return;
532        }
533    }
534    /**
535     * Remove all searches from the search database.
536     *  Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS}
537     * @param cr   The ContentResolver used to access the database.
538     */
539    public static final void clearSearches(ContentResolver cr) {
540        // FIXME: Should this clear the urls to which these searches lead?
541        // (i.e. remove google.com/query= blah blah blah)
542        try {
543            cr.delete(SEARCHES_URI, null, null);
544        } catch (IllegalStateException e) {
545            Log.e(LOGTAG, "clearSearches", e);
546        }
547    }
548
549    /**
550     *  Request all icons from the database.
551     *  Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS}
552     *  @param  cr The ContentResolver used to access the database.
553     *  @param  where Clause to be used to limit the query from the database.
554     *          Must be an allowable string to be passed into a database query.
555     *  @param  listener IconListener that gets the icons once they are
556     *          retrieved.
557     */
558    public static final void requestAllIcons(ContentResolver cr, String where,
559            WebIconDatabase.IconListener listener) {
560        try {
561            final Cursor c = cr.query(
562                    BOOKMARKS_URI,
563                    HISTORY_PROJECTION,
564                    where, null, null);
565            if (c.moveToFirst()) {
566                final WebIconDatabase db = WebIconDatabase.getInstance();
567                do {
568                    db.requestIconForPageUrl(
569                            c.getString(HISTORY_PROJECTION_URL_INDEX),
570                            listener);
571                } while (c.moveToNext());
572            }
573            c.deactivate();
574        } catch (IllegalStateException e) {
575            Log.e(LOGTAG, "requestAllIcons", e);
576        }
577    }
578
579    public static class BookmarkColumns implements BaseColumns {
580        public static final String URL = "url";
581        public static final String VISITS = "visits";
582        public static final String DATE = "date";
583        public static final String BOOKMARK = "bookmark";
584        public static final String TITLE = "title";
585        public static final String CREATED = "created";
586        public static final String FAVICON = "favicon";
587        /**
588         * @hide
589         */
590        public static final String THUMBNAIL = "thumbnail";
591        /**
592         * @hide
593         */
594        public static final String TOUCH_ICON = "touch_icon";
595        /**
596         * @hide
597         */
598        public static final String USER_ENTERED = "user_entered";
599    }
600
601    public static class SearchColumns implements BaseColumns {
602        public static final String URL = "url";
603        public static final String SEARCH = "search";
604        public static final String DATE = "date";
605    }
606}
607