blob: 479dc0efbaf0f34c415b4b5e22d36b802b80a212 [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
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}