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