2 * Copyright (C) 2006 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.browser;
19 import android.app.Activity;
20 import android.app.AlertDialog;
21 import android.content.DialogInterface;
22 import android.content.Intent;
23 import android.content.SharedPreferences;
24 import android.content.SharedPreferences.Editor;
25 import android.graphics.Bitmap;
26 import android.graphics.BitmapFactory;
27 import android.graphics.Canvas;
28 import android.graphics.Color;
29 import android.graphics.Paint;
30 import android.graphics.Path;
31 import android.graphics.PorterDuff;
32 import android.graphics.PorterDuffXfermode;
33 import android.graphics.RectF;
34 import android.net.Uri;
35 import android.os.Bundle;
36 import android.os.Handler;
37 import android.os.Message;
38 import android.os.ServiceManager;
39 import android.provider.Browser;
40 import android.text.IClipboard;
41 import android.util.Log;
42 import android.view.ContextMenu;
43 import android.view.KeyEvent;
44 import android.view.LayoutInflater;
45 import android.view.Menu;
46 import android.view.MenuInflater;
47 import android.view.MenuItem;
48 import android.view.View;
49 import android.view.ViewGroup;
50 import android.view.ViewGroup.LayoutParams;
51 import android.view.ViewStub;
52 import android.view.ContextMenu.ContextMenuInfo;
53 import android.widget.AdapterView;
54 import android.widget.GridView;
55 import android.widget.ListView;
56 import android.widget.Toast;
58 /*package*/ enum BookmarkViewMode { NONE, GRID, LIST }
60 * View showing the user's bookmarks in the browser.
62 public class BrowserBookmarksPage extends Activity implements
63 View.OnCreateContextMenuListener {
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";
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";
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
93 AdapterView.AdapterContextMenuInfo i =
94 (AdapterView.AdapterContextMenuInfo)item.getMenuInfo();
95 // If we have no menu info, we can't tell which item was selected.
100 switch (item.getItemId()) {
101 case R.id.new_context_menu_id:
104 case R.id.open_context_menu_id:
107 case R.id.edit_context_menu_id:
108 editBookmark(i.position);
110 case R.id.shortcut_context_menu_id:
111 final Intent send = createShortcutIntent(i.position);
112 send.setAction(INSTALL_SHORTCUT);
115 case R.id.delete_context_menu_id:
117 Browser.deleteFromHistory(getContentResolver(),
121 displayRemoveBookmarkDialog(i.position);
124 case R.id.new_window_context_menu_id:
125 openInNewWindow(i.position);
127 case R.id.share_link_context_menu_id:
128 Browser.sendString(BrowserBookmarksPage.this, getUrl(i.position),
129 getText(R.string.choosertitle_sharevia).toString());
131 case R.id.copy_url_context_menu_id:
132 copy(getUrl(i.position));
134 case R.id.homepage_context_menu_id:
135 BrowserSettings.getInstance().setHomePage(this,
137 Toast.makeText(this, R.string.homepage_set,
138 Toast.LENGTH_LONG).show();
140 // Only for the Most visited page
141 case R.id.save_to_bookmarks_menu_id:
145 if (mViewMode == BookmarkViewMode.GRID) {
146 isBookmark = mBookmarksAdapter.getIsBookmark(i.position);
147 name = mBookmarksAdapter.getTitle(i.position);
148 url = mBookmarksAdapter.getUrl(i.position);
150 HistoryItem historyItem = ((HistoryItem) i.targetView);
151 isBookmark = historyItem.isBookmark();
152 name = historyItem.getName();
153 url = historyItem.getUrl();
155 // If the site is bookmarked, the item becomes remove from
158 Bookmarks.removeFromBookmarks(this, getContentResolver(), url, name);
160 Browser.saveBookmark(this, name, url);
164 return super.onContextItemSelected(item);
170 public void onCreateContextMenu(ContextMenu menu, View v,
171 ContextMenuInfo menuInfo) {
172 AdapterView.AdapterContextMenuInfo i =
173 (AdapterView.AdapterContextMenuInfo) menuInfo;
175 MenuInflater inflater = getMenuInflater();
177 inflater.inflate(R.menu.historycontext, menu);
179 inflater.inflate(R.menu.bookmarkscontext, menu);
182 if (0 == i.position && !mMostVisited) {
183 menu.setGroupVisible(R.id.CONTEXT_MENU, false);
184 if (mAddHeader == null) {
185 mAddHeader = new AddNewBookmark(BrowserBookmarksPage.this);
186 } else if (mAddHeader.getParent() != null) {
187 ((ViewGroup) mAddHeader.getParent()).
188 removeView(mAddHeader);
190 mAddHeader.setUrl(getIntent().getStringExtra("url"));
191 menu.setHeaderView(mAddHeader);
195 if ((mViewMode == BookmarkViewMode.LIST
196 && ((HistoryItem) i.targetView).isBookmark())
197 || mBookmarksAdapter.getIsBookmark(i.position)) {
198 MenuItem item = menu.findItem(
199 R.id.save_to_bookmarks_menu_id);
200 item.setTitle(R.string.remove_from_bookmarks);
203 // The historycontext menu has no ADD_MENU group.
204 menu.setGroupVisible(R.id.ADD_MENU, false);
206 if (mDisableNewWindow) {
207 menu.findItem(R.id.new_window_context_menu_id).setVisible(
210 if (mContextHeader == null) {
211 mContextHeader = new BookmarkItem(BrowserBookmarksPage.this);
212 } else if (mContextHeader.getParent() != null) {
213 ((ViewGroup) mContextHeader.getParent()).
214 removeView(mContextHeader);
216 if (mViewMode == BookmarkViewMode.GRID) {
217 mBookmarksAdapter.populateBookmarkItem(mContextHeader,
220 BookmarkItem b = (BookmarkItem) i.targetView;
221 b.copyTo(mContextHeader);
223 menu.setHeaderView(mContextHeader);
227 * Create a new BrowserBookmarksPage.
230 protected void onCreate(Bundle icicle) {
231 super.onCreate(icicle);
233 if (Intent.ACTION_CREATE_SHORTCUT.equals(getIntent().getAction())) {
234 mCreateShortcut = true;
236 mDisableNewWindow = getIntent().getBooleanExtra("disable_new_window",
238 mMostVisited = getIntent().getBooleanExtra("mostVisited", false);
240 if (mCreateShortcut) {
241 setTitle(R.string.browser_bookmarks_page_bookmarks_text);
243 mBookmarksAdapter = new BrowserBookmarksAdapter(this,
244 getIntent().getStringExtra("url"),
245 getIntent().getStringExtra("title"),
246 (Bitmap) getIntent().getParcelableExtra("thumbnail"),
250 setContentView(R.layout.empty_history);
251 mEmptyView = findViewById(R.id.empty_view);
252 mEmptyView.setVisibility(View.GONE);
254 SharedPreferences p = getPreferences(MODE_PRIVATE);
256 // See if the user has set a preference for the view mode of their
257 // bookmarks. Otherwise default to grid mode.
258 BookmarkViewMode preference = BookmarkViewMode.NONE;
260 preference = BookmarkViewMode.values()[p.getInt(
261 PREF_MOST_VISITED_VIEW_MODE,
262 BookmarkViewMode.GRID.ordinal())];
264 preference = BookmarkViewMode.values()[p.getInt(
265 PREF_BOOKMARK_VIEW_MODE, BookmarkViewMode.GRID.ordinal())];
267 switchViewMode(preference);
271 * Set the ContentView to be either the grid of thumbnails or the vertical
274 private void switchViewMode(BookmarkViewMode gridMode) {
275 if (mViewMode == gridMode) {
279 mViewMode = gridMode;
281 // Update the preferences to make the new view mode sticky.
282 Editor ed = getPreferences(MODE_PRIVATE).edit();
284 ed.putInt(PREF_MOST_VISITED_VIEW_MODE, mViewMode.ordinal());
286 ed.putInt(PREF_BOOKMARK_VIEW_MODE, mViewMode.ordinal());
290 mBookmarksAdapter.switchViewMode(gridMode);
291 if (mViewMode == BookmarkViewMode.GRID) {
292 if (mGridPage == null) {
293 mGridPage = new GridView(this);
294 mGridPage.setAdapter(mBookmarksAdapter);
295 mGridPage.setOnItemClickListener(mListener);
296 mGridPage.setNumColumns(GridView.AUTO_FIT);
297 mGridPage.setColumnWidth(
298 BrowserActivity.getDesiredThumbnailWidth(this));
299 mGridPage.setFocusable(true);
300 mGridPage.setFocusableInTouchMode(true);
301 mGridPage.setSelector(android.R.drawable.gallery_thumb);
302 float density = getResources().getDisplayMetrics().density;
303 mGridPage.setVerticalSpacing((int) (14 * density));
304 mGridPage.setHorizontalSpacing((int) (8 * density));
305 mGridPage.setStretchMode(GridView.STRETCH_SPACING);
306 mGridPage.setDrawSelectorOnTop(true);
308 mGridPage.setEmptyView(mEmptyView);
310 if (!mCreateShortcut) {
311 mGridPage.setOnCreateContextMenuListener(this);
314 addContentView(mGridPage, FULL_SCREEN_PARAMS);
315 if (mVerticalList != null) {
316 ViewGroup parent = (ViewGroup) mVerticalList.getParent();
317 if (parent != null) {
318 parent.removeView(mVerticalList);
322 if (null == mVerticalList) {
323 ListView listView = new ListView(this);
324 listView.setAdapter(mBookmarksAdapter);
325 listView.setDrawSelectorOnTop(false);
326 listView.setVerticalScrollBarEnabled(true);
327 listView.setOnItemClickListener(mListener);
329 listView.setEmptyView(mEmptyView);
331 if (!mCreateShortcut) {
332 listView.setOnCreateContextMenuListener(this);
334 mVerticalList = listView;
336 addContentView(mVerticalList, FULL_SCREEN_PARAMS);
337 if (mGridPage != null) {
338 ViewGroup parent = (ViewGroup) mGridPage.getParent();
339 if (parent != null) {
340 parent.removeView(mGridPage);
346 private static final ViewGroup.LayoutParams FULL_SCREEN_PARAMS
347 = new ViewGroup.LayoutParams(
348 ViewGroup.LayoutParams.FILL_PARENT,
349 ViewGroup.LayoutParams.FILL_PARENT);
351 private static final int SAVE_CURRENT_PAGE = 1000;
352 private final Handler mHandler = new Handler() {
354 public void handleMessage(Message msg) {
355 if (msg.what == SAVE_CURRENT_PAGE) {
361 private AdapterView.OnItemClickListener mListener = new AdapterView.OnItemClickListener() {
362 public void onItemClick(AdapterView parent, View v, int position, long id) {
363 // It is possible that the view has been canceled when we get to
364 // this point as back has a higher priority
366 android.util.Log.e(LOGTAG, "item clicked when dismissing");
369 if (!mCreateShortcut) {
370 if (0 == position && !mMostVisited) {
371 // XXX: Work-around for a framework issue.
372 mHandler.sendEmptyMessage(SAVE_CURRENT_PAGE);
377 final Intent intent = createShortcutIntent(position);
378 setResultToParent(RESULT_OK, intent);
384 private Intent createShortcutIntent(int position) {
385 String url = getUrl(position);
386 String title = getBookmarkTitle(position);
387 Bitmap touchIcon = getTouchIcon(position);
389 final Intent i = new Intent();
390 final Intent shortcutIntent = new Intent(Intent.ACTION_VIEW,
392 long urlHash = url.hashCode();
393 long uniqueId = (urlHash << 32) | shortcutIntent.hashCode();
394 shortcutIntent.putExtra(Browser.EXTRA_APPLICATION_ID,
395 Long.toString(uniqueId));
396 i.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
397 i.putExtra(Intent.EXTRA_SHORTCUT_NAME, title);
398 // Use the apple-touch-icon if available
399 if (touchIcon != null) {
400 // Make a copy so we can modify the pixels.
401 Bitmap copy = touchIcon.copy(Bitmap.Config.ARGB_8888, true);
402 Canvas canvas = new Canvas(copy);
404 // Construct a path from a round rect. This will allow drawing with
405 // an inverse fill so we can punch a hole using the round rect.
406 Path path = new Path();
407 path.setFillType(Path.FillType.INVERSE_WINDING);
408 RectF rect = new RectF(0, 0, touchIcon.getWidth(),
409 touchIcon.getHeight());
411 path.addRoundRect(rect, 8f, 8f, Path.Direction.CW);
413 // Construct a paint that clears the outside of the rectangle and
415 Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
416 paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
417 canvas.drawPath(path, paint);
419 i.putExtra(Intent.EXTRA_SHORTCUT_ICON, copy);
421 Bitmap favicon = getFavicon(position);
422 if (favicon == null) {
423 i.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
424 Intent.ShortcutIconResource.fromContext(
425 BrowserBookmarksPage.this,
426 R.drawable.ic_launcher_shortcut_browser_bookmark));
428 Bitmap icon = BitmapFactory.decodeResource(getResources(),
429 R.drawable.ic_launcher_shortcut_browser_bookmark);
431 // Make a copy of the regular icon so we can modify the pixels.
432 Bitmap copy = icon.copy(Bitmap.Config.ARGB_8888, true);
433 Canvas canvas = new Canvas(copy);
435 // Make a Paint for the white background rectangle and for
436 // filtering the favicon.
437 Paint p = new Paint(Paint.ANTI_ALIAS_FLAG
438 | Paint.FILTER_BITMAP_FLAG);
439 p.setStyle(Paint.Style.FILL_AND_STROKE);
440 p.setColor(Color.WHITE);
442 // Create a rectangle that is slightly wider than the favicon
443 final float iconSize = 16; // 16x16 favicon
444 final float padding = 2; // white padding around icon
445 final float rectSize = iconSize + 2 * padding;
446 final float y = icon.getHeight() - rectSize;
447 RectF r = new RectF(0, y, rectSize, y + rectSize);
449 // Draw a white rounded rectangle behind the favicon
450 canvas.drawRoundRect(r, 2, 2, p);
452 // Draw the favicon in the same rectangle as the rounded
453 // rectangle but inset by the padding
454 // (results in a 16x16 favicon).
455 r.inset(padding, padding);
456 canvas.drawBitmap(favicon, null, r, p);
457 i.putExtra(Intent.EXTRA_SHORTCUT_ICON, copy);
460 // Do not allow duplicate items
461 i.putExtra("duplicate", false);
465 private void saveCurrentPage() {
466 Intent i = new Intent(BrowserBookmarksPage.this,
467 AddBookmarkPage.class);
468 i.putExtras(getIntent());
469 startActivityForResult(i, BOOKMARKS_SAVE);
472 private void loadUrl(int position) {
473 Intent intent = (new Intent()).setAction(getUrl(position));
474 setResultToParent(RESULT_OK, intent);
479 public boolean onCreateOptionsMenu(Menu menu) {
480 boolean result = super.onCreateOptionsMenu(menu);
481 if (!mCreateShortcut) {
482 MenuInflater inflater = getMenuInflater();
483 inflater.inflate(R.menu.bookmarks, menu);
484 // Most visited page does not have an option to bookmark the last
486 menu.findItem(R.id.new_context_menu_id).setVisible(!mMostVisited);
493 public boolean onPrepareOptionsMenu(Menu menu) {
494 boolean result = super.onPrepareOptionsMenu(menu);
495 if (mCreateShortcut || mBookmarksAdapter.getCount() == 0) {
496 // No need to show the menu if there are no items.
499 MenuItem switchItem = menu.findItem(R.id.switch_mode_menu_id);
502 if (mViewMode == BookmarkViewMode.GRID) {
503 titleResId = R.string.switch_to_list;
504 iconResId = R.drawable.ic_menu_list;
506 titleResId = R.string.switch_to_thumbnails;
507 iconResId = R.drawable.ic_menu_thumbnail;
509 switchItem.setTitle(titleResId);
510 switchItem.setIcon(iconResId);
515 public boolean onOptionsItemSelected(MenuItem item) {
516 switch (item.getItemId()) {
517 case R.id.new_context_menu_id:
521 case R.id.switch_mode_menu_id:
522 if (mViewMode == BookmarkViewMode.GRID) {
523 switchViewMode(BookmarkViewMode.LIST);
525 switchViewMode(BookmarkViewMode.GRID);
530 return super.onOptionsItemSelected(item);
535 private void openInNewWindow(int position) {
536 Bundle b = new Bundle();
537 b.putBoolean("new_window", true);
538 setResultToParent(RESULT_OK,
539 (new Intent()).setAction(getUrl(position)).putExtras(b));
545 private void editBookmark(int position) {
546 Intent intent = new Intent(BrowserBookmarksPage.this,
547 AddBookmarkPage.class);
548 intent.putExtra("bookmark", getRow(position));
549 startActivityForResult(intent, BOOKMARKS_SAVE);
553 protected void onActivityResult(int requestCode, int resultCode,
555 switch(requestCode) {
557 if (resultCode == RESULT_OK) {
559 if (data != null && (extras = data.getExtras()) != null) {
560 // If there are extras, then we need to save
561 // the edited bookmark. This is done in updateRow()
562 String title = extras.getString("title");
563 String url = extras.getString("url");
564 if (title != null && url != null) {
565 mBookmarksAdapter.updateRow(extras);
568 // extras == null then a new bookmark was added to
579 private void displayRemoveBookmarkDialog(int position) {
580 // Put up a dialog asking if the user really wants to
581 // delete the bookmark
582 final int deletePos = position;
583 new AlertDialog.Builder(this)
584 .setTitle(R.string.delete_bookmark)
585 .setIcon(android.R.drawable.ic_dialog_alert)
586 .setMessage(getText(R.string.delete_bookmark_warning).toString().replace(
587 "%s", getBookmarkTitle(deletePos)))
588 .setPositiveButton(R.string.ok,
589 new DialogInterface.OnClickListener() {
590 public void onClick(DialogInterface dialog, int whichButton) {
591 deleteBookmark(deletePos);
594 .setNegativeButton(R.string.cancel, null)
599 * Refresh the shown list after the database has changed.
601 private void refreshList() {
602 mBookmarksAdapter.refreshList();
606 * Return a hashmap representing the currently highlighted row.
608 public Bundle getRow(int position) {
609 return mBookmarksAdapter.getRow(position);
613 * Return the url of the currently highlighted row.
615 public String getUrl(int position) {
616 return mBookmarksAdapter.getUrl(position);
620 * Return the favicon of the currently highlighted row.
622 public Bitmap getFavicon(int position) {
623 return mBookmarksAdapter.getFavicon(position);
626 private Bitmap getTouchIcon(int position) {
627 return mBookmarksAdapter.getTouchIcon(position);
630 private void copy(CharSequence text) {
632 IClipboard clip = IClipboard.Stub.asInterface(ServiceManager.getService("clipboard"));
634 clip.setClipboardText(text);
636 } catch (android.os.RemoteException e) {
637 Log.e(LOGTAG, "Copy failed", e);
641 public String getBookmarkTitle(int position) {
642 return mBookmarksAdapter.getTitle(position);
646 * Delete the currently highlighted row.
648 public void deleteBookmark(int position) {
649 mBookmarksAdapter.deleteRow(position);
653 public void onBackPressed() {
654 setResultToParent(RESULT_CANCELED, null);
656 super.onBackPressed();
659 // This Activity is generally a sub-Activity of CombinedHistoryActivity. In
660 // that situation, we need to pass our result code up to our parent.
661 // However, if someone calls this Activity directly, then this has no
662 // parent, and it needs to set it on itself.
663 private void setResultToParent(int resultCode, Intent data) {
664 Activity a = getParent() == null ? this : getParent();
665 a.setResult(resultCode, data);