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