/* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.browser; import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.RectF; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.ServiceManager; import android.provider.Browser; import android.text.IClipboard; import android.util.Log; import android.view.ContextMenu; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.ContextMenu.ContextMenuInfo; import android.webkit.WebIconDatabase.IconListener; import android.widget.AdapterView; import android.widget.GridView; import android.widget.ListView; import android.widget.Toast; /*package*/ enum BookmarkViewMode { NONE, GRID, LIST } /** * View showing the user's bookmarks in the browser. */ public class BrowserBookmarksPage extends Activity implements View.OnCreateContextMenuListener { private BookmarkViewMode mViewMode = BookmarkViewMode.NONE; private GridView mGridPage; private ListView mVerticalList; private BrowserBookmarksAdapter mBookmarksAdapter; private static final int BOOKMARKS_SAVE = 1; private boolean mDisableNewWindow; private BookmarkItem mContextHeader; private AddNewBookmark mAddHeader; private boolean mCanceled = false; private boolean mCreateShortcut; private boolean mMostVisited; private View mEmptyView; private int mIconSize; // XXX: There is no public string defining this intent so if Home changes // the value, we have to update this string. private static final String INSTALL_SHORTCUT = "com.android.launcher.action.INSTALL_SHORTCUT"; private final static String LOGTAG = "browser"; private final static String PREF_BOOKMARK_VIEW_MODE = "pref_bookmark_view_mode"; private final static String PREF_MOST_VISITED_VIEW_MODE = "pref_most_visited_view_mode"; @Override public boolean onContextItemSelected(MenuItem item) { // It is possible that the view has been canceled when we get to // this point as back has a higher priority if (mCanceled) { return true; } AdapterView.AdapterContextMenuInfo i = (AdapterView.AdapterContextMenuInfo)item.getMenuInfo(); // If we have no menu info, we can't tell which item was selected. if (i == null) { return true; } switch (item.getItemId()) { case R.id.new_context_menu_id: saveCurrentPage(); break; case R.id.open_context_menu_id: loadUrl(i.position); break; case R.id.edit_context_menu_id: editBookmark(i.position); break; case R.id.shortcut_context_menu_id: final Intent send = createShortcutIntent(i.position); send.setAction(INSTALL_SHORTCUT); sendBroadcast(send); break; case R.id.delete_context_menu_id: if (mMostVisited) { Browser.deleteFromHistory(getContentResolver(), getUrl(i.position)); refreshList(); } else { displayRemoveBookmarkDialog(i.position); } break; case R.id.new_window_context_menu_id: openInNewWindow(i.position); break; case R.id.share_link_context_menu_id: BrowserActivity.sharePage(BrowserBookmarksPage.this, mBookmarksAdapter.getTitle(i.position), getUrl(i.position), getFavicon(i.position), mBookmarksAdapter.getScreenshot(i.position)); break; case R.id.copy_url_context_menu_id: copy(getUrl(i.position)); break; case R.id.homepage_context_menu_id: BrowserSettings.getInstance().setHomePage(this, getUrl(i.position)); Toast.makeText(this, R.string.homepage_set, Toast.LENGTH_LONG).show(); break; // Only for the Most visited page case R.id.save_to_bookmarks_menu_id: boolean isBookmark; String name; String url; if (mViewMode == BookmarkViewMode.GRID) { isBookmark = mBookmarksAdapter.getIsBookmark(i.position); name = mBookmarksAdapter.getTitle(i.position); url = mBookmarksAdapter.getUrl(i.position); } else { HistoryItem historyItem = ((HistoryItem) i.targetView); isBookmark = historyItem.isBookmark(); name = historyItem.getName(); url = historyItem.getUrl(); } // If the site is bookmarked, the item becomes remove from // bookmarks. if (isBookmark) { Bookmarks.removeFromBookmarks(this, getContentResolver(), url, name); } else { Browser.saveBookmark(this, name, url); } break; default: return super.onContextItemSelected(item); } return true; } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { AdapterView.AdapterContextMenuInfo i = (AdapterView.AdapterContextMenuInfo) menuInfo; MenuInflater inflater = getMenuInflater(); if (mMostVisited) { inflater.inflate(R.menu.historycontext, menu); } else { inflater.inflate(R.menu.bookmarkscontext, menu); } if (0 == i.position && !mMostVisited) { menu.setGroupVisible(R.id.CONTEXT_MENU, false); if (mAddHeader == null) { mAddHeader = new AddNewBookmark(BrowserBookmarksPage.this); } else if (mAddHeader.getParent() != null) { ((ViewGroup) mAddHeader.getParent()). removeView(mAddHeader); } mAddHeader.setUrl(getIntent().getStringExtra("url")); menu.setHeaderView(mAddHeader); return; } if (mMostVisited) { if ((mViewMode == BookmarkViewMode.LIST && ((HistoryItem) i.targetView).isBookmark()) || mBookmarksAdapter.getIsBookmark(i.position)) { MenuItem item = menu.findItem( R.id.save_to_bookmarks_menu_id); item.setTitle(R.string.remove_from_bookmarks); } } else { // The historycontext menu has no ADD_MENU group. menu.setGroupVisible(R.id.ADD_MENU, false); } if (mDisableNewWindow) { menu.findItem(R.id.new_window_context_menu_id).setVisible( false); } if (mContextHeader == null) { mContextHeader = new BookmarkItem(BrowserBookmarksPage.this); } else if (mContextHeader.getParent() != null) { ((ViewGroup) mContextHeader.getParent()). removeView(mContextHeader); } if (mViewMode == BookmarkViewMode.GRID) { mBookmarksAdapter.populateBookmarkItem(mContextHeader, i.position); } else { BookmarkItem b = (BookmarkItem) i.targetView; b.copyTo(mContextHeader); } menu.setHeaderView(mContextHeader); } /** * Create a new BrowserBookmarksPage. */ @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); // Grab the app icon size as a resource. mIconSize = getResources().getDimensionPixelSize( android.R.dimen.app_icon_size); Intent intent = getIntent(); if (Intent.ACTION_CREATE_SHORTCUT.equals(intent.getAction())) { mCreateShortcut = true; } mDisableNewWindow = intent.getBooleanExtra("disable_new_window", false); mMostVisited = intent.getBooleanExtra("mostVisited", false); if (mCreateShortcut) { setTitle(R.string.browser_bookmarks_page_bookmarks_text); } setContentView(R.layout.empty_history); mEmptyView = findViewById(R.id.empty_view); mEmptyView.setVisibility(View.GONE); SharedPreferences p = getPreferences(MODE_PRIVATE); // See if the user has set a preference for the view mode of their // bookmarks. Otherwise default to grid mode. BookmarkViewMode preference = BookmarkViewMode.NONE; if (mMostVisited) { // For the most visited page, only use list mode. preference = BookmarkViewMode.LIST; } else { preference = BookmarkViewMode.values()[p.getInt( PREF_BOOKMARK_VIEW_MODE, BookmarkViewMode.GRID.ordinal())]; } switchViewMode(preference); final boolean createShortcut = mCreateShortcut; final boolean mostVisited = mMostVisited; final String url = intent.getStringExtra("url"); final String title = intent.getStringExtra("title"); final Bitmap thumbnail = (Bitmap) intent.getParcelableExtra("thumbnail"); new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... unused) { BrowserBookmarksAdapter adapter = new BrowserBookmarksAdapter( BrowserBookmarksPage.this, url, title, thumbnail, createShortcut, mostVisited); mHandler.obtainMessage(ADAPTER_CREATED, adapter).sendToTarget(); return null; } }.execute(); } @Override protected void onDestroy() { mHandler.removeCallbacksAndMessages(null); super.onDestroy(); } /** * Set the ContentView to be either the grid of thumbnails or the vertical * list. */ private void switchViewMode(BookmarkViewMode viewMode) { if (mViewMode == viewMode) { return; } mViewMode = viewMode; // Update the preferences to make the new view mode sticky. Editor ed = getPreferences(MODE_PRIVATE).edit(); if (mMostVisited) { ed.putInt(PREF_MOST_VISITED_VIEW_MODE, mViewMode.ordinal()); } else { ed.putInt(PREF_BOOKMARK_VIEW_MODE, mViewMode.ordinal()); } ed.apply(); if (mBookmarksAdapter != null) { mBookmarksAdapter.switchViewMode(viewMode); } if (mViewMode == BookmarkViewMode.GRID) { if (mGridPage == null) { mGridPage = new GridView(this); if (mBookmarksAdapter != null) { mGridPage.setAdapter(mBookmarksAdapter); } mGridPage.setOnItemClickListener(mListener); mGridPage.setNumColumns(GridView.AUTO_FIT); mGridPage.setColumnWidth( BrowserActivity.getDesiredThumbnailWidth(this)); mGridPage.setFocusable(true); mGridPage.setFocusableInTouchMode(true); mGridPage.setSelector(android.R.drawable.gallery_thumb); float density = getResources().getDisplayMetrics().density; mGridPage.setVerticalSpacing((int) (14 * density)); mGridPage.setHorizontalSpacing((int) (8 * density)); mGridPage.setStretchMode(GridView.STRETCH_SPACING); mGridPage.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET); mGridPage.setDrawSelectorOnTop(true); if (mMostVisited) { mGridPage.setEmptyView(mEmptyView); } if (!mCreateShortcut) { mGridPage.setOnCreateContextMenuListener(this); } } addContentView(mGridPage, FULL_SCREEN_PARAMS); if (mVerticalList != null) { ViewGroup parent = (ViewGroup) mVerticalList.getParent(); if (parent != null) { parent.removeView(mVerticalList); } } } else { if (null == mVerticalList) { ListView listView = new ListView(this); if (mBookmarksAdapter != null) { listView.setAdapter(mBookmarksAdapter); } listView.setDrawSelectorOnTop(false); listView.setVerticalScrollBarEnabled(true); listView.setOnItemClickListener(mListener); if (mMostVisited) { listView.setEmptyView(mEmptyView); } if (!mCreateShortcut) { listView.setOnCreateContextMenuListener(this); } mVerticalList = listView; } addContentView(mVerticalList, FULL_SCREEN_PARAMS); if (mGridPage != null) { ViewGroup parent = (ViewGroup) mGridPage.getParent(); if (parent != null) { parent.removeView(mGridPage); } } } } private static final ViewGroup.LayoutParams FULL_SCREEN_PARAMS = new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); private static final int SAVE_CURRENT_PAGE = 1000; private static final int ADAPTER_CREATED = 1001; private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case SAVE_CURRENT_PAGE: saveCurrentPage(); break; case ADAPTER_CREATED: mBookmarksAdapter = (BrowserBookmarksAdapter) msg.obj; mBookmarksAdapter.switchViewMode(mViewMode); if (mGridPage != null) { mGridPage.setAdapter(mBookmarksAdapter); } if (mVerticalList != null) { mVerticalList.setAdapter(mBookmarksAdapter); } // Add our own listener in case there are favicons that // have yet to be loaded. if (mMostVisited) { IconListener listener = new IconListener() { public void onReceivedIcon(String url, Bitmap icon) { if (mGridPage != null) { mGridPage.setAdapter(mBookmarksAdapter); } if (mVerticalList != null) { mVerticalList.setAdapter(mBookmarksAdapter); } } }; CombinedBookmarkHistoryActivity.getIconListenerSet() .addListener(listener); } break; } } }; private AdapterView.OnItemClickListener mListener = new AdapterView.OnItemClickListener() { public void onItemClick(AdapterView parent, View v, int position, long id) { // It is possible that the view has been canceled when we get to // this point as back has a higher priority if (mCanceled) { android.util.Log.e(LOGTAG, "item clicked when dismissing"); return; } if (!mCreateShortcut) { if (0 == position && !mMostVisited) { // XXX: Work-around for a framework issue. mHandler.sendEmptyMessage(SAVE_CURRENT_PAGE); } else { loadUrl(position); } } else { final Intent intent = createShortcutIntent(position); setResultToParent(RESULT_OK, intent); finish(); } } }; private Intent createShortcutIntent(int position) { String url = getUrl(position); String title = getBookmarkTitle(position); Bitmap touchIcon = getTouchIcon(position); final Intent i = new Intent(); final Intent shortcutIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); long urlHash = url.hashCode(); long uniqueId = (urlHash << 32) | shortcutIntent.hashCode(); shortcutIntent.putExtra(Browser.EXTRA_APPLICATION_ID, Long.toString(uniqueId)); i.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); i.putExtra(Intent.EXTRA_SHORTCUT_NAME, title); // Use the apple-touch-icon if available if (touchIcon != null) { // Make a copy so we can modify the pixels. We can't use // createScaledBitmap or copy since they will preserve the config // and lose the ability to add alpha. Bitmap bm = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bm); Rect src = new Rect(0, 0, touchIcon.getWidth(), touchIcon.getHeight()); Rect dest = new Rect(0, 0, bm.getWidth(), bm.getHeight()); // Paint used for scaling the bitmap and drawing the rounded rect. Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setFilterBitmap(true); canvas.drawBitmap(touchIcon, src, dest, paint); // Construct a path from a round rect. This will allow drawing with // an inverse fill so we can punch a hole using the round rect. Path path = new Path(); path.setFillType(Path.FillType.INVERSE_WINDING); RectF rect = new RectF(0, 0, bm.getWidth(), bm.getHeight()); rect.inset(1, 1); path.addRoundRect(rect, 8f, 8f, Path.Direction.CW); // Reuse the paint and clear the outside of the rectangle. paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); canvas.drawPath(path, paint); i.putExtra(Intent.EXTRA_SHORTCUT_ICON, bm); } else { Bitmap favicon = getFavicon(position); if (favicon == null) { i.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext( BrowserBookmarksPage.this, R.drawable.ic_launcher_shortcut_browser_bookmark)); } else { Bitmap icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher_shortcut_browser_bookmark_icon); // Make a copy of the regular icon so we can modify the pixels. Bitmap copy = icon.copy(Bitmap.Config.ARGB_8888, true); Canvas canvas = new Canvas(copy); // Make a Paint for the white background rectangle and for // filtering the favicon. Paint p = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); p.setStyle(Paint.Style.FILL_AND_STROKE); p.setColor(Color.WHITE); final float density = getResources().getDisplayMetrics().density; // Create a rectangle that is slightly wider than the favicon final float iconSize = 16 * density; // 16x16 favicon final float padding = 2 * density; // white padding around icon final float rectSize = iconSize + 2 * padding; final Rect iconBounds = new Rect(0, 0, icon.getWidth(), icon.getHeight()); final float x = iconBounds.exactCenterX() - (rectSize / 2); // Note: Subtract 2 dip from the y position since the box is // slightly higher than center. Use padding since it is already // 2 * density. final float y = iconBounds.exactCenterY() - (rectSize / 2) - padding; RectF r = new RectF(x, y, x + rectSize, y + rectSize); // Draw a white rounded rectangle behind the favicon canvas.drawRoundRect(r, 2, 2, p); // Draw the favicon in the same rectangle as the rounded // rectangle but inset by the padding // (results in a 16x16 favicon). r.inset(padding, padding); canvas.drawBitmap(favicon, null, r, p); i.putExtra(Intent.EXTRA_SHORTCUT_ICON, copy); } } // Do not allow duplicate items i.putExtra("duplicate", false); return i; } private void saveCurrentPage() { Intent i = new Intent(BrowserBookmarksPage.this, AddBookmarkPage.class); i.putExtras(getIntent()); startActivityForResult(i, BOOKMARKS_SAVE); } private void loadUrl(int position) { Intent intent = (new Intent()).setAction(getUrl(position)); setResultToParent(RESULT_OK, intent); finish(); } @Override public boolean onCreateOptionsMenu(Menu menu) { boolean result = super.onCreateOptionsMenu(menu); if (!mCreateShortcut && !mMostVisited) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.bookmarks, menu); return true; } return result; } @Override public boolean onPrepareOptionsMenu(Menu menu) { boolean result = super.onPrepareOptionsMenu(menu); if (mCreateShortcut || mMostVisited || mBookmarksAdapter == null || mBookmarksAdapter.getCount() == 0) { // No need to show the menu if there are no items. return result; } MenuItem switchItem = menu.findItem(R.id.switch_mode_menu_id); int titleResId; int iconResId; if (mViewMode == BookmarkViewMode.GRID) { titleResId = R.string.switch_to_list; iconResId = R.drawable.ic_menu_list; } else { titleResId = R.string.switch_to_thumbnails; iconResId = R.drawable.ic_menu_thumbnail; } switchItem.setTitle(titleResId); switchItem.setIcon(iconResId); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.new_context_menu_id: saveCurrentPage(); break; case R.id.switch_mode_menu_id: if (mViewMode == BookmarkViewMode.GRID) { switchViewMode(BookmarkViewMode.LIST); } else { switchViewMode(BookmarkViewMode.GRID); } break; default: return super.onOptionsItemSelected(item); } return true; } private void openInNewWindow(int position) { Bundle b = new Bundle(); b.putBoolean("new_window", true); setResultToParent(RESULT_OK, (new Intent()).setAction(getUrl(position)).putExtras(b)); finish(); } private void editBookmark(int position) { Intent intent = new Intent(BrowserBookmarksPage.this, AddBookmarkPage.class); intent.putExtra("bookmark", getRow(position)); startActivityForResult(intent, BOOKMARKS_SAVE); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch(requestCode) { case BOOKMARKS_SAVE: if (resultCode == RESULT_OK) { Bundle extras; if (data != null && (extras = data.getExtras()) != null) { // If there are extras, then we need to save // the edited bookmark. This is done in updateRow() String title = extras.getString("title"); String url = extras.getString("url"); if (title != null && url != null) { mBookmarksAdapter.updateRow(extras); } } else { // extras == null then a new bookmark was added to // the database. refreshList(); } } break; default: break; } } private void displayRemoveBookmarkDialog(int position) { // Put up a dialog asking if the user really wants to // delete the bookmark final int deletePos = position; new AlertDialog.Builder(this) .setTitle(R.string.delete_bookmark) .setIcon(android.R.drawable.ic_dialog_alert) .setMessage(getText(R.string.delete_bookmark_warning).toString().replace( "%s", getBookmarkTitle(deletePos))) .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { deleteBookmark(deletePos); } }) .setNegativeButton(R.string.cancel, null) .show(); } /** * Refresh the shown list after the database has changed. */ private void refreshList() { if (mBookmarksAdapter == null) return; mBookmarksAdapter.refreshList(); } /** * Return a hashmap representing the currently highlighted row. */ public Bundle getRow(int position) { return mBookmarksAdapter == null ? null : mBookmarksAdapter.getRow(position); } /** * Return the url of the currently highlighted row. */ public String getUrl(int position) { return mBookmarksAdapter == null ? null : mBookmarksAdapter.getUrl(position); } /** * Return the favicon of the currently highlighted row. */ public Bitmap getFavicon(int position) { return mBookmarksAdapter == null ? null : mBookmarksAdapter.getFavicon(position); } private Bitmap getTouchIcon(int position) { return mBookmarksAdapter == null ? null : mBookmarksAdapter.getTouchIcon(position); } private void copy(CharSequence text) { try { IClipboard clip = IClipboard.Stub.asInterface(ServiceManager.getService("clipboard")); if (clip != null) { clip.setClipboardText(text); } } catch (android.os.RemoteException e) { Log.e(LOGTAG, "Copy failed", e); } } public String getBookmarkTitle(int position) { return mBookmarksAdapter == null ? null : mBookmarksAdapter.getTitle(position); } /** * Delete the currently highlighted row. */ public void deleteBookmark(int position) { if (mBookmarksAdapter == null) return; mBookmarksAdapter.deleteRow(position); } @Override public void onBackPressed() { setResultToParent(RESULT_CANCELED, null); mCanceled = true; super.onBackPressed(); } // This Activity is generally a sub-Activity of // CombinedBookmarkHistoryActivity. In that situation, we need to pass our // result code up to our parent. However, if someone calls this Activity // directly, then this has no parent, and it needs to set it on itself. private void setResultToParent(int resultCode, Intent data) { Activity parent = getParent(); if (parent == null) { setResult(resultCode, data); } else { ((CombinedBookmarkHistoryActivity) parent).setResultFromChild( resultCode, data); } } }