BrowserBookmarksAdapter.java revision 0c90888c75eed12f6e2e14a9044faf50bd4af8ed
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.View;
34import android.view.ViewGroup;
35import android.webkit.WebIconDatabase;
36import android.webkit.WebIconDatabase.IconListener;
37import android.widget.BaseAdapter;
38
39import java.io.ByteArrayOutputStream;
40
41class BrowserBookmarksAdapter extends BaseAdapter {
42
43    private final String            LOGTAG = "Bookmarks";
44
45    private String                  mCurrentPage;
46    private Cursor                  mCursor;
47    private int                     mCount;
48    private String                  mLastWhereClause;
49    private String[]                mLastSelectionArgs;
50    private String                  mLastOrderBy;
51    private BrowserBookmarksPage    mBookmarksPage;
52    private ContentResolver         mContentResolver;
53    private ChangeObserver          mChangeObserver;
54    private DataSetObserver         mDataSetObserver;
55    private boolean                 mDataValid;
56
57    // When true, this adapter is used to pick a bookmark to create a shortcut
58    private boolean mCreateShortcut;
59    private int mExtraOffset;
60
61    // Implementation of WebIconDatabase.IconListener
62    private class IconReceiver implements IconListener {
63        public void onReceivedIcon(String url, Bitmap icon) {
64            updateBookmarkFavicon(mContentResolver, url, icon);
65        }
66    }
67
68    // Instance of IconReceiver
69    private final IconReceiver mIconReceiver = new IconReceiver();
70
71    /**
72     *  Create a new BrowserBookmarksAdapter.
73     *  @param b        BrowserBookmarksPage that instantiated this.
74     *                  Necessary so it will adjust its focus
75     *                  appropriately after a search.
76     */
77    public BrowserBookmarksAdapter(BrowserBookmarksPage b, String curPage) {
78        this(b, curPage, false);
79    }
80
81    /**
82     *  Create a new BrowserBookmarksAdapter.
83     *  @param b        BrowserBookmarksPage that instantiated this.
84     *                  Necessary so it will adjust its focus
85     *                  appropriately after a search.
86     */
87    public BrowserBookmarksAdapter(BrowserBookmarksPage b, String curPage,
88            boolean createShortcut) {
89        mDataValid = false;
90        mCreateShortcut = createShortcut;
91        mExtraOffset = createShortcut ? 0 : 1;
92        mBookmarksPage = b;
93        mCurrentPage = b.getResources().getString(R.string.current_page) +
94                curPage;
95        mContentResolver = b.getContentResolver();
96        mLastOrderBy = Browser.BookmarkColumns.CREATED + " DESC";
97        mChangeObserver = new ChangeObserver();
98        mDataSetObserver = new MyDataSetObserver();
99        // FIXME: Should have a default sort order that the user selects.
100        search(null);
101        // FIXME: This requires another query of the database after the
102        // initial search(null). 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        WebIconDatabase.getInstance().releaseIconForPageUrl(url);
185        Uri uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI, mCursor
186                .getInt(Browser.HISTORY_PROJECTION_ID_INDEX));
187        int numVisits = mCursor.getInt(Browser.HISTORY_PROJECTION_VISITS_INDEX);
188        if (0 == numVisits) {
189            mContentResolver.delete(uri, null, null);
190        } else {
191            // It is no longer a bookmark, but it is still a visited site.
192            ContentValues values = new ContentValues();
193            values.put(Browser.BookmarkColumns.BOOKMARK, 0);
194            mContentResolver.update(uri, values, null, null);
195        }
196        refreshList();
197    }
198
199    /**
200     *  Delete all bookmarks from the db. Requeries the database.
201     *  All bookmarks with become visited URLs or if never visited
202     *  are removed
203     */
204    public void deleteAllRows() {
205        StringBuilder deleteIds = null;
206        StringBuilder convertIds = null;
207
208        for (mCursor.moveToFirst(); !mCursor.isAfterLast(); mCursor.moveToNext()) {
209            String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX);
210            WebIconDatabase.getInstance().releaseIconForPageUrl(url);
211            int id = mCursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX);
212            int numVisits = mCursor.getInt(Browser.HISTORY_PROJECTION_VISITS_INDEX);
213            if (0 == numVisits) {
214                if (deleteIds == null) {
215                    deleteIds = new StringBuilder();
216                    deleteIds.append("( ");
217                } else {
218                    deleteIds.append(" OR ( ");
219                }
220                deleteIds.append(BookmarkColumns._ID);
221                deleteIds.append(" = ");
222                deleteIds.append(id);
223                deleteIds.append(" )");
224            } else {
225                // It is no longer a bookmark, but it is still a visited site.
226                if (convertIds == null) {
227                    convertIds = new StringBuilder();
228                    convertIds.append("( ");
229                } else {
230                    convertIds.append(" OR ( ");
231                }
232                convertIds.append(BookmarkColumns._ID);
233                convertIds.append(" = ");
234                convertIds.append(id);
235                convertIds.append(" )");
236            }
237        }
238
239        if (deleteIds != null) {
240            mContentResolver.delete(Browser.BOOKMARKS_URI, deleteIds.toString(),
241                null);
242        }
243        if (convertIds != null) {
244            ContentValues values = new ContentValues();
245            values.put(Browser.BookmarkColumns.BOOKMARK, 0);
246            mContentResolver.update(Browser.BOOKMARKS_URI, values,
247                    convertIds.toString(), null);
248        }
249        refreshList();
250    }
251
252    /**
253     *  Refresh list to recognize a change in the database.
254     */
255    public void refreshList() {
256        // FIXME: consider using requery().
257        // Need to do more work to get it to function though.
258        searchInternal(mLastWhereClause, mLastSelectionArgs, mLastOrderBy);
259    }
260
261    /**
262     *  Search the database for bookmarks that match the input string.
263     *  @param like String to use to search the database.  Strings with spaces
264     *              are treated as having multiple search terms using the
265     *              OR operator.  Search both the title and url.
266     */
267    public void search(String like) {
268        String whereClause = Browser.BookmarkColumns.BOOKMARK + " == 1";
269        String[] selectionArgs = null;
270        if (like != null) {
271            String[] likes = like.split(" ");
272            int count = 0;
273            boolean firstTerm = true;
274            StringBuilder andClause = new StringBuilder(256);
275            for (int j = 0; j < likes.length; j++) {
276                if (likes[j].length() > 0) {
277                    if (firstTerm) {
278                        firstTerm = false;
279                    } else {
280                        andClause.append(" OR ");
281                    }
282                    andClause.append(Browser.BookmarkColumns.TITLE
283                            + " LIKE ? OR " + Browser.BookmarkColumns.URL
284                            + " LIKE ? ");
285                    count += 2;
286                }
287            }
288            if (count > 0) {
289                selectionArgs = new String[count];
290                count = 0;
291                for (int j = 0; j < likes.length; j++) {
292                    if (likes[j].length() > 0) {
293                        like = "%" + likes[j] + "%";
294                        selectionArgs[count++] = like;
295                        selectionArgs[count++] = like;
296                    }
297                }
298                whereClause += " AND (" + andClause + ")";
299            }
300        }
301        searchInternal(whereClause, selectionArgs, mLastOrderBy);
302    }
303
304    /**
305     * Update the bookmark's favicon.
306     * @param cr The ContentResolver to use.
307     * @param url The url of the bookmark to update.
308     * @param favicon The favicon bitmap to write to the db.
309     */
310    /* package */ static void updateBookmarkFavicon(ContentResolver cr,
311            String url, Bitmap favicon) {
312        if (url == null || favicon == null) {
313            return;
314        }
315        // Strip the query.
316        int query = url.indexOf('?');
317        String noQuery = url;
318        if (query != -1) {
319            noQuery = url.substring(0, query);
320        }
321        url = noQuery + '?';
322        // Use noQuery to search for the base url (i.e. if the url is
323        // http://www.yahoo.com/?rs=1, search for http://www.yahoo.com)
324        // Use url to match the base url with other queries (i.e. if the url is
325        // http://www.google.com/m, search for
326        // http://www.google.com/m?some_query)
327        final String[] selArgs = new String[] { noQuery, url };
328        final String where = "(" + Browser.BookmarkColumns.URL + " == ? OR "
329                + Browser.BookmarkColumns.URL + " GLOB ? || '*') AND "
330                + Browser.BookmarkColumns.BOOKMARK + " == 1";
331        final String[] projection = new String[] { Browser.BookmarkColumns._ID };
332        final Cursor c = cr.query(Browser.BOOKMARKS_URI, projection, where,
333                selArgs, null);
334        boolean succeed = c.moveToFirst();
335        ContentValues values = null;
336        while (succeed) {
337            if (values == null) {
338                final ByteArrayOutputStream os = new ByteArrayOutputStream();
339                favicon.compress(Bitmap.CompressFormat.PNG, 100, os);
340                values = new ContentValues();
341                values.put(Browser.BookmarkColumns.FAVICON, os.toByteArray());
342            }
343            cr.update(ContentUris.withAppendedId(Browser.BOOKMARKS_URI, c
344                    .getInt(0)), values, null, null);
345            succeed = c.moveToNext();
346        }
347        c.close();
348    }
349
350    /**
351     *  This sorts alphabetically, with non-capitalized titles before
352     *  capitalized.
353     */
354    public void sortAlphabetical() {
355        searchInternal(mLastWhereClause, mLastSelectionArgs,
356                Browser.BookmarkColumns.TITLE + " COLLATE UNICODE ASC");
357    }
358
359    /**
360     *  Internal function used in search, sort, and refreshList.
361     */
362    private void searchInternal(String whereClause, String[] selectionArgs,
363            String orderBy) {
364        if (mCursor != null) {
365            mCursor.unregisterContentObserver(mChangeObserver);
366            mCursor.unregisterDataSetObserver(mDataSetObserver);
367            mBookmarksPage.stopManagingCursor(mCursor);
368            mCursor.deactivate();
369        }
370
371        mLastWhereClause = whereClause;
372        mLastSelectionArgs = selectionArgs;
373        mLastOrderBy = orderBy;
374        mCursor = mContentResolver.query(
375            Browser.BOOKMARKS_URI,
376            Browser.HISTORY_PROJECTION,
377            whereClause,
378            selectionArgs,
379            orderBy);
380        mCursor.registerContentObserver(mChangeObserver);
381        mCursor.registerDataSetObserver(mDataSetObserver);
382        mBookmarksPage.startManagingCursor(mCursor);
383
384        mDataValid = true;
385        notifyDataSetChanged();
386
387        mCount = mCursor.getCount() + mExtraOffset;
388    }
389
390    /**
391     * How many items should be displayed in the list.
392     * @return Count of items.
393     */
394    public int getCount() {
395        if (mDataValid) {
396            return mCount;
397        } else {
398            return 0;
399        }
400    }
401
402    public boolean areAllItemsEnabled() {
403        return true;
404    }
405
406    public boolean isEnabled(int position) {
407        return true;
408    }
409
410    /**
411     * Get the data associated with the specified position in the list.
412     * @param position Index of the item whose data we want.
413     * @return The data at the specified position.
414     */
415    public Object getItem(int position) {
416        return null;
417    }
418
419    /**
420     * Get the row id associated with the specified position in the list.
421     * @param position Index of the item whose row id we want.
422     * @return The id of the item at the specified position.
423     */
424    public long getItemId(int position) {
425        return position;
426    }
427
428    /**
429     * Get a View that displays the data at the specified position
430     * in the list.
431     * @param position Index of the item whose view we want.
432     * @return A View corresponding to the data at the specified position.
433     */
434    public View getView(int position, View convertView, ViewGroup parent) {
435        if (!mDataValid) {
436            throw new IllegalStateException(
437                    "this should only be called when the cursor is valid");
438        }
439        if (position < 0 || position > mCount) {
440            throw new AssertionError(
441                    "BrowserBookmarksAdapter tried to get a view out of range");
442        }
443        if (position == 0 && !mCreateShortcut) {
444            AddNewBookmark b;
445            if (convertView instanceof AddNewBookmark) {
446                b = (AddNewBookmark) convertView;
447            } else {
448                b = new AddNewBookmark(mBookmarksPage);
449            }
450            b.setUrl(mCurrentPage);
451            return b;
452        }
453        if (convertView == null || convertView instanceof AddNewBookmark) {
454            convertView = new BookmarkItem(mBookmarksPage);
455        }
456        bind((BookmarkItem)convertView, position);
457        return convertView;
458    }
459
460    /**
461     *  Return the title for this item in the list.
462     */
463    public String getTitle(int position) {
464        return getString(Browser.HISTORY_PROJECTION_TITLE_INDEX, position);
465    }
466
467    /**
468     *  Return the Url for this item in the list.
469     */
470    public String getUrl(int position) {
471        return getString(Browser.HISTORY_PROJECTION_URL_INDEX, position);
472    }
473
474    /**
475     * Private helper function to return the title or url.
476     */
477    private String getString(int cursorIndex, int position) {
478        if (position < mExtraOffset || position > mCount) {
479            return "";
480        }
481        mCursor.moveToPosition(position- mExtraOffset);
482        return mCursor.getString(cursorIndex);
483    }
484
485    private void bind(BookmarkItem b, int position) {
486        mCursor.moveToPosition(position- mExtraOffset);
487
488        String title = mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX);
489        if (title.length() > BrowserSettings.MAX_TEXTVIEW_LEN) {
490            title = title.substring(0, BrowserSettings.MAX_TEXTVIEW_LEN);
491        }
492        b.setName(title);
493        String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX);
494        if (url.length() > BrowserSettings.MAX_TEXTVIEW_LEN) {
495            url = url.substring(0, BrowserSettings.MAX_TEXTVIEW_LEN);
496        }
497        b.setUrl(url);
498        byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX);
499        if (data != null) {
500            b.setFavicon(BitmapFactory.decodeByteArray(data, 0, data.length));
501        } else {
502            b.setFavicon(null);
503        }
504    }
505
506    private class ChangeObserver extends ContentObserver {
507        public ChangeObserver() {
508            super(new Handler());
509        }
510
511        @Override
512        public boolean deliverSelfNotifications() {
513            return true;
514        }
515
516        @Override
517        public void onChange(boolean selfChange) {
518            refreshList();
519        }
520    }
521
522    private class MyDataSetObserver extends DataSetObserver {
523        @Override
524        public void onChanged() {
525            mDataValid = true;
526            notifyDataSetChanged();
527        }
528
529        @Override
530        public void onInvalidated() {
531            mDataValid = false;
532            notifyDataSetInvalidated();
533        }
534    }
535}
536