1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.browser;
18
19import android.content.ContentResolver;
20import android.content.ContentUris;
21import android.content.ContentValues;
22import android.content.Context;
23import android.content.SharedPreferences;
24import android.database.Cursor;
25import android.graphics.Bitmap;
26import android.net.Uri;
27import android.os.AsyncTask;
28import android.preference.PreferenceManager;
29import android.provider.BrowserContract;
30import android.provider.BrowserContract.Combined;
31import android.provider.BrowserContract.Images;
32import android.text.TextUtils;
33import android.util.Log;
34import android.webkit.WebIconDatabase;
35import android.widget.Toast;
36
37import java.io.ByteArrayOutputStream;
38
39/**
40 *  This class is purely to have a common place for adding/deleting bookmarks.
41 */
42public class Bookmarks {
43    // We only want the user to be able to bookmark content that
44    // the browser can handle directly.
45    private static final String acceptableBookmarkSchemes[] = {
46            "http:",
47            "https:",
48            "about:",
49            "data:",
50            "javascript:",
51            "file:",
52            "content:"
53    };
54
55    private final static String LOGTAG = "Bookmarks";
56    /**
57     *  Add a bookmark to the database.
58     *  @param context Context of the calling Activity.  This is used to make
59     *          Toast confirming that the bookmark has been added.  If the
60     *          caller provides null, the Toast will not be shown.
61     *  @param url URL of the website to be bookmarked.
62     *  @param name Provided name for the bookmark.
63     *  @param thumbnail A thumbnail for the bookmark.
64     *  @param retainIcon Whether to retain the page's icon in the icon database.
65     *          This will usually be <code>true</code> except when bookmarks are
66     *          added by a settings restore agent.
67     *  @param parent ID of the parent folder.
68     */
69    /* package */ static void addBookmark(Context context, boolean showToast, String url,
70            String name, Bitmap thumbnail, long parent) {
71        // Want to append to the beginning of the list
72        ContentValues values = new ContentValues();
73        try {
74            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
75            values.put(BrowserContract.Bookmarks.TITLE, name);
76            values.put(BrowserContract.Bookmarks.URL, url);
77            values.put(BrowserContract.Bookmarks.IS_FOLDER, 0);
78            values.put(BrowserContract.Bookmarks.THUMBNAIL,
79                    bitmapToBytes(thumbnail));
80            values.put(BrowserContract.Bookmarks.PARENT, parent);
81            context.getContentResolver().insert(BrowserContract.Bookmarks.CONTENT_URI, values);
82        } catch (IllegalStateException e) {
83            Log.e(LOGTAG, "addBookmark", e);
84        }
85        if (showToast) {
86            Toast.makeText(context, R.string.added_to_bookmarks,
87                    Toast.LENGTH_LONG).show();
88        }
89    }
90
91    /**
92     *  Remove a bookmark from the database.  If the url is a visited site, it
93     *  will remain in the database, but only as a history item, and not as a
94     *  bookmarked site.
95     *  @param context Context of the calling Activity.  This is used to make
96     *          Toast confirming that the bookmark has been removed and to
97     *          lookup the correct content uri.  It must not be null.
98     *  @param cr The ContentResolver being used to remove the bookmark.
99     *  @param url URL of the website to be removed.
100     */
101    /* package */ static void removeFromBookmarks(Context context,
102            ContentResolver cr, String url, String title) {
103        Cursor cursor = null;
104        try {
105            Uri uri = BookmarkUtils.getBookmarksUri(context);
106            cursor = cr.query(uri,
107                    new String[] { BrowserContract.Bookmarks._ID },
108                    BrowserContract.Bookmarks.URL + " = ? AND " +
109                            BrowserContract.Bookmarks.TITLE + " = ?",
110                    new String[] { url, title },
111                    null);
112
113            if (!cursor.moveToFirst()) {
114                return;
115            }
116
117            // Remove from bookmarks
118            WebIconDatabase.getInstance().releaseIconForPageUrl(url);
119            uri = ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI,
120                    cursor.getLong(0));
121            cr.delete(uri, null, null);
122            if (context != null) {
123                Toast.makeText(context, R.string.removed_from_bookmarks,
124                        Toast.LENGTH_LONG).show();
125            }
126        } catch (IllegalStateException e) {
127            Log.e(LOGTAG, "removeFromBookmarks", e);
128        } finally {
129            if (cursor != null) cursor.close();
130        }
131    }
132
133    private static byte[] bitmapToBytes(Bitmap bm) {
134        if (bm == null) {
135            return null;
136        }
137
138        final ByteArrayOutputStream os = new ByteArrayOutputStream();
139        bm.compress(Bitmap.CompressFormat.PNG, 100, os);
140        return os.toByteArray();
141    }
142
143    /* package */ static boolean urlHasAcceptableScheme(String url) {
144        if (url == null) {
145            return false;
146        }
147
148        for (int i = 0; i < acceptableBookmarkSchemes.length; i++) {
149            if (url.startsWith(acceptableBookmarkSchemes[i])) {
150                return true;
151            }
152        }
153        return false;
154    }
155
156    static final String QUERY_BOOKMARKS_WHERE =
157            Combined.URL + " == ? OR " +
158            Combined.URL + " == ?";
159
160    public static Cursor queryCombinedForUrl(ContentResolver cr,
161            String originalUrl, String url) {
162        if (cr == null || url == null) {
163            return null;
164        }
165
166        // If originalUrl is null, just set it to url.
167        if (originalUrl == null) {
168            originalUrl = url;
169        }
170
171        // Look for both the original url and the actual url. This takes in to
172        // account redirects.
173
174        final String[] selArgs = new String[] { originalUrl, url };
175        final String[] projection = new String[] { Combined.URL };
176        return cr.query(Combined.CONTENT_URI, projection, QUERY_BOOKMARKS_WHERE, selArgs, null);
177    }
178
179    // Strip the query from the given url.
180    static String removeQuery(String url) {
181        if (url == null) {
182            return null;
183        }
184        int query = url.indexOf('?');
185        String noQuery = url;
186        if (query != -1) {
187            noQuery = url.substring(0, query);
188        }
189        return noQuery;
190    }
191
192    /**
193     * Update the bookmark's favicon. This is a convenience method for updating
194     * a bookmark favicon for the originalUrl and url of the passed in WebView.
195     * @param cr The ContentResolver to use.
196     * @param originalUrl The original url before any redirects.
197     * @param url The current url.
198     * @param favicon The favicon bitmap to write to the db.
199     */
200    /* package */ static void updateFavicon(final ContentResolver cr,
201            final String originalUrl, final String url, final Bitmap favicon) {
202        new AsyncTask<Void, Void, Void>() {
203            @Override
204            protected Void doInBackground(Void... unused) {
205                final ByteArrayOutputStream os = new ByteArrayOutputStream();
206                favicon.compress(Bitmap.CompressFormat.PNG, 100, os);
207
208                // The Images update will insert if it doesn't exist
209                ContentValues values = new ContentValues();
210                values.put(Images.FAVICON, os.toByteArray());
211                updateImages(cr, originalUrl, values);
212                updateImages(cr, url, values);
213                return null;
214            }
215
216            private void updateImages(final ContentResolver cr,
217                    final String url, ContentValues values) {
218                String iurl = removeQuery(url);
219                if (!TextUtils.isEmpty(iurl)) {
220                    values.put(Images.URL, iurl);
221                    cr.update(BrowserContract.Images.CONTENT_URI, values, null, null);
222                }
223            }
224        }.execute();
225    }
226}
227