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