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