BrowserBookmarksAdapter.java revision 3918d4443ff38ef1870e02aa51a8b29f8352bb1a
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 com.android.browser;
18
19import android.content.ContentResolver;
20import android.content.ContentUris;
21import android.content.ContentValues;
22import android.database.ContentObserver;
23import android.database.Cursor;
24import android.database.DataSetObserver;
25import android.graphics.Bitmap;
26import android.graphics.BitmapFactory;
27import android.net.Uri;
28import android.os.Bundle;
29import android.os.Handler;
30import android.provider.Browser;
31import android.provider.Browser.BookmarkColumns;
32import android.view.KeyEvent;
33import android.view.LayoutInflater;
34import android.view.View;
35import android.view.ViewGroup;
36import android.webkit.WebIconDatabase;
37import android.webkit.WebIconDatabase.IconListener;
38import android.webkit.WebView;
39import android.widget.BaseAdapter;
40import android.widget.ImageView;
41import android.widget.TextView;
42
43import java.io.ByteArrayOutputStream;
44
45class BrowserBookmarksAdapter extends BaseAdapter {
46
47    private String                  mCurrentPage;
48    private String                  mCurrentTitle;
49    private Cursor                  mCursor;
50    private int                     mCount;
51    private BrowserBookmarksPage    mBookmarksPage;
52    private ContentResolver         mContentResolver;
53    private boolean                 mDataValid;
54    private boolean                 mGridMode;
55
56    // When true, this adapter is used to pick a bookmark to create a shortcut
57    private boolean mCreateShortcut;
58    private int mExtraOffset;
59
60    // Implementation of WebIconDatabase.IconListener
61    private class IconReceiver implements IconListener {
62        public void onReceivedIcon(String url, Bitmap icon) {
63            updateBookmarkFavicon(mContentResolver, null, url, icon);
64        }
65    }
66
67    // Instance of IconReceiver
68    private final IconReceiver mIconReceiver = new IconReceiver();
69
70    /**
71     *  Create a new BrowserBookmarksAdapter.
72     *  @param b        BrowserBookmarksPage that instantiated this.
73     *                  Necessary so it will adjust its focus
74     *                  appropriately after a search.
75     */
76    public BrowserBookmarksAdapter(BrowserBookmarksPage b, String curPage,
77            String curTitle, boolean createShortcut) {
78        mDataValid = false;
79        mCreateShortcut = createShortcut;
80        mExtraOffset = createShortcut ? 0 : 1;
81        mBookmarksPage = b;
82        mCurrentPage = b.getResources().getString(R.string.current_page)
83                + curPage;
84        mCurrentTitle = curTitle;
85        mContentResolver = b.getContentResolver();
86        mGridMode = false;
87
88        // FIXME: Should have a default sort order that the user selects.
89        String whereClause = Browser.BookmarkColumns.BOOKMARK + " != 0";
90        String orderBy = Browser.BookmarkColumns.VISITS + " DESC";
91        mCursor = b.managedQuery(Browser.BOOKMARKS_URI,
92                Browser.HISTORY_PROJECTION, whereClause, null, orderBy);
93        mCursor.registerContentObserver(new ChangeObserver());
94        mCursor.registerDataSetObserver(new MyDataSetObserver());
95
96        mDataValid = true;
97        notifyDataSetChanged();
98
99        mCount = mCursor.getCount() + mExtraOffset;
100
101        // FIXME: This requires another query of the database after the
102        // managedQuery. Can we optimize this?
103        Browser.requestAllIcons(mContentResolver,
104                Browser.BookmarkColumns.FAVICON + " is NULL AND " +
105                Browser.BookmarkColumns.BOOKMARK + " == 1", mIconReceiver);
106    }
107
108    /**
109     *  Return a hashmap with one row's Title, Url, and favicon.
110     *  @param position  Position in the list.
111     *  @return Bundle  Stores title, url of row position, favicon, and id
112     *                   for the url.  Return a blank map if position is out of
113     *                   range.
114     */
115    public Bundle getRow(int position) {
116        Bundle map = new Bundle();
117        if (position < mExtraOffset || position >= mCount) {
118            return map;
119        }
120        mCursor.moveToPosition(position- mExtraOffset);
121        String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX);
122        map.putString(Browser.BookmarkColumns.TITLE,
123                mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX));
124        map.putString(Browser.BookmarkColumns.URL, url);
125        byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX);
126        if (data != null) {
127            map.putParcelable(Browser.BookmarkColumns.FAVICON,
128                    BitmapFactory.decodeByteArray(data, 0, data.length));
129        }
130        map.putInt("id", mCursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX));
131        return map;
132    }
133
134    /**
135     *  Update a row in the database with new information.
136     *  Requeries the database if the information has changed.
137     *  @param map  Bundle storing id, title and url of new information
138     */
139    public void updateRow(Bundle map) {
140
141        // Find the record
142        int id = map.getInt("id");
143        int position = -1;
144        for (mCursor.moveToFirst(); !mCursor.isAfterLast(); mCursor.moveToNext()) {
145            if (mCursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX) == id) {
146                position = mCursor.getPosition();
147                break;
148            }
149        }
150        if (position < 0) {
151            return;
152        }
153
154        mCursor.moveToPosition(position);
155        ContentValues values = new ContentValues();
156        String title = map.getString(Browser.BookmarkColumns.TITLE);
157        if (!title.equals(mCursor
158                .getString(Browser.HISTORY_PROJECTION_TITLE_INDEX))) {
159            values.put(Browser.BookmarkColumns.TITLE, title);
160        }
161        String url = map.getString(Browser.BookmarkColumns.URL);
162        if (!url.equals(mCursor.
163                getString(Browser.HISTORY_PROJECTION_URL_INDEX))) {
164            values.put(Browser.BookmarkColumns.URL, url);
165        }
166        if (values.size() > 0
167                && mContentResolver.update(Browser.BOOKMARKS_URI, values,
168                        "_id = " + id, null) != -1) {
169            refreshList();
170        }
171    }
172
173    /**
174     *  Delete a row from the database.  Requeries the database.
175     *  Does nothing if the provided position is out of range.
176     *  @param position Position in the list.
177     */
178    public void deleteRow(int position) {
179        if (position < mExtraOffset || position >= getCount()) {
180            return;
181        }
182        mCursor.moveToPosition(position- mExtraOffset);
183        String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX);
184        Bookmarks.removeFromBookmarks(null, mContentResolver, url);
185        refreshList();
186    }
187
188    /**
189     *  Delete all bookmarks from the db. Requeries the database.
190     *  All bookmarks with become visited URLs or if never visited
191     *  are removed
192     */
193    public void deleteAllRows() {
194        StringBuilder deleteIds = null;
195        StringBuilder convertIds = null;
196
197        for (mCursor.moveToFirst(); !mCursor.isAfterLast(); mCursor.moveToNext()) {
198            String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX);
199            WebIconDatabase.getInstance().releaseIconForPageUrl(url);
200            int id = mCursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX);
201            int numVisits = mCursor.getInt(Browser.HISTORY_PROJECTION_VISITS_INDEX);
202            if (0 == numVisits) {
203                if (deleteIds == null) {
204                    deleteIds = new StringBuilder();
205                    deleteIds.append("( ");
206                } else {
207                    deleteIds.append(" OR ( ");
208                }
209                deleteIds.append(BookmarkColumns._ID);
210                deleteIds.append(" = ");
211                deleteIds.append(id);
212                deleteIds.append(" )");
213            } else {
214                // It is no longer a bookmark, but it is still a visited site.
215                if (convertIds == null) {
216                    convertIds = new StringBuilder();
217                    convertIds.append("( ");
218                } else {
219                    convertIds.append(" OR ( ");
220                }
221                convertIds.append(BookmarkColumns._ID);
222                convertIds.append(" = ");
223                convertIds.append(id);
224                convertIds.append(" )");
225            }
226        }
227
228        if (deleteIds != null) {
229            mContentResolver.delete(Browser.BOOKMARKS_URI, deleteIds.toString(),
230                null);
231        }
232        if (convertIds != null) {
233            ContentValues values = new ContentValues();
234            values.put(Browser.BookmarkColumns.BOOKMARK, 0);
235            mContentResolver.update(Browser.BOOKMARKS_URI, values,
236                    convertIds.toString(), null);
237        }
238        refreshList();
239    }
240
241    /**
242     *  Refresh list to recognize a change in the database.
243     */
244    public void refreshList() {
245        mCursor.requery();
246        mCount = mCursor.getCount() + mExtraOffset;
247        notifyDataSetChanged();
248    }
249
250    /**
251     * Update the bookmark's favicon. This is a convenience method for updating
252     * a bookmark favicon for the originalUrl and url of the passed in WebView.
253     * @param cr The ContentResolver to use.
254     * @param WebView The WebView containing the url to update.
255     * @param favicon The favicon bitmap to write to the db.
256     */
257    /* package */ static void updateBookmarkFavicon(ContentResolver cr,
258            WebView view, Bitmap favicon) {
259        if (view != null) {
260            updateBookmarkFavicon(cr, view.getOriginalUrl(), view.getUrl(),
261                    favicon);
262        }
263    }
264
265    private static void updateBookmarkFavicon(ContentResolver cr,
266            String originalUrl, String url, Bitmap favicon) {
267        final Cursor c = queryBookmarksForUrl(cr, originalUrl, url);
268        if (c == null) {
269            return;
270        }
271        boolean succeed = c.moveToFirst();
272        ContentValues values = null;
273        while (succeed) {
274            if (values == null) {
275                final ByteArrayOutputStream os = new ByteArrayOutputStream();
276                favicon.compress(Bitmap.CompressFormat.PNG, 100, os);
277                values = new ContentValues();
278                values.put(Browser.BookmarkColumns.FAVICON, os.toByteArray());
279            }
280            cr.update(ContentUris.withAppendedId(Browser.BOOKMARKS_URI, c
281                    .getInt(0)), values, null, null);
282            succeed = c.moveToNext();
283        }
284        c.close();
285    }
286
287    /* package */ static Cursor queryBookmarksForUrl(ContentResolver cr,
288            String originalUrl, String url) {
289        if (cr == null || url == null) {
290            return null;
291        }
292
293        // If originalUrl is null, just set it to url.
294        if (originalUrl == null) {
295            originalUrl = url;
296        }
297
298        // Look for both the original url and the actual url. This takes in to
299        // account redirects.
300        String originalUrlNoQuery = removeQuery(originalUrl);
301        String urlNoQuery = removeQuery(url);
302        originalUrl = originalUrlNoQuery + '?';
303        url = urlNoQuery + '?';
304
305        // Use NoQuery to search for the base url (i.e. if the url is
306        // http://www.yahoo.com/?rs=1, search for http://www.yahoo.com)
307        // Use url to match the base url with other queries (i.e. if the url is
308        // http://www.google.com/m, search for
309        // http://www.google.com/m?some_query)
310        final String[] selArgs = new String[] {
311            originalUrlNoQuery, urlNoQuery, originalUrl, url };
312        final String where = "(" + BookmarkColumns.URL + " == ? OR "
313                + BookmarkColumns.URL + " == ? OR "
314                + BookmarkColumns.URL + " GLOB ? || '*' OR "
315                + BookmarkColumns.URL + " GLOB ? || '*') AND "
316                + BookmarkColumns.BOOKMARK + " == 1";
317        final String[] projection =
318                new String[] { Browser.BookmarkColumns._ID };
319        return cr.query(Browser.BOOKMARKS_URI, projection, where, selArgs,
320                null);
321    }
322
323    // Strip the query from the given url.
324    private static String removeQuery(String url) {
325        if (url == null) {
326            return null;
327        }
328        int query = url.indexOf('?');
329        String noQuery = url;
330        if (query != -1) {
331            noQuery = url.substring(0, query);
332        }
333        return noQuery;
334    }
335
336    /**
337     * How many items should be displayed in the list.
338     * @return Count of items.
339     */
340    public int getCount() {
341        if (mDataValid) {
342            return mCount;
343        } else {
344            return 0;
345        }
346    }
347
348    public boolean areAllItemsEnabled() {
349        return true;
350    }
351
352    public boolean isEnabled(int position) {
353        return true;
354    }
355
356    /**
357     * Get the data associated with the specified position in the list.
358     * @param position Index of the item whose data we want.
359     * @return The data at the specified position.
360     */
361    public Object getItem(int position) {
362        return null;
363    }
364
365    /**
366     * Get the row id associated with the specified position in the list.
367     * @param position Index of the item whose row id we want.
368     * @return The id of the item at the specified position.
369     */
370    public long getItemId(int position) {
371        return position;
372    }
373
374    /* package */ void switchViewMode(boolean toGrid) {
375        mGridMode = toGrid;
376    }
377
378    /* package */ void populateBookmarkItem(BookmarkItem b, int position) {
379        mCursor.moveToPosition(position - mExtraOffset);
380        b.setUrl(mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX));
381        b.setName(mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX));
382        byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX);
383        Bitmap bitmap = (null == data) ? null :
384                BitmapFactory.decodeByteArray(data, 0, data.length);
385        b.setFavicon(bitmap);
386    }
387
388    /**
389     * Get a View that displays the data at the specified position
390     * in the list.
391     * @param position Index of the item whose view we want.
392     * @return A View corresponding to the data at the specified position.
393     */
394    public View getView(int position, View convertView, ViewGroup parent) {
395        if (!mDataValid) {
396            throw new IllegalStateException(
397                    "this should only be called when the cursor is valid");
398        }
399        if (position < 0 || position > mCount) {
400            throw new AssertionError(
401                    "BrowserBookmarksAdapter tried to get a view out of range");
402        }
403        if (mGridMode) {
404            if (convertView == null || convertView instanceof AddNewBookmark
405                    || convertView instanceof BookmarkItem) {
406                LayoutInflater factory = LayoutInflater.from(mBookmarksPage);
407                convertView
408                        = factory.inflate(R.layout.bookmark_thumbnail, null);
409            }
410            View holder = convertView.findViewById(R.id.holder);
411            ImageView thumb = (ImageView) convertView.findViewById(R.id.thumb);
412            TextView tv = (TextView) convertView.findViewById(R.id.label);
413
414            if (0 == position && !mCreateShortcut) {
415                // This is to create a bookmark for the current page.
416                holder.setVisibility(View.VISIBLE);
417                tv.setText(mCurrentTitle);
418                // FIXME: Want to show the screenshot of the current page
419                thumb.setImageResource(R.drawable.blank);
420                return convertView;
421            }
422            holder.setVisibility(View.GONE);
423            mCursor.moveToPosition(position - mExtraOffset);
424            tv.setText(mCursor.getString(
425                    Browser.HISTORY_PROJECTION_TITLE_INDEX));
426            byte[] data = mCursor.getBlob(
427                    Browser.HISTORY_PROJECTION_THUMBNAIL_INDEX);
428            if (data == null) {
429                // Backup is to just show white
430                thumb.setImageResource(R.drawable.blank);
431            } else {
432                thumb.setImageBitmap(
433                        BitmapFactory.decodeByteArray(data, 0, data.length));
434            }
435
436            return convertView;
437
438        }
439        if (position == 0 && !mCreateShortcut) {
440            AddNewBookmark b;
441            if (convertView instanceof AddNewBookmark) {
442                b = (AddNewBookmark) convertView;
443            } else {
444                b = new AddNewBookmark(mBookmarksPage);
445            }
446            b.setUrl(mCurrentPage);
447            return b;
448        }
449        if (convertView == null || !(convertView instanceof BookmarkItem)) {
450            convertView = new BookmarkItem(mBookmarksPage);
451        }
452        bind((BookmarkItem)convertView, position);
453        return convertView;
454    }
455
456    /**
457     *  Return the title for this item in the list.
458     */
459    public String getTitle(int position) {
460        return getString(Browser.HISTORY_PROJECTION_TITLE_INDEX, position);
461    }
462
463    /**
464     *  Return the Url for this item in the list.
465     */
466    public String getUrl(int position) {
467        return getString(Browser.HISTORY_PROJECTION_URL_INDEX, position);
468    }
469
470    /**
471     * Return the favicon for this item in the list.
472     */
473    public Bitmap getFavicon(int position) {
474        return getBitmap(Browser.HISTORY_PROJECTION_FAVICON_INDEX, position);
475    }
476
477    public Bitmap getTouchIcon(int position) {
478        return getBitmap(Browser.HISTORY_PROJECTION_TOUCH_ICON_INDEX, position);
479    }
480
481    private Bitmap getBitmap(int cursorIndex, int position) {
482        if (position < mExtraOffset || position > mCount) {
483            return null;
484        }
485        mCursor.moveToPosition(position - mExtraOffset);
486        byte[] data = mCursor.getBlob(cursorIndex);
487        if (data == null) {
488            return null;
489        }
490        return BitmapFactory.decodeByteArray(data, 0, data.length);
491    }
492
493    /**
494     * Private helper function to return the title or url.
495     */
496    private String getString(int cursorIndex, int position) {
497        if (position < mExtraOffset || position > mCount) {
498            return "";
499        }
500        mCursor.moveToPosition(position- mExtraOffset);
501        return mCursor.getString(cursorIndex);
502    }
503
504    private void bind(BookmarkItem b, int position) {
505        mCursor.moveToPosition(position- mExtraOffset);
506
507        String title = mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX);
508        if (title.length() > BrowserSettings.MAX_TEXTVIEW_LEN) {
509            title = title.substring(0, BrowserSettings.MAX_TEXTVIEW_LEN);
510        }
511        b.setName(title);
512        String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX);
513        if (url.length() > BrowserSettings.MAX_TEXTVIEW_LEN) {
514            url = url.substring(0, BrowserSettings.MAX_TEXTVIEW_LEN);
515        }
516        b.setUrl(url);
517        byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX);
518        if (data != null) {
519            b.setFavicon(BitmapFactory.decodeByteArray(data, 0, data.length));
520        } else {
521            b.setFavicon(null);
522        }
523    }
524
525    private class ChangeObserver extends ContentObserver {
526        public ChangeObserver() {
527            super(new Handler());
528        }
529
530        @Override
531        public boolean deliverSelfNotifications() {
532            return true;
533        }
534
535        @Override
536        public void onChange(boolean selfChange) {
537            refreshList();
538        }
539    }
540
541    private class MyDataSetObserver extends DataSetObserver {
542        @Override
543        public void onChanged() {
544            mDataValid = true;
545            notifyDataSetChanged();
546        }
547
548        @Override
549        public void onInvalidated() {
550            mDataValid = false;
551            notifyDataSetInvalidated();
552        }
553    }
554}
555