BrowserBookmarksPage.java revision 5942df0c38dff7e4335e352e2d03f100b07b8907
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.app.Activity; 20import android.app.AlertDialog; 21import android.content.DialogInterface; 22import android.content.Intent; 23import android.content.SharedPreferences; 24import android.content.SharedPreferences.Editor; 25import android.graphics.Bitmap; 26import android.graphics.BitmapFactory; 27import android.graphics.Canvas; 28import android.graphics.Color; 29import android.graphics.Paint; 30import android.graphics.Path; 31import android.graphics.PorterDuff; 32import android.graphics.PorterDuffXfermode; 33import android.graphics.RectF; 34import android.net.Uri; 35import android.os.Bundle; 36import android.os.Handler; 37import android.os.Message; 38import android.os.ServiceManager; 39import android.provider.Browser; 40import android.text.IClipboard; 41import android.util.Log; 42import android.view.ContextMenu; 43import android.view.KeyEvent; 44import android.view.LayoutInflater; 45import android.view.Menu; 46import android.view.MenuInflater; 47import android.view.MenuItem; 48import android.view.View; 49import android.view.ViewGroup; 50import android.view.ViewGroup.LayoutParams; 51import android.view.ViewStub; 52import android.view.ContextMenu.ContextMenuInfo; 53import android.widget.AdapterView; 54import android.widget.GridView; 55import android.widget.ListView; 56import android.widget.Toast; 57 58/*package*/ enum BookmarkViewMode { NONE, GRID, LIST } 59/** 60 * View showing the user's bookmarks in the browser. 61 */ 62public class BrowserBookmarksPage extends Activity implements 63 View.OnCreateContextMenuListener { 64 65 private BookmarkViewMode mViewMode = BookmarkViewMode.NONE; 66 private GridView mGridPage; 67 private View mVerticalList; 68 private BrowserBookmarksAdapter mBookmarksAdapter; 69 private static final int BOOKMARKS_SAVE = 1; 70 private boolean mDisableNewWindow; 71 private BookmarkItem mContextHeader; 72 private AddNewBookmark mAddHeader; 73 private boolean mCanceled = false; 74 private boolean mCreateShortcut; 75 private boolean mMostVisited; 76 private View mEmptyView; 77 // XXX: There is no public string defining this intent so if Home changes 78 // the value, we have to update this string. 79 private static final String INSTALL_SHORTCUT = 80 "com.android.launcher.action.INSTALL_SHORTCUT"; 81 82 private final static String LOGTAG = "browser"; 83 private final static String PREF_BOOKMARK_VIEW_MODE = "pref_bookmark_view_mode"; 84 private final static String PREF_MOST_VISITED_VIEW_MODE = "pref_most_visited_view_mode"; 85 86 @Override 87 public boolean onContextItemSelected(MenuItem item) { 88 // It is possible that the view has been canceled when we get to 89 // this point as back has a higher priority 90 if (mCanceled) { 91 return true; 92 } 93 AdapterView.AdapterContextMenuInfo i = 94 (AdapterView.AdapterContextMenuInfo)item.getMenuInfo(); 95 // If we have no menu info, we can't tell which item was selected. 96 if (i == null) { 97 return true; 98 } 99 100 switch (item.getItemId()) { 101 case R.id.new_context_menu_id: 102 saveCurrentPage(); 103 break; 104 case R.id.open_context_menu_id: 105 loadUrl(i.position); 106 break; 107 case R.id.edit_context_menu_id: 108 editBookmark(i.position); 109 break; 110 case R.id.shortcut_context_menu_id: 111 final Intent send = createShortcutIntent(i.position); 112 send.setAction(INSTALL_SHORTCUT); 113 sendBroadcast(send); 114 break; 115 case R.id.delete_context_menu_id: 116 if (mMostVisited) { 117 Browser.deleteFromHistory(getContentResolver(), 118 getUrl(i.position)); 119 refreshList(); 120 } else { 121 displayRemoveBookmarkDialog(i.position); 122 } 123 break; 124 case R.id.new_window_context_menu_id: 125 openInNewWindow(i.position); 126 break; 127 case R.id.share_link_context_menu_id: 128 Browser.sendString(BrowserBookmarksPage.this, getUrl(i.position)); 129 break; 130 case R.id.copy_url_context_menu_id: 131 copy(getUrl(i.position)); 132 break; 133 case R.id.homepage_context_menu_id: 134 BrowserSettings.getInstance().setHomePage(this, 135 getUrl(i.position)); 136 Toast.makeText(this, R.string.homepage_set, 137 Toast.LENGTH_LONG).show(); 138 break; 139 // Only for the Most visited page 140 case R.id.save_to_bookmarks_menu_id: 141 boolean isBookmark; 142 String name; 143 String url; 144 if (mViewMode == BookmarkViewMode.GRID) { 145 isBookmark = mBookmarksAdapter.getIsBookmark(i.position); 146 name = mBookmarksAdapter.getTitle(i.position); 147 url = mBookmarksAdapter.getUrl(i.position); 148 } else { 149 HistoryItem historyItem = ((HistoryItem) i.targetView); 150 isBookmark = historyItem.isBookmark(); 151 name = historyItem.getName(); 152 url = historyItem.getUrl(); 153 } 154 // If the site is bookmarked, the item becomes remove from 155 // bookmarks. 156 if (isBookmark) { 157 Bookmarks.removeFromBookmarks(this, getContentResolver(), url); 158 } else { 159 Browser.saveBookmark(this, name, url); 160 } 161 break; 162 default: 163 return super.onContextItemSelected(item); 164 } 165 return true; 166 } 167 168 @Override 169 public void onCreateContextMenu(ContextMenu menu, View v, 170 ContextMenuInfo menuInfo) { 171 AdapterView.AdapterContextMenuInfo i = 172 (AdapterView.AdapterContextMenuInfo) menuInfo; 173 174 MenuInflater inflater = getMenuInflater(); 175 if (mMostVisited) { 176 inflater.inflate(R.menu.historycontext, menu); 177 } else { 178 inflater.inflate(R.menu.bookmarkscontext, menu); 179 } 180 181 if (0 == i.position && !mMostVisited) { 182 menu.setGroupVisible(R.id.CONTEXT_MENU, false); 183 if (mAddHeader == null) { 184 mAddHeader = new AddNewBookmark(BrowserBookmarksPage.this); 185 } else if (mAddHeader.getParent() != null) { 186 ((ViewGroup) mAddHeader.getParent()). 187 removeView(mAddHeader); 188 } 189 mAddHeader.setUrl(getIntent().getStringExtra("url")); 190 menu.setHeaderView(mAddHeader); 191 return; 192 } 193 if (mMostVisited) { 194 if ((mViewMode == BookmarkViewMode.LIST 195 && ((HistoryItem) i.targetView).isBookmark()) 196 || mBookmarksAdapter.getIsBookmark(i.position)) { 197 MenuItem item = menu.findItem( 198 R.id.save_to_bookmarks_menu_id); 199 item.setTitle(R.string.remove_from_bookmarks); 200 } 201 } else { 202 // The historycontext menu has no ADD_MENU group. 203 menu.setGroupVisible(R.id.ADD_MENU, false); 204 } 205 if (mDisableNewWindow) { 206 menu.findItem(R.id.new_window_context_menu_id).setVisible( 207 false); 208 } 209 if (mContextHeader == null) { 210 mContextHeader = new BookmarkItem(BrowserBookmarksPage.this); 211 } else if (mContextHeader.getParent() != null) { 212 ((ViewGroup) mContextHeader.getParent()). 213 removeView(mContextHeader); 214 } 215 if (mViewMode == BookmarkViewMode.GRID) { 216 mBookmarksAdapter.populateBookmarkItem(mContextHeader, 217 i.position); 218 } else { 219 BookmarkItem b = (BookmarkItem) i.targetView; 220 b.copyTo(mContextHeader); 221 } 222 menu.setHeaderView(mContextHeader); 223 } 224 225 /** 226 * Create a new BrowserBookmarksPage. 227 */ 228 @Override 229 protected void onCreate(Bundle icicle) { 230 super.onCreate(icicle); 231 232 if (Intent.ACTION_CREATE_SHORTCUT.equals(getIntent().getAction())) { 233 mCreateShortcut = true; 234 } 235 mDisableNewWindow = getIntent().getBooleanExtra("disable_new_window", 236 false); 237 mMostVisited = getIntent().getBooleanExtra("mostVisited", false); 238 239 if (mCreateShortcut) { 240 setTitle(R.string.browser_bookmarks_page_bookmarks_text); 241 } 242 mBookmarksAdapter = new BrowserBookmarksAdapter(this, 243 getIntent().getStringExtra("url"), 244 getIntent().getStringExtra("title"), mCreateShortcut, 245 mMostVisited); 246 247 setContentView(R.layout.empty_history); 248 mEmptyView = findViewById(R.id.empty_view); 249 mEmptyView.setVisibility(View.GONE); 250 251 SharedPreferences p = getPreferences(MODE_PRIVATE); 252 253 // See if the user has set a preference for the view mode of their 254 // bookmarks. Otherwise default to grid mode. 255 BookmarkViewMode preference = BookmarkViewMode.NONE; 256 if (mMostVisited) { 257 preference = BookmarkViewMode.values()[p.getInt( 258 PREF_MOST_VISITED_VIEW_MODE, 259 BookmarkViewMode.GRID.ordinal())]; 260 } else { 261 preference = BookmarkViewMode.values()[p.getInt( 262 PREF_BOOKMARK_VIEW_MODE, BookmarkViewMode.GRID.ordinal())]; 263 } 264 switchViewMode(preference); 265 } 266 267 /** 268 * Set the ContentView to be either the grid of thumbnails or the vertical 269 * list. 270 */ 271 private void switchViewMode(BookmarkViewMode gridMode) { 272 if (mViewMode == gridMode) { 273 return; 274 } 275 276 mViewMode = gridMode; 277 278 // Update the preferences to make the new view mode sticky. 279 Editor ed = getPreferences(MODE_PRIVATE).edit(); 280 if (mMostVisited) { 281 ed.putInt(PREF_MOST_VISITED_VIEW_MODE, mViewMode.ordinal()); 282 } else { 283 ed.putInt(PREF_BOOKMARK_VIEW_MODE, mViewMode.ordinal()); 284 } 285 ed.commit(); 286 287 mBookmarksAdapter.switchViewMode(gridMode); 288 if (mViewMode == BookmarkViewMode.GRID) { 289 if (mGridPage == null) { 290 mGridPage = new GridView(this); 291 mGridPage.setAdapter(mBookmarksAdapter); 292 mGridPage.setOnItemClickListener(mListener); 293 mGridPage.setNumColumns(GridView.AUTO_FIT); 294 // Keep this in sync with bookmark_thumb and 295 // BrowserActivity.updateScreenshot 296 mGridPage.setColumnWidth(100); 297 mGridPage.setFocusable(true); 298 mGridPage.setFocusableInTouchMode(true); 299 mGridPage.setSelector(android.R.drawable.gallery_thumb); 300 mGridPage.setVerticalSpacing(10); 301 if (mMostVisited) { 302 mGridPage.setEmptyView(mEmptyView); 303 } 304 if (!mCreateShortcut) { 305 mGridPage.setOnCreateContextMenuListener(this); 306 } 307 } 308 addContentView(mGridPage, FULL_SCREEN_PARAMS); 309 if (mVerticalList != null) { 310 ViewGroup parent = (ViewGroup) mVerticalList.getParent(); 311 if (parent != null) { 312 parent.removeView(mVerticalList); 313 } 314 } 315 } else { 316 if (null == mVerticalList) { 317 ListView listView = new ListView(this); 318 listView.setAdapter(mBookmarksAdapter); 319 listView.setDrawSelectorOnTop(false); 320 listView.setVerticalScrollBarEnabled(true); 321 listView.setOnItemClickListener(mListener); 322 if (mMostVisited) { 323 listView.setEmptyView(mEmptyView); 324 } 325 if (!mCreateShortcut) { 326 listView.setOnCreateContextMenuListener(this); 327 } 328 mVerticalList = listView; 329 } 330 addContentView(mVerticalList, FULL_SCREEN_PARAMS); 331 if (mGridPage != null) { 332 ViewGroup parent = (ViewGroup) mGridPage.getParent(); 333 if (parent != null) { 334 parent.removeView(mGridPage); 335 } 336 } 337 } 338 } 339 340 private static final ViewGroup.LayoutParams FULL_SCREEN_PARAMS 341 = new ViewGroup.LayoutParams( 342 ViewGroup.LayoutParams.FILL_PARENT, 343 ViewGroup.LayoutParams.FILL_PARENT); 344 345 private static final int SAVE_CURRENT_PAGE = 1000; 346 private final Handler mHandler = new Handler() { 347 @Override 348 public void handleMessage(Message msg) { 349 if (msg.what == SAVE_CURRENT_PAGE) { 350 saveCurrentPage(); 351 } 352 } 353 }; 354 355 private AdapterView.OnItemClickListener mListener = new AdapterView.OnItemClickListener() { 356 public void onItemClick(AdapterView parent, View v, int position, long id) { 357 // It is possible that the view has been canceled when we get to 358 // this point as back has a higher priority 359 if (mCanceled) { 360 android.util.Log.e(LOGTAG, "item clicked when dismissing"); 361 return; 362 } 363 if (!mCreateShortcut) { 364 if (0 == position && !mMostVisited) { 365 // XXX: Work-around for a framework issue. 366 mHandler.sendEmptyMessage(SAVE_CURRENT_PAGE); 367 } else { 368 loadUrl(position); 369 } 370 } else { 371 final Intent intent = createShortcutIntent(position); 372 setResultToParent(RESULT_OK, intent); 373 finish(); 374 } 375 } 376 }; 377 378 private Intent createShortcutIntent(int position) { 379 String url = getUrl(position); 380 String title = getBookmarkTitle(position); 381 Bitmap touchIcon = getTouchIcon(position); 382 383 final Intent i = new Intent(); 384 final Intent shortcutIntent = new Intent(Intent.ACTION_VIEW, 385 Uri.parse(url)); 386 long urlHash = url.hashCode(); 387 long uniqueId = (urlHash << 32) | shortcutIntent.hashCode(); 388 shortcutIntent.putExtra(Browser.EXTRA_APPLICATION_ID, 389 Long.toString(uniqueId)); 390 i.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); 391 i.putExtra(Intent.EXTRA_SHORTCUT_NAME, title); 392 // Use the apple-touch-icon if available 393 if (touchIcon != null) { 394 // Make a copy so we can modify the pixels. 395 Bitmap copy = touchIcon.copy(Bitmap.Config.ARGB_8888, true); 396 Canvas canvas = new Canvas(copy); 397 398 // Construct a path from a round rect. This will allow drawing with 399 // an inverse fill so we can punch a hole using the round rect. 400 Path path = new Path(); 401 path.setFillType(Path.FillType.INVERSE_WINDING); 402 path.addRoundRect(new RectF(0, 0, touchIcon.getWidth(), 403 touchIcon.getHeight()), 8f, 8f, Path.Direction.CW); 404 405 // Construct a paint that clears the outside of the rectangle and 406 // draw. 407 Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); 408 paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 409 canvas.drawPath(path, paint); 410 411 i.putExtra(Intent.EXTRA_SHORTCUT_ICON, copy); 412 } else { 413 Bitmap favicon = getFavicon(position); 414 if (favicon == null) { 415 i.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, 416 Intent.ShortcutIconResource.fromContext( 417 BrowserBookmarksPage.this, 418 R.drawable.ic_launcher_shortcut_browser_bookmark)); 419 } else { 420 Bitmap icon = BitmapFactory.decodeResource(getResources(), 421 R.drawable.ic_launcher_shortcut_browser_bookmark); 422 423 // Make a copy of the regular icon so we can modify the pixels. 424 Bitmap copy = icon.copy(Bitmap.Config.ARGB_8888, true); 425 Canvas canvas = new Canvas(copy); 426 427 // Make a Paint for the white background rectangle and for 428 // filtering the favicon. 429 Paint p = new Paint(Paint.ANTI_ALIAS_FLAG 430 | Paint.FILTER_BITMAP_FLAG); 431 p.setStyle(Paint.Style.FILL_AND_STROKE); 432 p.setColor(Color.WHITE); 433 434 // Create a rectangle that is slightly wider than the favicon 435 final float iconSize = 16; // 16x16 favicon 436 final float padding = 2; // white padding around icon 437 final float rectSize = iconSize + 2 * padding; 438 final float y = icon.getHeight() - rectSize; 439 RectF r = new RectF(0, y, rectSize, y + rectSize); 440 441 // Draw a white rounded rectangle behind the favicon 442 canvas.drawRoundRect(r, 2, 2, p); 443 444 // Draw the favicon in the same rectangle as the rounded 445 // rectangle but inset by the padding 446 // (results in a 16x16 favicon). 447 r.inset(padding, padding); 448 canvas.drawBitmap(favicon, null, r, p); 449 i.putExtra(Intent.EXTRA_SHORTCUT_ICON, copy); 450 } 451 } 452 // Do not allow duplicate items 453 i.putExtra("duplicate", false); 454 return i; 455 } 456 457 private void saveCurrentPage() { 458 Intent i = new Intent(BrowserBookmarksPage.this, 459 AddBookmarkPage.class); 460 i.putExtras(getIntent()); 461 startActivityForResult(i, BOOKMARKS_SAVE); 462 } 463 464 private void loadUrl(int position) { 465 Intent intent = (new Intent()).setAction(getUrl(position)); 466 setResultToParent(RESULT_OK, intent); 467 finish(); 468 } 469 470 @Override 471 public boolean onCreateOptionsMenu(Menu menu) { 472 boolean result = super.onCreateOptionsMenu(menu); 473 if (!mCreateShortcut) { 474 MenuInflater inflater = getMenuInflater(); 475 inflater.inflate(R.menu.bookmarks, menu); 476 // Most visited page does not have an option to bookmark the last 477 // viewed page. 478 menu.findItem(R.id.new_context_menu_id).setVisible(!mMostVisited); 479 return true; 480 } 481 return result; 482 } 483 484 @Override 485 public boolean onPrepareOptionsMenu(Menu menu) { 486 boolean result = super.onPrepareOptionsMenu(menu); 487 if (mCreateShortcut || mBookmarksAdapter.getCount() == 0) { 488 // No need to show the menu if there are no items. 489 return result; 490 } 491 menu.findItem(R.id.switch_mode_menu_id).setTitle( 492 mViewMode == BookmarkViewMode.GRID ? R.string.switch_to_list 493 : R.string.switch_to_thumbnails); 494 return true; 495 } 496 497 @Override 498 public boolean onOptionsItemSelected(MenuItem item) { 499 switch (item.getItemId()) { 500 case R.id.new_context_menu_id: 501 saveCurrentPage(); 502 break; 503 504 case R.id.switch_mode_menu_id: 505 if (mViewMode == BookmarkViewMode.GRID) { 506 switchViewMode(BookmarkViewMode.LIST); 507 } else { 508 switchViewMode(BookmarkViewMode.GRID); 509 } 510 break; 511 512 default: 513 return super.onOptionsItemSelected(item); 514 } 515 return true; 516 } 517 518 private void openInNewWindow(int position) { 519 Bundle b = new Bundle(); 520 b.putBoolean("new_window", true); 521 setResultToParent(RESULT_OK, 522 (new Intent()).setAction(getUrl(position)).putExtras(b)); 523 524 finish(); 525 } 526 527 528 private void editBookmark(int position) { 529 Intent intent = new Intent(BrowserBookmarksPage.this, 530 AddBookmarkPage.class); 531 intent.putExtra("bookmark", getRow(position)); 532 startActivityForResult(intent, BOOKMARKS_SAVE); 533 } 534 535 @Override 536 protected void onActivityResult(int requestCode, int resultCode, 537 Intent data) { 538 switch(requestCode) { 539 case BOOKMARKS_SAVE: 540 if (resultCode == RESULT_OK) { 541 Bundle extras; 542 if (data != null && (extras = data.getExtras()) != null) { 543 // If there are extras, then we need to save 544 // the edited bookmark. This is done in updateRow() 545 String title = extras.getString("title"); 546 String url = extras.getString("url"); 547 if (title != null && url != null) { 548 mBookmarksAdapter.updateRow(extras); 549 } 550 } else { 551 // extras == null then a new bookmark was added to 552 // the database. 553 refreshList(); 554 } 555 } 556 break; 557 default: 558 break; 559 } 560 } 561 562 private void displayRemoveBookmarkDialog(int position) { 563 // Put up a dialog asking if the user really wants to 564 // delete the bookmark 565 final int deletePos = position; 566 new AlertDialog.Builder(this) 567 .setTitle(R.string.delete_bookmark) 568 .setIcon(android.R.drawable.ic_dialog_alert) 569 .setMessage(getText(R.string.delete_bookmark_warning).toString().replace( 570 "%s", getBookmarkTitle(deletePos))) 571 .setPositiveButton(R.string.ok, 572 new DialogInterface.OnClickListener() { 573 public void onClick(DialogInterface dialog, int whichButton) { 574 deleteBookmark(deletePos); 575 } 576 }) 577 .setNegativeButton(R.string.cancel, null) 578 .show(); 579 } 580 581 /** 582 * Refresh the shown list after the database has changed. 583 */ 584 private void refreshList() { 585 mBookmarksAdapter.refreshList(); 586 } 587 588 /** 589 * Return a hashmap representing the currently highlighted row. 590 */ 591 public Bundle getRow(int position) { 592 return mBookmarksAdapter.getRow(position); 593 } 594 595 /** 596 * Return the url of the currently highlighted row. 597 */ 598 public String getUrl(int position) { 599 return mBookmarksAdapter.getUrl(position); 600 } 601 602 /** 603 * Return the favicon of the currently highlighted row. 604 */ 605 public Bitmap getFavicon(int position) { 606 return mBookmarksAdapter.getFavicon(position); 607 } 608 609 private Bitmap getTouchIcon(int position) { 610 return mBookmarksAdapter.getTouchIcon(position); 611 } 612 613 private void copy(CharSequence text) { 614 try { 615 IClipboard clip = IClipboard.Stub.asInterface(ServiceManager.getService("clipboard")); 616 if (clip != null) { 617 clip.setClipboardText(text); 618 } 619 } catch (android.os.RemoteException e) { 620 Log.e(LOGTAG, "Copy failed", e); 621 } 622 } 623 624 public String getBookmarkTitle(int position) { 625 return mBookmarksAdapter.getTitle(position); 626 } 627 628 /** 629 * Delete the currently highlighted row. 630 */ 631 public void deleteBookmark(int position) { 632 mBookmarksAdapter.deleteRow(position); 633 } 634 635 @Override 636 public void onBackPressed() { 637 setResultToParent(RESULT_CANCELED, null); 638 mCanceled = true; 639 super.onBackPressed(); 640 } 641 642 // This Activity is generally a sub-Activity of CombinedHistoryActivity. In 643 // that situation, we need to pass our result code up to our parent. 644 // However, if someone calls this Activity directly, then this has no 645 // parent, and it needs to set it on itself. 646 private void setResultToParent(int resultCode, Intent data) { 647 Activity a = getParent() == null ? this : getParent(); 648 a.setResult(resultCode, data); 649 } 650} 651