diff options
Diffstat (limited to 'src/com/android/browser')
72 files changed, 14836 insertions, 7797 deletions
diff --git a/src/com/android/browser/ActiveTabsPage.java b/src/com/android/browser/ActiveTabsPage.java index 2de778712..fb5ed3b4f 100644 --- a/src/com/android/browser/ActiveTabsPage.java +++ b/src/com/android/browser/ActiveTabsPage.java @@ -20,7 +20,7 @@ import android.content.Context; import android.graphics.Bitmap; import android.os.Handler; import android.util.AttributeSet; -import android.view.KeyEvent; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -32,16 +32,19 @@ import android.widget.ListView; import android.widget.TextView; public class ActiveTabsPage extends LinearLayout { - private final BrowserActivity mBrowserActivity; - private final LayoutInflater mFactory; - private final TabControl mControl; - private final TabsListAdapter mAdapter; - private final ListView mListView; - public ActiveTabsPage(BrowserActivity context, TabControl control) { + private static final String LOGTAG = "TabPicker"; + + private final LayoutInflater mFactory; + private final UiController mUiController; + private final TabControl mControl; + private final TabsListAdapter mAdapter; + private final ListView mListView; + + public ActiveTabsPage(Context context, UiController control) { super(context); - mBrowserActivity = context; - mControl = control; + mUiController = control; + mControl = control.getTabControl(); mFactory = LayoutInflater.from(context); mFactory.inflate(R.layout.active_tabs, this); mListView = (ListView) findViewById(R.id.list); @@ -51,21 +54,24 @@ public class ActiveTabsPage extends LinearLayout { public void onItemClick(AdapterView<?> parent, View view, int position, long id) { if (mControl.canCreateNewTab()) { - position--; + position -= 2; } boolean needToAttach = false; - if (position == -1) { + if (position == -2) { // Create a new tab - mBrowserActivity.openTabToHomePage(); + mUiController.openTabToHomePage(); + } else if (position == -1) { + // Create a new incognito tab + mUiController.openIncognitoTab(); } else { // Open the corresponding tab // If the tab is the current one, switchToTab will // do nothing and return, so we need to make sure // it gets attached back to its mContentView in // removeActiveTabPage - needToAttach = !mBrowserActivity.switchToTab(position); + needToAttach = !mUiController.switchToTab(position); } - mBrowserActivity.removeActiveTabPage(needToAttach); + mUiController.removeActiveTabsPage(needToAttach); } }); } @@ -98,7 +104,7 @@ public class ActiveTabsPage extends LinearLayout { public int getCount() { int count = mControl.getTabCount(); if (mControl.canCreateNewTab()) { - count++; + count += 2; } // XXX: This is a workaround to be more like a real adapter. Most // adapters call notifyDataSetChanged() whenever the internal data @@ -128,23 +134,28 @@ public class ActiveTabsPage extends LinearLayout { } public int getItemViewType(int position) { if (mControl.canCreateNewTab()) { - position--; + position -= 2; } // Do not recycle the "add new tab" item. - return position == -1 ? IGNORE_ITEM_VIEW_TYPE : 1; + return position < 0 ? IGNORE_ITEM_VIEW_TYPE : 1; } public View getView(int position, View convertView, ViewGroup parent) { final int tabCount = mControl.getTabCount(); if (mControl.canCreateNewTab()) { - position--; + position -= 2; } if (convertView == null) { - convertView = mFactory.inflate(position == -1 ? - R.layout.tab_view_add_tab : R.layout.tab_view, null); + if (position == -2) { + convertView = mFactory.inflate(R.layout.tab_view_add_tab, null); + } else if (position == -1) { + convertView = mFactory.inflate(R.layout.tab_view_add_incognito_tab, null); + } else { + convertView = mFactory.inflate(R.layout.tab_view, null); + } } - if (position != -1) { + if (position >= 0) { TextView title = (TextView) convertView.findViewById(R.id.title); TextView url = (TextView) convertView.findViewById(R.id.url); @@ -152,7 +163,19 @@ public class ActiveTabsPage extends LinearLayout { (ImageView) convertView.findViewById(R.id.favicon); View close = convertView.findViewById(R.id.close); Tab tab = mControl.getTab(position); + if (tab.getWebView() == null) { + // This means that populatePickerData will have to use the + // saved state. + Log.w(LOGTAG, "Tab " + position + " has a null WebView and " + + (tab.getSavedState() == null ? "null" : "non-null") + + " saved state "); + } tab.populatePickerData(); + if (tab.getTitle() == null || tab.getTitle().length() == 0) { + Log.w(LOGTAG, "Tab " + position + " has no title. " + + "Check above in the Logs to see whether it has a " + + "null WebView or null WebHistoryItem"); + } title.setText(tab.getTitle()); url.setText(tab.getUrl()); Bitmap icon = tab.getFavicon(); @@ -164,11 +187,11 @@ public class ActiveTabsPage extends LinearLayout { final int closePosition = position; close.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { - mBrowserActivity.closeTab( + mUiController.closeTab( mControl.getTab(closePosition)); if (tabCount == 1) { - mBrowserActivity.openTabToHomePage(); - mBrowserActivity.removeActiveTabPage(false); + mUiController.openTabToHomePage(); + mUiController.removeActiveTabsPage(false); } else { mNotified = true; notifyDataSetChanged(); diff --git a/src/com/android/browser/AddBookmarkPage.java b/src/com/android/browser/AddBookmarkPage.java index 594f985a7..de256a838 100644 --- a/src/com/android/browser/AddBookmarkPage.java +++ b/src/com/android/browser/AddBookmarkPage.java @@ -16,106 +16,616 @@ package com.android.browser; +import com.android.browser.provider.BrowserProvider2; + import android.app.Activity; +import android.app.LoaderManager; import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; import android.content.Context; -import android.content.Intent; +import android.content.CursorLoader; +import android.content.Loader; +import android.content.SharedPreferences; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; import android.net.ParseException; +import android.net.Uri; import android.net.WebAddress; +import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Message; -import android.provider.Browser; +import android.preference.PreferenceManager; +import android.provider.BrowserContract; +import android.text.TextUtils; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; import android.view.Window; +import android.view.WindowManager; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.CursorAdapter; import android.widget.EditText; +import android.widget.ListView; +import android.widget.PopupMenu; import android.widget.TextView; import android.widget.Toast; import java.net.URI; import java.net.URISyntaxException; -import java.util.Date; +import java.util.Stack; + +public class AddBookmarkPage extends Activity + implements View.OnClickListener, TextView.OnEditorActionListener, + AdapterView.OnItemClickListener, LoaderManager.LoaderCallbacks<Cursor>, + BreadCrumbView.Controller, PopupMenu.OnMenuItemClickListener { + + public static final long DEFAULT_FOLDER_ID = -1; + public static final String TOUCH_ICON_URL = "touch_icon_url"; + // Place on an edited bookmark to remove the saved thumbnail + public static final String REMOVE_THUMBNAIL = "remove_thumbnail"; + public static final String USER_AGENT = "user_agent"; -public class AddBookmarkPage extends Activity { + /* package */ static final String EXTRA_EDIT_BOOKMARK = "bookmark"; + /* package */ static final String EXTRA_IS_FOLDER = "is_folder"; + + private static final int MAX_CRUMBS_SHOWN = 2; private final String LOGTAG = "Bookmarks"; + // Set to true to see the crash on the code I would like to run. + private final boolean DEBUG_CRASH = false; + + // IDs for the CursorLoaders that are used. + private final int LOADER_ID_FOLDER_CONTENTS = 0; + private final int LOADER_ID_ALL_FOLDERS = 1; private EditText mTitle; private EditText mAddress; private TextView mButton; private View mCancelButton; private boolean mEditingExisting; + private boolean mEditingFolder; private Bundle mMap; private String mTouchIconUrl; - private Bitmap mThumbnail; private String mOriginalUrl; + private TextView mFolder; + private View mDefaultView; + private View mFolderSelector; + private EditText mFolderNamer; + private View mAddNewFolder; + private View mAddSeparator; + private long mCurrentFolder = 0; + private FolderAdapter mAdapter; + private BreadCrumbView mCrumbs; + private TextView mFakeTitle; + private View mCrumbHolder; + private ListView mListView; + private boolean mSaveToHomeScreen; + private long mRootFolder; + + private static class Folder { + String Name; + long Id; + Folder(String name, long id) { + Name = name; + Id = id; + } + } // Message IDs private static final int SAVE_BOOKMARK = 100; + private static final int TOUCH_ICON_DOWNLOADED = 101; private Handler mHandler; - private View.OnClickListener mSaveBookmark = new View.OnClickListener() { - public void onClick(View v) { - if (save()) { + private InputMethodManager getInputMethodManager() { + return (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); + } + + @Override + public void onTop(int level, Object data) { + if (null == data) return; + Folder folderData = (Folder) data; + long folder = folderData.Id; + Uri uri = BrowserContract.Bookmarks.buildFolderUri(folder); + LoaderManager manager = getLoaderManager(); + CursorLoader loader = (CursorLoader) ((Loader) manager.getLoader( + LOADER_ID_FOLDER_CONTENTS)); + loader.setUri(uri); + loader.forceLoad(); + updateVisible(); + if (mFolderNamer.getVisibility() == View.VISIBLE) { + completeOrCancelFolderNaming(true); + } + } + + /** + * Update the views shown to only show the two deepest levels of crumbs. + * Note that this method depends on internal knowledge of BreadCrumbView. + */ + private void updateVisible() { + if (MAX_CRUMBS_SHOWN > 0) { + int invisibleCrumbs = mCrumbs.size() - MAX_CRUMBS_SHOWN; + // This class always uses a back button, which is the first child. + int childIndex = 1; + if (invisibleCrumbs > 0) { + int crumbIndex = 0; + while (crumbIndex < invisibleCrumbs) { + // Set the crumb to GONE. + mCrumbs.getChildAt(childIndex).setVisibility(View.GONE); + childIndex++; + // Each crumb is followed by a separator (except the last + // one). Also make it GONE + mCrumbs.getChildAt(childIndex).setVisibility(View.GONE); + childIndex++; + // Move to the next crumb. + crumbIndex++; + } + } + // Make sure the last two are visible. + int childCount = mCrumbs.getChildCount(); + while (childIndex < childCount) { + mCrumbs.getChildAt(childIndex).setVisibility(View.VISIBLE); + childIndex++; + } + } + } + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (v == mFolderNamer) { + if (v.getText().length() > 0) { + if (actionId == EditorInfo.IME_NULL) { + // Only want to do this once. + if (event.getAction() == KeyEvent.ACTION_UP) { + completeOrCancelFolderNaming(false); + } + } + } + // Steal the key press; otherwise a newline will be added + return true; + } + return false; + } + + private void switchToDefaultView(boolean changedFolder) { + mFolderSelector.setVisibility(View.GONE); + mDefaultView.setVisibility(View.VISIBLE); + mCrumbHolder.setVisibility(View.GONE); + mFakeTitle.setVisibility(View.VISIBLE); + if (changedFolder) { + Object data = mCrumbs.getTopData(); + if (data != null) { + Folder folder = (Folder) data; + mCurrentFolder = folder.Id; + int resource = mCurrentFolder == mRootFolder ? + R.drawable.ic_menu_bookmarks : + com.android.internal.R.drawable.ic_menu_archive; + Drawable drawable = getResources().getDrawable(resource); + updateFolderLabel(folder.Name, drawable); + } + } + } + + @Override + public void onClick(View v) { + if (v == mButton) { + if (mFolderSelector.getVisibility() == View.VISIBLE) { + // We are showing the folder selector. + if (mFolderNamer.getVisibility() == View.VISIBLE) { + completeOrCancelFolderNaming(false); + } else { + // User has selected a folder. Go back to the opening page + mSaveToHomeScreen = false; + switchToDefaultView(true); + } + } else if (save()) { + finish(); + } + } else if (v == mCancelButton) { + if (mFolderNamer.getVisibility() == View.VISIBLE) { + completeOrCancelFolderNaming(true); + } else if (mFolderSelector.getVisibility() == View.VISIBLE) { + switchToDefaultView(false); + } else { finish(); } + } else if (v == mFolder) { + PopupMenu popup = new PopupMenu(this, mFolder); + popup.getMenuInflater().inflate(R.menu.folder_choice, + popup.getMenu()); + if (mEditingFolder) { + popup.getMenu().removeItem(R.id.home_screen); + } + popup.setOnMenuItemClickListener(this); + popup.show(); + } else if (v == mAddNewFolder) { + mFolderNamer.setVisibility(View.VISIBLE); + mFolderNamer.setText(R.string.new_folder); + mFolderNamer.requestFocus(); + updateList(); + mAddNewFolder.setVisibility(View.GONE); + mAddSeparator.setVisibility(View.GONE); + getInputMethodManager().showSoftInput(mFolderNamer, + InputMethodManager.SHOW_IMPLICIT); } - }; + } - private View.OnClickListener mCancel = new View.OnClickListener() { - public void onClick(View v) { - finish(); + @Override + public boolean onMenuItemClick(MenuItem item) { + switch(item.getItemId()) { + case R.id.bookmarks: + mCurrentFolder = mRootFolder; + updateFolderLabel(item.getTitle(), item.getIcon()); + mSaveToHomeScreen = false; + break; + case R.id.home_screen: + // Create a short cut to the home screen + mSaveToHomeScreen = true; + updateFolderLabel(item.getTitle(), item.getIcon()); + break; + case R.id.other: + switchToFolderSelector(); + break; + default: + return false; } - }; + return true; + } + + // Refresh the ListView to hide or show the empty view, as necessary. + // Should be called after mFolderNamer is shown or hidden. + private void updateList() { + if (mAdapter.getCount() == 0) { + // XXX: Is there a better way to refresh the ListView? + mListView.setAdapter(mAdapter); + } + } + + private void completeOrCancelFolderNaming(boolean cancel) { + if (!cancel && !TextUtils.isEmpty(mFolderNamer.getText())) { + String name = mFolderNamer.getText().toString(); + long id = addFolderToCurrent(mFolderNamer.getText().toString()); + descendInto(name, id); + } + mFolderNamer.setVisibility(View.GONE); + mAddNewFolder.setVisibility(View.VISIBLE); + mAddSeparator.setVisibility(View.VISIBLE); + getInputMethodManager().hideSoftInputFromWindow( + mFolderNamer.getWindowToken(), 0); + updateList(); + } + + private long addFolderToCurrent(String name) { + // Add the folder to the database + ContentValues values = new ContentValues(); + values.put(BrowserContract.Bookmarks.TITLE, + name); + values.put(BrowserContract.Bookmarks.IS_FOLDER, 1); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + String accountType = prefs.getString(BrowserBookmarksPage.PREF_ACCOUNT_TYPE, null); + String accountName = prefs.getString(BrowserBookmarksPage.PREF_ACCOUNT_NAME, null); + if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { + values.put(BrowserContract.Bookmarks.ACCOUNT_TYPE, accountType); + values.put(BrowserContract.Bookmarks.ACCOUNT_NAME, accountName); + } + long currentFolder; + Object data = mCrumbs.getTopData(); + if (data != null) { + currentFolder = ((Folder) data).Id; + } else { + currentFolder = mRootFolder; + } + values.put(BrowserContract.Bookmarks.PARENT, currentFolder); + Uri uri = getContentResolver().insert( + BrowserContract.Bookmarks.CONTENT_URI, values); + if (uri != null) { + return ContentUris.parseId(uri); + } else { + return -1; + } + } + + private void switchToFolderSelector() { + mDefaultView.setVisibility(View.GONE); + mFolderSelector.setVisibility(View.VISIBLE); + mCrumbHolder.setVisibility(View.VISIBLE); + mFakeTitle.setVisibility(View.GONE); + mAddNewFolder.setVisibility(View.VISIBLE); + mAddSeparator.setVisibility(View.VISIBLE); + } + + private void descendInto(String foldername, long id) { + if (id != DEFAULT_FOLDER_ID) { + mCrumbs.pushView(foldername, new Folder(foldername, id)); + mCrumbs.notifyController(); + } + } + + @Override + public Loader<Cursor> onCreateLoader(int id, Bundle args) { + String[] projection; + switch (id) { + case LOADER_ID_ALL_FOLDERS: + projection = new String[] { + BrowserContract.Bookmarks._ID, + BrowserContract.Bookmarks.PARENT, + BrowserContract.Bookmarks.TITLE, + BrowserContract.Bookmarks.IS_FOLDER + }; + return new CursorLoader(this, + BrowserContract.Bookmarks.CONTENT_URI, + projection, + BrowserContract.Bookmarks.IS_FOLDER + " != 0", + null, + null); + case LOADER_ID_FOLDER_CONTENTS: + projection = new String[] { + BrowserContract.Bookmarks._ID, + BrowserContract.Bookmarks.TITLE, + BrowserContract.Bookmarks.IS_FOLDER + }; + return new CursorLoader(this, + BrowserContract.Bookmarks.buildFolderUri( + mCurrentFolder), + projection, + BrowserContract.Bookmarks.IS_FOLDER + " != 0", + null, + null); + default: + throw new AssertionError("Asking for nonexistant loader!"); + } + } + + @Override + public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { + switch (loader.getId()) { + case LOADER_ID_FOLDER_CONTENTS: + mAdapter.changeCursor(cursor); + break; + case LOADER_ID_ALL_FOLDERS: + long parent = mCurrentFolder; + int idIndex = cursor.getColumnIndexOrThrow( + BrowserContract.Bookmarks._ID); + int titleIndex = cursor.getColumnIndexOrThrow( + BrowserContract.Bookmarks.TITLE); + int parentIndex = cursor.getColumnIndexOrThrow( + BrowserContract.Bookmarks.PARENT); + Stack folderStack = new Stack(); + while ((parent != BrowserProvider2.FIXED_ID_ROOT) && + (parent != 0)) { + // First, find the folder corresponding to the current + // folder + if (!cursor.moveToFirst()) { + throw new AssertionError("No folders in the database!"); + } + long folder; + do { + folder = cursor.getLong(idIndex); + } while (folder != parent && cursor.moveToNext()); + if (cursor.isAfterLast()) { + throw new AssertionError("Folder(id=" + parent + + ") holding this bookmark does not exist!"); + } + String name = cursor.getString(titleIndex); + if (parent == mCurrentFolder) { + Drawable draw = getResources().getDrawable( + com.android.internal.R.drawable.ic_menu_archive); + updateFolderLabel(name, draw); + } + folderStack.push(new Folder(name, parent)); + parent = cursor.getLong(parentIndex); + } + while (!folderStack.isEmpty()) { + Folder thisFolder = (Folder) folderStack.pop(); + mCrumbs.pushView(thisFolder.Name, thisFolder); + } + getLoaderManager().stopLoader(LOADER_ID_ALL_FOLDERS); + updateVisible(); + break; + default: + break; + } + } + + /** + * Update the name and image to show where the bookmark will be added + * @param name Name of the location to save (folder name, bookmarks, or home + * screen. + * @param drawable Image to show corresponding to the save location. + */ + void updateFolderLabel(CharSequence name, Drawable drawable) { + mFolder.setText(name); + mFolder.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, + null); + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, + long id) { + TextView tv = (TextView) view.findViewById(android.R.id.text1); + // Switch to the folder that was clicked on. + descendInto(tv.getText().toString(), id); + } + + /** + * Shows a list of names of folders. + */ + private class FolderAdapter extends CursorAdapter { + public FolderAdapter(Context context) { + super(context, null); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + ((TextView) view.findViewById(android.R.id.text1)).setText( + cursor.getString(cursor.getColumnIndexOrThrow( + BrowserContract.Bookmarks.TITLE))); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + View view = LayoutInflater.from(context).inflate( + R.layout.folder_list_item, null); + view.setBackgroundDrawable(context.getResources(). + getDrawable(android.R.drawable.list_selector_background)); + return view; + } + + @Override + public boolean isEmpty() { + // Do not show the empty view if the user is creating a new folder. + return super.isEmpty() && mFolderNamer.getVisibility() == View.GONE; + } + } protected void onCreate(Bundle icicle) { super.onCreate(icicle); - requestWindowFeature(Window.FEATURE_LEFT_ICON); + if (DEBUG_CRASH) { + requestWindowFeature(Window.FEATURE_NO_TITLE); + } + + mMap = getIntent().getExtras(); + setContentView(R.layout.browser_add_bookmark); - setTitle(R.string.save_to_bookmarks); - getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON, R.drawable.ic_list_bookmark); - + + Window window = getWindow(); + if (!DEBUG_CRASH) { + setTitle(""); + } + String title = null; String url = null; - mMap = getIntent().getExtras(); + + mFakeTitle = (TextView) findViewById(R.id.fake_title); + if (mMap != null) { - Bundle b = mMap.getBundle("bookmark"); + Bundle b = mMap.getBundle(EXTRA_EDIT_BOOKMARK); if (b != null) { + mEditingFolder = mMap.getBoolean(EXTRA_IS_FOLDER, false); mMap = b; mEditingExisting = true; - setTitle(R.string.edit_bookmark); + mFakeTitle.setText(R.string.edit_bookmark); + if (mEditingFolder) { + findViewById(R.id.row_address).setVisibility(View.GONE); + } + } else { + int gravity = mMap.getInt("gravity", -1); + if (gravity != -1) { + WindowManager.LayoutParams l = window.getAttributes(); + l.gravity = gravity; + window.setAttributes(l); + } } - title = mMap.getString("title"); - url = mOriginalUrl = mMap.getString("url"); - mTouchIconUrl = mMap.getString("touch_icon_url"); - mThumbnail = (Bitmap) mMap.getParcelable("thumbnail"); + title = mMap.getString(BrowserContract.Bookmarks.TITLE); + url = mOriginalUrl = mMap.getString(BrowserContract.Bookmarks.URL); + mTouchIconUrl = mMap.getString(TOUCH_ICON_URL); + mCurrentFolder = mMap.getLong(BrowserContract.Bookmarks.PARENT, DEFAULT_FOLDER_ID); + } + mRootFolder = getBookmarksBarId(this); + if (mCurrentFolder == DEFAULT_FOLDER_ID) { + mCurrentFolder = mRootFolder; } mTitle = (EditText) findViewById(R.id.title); mTitle.setText(title); + mAddress = (EditText) findViewById(R.id.address); mAddress.setText(url); - View.OnClickListener accept = mSaveBookmark; mButton = (TextView) findViewById(R.id.OK); - mButton.setOnClickListener(accept); + mButton.setOnClickListener(this); mCancelButton = findViewById(R.id.cancel); - mCancelButton.setOnClickListener(mCancel); - - if (!getWindow().getDecorView().isInTouchMode()) { + mCancelButton.setOnClickListener(this); + + mFolder = (TextView) findViewById(R.id.folder); + mFolder.setOnClickListener(this); + + mDefaultView = findViewById(R.id.default_view); + mFolderSelector = findViewById(R.id.folder_selector); + + mFolderNamer = (EditText) findViewById(R.id.folder_namer); + mFolderNamer.setOnEditorActionListener(this); + + mAddNewFolder = findViewById(R.id.add_new_folder); + mAddNewFolder.setOnClickListener(this); + mAddSeparator = findViewById(R.id.add_divider); + + mCrumbs = (BreadCrumbView) findViewById(R.id.crumbs); + mCrumbs.setUseBackButton(true); + mCrumbs.setController(this); + String name = getString(R.string.bookmarks); + mCrumbs.pushView(name, false, + new Folder(name, BrowserProvider2.FIXED_ID_ROOT)); + mCrumbHolder = findViewById(R.id.crumb_holder); + + mAdapter = new FolderAdapter(this); + mListView = (ListView) findViewById(R.id.list); + View empty = findViewById(R.id.empty); + mListView.setEmptyView(empty); + mListView.setAdapter(mAdapter); + mListView.setOnItemClickListener(this); + LoaderManager manager = getLoaderManager(); + if (mCurrentFolder != BrowserProvider2.FIXED_ID_ROOT) { + // Find all the folders + manager.initLoader(LOADER_ID_ALL_FOLDERS, null, this); + } + // Find the contents of the current folder + manager.initLoader(LOADER_ID_FOLDER_CONTENTS, null, this); + + + if (!window.getDecorView().isInTouchMode()) { mButton.requestFocus(); } } + // FIXME: Use a CursorLoader + private long getBookmarksBarId(Context context) { + SharedPreferences prefs + = PreferenceManager.getDefaultSharedPreferences(context); + String accountName = + prefs.getString(BrowserBookmarksPage.PREF_ACCOUNT_NAME, null); + String accountType = + prefs.getString(BrowserBookmarksPage.PREF_ACCOUNT_TYPE, null); + if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) { + return BrowserProvider2.FIXED_ID_ROOT; + } + Cursor cursor = null; + try { + cursor = context.getContentResolver().query( + BrowserContract.Bookmarks.CONTENT_URI, + new String[] { BrowserContract.Bookmarks._ID }, + BrowserContract.ChromeSyncColumns.SERVER_UNIQUE + "=? AND " + + BrowserContract.Bookmarks.ACCOUNT_NAME + "=? AND " + + BrowserContract.Bookmarks.ACCOUNT_TYPE + "=?", + new String[] { + BrowserContract.ChromeSyncColumns + .FOLDER_NAME_BOOKMARKS_BAR, + accountName, + accountType }, + null); + if (cursor != null && cursor.moveToFirst()) { + return cursor.getLong(0); + } + } finally { + if (cursor != null) cursor.close(); + } + return BrowserProvider2.FIXED_ID_ROOT; + } + /** * Runnable to save a bookmark, so it can be performed in its own thread. */ private class SaveBookmarkRunnable implements Runnable { + // FIXME: This should be an async task. private Message mMessage; private Context mContext; public SaveBookmarkRunnable(Context ctx, Message msg) { @@ -125,18 +635,18 @@ public class AddBookmarkPage extends Activity { public void run() { // Unbundle bookmark data. Bundle bundle = mMessage.getData(); - String title = bundle.getString("title"); - String url = bundle.getString("url"); - boolean invalidateThumbnail = bundle.getBoolean( - "invalidateThumbnail"); + String title = bundle.getString(BrowserContract.Bookmarks.TITLE); + String url = bundle.getString(BrowserContract.Bookmarks.URL); + boolean invalidateThumbnail = bundle.getBoolean(REMOVE_THUMBNAIL); Bitmap thumbnail = invalidateThumbnail ? null - : (Bitmap) bundle.getParcelable("thumbnail"); - String touchIconUrl = bundle.getString("touchIconUrl"); + : (Bitmap) bundle.getParcelable(BrowserContract.Bookmarks.THUMBNAIL); + String touchIconUrl = bundle.getString(TOUCH_ICON_URL); // Save to the bookmarks DB. try { final ContentResolver cr = getContentResolver(); - Bookmarks.addBookmark(null, cr, url, title, thumbnail, true); + Bookmarks.addBookmark(AddBookmarkPage.this, false, url, + title, thumbnail, true, mCurrentFolder); if (touchIconUrl != null) { new DownloadTouchIcon(mContext, cr, url).execute(mTouchIconUrl); } @@ -148,6 +658,28 @@ public class AddBookmarkPage extends Activity { } } + private static class UpdateBookmarkTask extends AsyncTask<ContentValues, Void, Void> { + Context mContext; + Long mId; + + public UpdateBookmarkTask(Context context, long id) { + mContext = context; + mId = id; + } + + @Override + protected Void doInBackground(ContentValues... params) { + if (params.length != 1) { + throw new IllegalArgumentException("No ContentValues provided!"); + } + Uri uri = ContentUris.withAppendedId(BookmarkUtils.getBookmarksUri(mContext), mId); + mContext.getContentResolver().update( + uri, + params[0], null, null); + return null; + } + } + private void createHandler() { if (mHandler == null) { mHandler = new Handler() { @@ -163,6 +695,15 @@ public class AddBookmarkPage extends Activity { Toast.LENGTH_LONG).show(); } break; + case TOUCH_ICON_DOWNLOADED: + Bundle b = msg.getData(); + sendBroadcast(BookmarkUtils.createAddToHomeIntent( + AddBookmarkPage.this, + b.getString(BrowserContract.Bookmarks.URL), + b.getString(BrowserContract.Bookmarks.TITLE), + (Bitmap) b.getParcelable(BrowserContract.Bookmarks.TOUCH_ICON), + (Bitmap) b.getParcelable(BrowserContract.Bookmarks.FAVICON))); + break; } } }; @@ -176,12 +717,13 @@ public class AddBookmarkPage extends Activity { createHandler(); String title = mTitle.getText().toString().trim(); - String unfilteredUrl = - BrowserActivity.fixUrl(mAddress.getText().toString()); + String unfilteredUrl; + unfilteredUrl = UrlUtils.fixUrl(mAddress.getText().toString()); + boolean emptyTitle = title.length() == 0; boolean emptyUrl = unfilteredUrl.trim().length() == 0; Resources r = getResources(); - if (emptyTitle || emptyUrl) { + if (emptyTitle || (emptyUrl && !mEditingFolder)) { if (emptyTitle) { mTitle.setError(r.getText(R.string.bookmark_needs_title)); } @@ -189,59 +731,105 @@ public class AddBookmarkPage extends Activity { mAddress.setError(r.getText(R.string.bookmark_needs_url)); } return false; + } String url = unfilteredUrl.trim(); - try { - // We allow bookmarks with a javascript: scheme, but these will in most cases - // fail URI parsing, so don't try it if that's the kind of bookmark we have. - - if (!url.toLowerCase().startsWith("javascript:")) { - URI uriObj = new URI(url); - String scheme = uriObj.getScheme(); - if (!Bookmarks.urlHasAcceptableScheme(url)) { - // If the scheme was non-null, let the user know that we - // can't save their bookmark. If it was null, we'll assume - // they meant http when we parse it in the WebAddress class. - if (scheme != null) { - mAddress.setError(r.getText(R.string.bookmark_cannot_save_url)); - return false; - } - WebAddress address; - try { - address = new WebAddress(unfilteredUrl); - } catch (ParseException e) { - throw new URISyntaxException("", ""); - } - if (address.mHost.length() == 0) { - throw new URISyntaxException("", ""); + if (!mEditingFolder) { + try { + // We allow bookmarks with a javascript: scheme, but these will in most cases + // fail URI parsing, so don't try it if that's the kind of bookmark we have. + + if (!url.toLowerCase().startsWith("javascript:")) { + URI uriObj = new URI(url); + String scheme = uriObj.getScheme(); + if (!Bookmarks.urlHasAcceptableScheme(url)) { + // If the scheme was non-null, let the user know that we + // can't save their bookmark. If it was null, we'll assume + // they meant http when we parse it in the WebAddress class. + if (scheme != null) { + mAddress.setError(r.getText(R.string.bookmark_cannot_save_url)); + return false; + } + WebAddress address; + try { + address = new WebAddress(unfilteredUrl); + } catch (ParseException e) { + throw new URISyntaxException("", ""); + } + if (address.getHost().length() == 0) { + throw new URISyntaxException("", ""); + } + url = address.toString(); } - url = address.toString(); } + } catch (URISyntaxException e) { + mAddress.setError(r.getText(R.string.bookmark_url_not_valid)); + return false; } - } catch (URISyntaxException e) { - mAddress.setError(r.getText(R.string.bookmark_url_not_valid)); - return false; } + if (mSaveToHomeScreen) { + mEditingExisting = false; + } + + boolean urlUnmodified = url.equals(mOriginalUrl); + if (mEditingExisting) { - mMap.putString("title", title); - mMap.putString("url", url); - mMap.putBoolean("invalidateThumbnail", !url.equals(mOriginalUrl)); - setResult(RESULT_OK, (new Intent()).setAction( - getIntent().toString()).putExtras(mMap)); + Long id = mMap.getLong(BrowserContract.Bookmarks._ID); + ContentValues values = new ContentValues(); + values.put(BrowserContract.Bookmarks.TITLE, title); + values.put(BrowserContract.Bookmarks.PARENT, mCurrentFolder); + if (!mEditingFolder) { + values.put(BrowserContract.Bookmarks.URL, url); + if (!urlUnmodified) { + values.putNull(BrowserContract.Bookmarks.THUMBNAIL); + } + } + if (values.size() > 0) { + new UpdateBookmarkTask(getApplicationContext(), id).execute(values); + } + setResult(RESULT_OK); } else { - // Post a message to write to the DB. + Bitmap thumbnail; + Bitmap favicon; + if (urlUnmodified) { + thumbnail = (Bitmap) mMap.getParcelable( + BrowserContract.Bookmarks.THUMBNAIL); + favicon = (Bitmap) mMap.getParcelable( + BrowserContract.Bookmarks.FAVICON); + } else { + thumbnail = null; + favicon = null; + } + Bundle bundle = new Bundle(); - bundle.putString("title", title); - bundle.putString("url", url); - bundle.putParcelable("thumbnail", mThumbnail); - bundle.putBoolean("invalidateThumbnail", !url.equals(mOriginalUrl)); - bundle.putString("touchIconUrl", mTouchIconUrl); - Message msg = Message.obtain(mHandler, SAVE_BOOKMARK); - msg.setData(bundle); - // Start a new thread so as to not slow down the UI - Thread t = new Thread(new SaveBookmarkRunnable(getApplicationContext(), msg)); - t.start(); + bundle.putString(BrowserContract.Bookmarks.TITLE, title); + bundle.putString(BrowserContract.Bookmarks.URL, url); + bundle.putParcelable(BrowserContract.Bookmarks.FAVICON, favicon); + + if (mSaveToHomeScreen) { + if (mTouchIconUrl != null && urlUnmodified) { + Message msg = Message.obtain(mHandler, + TOUCH_ICON_DOWNLOADED); + msg.setData(bundle); + DownloadTouchIcon icon = new DownloadTouchIcon(this, msg, + mMap.getString(USER_AGENT)); + icon.execute(mTouchIconUrl); + } else { + sendBroadcast(BookmarkUtils.createAddToHomeIntent(this, url, + title, null /*touchIcon*/, favicon)); + } + } else { + bundle.putParcelable(BrowserContract.Bookmarks.THUMBNAIL, thumbnail); + bundle.putBoolean(REMOVE_THUMBNAIL, !urlUnmodified); + bundle.putString(TOUCH_ICON_URL, mTouchIconUrl); + // Post a message to write to the DB. + Message msg = Message.obtain(mHandler, SAVE_BOOKMARK); + msg.setData(bundle); + // Start a new thread so as to not slow down the UI + Thread t = new Thread(new SaveBookmarkRunnable(getApplicationContext(), msg)); + t.start(); + } setResult(RESULT_OK); LogTag.logBookmarkAdded(url, "bookmarkview"); } diff --git a/src/com/android/browser/AutoFillProfileDatabase.java b/src/com/android/browser/AutoFillProfileDatabase.java new file mode 100644 index 000000000..3345e9258 --- /dev/null +++ b/src/com/android/browser/AutoFillProfileDatabase.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2010 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.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.provider.BaseColumns; +import android.util.Log; +import android.webkit.WebSettings.AutoFillProfile; + +public class AutoFillProfileDatabase { + + static final String LOGTAG = "AutoFillProfileDatabase"; + + static final String DATABASE_NAME = "autofill.db"; + static final int DATABASE_VERSION = 2; + static final String PROFILES_TABLE_NAME = "profiles"; + private AutoFillProfileDatabaseHelper mOpenHelper; + private static AutoFillProfileDatabase sInstance; + + public static final class Profiles implements BaseColumns { + private Profiles() { } + + static final String FULL_NAME = "fullname"; + static final String EMAIL_ADDRESS = "email"; + static final String COMPANY_NAME = "companyname"; + static final String ADDRESS_LINE_1 = "addressline1"; + static final String ADDRESS_LINE_2 = "addressline2"; + static final String CITY = "city"; + static final String STATE = "state"; + static final String ZIP_CODE = "zipcode"; + static final String COUNTRY = "country"; + static final String PHONE_NUMBER = "phone"; + } + + private static class AutoFillProfileDatabaseHelper extends SQLiteOpenHelper { + AutoFillProfileDatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + PROFILES_TABLE_NAME + " (" + + Profiles._ID + " INTEGER PRIMARY KEY," + + Profiles.FULL_NAME + " TEXT," + + Profiles.EMAIL_ADDRESS + " TEXT," + + Profiles.COMPANY_NAME + " TEXT," + + Profiles.ADDRESS_LINE_1 + " TEXT," + + Profiles.ADDRESS_LINE_2 + " TEXT," + + Profiles.CITY + " TEXT," + + Profiles.STATE + " TEXT," + + Profiles.ZIP_CODE + " TEXT," + + Profiles.COUNTRY + " TEXT," + + Profiles.PHONE_NUMBER + " TEXT" + + " );"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.w(LOGTAG, "Upgrading database from version " + oldVersion + " to " + + newVersion + ", which will destroy all old data"); + db.execSQL("DROP TABLE IF EXISTS " + PROFILES_TABLE_NAME); + onCreate(db); + } + } + + private AutoFillProfileDatabase(Context context) { + mOpenHelper = new AutoFillProfileDatabaseHelper(context); + } + + public static AutoFillProfileDatabase getInstance(Context context) { + if (sInstance == null) { + sInstance = new AutoFillProfileDatabase(context); + } + return sInstance; + } + + private SQLiteDatabase getDatabase(boolean writable) { + return writable ? mOpenHelper.getWritableDatabase() : mOpenHelper.getReadableDatabase(); + } + + public void addOrUpdateProfile(final int id, AutoFillProfile profile) { + final String sql = "INSERT OR REPLACE INTO " + PROFILES_TABLE_NAME + " (" + + Profiles._ID + "," + + Profiles.FULL_NAME + "," + + Profiles.EMAIL_ADDRESS + "," + + Profiles.COMPANY_NAME + "," + + Profiles.ADDRESS_LINE_1 + "," + + Profiles.ADDRESS_LINE_2 + "," + + Profiles.CITY + "," + + Profiles.STATE + "," + + Profiles.ZIP_CODE + "," + + Profiles.COUNTRY + "," + + Profiles.PHONE_NUMBER + + ") VALUES (?,?,?,?,?,?,?,?,?,?,?);"; + final Object[] params = { id, + profile.getFullName(), + profile.getEmailAddress(), + profile.getCompanyName(), + profile.getAddressLine1(), + profile.getAddressLine2(), + profile.getCity(), + profile.getState(), + profile.getZipCode(), + profile.getCountry(), + profile.getPhoneNumber() }; + getDatabase(true).execSQL(sql, params); + } + + public Cursor getProfile(int id) { + final String[] cols = { + Profiles.FULL_NAME, + Profiles.EMAIL_ADDRESS, + Profiles.COMPANY_NAME, + Profiles.ADDRESS_LINE_1, + Profiles.ADDRESS_LINE_2, + Profiles.CITY, + Profiles.STATE, + Profiles.ZIP_CODE, + Profiles.COUNTRY, + Profiles.PHONE_NUMBER + }; + + final String[] selectArgs = { Integer.toString(id) }; + return getDatabase(false).query(PROFILES_TABLE_NAME, cols, Profiles._ID + "=?", selectArgs, + null, null, null, "1"); + } + + public void dropProfile(int id) { + final String sql = "DELETE FROM " + PROFILES_TABLE_NAME +" WHERE " + Profiles._ID + " = ?;"; + final Object[] params = { id }; + getDatabase(true).execSQL(sql, params); + } + + public void close() { + mOpenHelper.close(); + } +} diff --git a/src/com/android/browser/AutoFillSettingsFragment.java b/src/com/android/browser/AutoFillSettingsFragment.java new file mode 100644 index 000000000..06a425638 --- /dev/null +++ b/src/com/android/browser/AutoFillSettingsFragment.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2010 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.Fragment; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.View.OnClickListener; +import android.view.LayoutInflater; +import android.webkit.WebSettings.AutoFillProfile; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +public class AutoFillSettingsFragment extends Fragment { + + private static final String LOGTAG = "AutoFillSettingsFragment"; + + private EditText mFullNameEdit; + private EditText mEmailEdit; + private EditText mCompanyEdit; + private EditText mAddressLine1Edit; + private EditText mAddressLine2Edit; + private EditText mCityEdit; + private EditText mStateEdit; + private EditText mZipEdit; + private EditText mCountryEdit; + private EditText mPhoneEdit; + + // Used to display toast after DB interactions complete. + private Handler mHandler; + + private final static int PROFILE_SAVED_MSG = 100; + private final static int PROFILE_DELETED_MSG = 101; + + // For now we support just one profile so it's safe to hardcode the + // id to 1 here. In the future this unique identifier will be set + // dynamically. + private int mUniqueId = 1; + + public AutoFillSettingsFragment() { + mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case PROFILE_SAVED_MSG: + Toast.makeText(getActivity(), R.string.autofill_profile_successful_save, + Toast.LENGTH_SHORT).show(); + break; + + case PROFILE_DELETED_MSG: + Toast.makeText(getActivity(), R.string.autofill_profile_successful_delete, + Toast.LENGTH_SHORT).show(); + break; + } + } + }; + } + + @Override + public void onCreate(Bundle savedState) { + super.onCreate(savedState); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.autofill_settings_fragment, container, false); + + mFullNameEdit = (EditText)v.findViewById(R.id.autofill_profile_editor_name_edit); + mEmailEdit = (EditText)v.findViewById(R.id.autofill_profile_editor_email_address_edit); + mCompanyEdit = (EditText)v.findViewById(R.id.autofill_profile_editor_company_name_edit); + mAddressLine1Edit = (EditText)v.findViewById( + R.id.autofill_profile_editor_address_line_1_edit); + mAddressLine2Edit = (EditText)v.findViewById( + R.id.autofill_profile_editor_address_line_2_edit); + mCityEdit = (EditText)v.findViewById(R.id.autofill_profile_editor_city_edit); + mStateEdit = (EditText)v.findViewById(R.id.autofill_profile_editor_state_edit); + mZipEdit = (EditText)v.findViewById(R.id.autofill_profile_editor_zip_code_edit); + mCountryEdit = (EditText)v.findViewById(R.id.autofill_profile_editor_country_edit); + mPhoneEdit = (EditText)v.findViewById(R.id.autofill_profile_editor_phone_number_edit); + + Button saveButton = (Button)v.findViewById(R.id.autofill_profile_editor_save_button); + saveButton.setOnClickListener(new OnClickListener() { + public void onClick(View button) { + AutoFillProfile newProfile = new AutoFillProfile( + mUniqueId, + mFullNameEdit.getText().toString(), + mEmailEdit.getText().toString(), + mCompanyEdit.getText().toString(), + mAddressLine1Edit.getText().toString(), + mAddressLine2Edit.getText().toString(), + mCityEdit.getText().toString(), + mStateEdit.getText().toString(), + mZipEdit.getText().toString(), + mCountryEdit.getText().toString(), + mPhoneEdit.getText().toString()); + + BrowserSettings.getInstance().setAutoFillProfile(getActivity(), newProfile, + mHandler.obtainMessage(PROFILE_SAVED_MSG)); + } + }); + + Button deleteButton = (Button)v.findViewById(R.id.autofill_profile_editor_delete_button); + deleteButton.setOnClickListener(new OnClickListener() { + public void onClick(View button) { + // Clear the UI. + mFullNameEdit.setText(""); + mEmailEdit.setText(""); + mCompanyEdit.setText(""); + mAddressLine1Edit.setText(""); + mAddressLine2Edit.setText(""); + mCityEdit.setText(""); + mStateEdit.setText(""); + mZipEdit.setText(""); + mCountryEdit.setText(""); + mPhoneEdit.setText(""); + + // Update browser settings and native with a null profile. This will + // trigger the current profile to get deleted from the DB. + BrowserSettings.getInstance().setAutoFillProfile(getActivity(), null, + mHandler.obtainMessage(PROFILE_DELETED_MSG)); + } + }); + + Button cancelButton = (Button)v.findViewById(R.id.autofill_profile_editor_cancel_button); + cancelButton.setOnClickListener(new OnClickListener() { + public void onClick(View button) { + getFragmentManager().popBackStack(); + } + }); + + // Populate the text boxes with any pre existing AutoFill data. + AutoFillProfile activeProfile = BrowserSettings.getInstance().getAutoFillProfile(); + if (activeProfile != null) { + mFullNameEdit.setText(activeProfile.getFullName()); + mEmailEdit.setText(activeProfile.getEmailAddress()); + mCompanyEdit.setText(activeProfile.getCompanyName()); + mAddressLine1Edit.setText(activeProfile.getAddressLine1()); + mAddressLine2Edit.setText(activeProfile.getAddressLine2()); + mCityEdit.setText(activeProfile.getCity()); + mStateEdit.setText(activeProfile.getState()); + mZipEdit.setText(activeProfile.getZipCode()); + mCountryEdit.setText(activeProfile.getCountry()); + mPhoneEdit.setText(activeProfile.getPhoneNumber()); + } + + return v; + } +} diff --git a/src/com/android/browser/BaseUi.java b/src/com/android/browser/BaseUi.java new file mode 100644 index 000000000..5f8944fc8 --- /dev/null +++ b/src/com/android/browser/BaseUi.java @@ -0,0 +1,895 @@ +/* + * Copyright (C) 2010 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.ActionBar; +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.ActionMode; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.view.WindowManager; +import android.webkit.WebChromeClient; +import android.webkit.WebHistoryItem; +import android.webkit.WebView; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.Toast; + +import java.util.List; + +/** + * UI interface definitions + */ +public class BaseUi implements UI, WebViewFactory { + + private static final String LOGTAG = "BaseUi"; + + private static final FrameLayout.LayoutParams COVER_SCREEN_PARAMS = + new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + + private static final FrameLayout.LayoutParams COVER_SCREEN_GRAVITY_CENTER = + new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + Gravity.CENTER); + + Activity mActivity; + UiController mUiController; + TabControl mTabControl; + private Tab mActiveTab; + + private Drawable mSecLockIcon; + private Drawable mMixLockIcon; + + private boolean mXLargeScreenSize; + private FrameLayout mBrowserFrameLayout; + private FrameLayout mContentView; + private FrameLayout mCustomViewContainer; + private TitleBarBase mTitleBar; + private TitleBarBase mFakeTitleBar; + private TabBar mTabBar; + + private View mCustomView; + private WebChromeClient.CustomViewCallback mCustomViewCallback; + + private CombinedBookmarkHistoryView mComboView; + + private LinearLayout mErrorConsoleContainer = null; + + private Toast mStopToast; + private ActiveTabsPage mActiveTabsPage; + + // the default <video> poster + private Bitmap mDefaultVideoPoster; + // the video progress view + private View mVideoProgressView; + + boolean mExtendedMenuOpen; + boolean mOptionsMenuOpen; + + private boolean mActivityPaused; + + public BaseUi(Activity browser, UiController controller) { + mActivity = browser; + mUiController = controller; + mTabControl = controller.getTabControl(); + Resources res = mActivity.getResources(); + mSecLockIcon = res.getDrawable(R.drawable.ic_secure); + mMixLockIcon = res.getDrawable(R.drawable.ic_partial_secure); + + + mXLargeScreenSize = (res.getConfiguration().screenLayout + & Configuration.SCREENLAYOUT_SIZE_MASK) + == Configuration.SCREENLAYOUT_SIZE_XLARGE; + + FrameLayout frameLayout = (FrameLayout) mActivity.getWindow() + .getDecorView().findViewById(android.R.id.content); + mBrowserFrameLayout = (FrameLayout) LayoutInflater.from(mActivity) + .inflate(R.layout.custom_screen, null); + mContentView = (FrameLayout) mBrowserFrameLayout.findViewById( + R.id.main_content); + mErrorConsoleContainer = (LinearLayout) mBrowserFrameLayout + .findViewById(R.id.error_console); + mCustomViewContainer = (FrameLayout) mBrowserFrameLayout + .findViewById(R.id.fullscreen_custom_content); + frameLayout.addView(mBrowserFrameLayout, COVER_SCREEN_PARAMS); + + if (mXLargeScreenSize) { + mTitleBar = new TitleBarXLarge(mActivity, mUiController); + mTitleBar.setProgress(100); + mFakeTitleBar = new TitleBarXLarge(mActivity, mUiController); + ActionBar actionBar = mActivity.getActionBar(); + mTabBar = new TabBar(mActivity, mUiController, this); + actionBar.setCustomNavigationMode(mTabBar); + } else { + mTitleBar = new TitleBar(mActivity, mUiController); + // mTitleBar will be always be shown in the fully loaded mode on + // phone + mTitleBar.setProgress(100); + mFakeTitleBar = new TitleBar(mActivity, mUiController); + } + } + + // webview factory + + @Override + public WebView createWebView(boolean privateBrowsing) { + // Create a new WebView + ScrollWebView w = new ScrollWebView(mActivity, null, + android.R.attr.webViewStyle, privateBrowsing); + w.setScrollbarFadingEnabled(true); + w.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); + w.setMapTrackballToArrowKeys(false); // use trackball directly + // Enable the built-in zoom + w.getSettings().setBuiltInZoomControls(true); + if (mXLargeScreenSize) { + w.setScrollListener(mTabBar); + w.getSettings().setDisplayZoomControls(false); + } + + // Add this WebView to the settings observer list and update the + // settings + final BrowserSettings s = BrowserSettings.getInstance(); + s.addObserver(w.getSettings()).update(s, null); + return w; + } + + @Override + public WebView createSubWebView(boolean privateBrowsing) { + ScrollWebView web = (ScrollWebView) createWebView(privateBrowsing); + if (mXLargeScreenSize) { + // no scroll listener for subview + web.setScrollListener(null); + } + return web; + } + + void stopWebViewScrolling() { + ScrollWebView web = (ScrollWebView) mUiController.getCurrentWebView(); + if (web != null) { + web.stopScroll(); + } + } + + private void cancelStopToast() { + if (mStopToast != null) { + mStopToast.cancel(); + mStopToast = null; + } + } + + // lifecycle + + public void onPause() { + // FIXME: This removes the active tabs page and resets the menu to + // MAIN_MENU. A better solution might be to do this work in onNewIntent + // but then we would need to save it in onSaveInstanceState and restore + // it in onCreate/onRestoreInstanceState + if (mActiveTabsPage != null) { + mUiController.removeActiveTabsPage(true); + } + cancelStopToast(); + mActivityPaused = true; + } + + public void onResume() { + mActivityPaused = false; + } + + public void onDestroy() { + hideFakeTitleBar(); + } + + public void onConfigurationChanged(Configuration config) { + } + + // key handling + + @Override + public boolean onBackKey() { + if (mActiveTabsPage != null) { + // if tab page is showing, hide it + mUiController.removeActiveTabsPage(true); + return true; + } + if (mComboView != null) { + if (!mComboView.onBackPressed()) { + mUiController.removeComboView(); + } + return true; + } + if (mCustomView != null) { + mUiController.hideCustomView(); + return true; + } + return false; + } + + // WebView callbacks + + @Override + public void onPageStarted(Tab tab, String url, Bitmap favicon) { + if (mXLargeScreenSize) { + mTabBar.onPageStarted(tab, url, favicon); + } + if (tab.inForeground()) { + resetLockIcon(tab, url); + setUrlTitle(tab, url, null); + setFavicon(tab, favicon); + } + + } + + @Override + public void onPageFinished(Tab tab, String url) { + if (mXLargeScreenSize) { + mTabBar.onPageFinished(tab); + } + if (tab.inForeground()) { + // Reset the title and icon in case we stopped a provisional load. + resetTitleAndIcon(tab); + // Update the lock icon image only once we are done loading + updateLockIconToLatest(tab); + } + } + + @Override + public void onPageStopped(Tab tab) { + cancelStopToast(); + if (tab.inForeground()) { + mStopToast = Toast + .makeText(mActivity, R.string.stopping, Toast.LENGTH_SHORT); + mStopToast.show(); + } + } + + @Override + public void onProgressChanged(Tab tab, int progress) { + if (mXLargeScreenSize) { + mTabBar.onProgress(tab, progress); + } + if (tab.inForeground()) { + mFakeTitleBar.setProgress(progress); + if (progress == 100) { + if (!mOptionsMenuOpen || !mExtendedMenuOpen) { + hideFakeTitleBar(); + } + } else { + if (!mOptionsMenuOpen || mExtendedMenuOpen) { + showFakeTitleBar(); + } + } + } + } + + @Override + public boolean needsRestoreAllTabs() { + return mXLargeScreenSize; + } + + @Override + public void addTab(Tab tab) { + if (mXLargeScreenSize) { + mTabBar.onNewTab(tab); + } + } + + @Override + public void setActiveTab(Tab tab) { + if ((tab != mActiveTab) && (mActiveTab != null)) { + removeTabFromContentView(mActiveTab); + } + mActiveTab = tab; + attachTabToContentView(tab); + setShouldShowErrorConsole(tab, mUiController.shouldShowErrorConsole()); + + WebView view = tab.getWebView(); + // TabControl.setCurrentTab has been called before this, + // so the tab is guaranteed to have a webview + if (view == null) { + Log.e(LOGTAG, "active tab with no webview detected"); + return; + } + view.setEmbeddedTitleBar(mTitleBar); + if (tab.isInVoiceSearchMode()) { + showVoiceTitleBar(tab.getVoiceDisplayTitle()); + } else { + revertVoiceTitleBar(tab); + } + + if (mXLargeScreenSize) { + // Request focus on the top window. + mTabBar.onSetActiveTab(tab); + } + resetTitleIconAndProgress(tab); + updateLockIconToLatest(tab); + tab.getTopWindow().requestFocus(); + } + + @Override + public void updateTabs(List<Tab> tabs) { + if (mXLargeScreenSize) { + mTabBar.updateTabs(tabs); + } + } + + @Override + public void removeTab(Tab tab) { + if (mActiveTab == tab) { + removeTabFromContentView(tab); + mActiveTab = null; + } + if (mXLargeScreenSize) { + mTabBar.onRemoveTab(tab); + } + } + + @Override + public void detachTab(Tab tab) { + removeTabFromContentView(tab); + } + + @Override + public void attachTab(Tab tab) { + attachTabToContentView(tab); + } + + private void attachTabToContentView(Tab tab) { + if (tab.getWebView() == null) { + return; + } + View container = tab.getViewContainer(); + WebView mainView = tab.getWebView(); + // Attach the WebView to the container and then attach the + // container to the content view. + FrameLayout wrapper = + (FrameLayout) container.findViewById(R.id.webview_wrapper); + ViewGroup parent = (ViewGroup) mainView.getParent(); + if (parent != wrapper) { + if (parent != null) { + Log.w(LOGTAG, "mMainView already has a parent in" + + " attachTabToContentView!"); + parent.removeView(mainView); + } + wrapper.addView(mainView); + } else { + Log.w(LOGTAG, "mMainView is already attached to wrapper in" + + " attachTabToContentView!"); + } + parent = (ViewGroup) container.getParent(); + if (parent != mContentView) { + if (parent != null) { + Log.w(LOGTAG, "mContainer already has a parent in" + + " attachTabToContentView!"); + parent.removeView(container); + } + mContentView.addView(container, COVER_SCREEN_PARAMS); + } else { + Log.w(LOGTAG, "mContainer is already attached to content in" + + " attachTabToContentView!"); + } + mUiController.attachSubWindow(tab); + } + + private void removeTabFromContentView(Tab tab) { + // Remove the container that contains the main WebView. + WebView mainView = tab.getWebView(); + View container = tab.getViewContainer(); + if (mainView == null) { + return; + } + // Remove the container from the content and then remove the + // WebView from the container. This will trigger a focus change + // needed by WebView. + FrameLayout wrapper = + (FrameLayout) container.findViewById(R.id.webview_wrapper); + wrapper.removeView(mainView); + mContentView.removeView(container); + mUiController.endActionMode(); + mUiController.removeSubWindow(tab); + ErrorConsoleView errorConsole = tab.getErrorConsole(false); + if (errorConsole != null) { + mErrorConsoleContainer.removeView(errorConsole); + } + mainView.setEmbeddedTitleBar(null); + } + + /** + * create a sub window container and webview for the tab + * Note: this methods operates through side-effects for now + * it sets both the subView and subViewContainer for the given tab + * @param tab tab to create the sub window for + * @param subView webview to be set as a subwindow for the tab + */ + @Override + public void createSubWindow(Tab tab, WebView subView) { + View subViewContainer = mActivity.getLayoutInflater().inflate( + R.layout.browser_subwindow, null); + ViewGroup inner = (ViewGroup) subViewContainer + .findViewById(R.id.inner_container); + inner.addView(subView, new LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT)); + final ImageButton cancel = (ImageButton) subViewContainer + .findViewById(R.id.subwindow_close); + final WebView cancelSubView = subView; + cancel.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + cancelSubView.getWebChromeClient().onCloseWindow(cancelSubView); + } + }); + tab.setSubWebView(subView); + tab.setSubViewContainer(subViewContainer); + } + + /** + * Remove the sub window from the content view. + */ + @Override + public void removeSubWindow(View subviewContainer) { + mContentView.removeView(subviewContainer); + mUiController.endActionMode(); + } + + /** + * Attach the sub window to the content view. + */ + @Override + public void attachSubWindow(View container) { + if (container.getParent() != null) { + // already attached, remove first + ((ViewGroup) container.getParent()).removeView(container); + } + mContentView.addView(container, COVER_SCREEN_PARAMS); + } + + void showFakeTitleBar() { + if (!isFakeTitleBarShowing() && mActiveTabsPage == null && + !mActivityPaused) { + WebView mainView = mUiController.getCurrentWebView(); + // if there is no current WebView, don't show the faked title bar; + if (mainView == null) { + return; + } + // Do not need to check for null, since the current tab will have + // at least a main WebView, or we would have returned above. + if (mUiController.isInCustomActionMode()) { + // Do not show the fake title bar, while a custom ActionMode + // (i.e. find or select) is showing. + return; + } + if (mXLargeScreenSize) { + mContentView.addView(mFakeTitleBar); + mTabBar.onShowTitleBar(); + } else { + WindowManager manager = (WindowManager) + mActivity.getSystemService(Context.WINDOW_SERVICE); + + // Add the title bar to the window manager so it can receive + // touches + // while the menu is up + WindowManager.LayoutParams params = + new WindowManager.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT); + params.gravity = Gravity.TOP; + boolean atTop = mainView.getScrollY() == 0; + params.windowAnimations = atTop ? 0 : R.style.TitleBar; + manager.addView(mFakeTitleBar, params); + } + } + } + + void hideFakeTitleBar() { + if (!isFakeTitleBarShowing()) return; + if (mXLargeScreenSize) { + mContentView.removeView(mFakeTitleBar); + mTabBar.onHideTitleBar(); + } else { + WindowManager.LayoutParams params = + (WindowManager.LayoutParams) mFakeTitleBar.getLayoutParams(); + WebView mainView = mUiController.getCurrentWebView(); + // Although we decided whether or not to animate based on the + // current + // scroll position, the scroll position may have changed since the + // fake title bar was displayed. Make sure it has the appropriate + // animation/lack thereof before removing. + params.windowAnimations = + mainView != null && mainView.getScrollY() == 0 ? + 0 : R.style.TitleBar; + WindowManager manager = (WindowManager) mActivity + .getSystemService(Context.WINDOW_SERVICE); + manager.updateViewLayout(mFakeTitleBar, params); + manager.removeView(mFakeTitleBar); + } + } + + boolean isFakeTitleBarShowing() { + return (mFakeTitleBar.getParent() != null); + } + + @Override + public void showComboView(boolean startWithHistory, Bundle extras) { + mComboView = new CombinedBookmarkHistoryView(mActivity, + mUiController, + startWithHistory ? + CombinedBookmarkHistoryView.FRAGMENT_ID_HISTORY + : CombinedBookmarkHistoryView.FRAGMENT_ID_BOOKMARKS, + extras); + mTitleBar.setVisibility(View.GONE); + hideFakeTitleBar(); + mContentView.addView(mComboView, COVER_SCREEN_PARAMS); + } + + /** + * dismiss the ComboPage + */ + @Override + public void hideComboView() { + if (mComboView != null) { + mContentView.removeView(mComboView); + mTitleBar.setVisibility(View.VISIBLE); + mComboView = null; + } + } + + @Override + public void showCustomView(View view, + WebChromeClient.CustomViewCallback callback) { + // if a view already exists then immediately terminate the new one + if (mCustomView != null) { + callback.onCustomViewHidden(); + return; + } + + // Add the custom view to its container. + mCustomViewContainer.addView(view, COVER_SCREEN_GRAVITY_CENTER); + mCustomView = view; + mCustomViewCallback = callback; + // Hide the content view. + mContentView.setVisibility(View.GONE); + // Finally show the custom view container. + setStatusBarVisibility(false); + mCustomViewContainer.setVisibility(View.VISIBLE); + mCustomViewContainer.bringToFront(); + } + + @Override + public void onHideCustomView() { + if (mCustomView == null) + return; + + // Hide the custom view. + mCustomView.setVisibility(View.GONE); + // Remove the custom view from its container. + mCustomViewContainer.removeView(mCustomView); + mCustomView = null; + mCustomViewContainer.setVisibility(View.GONE); + mCustomViewCallback.onCustomViewHidden(); + // Show the content view. + setStatusBarVisibility(true); + mContentView.setVisibility(View.VISIBLE); + } + + @Override + public boolean isCustomViewShowing() { + return mCustomView != null; + } + + @Override + public void showVoiceTitleBar(String title) { + mTitleBar.setInVoiceMode(true); + mTitleBar.setDisplayTitle(title); + mFakeTitleBar.setInVoiceMode(true); + mFakeTitleBar.setDisplayTitle(title); + } + + @Override + public void revertVoiceTitleBar(Tab tab) { + mTitleBar.setInVoiceMode(false); + String url = tab.getCurrentUrl(); + mTitleBar.setDisplayTitle(url); + mFakeTitleBar.setInVoiceMode(false); + mFakeTitleBar.setDisplayTitle(url); + } + + // ------------------------------------------------------------------------- + + @Override + public void resetTitleAndRevertLockIcon(Tab tab) { + tab.revertLockIcon(); + updateLockIconToLatest(tab); + resetTitleIconAndProgress(tab); + } + + /** + * Resets the lock icon. This method is called when we start a new load and + * know the url to be loaded. + */ + private void resetLockIcon(Tab tab, String url) { + // Save the lock-icon state (we revert to it if the load gets cancelled) + tab.resetLockIcon(url); + updateLockIconImage(Tab.LOCK_ICON_UNSECURE); + } + + /** + * Update the lock icon to correspond to our latest state. + */ + private void updateLockIconToLatest(Tab t) { + if (t != null) { + updateLockIconImage(t.getLockIconType()); + } + } + + /** + * Reset the title, favicon, and progress. + */ + private void resetTitleIconAndProgress(Tab tab) { + WebView current = tab.getWebView(); + if (current == null) { + return; + } + resetTitleAndIcon(current); + int progress = current.getProgress(); + current.getWebChromeClient().onProgressChanged(current, progress); + } + + @Override + public void resetTitleAndIcon(Tab tab) { + WebView current = tab.getWebView(); + if (current != null) { + resetTitleAndIcon(current); + } + } + + // Reset the title and the icon based on the given item. + private void resetTitleAndIcon(WebView view) { + WebHistoryItem item = view.copyBackForwardList().getCurrentItem(); + Tab tab = mTabControl.getTabFromView(view); + if (item != null) { + setUrlTitle(tab, item.getUrl(), item.getTitle()); + setFavicon(tab, item.getFavicon()); + } else { + setUrlTitle(tab, null, null); + setFavicon(tab, null); + } + } + + /** + * Updates the lock-icon image in the title-bar. + */ + private void updateLockIconImage(int lockIconType) { + Drawable d = null; + if (lockIconType == Tab.LOCK_ICON_SECURE) { + d = mSecLockIcon; + } else if (lockIconType == Tab.LOCK_ICON_MIXED) { + d = mMixLockIcon; + } + mTitleBar.setLock(d); + mFakeTitleBar.setLock(d); + } + + // active tabs page + + public void showActiveTabsPage() { + mActiveTabsPage = new ActiveTabsPage(mActivity, mUiController); + mTitleBar.setVisibility(View.GONE); + hideFakeTitleBar(); + mContentView.addView(mActiveTabsPage, COVER_SCREEN_PARAMS); + mActiveTabsPage.requestFocus(); + } + + /** + * Remove the active tabs page. + * @param needToAttach If true, the active tabs page did not attach a tab + * to the content view, so we need to do that here. + */ + public void removeActiveTabsPage() { + mContentView.removeView(mActiveTabsPage); + mTitleBar.setVisibility(View.VISIBLE); + mActiveTabsPage = null; + } + + // action mode callbacks + + @Override + public void onActionModeStarted(ActionMode mode) { + // hide the fake title bar when CAB is shown + hideFakeTitleBar(); + } + + @Override + public void onActionModeFinished(boolean inLoad) { + if (inLoad) { + // the titlebar was removed when the CAB was shown + // if the page is loading, show it again + showFakeTitleBar(); + } + } + + // menu handling callbacks + + @Override + public void onOptionsMenuOpened() { + mOptionsMenuOpen = true; + // options menu opened, show fake title bar + showFakeTitleBar(); + } + + @Override + public void onExtendedMenuOpened() { + // Switching the menu to expanded view, so hide the + // title bar. + mExtendedMenuOpen = true; + hideFakeTitleBar(); + } + + @Override + public void onOptionsMenuClosed(boolean inLoad) { + mOptionsMenuOpen = false; + if (!inLoad) { + hideFakeTitleBar(); + } + } + + @Override + public void onExtendedMenuClosed(boolean inLoad) { + mExtendedMenuOpen = false; + if (inLoad) { + showFakeTitleBar(); + } + } + + @Override + public void onContextMenuCreated(Menu menu) { + hideFakeTitleBar(); + } + + @Override + public void onContextMenuClosed(Menu menu, boolean inLoad) { + if (inLoad) { + showFakeTitleBar(); + } + } + + @Override + public void onScroll(boolean titleVisible) { + if (mTabBar != null) { + mTabBar.onScroll(titleVisible); + } + } + + // error console + + @Override + public void setShouldShowErrorConsole(Tab tab, boolean flag) { + ErrorConsoleView errorConsole = tab.getErrorConsole(true); + if (flag) { + // Setting the show state of the console will cause it's the layout + // to be inflated. + if (errorConsole.numberOfErrors() > 0) { + errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED); + } else { + errorConsole.showConsole(ErrorConsoleView.SHOW_NONE); + } + if (errorConsole.getParent() != null) { + mErrorConsoleContainer.removeView(errorConsole); + } + // Now we can add it to the main view. + mErrorConsoleContainer.addView(errorConsole, + new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + } else { + mErrorConsoleContainer.removeView(errorConsole); + } + } + + private void setStatusBarVisibility(boolean visible) { + int flag = visible ? 0 : WindowManager.LayoutParams.FLAG_FULLSCREEN; + mActivity.getWindow().setFlags(flag, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + + @Override + public void setUrlTitle(Tab tab, String url, String title) { + if (TextUtils.isEmpty(title)) { + if (TextUtils.isEmpty(url)) { + title = mActivity.getResources() + .getString(R.string.title_bar_loading); + } else { + title = url; + } + } + if (tab.isInVoiceSearchMode()) return; + if (tab.inForeground()) { + mTitleBar.setDisplayTitle(url); + mFakeTitleBar.setDisplayTitle(url); + } + if (mXLargeScreenSize) { + mTabBar.onUrlAndTitle(tab, url, title); + } + } + + // Set the favicon in the title bar. + @Override + public void setFavicon(Tab tab, Bitmap icon) { + mTitleBar.setFavicon(icon); + mFakeTitleBar.setFavicon(icon); + if (mXLargeScreenSize) { + mTabBar.onFavicon(tab, icon); + } + } + @Override + public boolean showsWeb() { + return mCustomView == null && mActiveTabsPage == null + && mComboView == null; + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + if (!mXLargeScreenSize) { + final MenuItem newtab = menu.findItem(R.id.new_tab_menu_id); + newtab.setEnabled(mUiController.getTabControl().canCreateNewTab()); + } + } + + // ------------------------------------------------------------------------- + // Helper function for WebChromeClient + // ------------------------------------------------------------------------- + + @Override + public Bitmap getDefaultVideoPoster() { + if (mDefaultVideoPoster == null) { + mDefaultVideoPoster = BitmapFactory.decodeResource( + mActivity.getResources(), R.drawable.default_video_poster); + } + return mDefaultVideoPoster; + } + + @Override + public View getVideoLoadingProgressView() { + if (mVideoProgressView == null) { + LayoutInflater inflater = LayoutInflater.from(mActivity); + mVideoProgressView = inflater.inflate( + R.layout.video_loading_progress, null); + } + return mVideoProgressView; + } + +} diff --git a/src/com/android/browser/BookmarkUtils.java b/src/com/android/browser/BookmarkUtils.java new file mode 100644 index 000000000..a63b90fb0 --- /dev/null +++ b/src/com/android/browser/BookmarkUtils.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2010 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.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +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.preference.PreferenceManager; +import android.provider.Browser; +import android.provider.BrowserContract; +import android.text.TextUtils; + +class BookmarkUtils { + private final static String LOGTAG = "BookmarkUtils"; + + // 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"; + + enum BookmarkIconType { + ICON_INSTALLABLE_WEB_APP, // Icon for an installable web app (launches WebAppRuntime). + ICON_HOME_SHORTCUT // Icon for a shortcut on the home screen (launches Browser). + }; + + /** + * Creates an icon to be associated with this bookmark. If available, the apple touch icon + * will be used, else we draw our own depending on the type of "bookmark" being created. + */ + static Bitmap createIcon(Context context, Bitmap touchIcon, Bitmap favicon, + BookmarkIconType type) { + int iconDimension = context.getResources().getDimensionPixelSize( + android.R.dimen.app_icon_size); + + Bitmap bm = Bitmap.createBitmap(iconDimension, iconDimension, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bm); + Rect iconBounds = new Rect(0, 0, bm.getWidth(), bm.getHeight()); + + // Use the apple-touch-icon if available + if (touchIcon != null) { + drawTouchIconToCanvas(touchIcon, canvas, iconBounds); + } else { + // No touch icon so create our own. + // Set the background based on the type of shortcut (either webapp or home shortcut). + Bitmap icon = getIconBackground(context, type); + + if (icon != null) { + // Now draw the correct icon background into our new bitmap. + canvas.drawBitmap(icon, null, iconBounds, null); + } + + // If we have a favicon, overlay it in a nice rounded white box on top of the + // background. + if (favicon != null) { + drawFaviconToCanvas(favicon, canvas, iconBounds, + context.getResources().getDisplayMetrics().density); + } + } + return bm; + } + + /** + * Convenience method for creating an intent that will add a shortcut to the home screen. + */ + static Intent createAddToHomeIntent(Context context, String url, String title, + Bitmap touchIcon, Bitmap favicon) { + Intent i = new Intent(INSTALL_SHORTCUT); + 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); + i.putExtra(Intent.EXTRA_SHORTCUT_ICON, createIcon(context, touchIcon, favicon, + BookmarkIconType.ICON_HOME_SHORTCUT)); + + // Do not allow duplicate items + i.putExtra("duplicate", false); + return i; + } + + private static Bitmap getIconBackground(Context context, BookmarkIconType type) { + if (type == BookmarkIconType.ICON_HOME_SHORTCUT) { + // Want to create a shortcut icon on the homescreen, so the icon + // background is the red bookmark. + return BitmapFactory.decodeResource(context.getResources(), + R.mipmap.ic_launcher_shortcut_browser_bookmark); + } else if (type == BookmarkIconType.ICON_INSTALLABLE_WEB_APP) { + // Use the web browser icon as the background for the icon for an installable + // web app. + return BitmapFactory.decodeResource(context.getResources(), + R.mipmap.ic_launcher_browser); + } + return null; + } + + private static void drawTouchIconToCanvas(Bitmap touchIcon, Canvas canvas, Rect iconBounds) { + Rect src = new Rect(0, 0, touchIcon.getWidth(), touchIcon.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, iconBounds, 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(iconBounds); + 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); + } + + private static void drawFaviconToCanvas(Bitmap favicon, Canvas canvas, Rect iconBounds, + float density) { + // 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); + + // 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 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); + } + + /* package */ static Uri getBookmarksUri(Context context) { + Uri uri = BrowserContract.Bookmarks.CONTENT_URI; + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + String accountType = prefs.getString(BrowserBookmarksPage.PREF_ACCOUNT_TYPE, null); + String accountName = prefs.getString(BrowserBookmarksPage.PREF_ACCOUNT_NAME, null); + if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { + uri = uri.buildUpon() + .appendQueryParameter(BrowserContract.Bookmarks.PARAM_ACCOUNT_NAME, accountName) + .appendQueryParameter(BrowserContract.Bookmarks.PARAM_ACCOUNT_TYPE, accountType) + .build(); + } + return uri; + } +}; diff --git a/src/com/android/browser/Bookmarks.java b/src/com/android/browser/Bookmarks.java index 5ada9dcb8..383ae7fc5 100644 --- a/src/com/android/browser/Bookmarks.java +++ b/src/com/android/browser/Bookmarks.java @@ -20,16 +20,20 @@ import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; +import android.content.SharedPreferences; import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; -import android.provider.Browser; +import android.os.AsyncTask; +import android.preference.PreferenceManager; +import android.provider.BrowserContract; +import android.provider.BrowserContract.Combined; +import android.provider.BrowserContract.Images; import android.util.Log; import android.webkit.WebIconDatabase; import android.widget.Toast; import java.io.ByteArrayOutputStream; -import java.util.Date; /** * This class is purely to have a common place for adding/deleting bookmarks. @@ -53,89 +57,38 @@ import java.util.Date; * @param context Context of the calling Activity. This is used to make * Toast confirming that the bookmark has been added. If the * caller provides null, the Toast will not be shown. - * @param cr The ContentResolver being used to add the bookmark to the db. * @param url URL of the website to be bookmarked. * @param name Provided name for the bookmark. * @param thumbnail A thumbnail for the bookmark. * @param retainIcon Whether to retain the page's icon in the icon database. * This will usually be <code>true</code> except when bookmarks are * added by a settings restore agent. + * @param parent ID of the parent folder. */ - /* package */ static void addBookmark(Context context, - ContentResolver cr, String url, String name, - Bitmap thumbnail, boolean retainIcon) { + /* package */ static void addBookmark(Context context, boolean showToast, String url, + String name, Bitmap thumbnail, boolean retainIcon, long parent) { // Want to append to the beginning of the list - long creationTime = new Date().getTime(); - ContentValues map = new ContentValues(); - Cursor cursor = null; + ContentValues values = new ContentValues(); try { - cursor = Browser.getVisitedLike(cr, url); - if (cursor.moveToFirst() && cursor.getInt( - Browser.HISTORY_PROJECTION_BOOKMARK_INDEX) == 0) { - // This means we have been to this site but not bookmarked - // it, so convert the history item to a bookmark - map.put(Browser.BookmarkColumns.CREATED, creationTime); - map.put(Browser.BookmarkColumns.TITLE, name); - map.put(Browser.BookmarkColumns.BOOKMARK, 1); - map.put(Browser.BookmarkColumns.THUMBNAIL, - bitmapToBytes(thumbnail)); - cr.update(Browser.BOOKMARKS_URI, map, - "_id = " + cursor.getInt(0), null); - } else { - int count = cursor.getCount(); - boolean matchedTitle = false; - for (int i = 0; i < count; i++) { - // One or more bookmarks already exist for this site. - // Check the names of each - cursor.moveToPosition(i); - if (cursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX) - .equals(name)) { - // The old bookmark has the same name. - // Update its creation time. - map.put(Browser.BookmarkColumns.CREATED, - creationTime); - cr.update(Browser.BOOKMARKS_URI, map, - "_id = " + cursor.getInt(0), null); - matchedTitle = true; - break; - } - } - if (!matchedTitle) { - // Adding a bookmark for a site the user has visited, - // or a new bookmark (with a different name) for a site - // the user has visited - map.put(Browser.BookmarkColumns.TITLE, name); - map.put(Browser.BookmarkColumns.URL, url); - map.put(Browser.BookmarkColumns.CREATED, creationTime); - map.put(Browser.BookmarkColumns.BOOKMARK, 1); - map.put(Browser.BookmarkColumns.DATE, 0); - map.put(Browser.BookmarkColumns.THUMBNAIL, - bitmapToBytes(thumbnail)); - int visits = 0; - if (count > 0) { - // The user has already bookmarked, and possibly - // visited this site. However, they are creating - // a new bookmark with the same url but a different - // name. The new bookmark should have the same - // number of visits as the already created bookmark. - visits = cursor.getInt( - Browser.HISTORY_PROJECTION_VISITS_INDEX); - } - // Bookmark starts with 3 extra visits so that it will - // bubble up in the most visited and goto search box - map.put(Browser.BookmarkColumns.VISITS, visits + 3); - cr.insert(Browser.BOOKMARKS_URI, map); - } - } + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + String accountType = prefs.getString(BrowserBookmarksPage.PREF_ACCOUNT_TYPE, null); + String accountName = prefs.getString(BrowserBookmarksPage.PREF_ACCOUNT_NAME, null); + values.put(BrowserContract.Bookmarks.ACCOUNT_TYPE, accountType); + values.put(BrowserContract.Bookmarks.ACCOUNT_NAME, accountName); + values.put(BrowserContract.Bookmarks.TITLE, name); + values.put(BrowserContract.Bookmarks.URL, url); + values.put(BrowserContract.Bookmarks.IS_FOLDER, 0); + values.put(BrowserContract.Bookmarks.THUMBNAIL, + bitmapToBytes(thumbnail)); + values.put(BrowserContract.Bookmarks.PARENT, parent); + context.getContentResolver().insert(BrowserContract.Bookmarks.CONTENT_URI, values); } catch (IllegalStateException e) { Log.e(LOGTAG, "addBookmark", e); - } finally { - if (cursor != null) cursor.close(); } if (retainIcon) { WebIconDatabase.getInstance().retainIconForPageUrl(url); } - if (context != null) { + if (showToast) { Toast.makeText(context, R.string.added_to_bookmarks, Toast.LENGTH_LONG).show(); } @@ -155,40 +108,26 @@ import java.util.Date; ContentResolver cr, String url, String title) { Cursor cursor = null; try { - cursor = cr.query( - Browser.BOOKMARKS_URI, - Browser.HISTORY_PROJECTION, - "url = ? AND title = ?", + cursor = cr.query(BrowserContract.Bookmarks.CONTENT_URI, + new String[] { BrowserContract.Bookmarks._ID }, + BrowserContract.Bookmarks.URL + " = ? AND " + + BrowserContract.Bookmarks.TITLE + " = ?", new String[] { url, title }, null); - boolean first = cursor.moveToFirst(); + // Should be in the database no matter what - if (!first) { + if (!cursor.moveToFirst()) { throw new AssertionError("URL is not in the database! " + url + " " + title); } + // Remove from bookmarks WebIconDatabase.getInstance().releaseIconForPageUrl(url); - Uri uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI, - cursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX)); - int numVisits = cursor.getInt( - Browser.HISTORY_PROJECTION_VISITS_INDEX); - if (0 == numVisits) { - cr.delete(uri, null, null); - } else { - // It is no longer a bookmark, but it is still a visited - // site. - ContentValues values = new ContentValues(); - values.put(Browser.BookmarkColumns.BOOKMARK, 0); - try { - cr.update(uri, values, null, null); - } catch (IllegalStateException e) { - Log.e("removeFromBookmarks", "no database!"); - } - } + Uri uri = ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, + cursor.getLong(0)); + cr.delete(uri, null, null); if (context != null) { - Toast.makeText(context, R.string.removed_from_bookmarks, - Toast.LENGTH_LONG).show(); + Toast.makeText(context, R.string.removed_from_bookmarks, Toast.LENGTH_LONG).show(); } } catch (IllegalStateException e) { Log.e(LOGTAG, "removeFromBookmarks", e); @@ -219,4 +158,86 @@ import java.util.Date; } return false; } + + static final String QUERY_BOOKMARKS_WHERE = + Combined.URL + " == ? OR " + + Combined.URL + " == ? OR " + + Combined.URL + " LIKE ? || '%' OR " + + Combined.URL + " LIKE ? || '%'"; + + /* package */ static Cursor queryCombinedForUrl(ContentResolver cr, + String originalUrl, String url) { + if (cr == null || url == null) { + return null; + } + + // If originalUrl is null, just set it to url. + if (originalUrl == null) { + originalUrl = url; + } + + // Look for both the original url and the actual url. This takes in to + // account redirects. + String originalUrlNoQuery = removeQuery(originalUrl); + String urlNoQuery = removeQuery(url); + originalUrl = originalUrlNoQuery + '?'; + url = urlNoQuery + '?'; + + // Use NoQuery to search for the base url (i.e. if the url is + // http://www.yahoo.com/?rs=1, search for http://www.yahoo.com) + // Use url to match the base url with other queries (i.e. if the url is + // http://www.google.com/m, search for + // http://www.google.com/m?some_query) + final String[] selArgs = new String[] { originalUrlNoQuery, urlNoQuery, originalUrl, url }; + final String[] projection = new String[] { Combined.URL }; + return cr.query(Combined.CONTENT_URI, projection, QUERY_BOOKMARKS_WHERE, selArgs, null); + } + + // Strip the query from the given url. + static String removeQuery(String url) { + if (url == null) { + return null; + } + int query = url.indexOf('?'); + String noQuery = url; + if (query != -1) { + noQuery = url.substring(0, query); + } + return noQuery; + } + + /** + * Update the bookmark's favicon. This is a convenience method for updating + * a bookmark favicon for the originalUrl and url of the passed in WebView. + * @param cr The ContentResolver to use. + * @param originalUrl The original url before any redirects. + * @param url The current url. + * @param favicon The favicon bitmap to write to the db. + */ + /* package */ static void updateFavicon(final ContentResolver cr, + final String originalUrl, final String url, final Bitmap favicon) { + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... unused) { + Cursor cursor = queryCombinedForUrl(cr, originalUrl, url); + try { + if (cursor.moveToFirst()) { + final ByteArrayOutputStream os = new ByteArrayOutputStream(); + favicon.compress(Bitmap.CompressFormat.PNG, 100, os); + + ContentValues values = new ContentValues(); + values.put(Images.FAVICON, os.toByteArray()); + values.put(Images.URL, cursor.getString(0)); + + do { + cr.update(Images.CONTENT_URI, values, null, null); + } while (cursor.moveToNext()); + } + } finally { + if (cursor != null) cursor.close(); + } + return null; + } + }.execute(); + } } diff --git a/src/com/android/browser/BookmarksLoader.java b/src/com/android/browser/BookmarksLoader.java new file mode 100644 index 000000000..770ca600b --- /dev/null +++ b/src/com/android/browser/BookmarksLoader.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2010 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.content.Context; +import android.content.CursorLoader; +import android.net.Uri; +import android.provider.BrowserContract.Bookmarks; +import android.text.TextUtils; + +public class BookmarksLoader extends CursorLoader { + public static final String ARG_ACCOUNT_TYPE = "acct_type"; + public static final String ARG_ACCOUNT_NAME = "acct_name"; + + public static final int COLUMN_INDEX_ID = 0; + public static final int COLUMN_INDEX_URL = 1; + public static final int COLUMN_INDEX_TITLE = 2; + public static final int COLUMN_INDEX_FAVICON = 3; + public static final int COLUMN_INDEX_THUMBNAIL = 4; + public static final int COLUMN_INDEX_TOUCH_ICON = 5; + public static final int COLUMN_INDEX_IS_FOLDER = 6; + public static final int COLUMN_INDEX_PARENT = 8; + + public static final String[] PROJECTION = new String[] { + Bookmarks._ID, // 0 + Bookmarks.URL, // 1 + Bookmarks.TITLE, // 2 + Bookmarks.FAVICON, // 3 + Bookmarks.THUMBNAIL, // 4 + Bookmarks.TOUCH_ICON, // 5 + Bookmarks.IS_FOLDER, // 6 + Bookmarks.POSITION, // 7 + Bookmarks.PARENT, // 8 + }; + + private String mAccountType; + private String mAccountName; + + public BookmarksLoader(Context context, String accountType, String accountName) { + super(context, addAccount(Bookmarks.CONTENT_URI_DEFAULT_FOLDER, accountType, accountName), + PROJECTION, null, null, null); + mAccountType = accountType; + mAccountName = accountName; + } + + @Override + public void setUri(Uri uri) { + super.setUri(addAccount(uri, mAccountType, mAccountName)); + } + + private static Uri addAccount(Uri uri, String accountType, String accountName) { + if (!TextUtils.isEmpty(accountType) && !TextUtils.isEmpty(accountName)) { + return uri.buildUpon().appendQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE, accountType). + appendQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME, accountName).build(); + } + return uri; + } +} diff --git a/src/com/android/browser/BreadCrumbView.java b/src/com/android/browser/BreadCrumbView.java new file mode 100644 index 000000000..4939b48e3 --- /dev/null +++ b/src/com/android/browser/BreadCrumbView.java @@ -0,0 +1,299 @@ +/* + * Copyright (C) 2010 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.content.Context; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +/** + * Simple bread crumb view + * Use setController to receive callbacks from user interactions + * Use pushView, popView, clear, and getTopData to change/access the view stack + */ +public class BreadCrumbView extends LinearLayout implements OnClickListener { + + interface Controller { + public void onTop(int level, Object data); + } + + private ImageButton mBackButton; + private Controller mController; + private List<Crumb> mCrumbs; + private boolean mUseBackButton; + private Drawable mSeparatorDrawable; + + /** + * @param context + * @param attrs + * @param defStyle + */ + public BreadCrumbView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + /** + * @param context + * @param attrs + */ + public BreadCrumbView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + /** + * @param context + */ + public BreadCrumbView(Context context) { + super(context); + init(context); + } + + private void init(Context ctx) { + mUseBackButton = false; + mCrumbs = new ArrayList<Crumb>(); + mSeparatorDrawable = ctx.getResources().getDrawable( + R.drawable.crumb_divider); + } + + public void setUseBackButton(boolean useflag) { + mUseBackButton = useflag; + if (mUseBackButton && (mBackButton == null)) { + addBackButton(); + } else if (!mUseBackButton && (mBackButton != null)) { + removeView(mBackButton); + mBackButton = null; + } + } + + public void setController(Controller ctl) { + mController = ctl; + } + + public Object getTopData() { + Crumb c = getTopCrumb(); + if (c != null) { + return c.data; + } + return null; + } + + public int size() { + return mCrumbs.size(); + } + + public void clear() { + while (mCrumbs.size() > 1) { + pop(false); + } + pop(true); + } + + public void notifyController() { + if (mController != null) { + if (mCrumbs.size() > 0) { + mController.onTop(mCrumbs.size(), getTopCrumb().data); + } else { + mController.onTop(0, null); + } + } + } + + public void pushView(String name, Object data) { + pushView(name, true, data); + } + + public void pushView(String name, boolean canGoBack, Object data) { + Crumb crumb = new Crumb(name, canGoBack, data); + pushCrumb(crumb); + } + + public void pushView(View view, Object data) { + Crumb crumb = new Crumb(view, true, data); + pushCrumb(crumb); + } + + public void popView() { + pop(true); + } + + private void addBackButton() { + mBackButton = new ImageButton(mContext); + mBackButton.setImageResource(R.drawable.ic_back_normal); + mBackButton.setBackgroundResource(R.drawable.browserbarbutton); + mBackButton.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.MATCH_PARENT)); + mBackButton.setOnClickListener(this); + mBackButton.setVisibility(View.INVISIBLE); + addView(mBackButton, 0); + } + + private void pushCrumb(Crumb crumb) { + if (!mUseBackButton || (mCrumbs.size() > 0)) { + addSeparator(); + } + mCrumbs.add(crumb); + addView(crumb.crumbView); + if (mUseBackButton) { + mBackButton.setVisibility(crumb.canGoBack ? View.VISIBLE : View.INVISIBLE); + } + crumb.crumbView.setOnClickListener(this); + } + + private void addSeparator() { + ImageView sep = new ImageView(mContext); + sep.setImageDrawable(mSeparatorDrawable); + sep.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.MATCH_PARENT)); + addView(sep); + } + + private void pop(boolean notify) { + int n = mCrumbs.size(); + if (n > 0) { + removeLastView(); + if (!mUseBackButton || (n > 1)) { + // remove separator + removeLastView(); + } + mCrumbs.remove(n - 1); + if (mUseBackButton) { + Crumb top = getTopCrumb(); + if (top != null && top.canGoBack) { + mBackButton.setVisibility(View.VISIBLE); + } else { + mBackButton.setVisibility(View.INVISIBLE); + } + } + if (notify) { + notifyController(); + } + } + } + + private void removeLastView() { + int ix = getChildCount(); + if (ix > 0) { + removeViewAt(ix-1); + } + } + + private Crumb getTopCrumb() { + Crumb crumb = null; + if (mCrumbs.size() > 0) { + crumb = mCrumbs.get(mCrumbs.size() - 1); + } + return crumb; + } + + @Override + public void onClick(View v) { + if (mBackButton == v) { + popView(); + notifyController(); + } else { + // pop until view matches crumb view + while (v != getTopCrumb().crumbView) { + pop(false); + } + notifyController(); + } + } + @Override + public int getBaseline() { + int ix = getChildCount(); + if (ix > 0) { + // If there is at least one crumb, the baseline will be its + // baseline. + return getChildAt(ix-1).getBaseline(); + } + return super.getBaseline(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int height = mSeparatorDrawable.getIntrinsicHeight(); + if (mMeasuredHeight < height) { + // This should only be an issue if there are currently no separators + // showing; i.e. if there is one crumb and no back button. + int mode = View.MeasureSpec.getMode(heightMeasureSpec); + switch(mode) { + case View.MeasureSpec.AT_MOST: + if (View.MeasureSpec.getSize(heightMeasureSpec) < height) { + return; + } + break; + case View.MeasureSpec.EXACTLY: + return; + default: + break; + } + setMeasuredDimension(mMeasuredWidth, height); + } + } + + class Crumb { + + public View crumbView; + public boolean canGoBack; + public Object data; + + public Crumb(String title, boolean backEnabled, Object tag) { + init(makeCrumbView(title), backEnabled, tag); + } + + public Crumb(View view, boolean backEnabled, Object tag) { + init(view, backEnabled, tag); + } + + private void init(View view, boolean back, Object tag) { + canGoBack = back; + crumbView = view; + data = tag; + } + + private TextView makeCrumbView(String name) { + TextView tv = new TextView(mContext); + tv.setTextAppearance(mContext, android.R.style.TextAppearance_Medium); + tv.setPadding(16, 0, 16, 0); + tv.setGravity(Gravity.CENTER_VERTICAL); + tv.setText(name); + tv.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.MATCH_PARENT)); + tv.setMaxWidth(mContext.getResources().getInteger( + R.integer.max_width_crumb)); + tv.setMaxLines(1); + tv.setEllipsize(TextUtils.TruncateAt.END); + return tv; + } + + } + +} diff --git a/src/com/android/browser/Browser.java b/src/com/android/browser/Browser.java index 7822ec88f..97b99672a 100644 --- a/src/com/android/browser/Browser.java +++ b/src/com/android/browser/Browser.java @@ -57,7 +57,7 @@ public class Browser extends Application { CookieSyncManager.createInstance(this); // remove all expired cookies CookieManager.getInstance().removeExpiredCookie(); - BrowserSettings.getInstance().loadFromDb(this); + BrowserSettings.getInstance().asyncLoadFromDb(this); } static Intent createBrowserViewIntent() { diff --git a/src/com/android/browser/BrowserActivity.java b/src/com/android/browser/BrowserActivity.java index 0a3fec9a4..984454745 100644 --- a/src/com/android/browser/BrowserActivity.java +++ b/src/com/android/browser/BrowserActivity.java @@ -17,154 +17,33 @@ package com.android.browser; import android.app.Activity; -import android.app.AlertDialog; -import android.app.DownloadManager; -import android.app.ProgressDialog; -import android.app.SearchManager; -import android.content.ActivityNotFoundException; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.ContentProvider; -import android.content.ContentProviderClient; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.content.res.Configuration; -import android.content.res.Resources; -import android.database.Cursor; -import android.database.DatabaseUtils; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Picture; import android.graphics.PixelFormat; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.net.Uri; -import android.net.WebAddress; -import android.net.http.SslCertificate; -import android.net.http.SslError; -import android.os.AsyncTask; import android.os.Bundle; -import android.os.Debug; -import android.os.Environment; -import android.os.Handler; -import android.os.Message; -import android.os.PowerManager; -import android.os.Process; -import android.os.ServiceManager; -import android.os.SystemClock; -import android.provider.Browser; -import android.provider.ContactsContract; -import android.provider.ContactsContract.Intents.Insert; -import android.provider.Downloads; -import android.provider.MediaStore; -import android.speech.RecognizerResultsIntent; -import android.text.IClipboard; -import android.text.TextUtils; -import android.text.format.DateFormat; -import android.util.AttributeSet; import android.util.Log; -import android.util.Patterns; +import android.view.ActionMode; import android.view.ContextMenu; -import android.view.Gravity; +import android.view.ContextMenu.ContextMenuInfo; import android.view.KeyEvent; -import android.view.LayoutInflater; import android.view.Menu; -import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.MenuItem.OnMenuItemClickListener; -import android.webkit.CookieManager; -import android.webkit.CookieSyncManager; -import android.webkit.DownloadListener; -import android.webkit.HttpAuthHandler; -import android.webkit.PluginManager; -import android.webkit.SslErrorHandler; -import android.webkit.URLUtil; -import android.webkit.ValueCallback; -import android.webkit.WebChromeClient; -import android.webkit.WebHistoryItem; -import android.webkit.WebIconDatabase; -import android.webkit.WebView; -import android.widget.EditText; -import android.widget.FrameLayout; -import android.widget.LinearLayout; -import android.widget.TextView; -import android.widget.Toast; -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AccountManagerFuture; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; -import android.accounts.AccountManagerCallback; - -import com.android.browser.search.SearchEngine; -import com.android.common.Search; -import com.android.common.speech.LoggingEvents; +import android.view.accessibility.AccessibilityManager; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.net.URLEncoder; -import java.text.ParseException; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +public class BrowserActivity extends Activity { -public class BrowserActivity extends Activity - implements View.OnCreateContextMenuListener, DownloadListener { - - /* Define some aliases to make these debugging flags easier to refer to. - * This file imports android.provider.Browser, so we can't just refer to "Browser.DEBUG". - */ - private final static boolean DEBUG = com.android.browser.Browser.DEBUG; - private final static boolean LOGV_ENABLED = com.android.browser.Browser.LOGV_ENABLED; - private final static boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED; + private final static String LOGTAG = "browser"; - private static class ClearThumbnails extends AsyncTask<File, Void, Void> { - @Override - public Void doInBackground(File... files) { - if (files != null) { - for (File f : files) { - if (!f.delete()) { - Log.e(LOGTAG, f.getPath() + " was not deleted"); - } - } - } - return null; - } - } + private final static boolean LOGV_ENABLED = + com.android.browser.Browser.LOGV_ENABLED; - /** - * This layout holds everything you see below the status bar, including the - * error console, the custom view container, and the webviews. - */ - private FrameLayout mBrowserFrameLayout; + private Controller mController; + private UI mUi; @Override public void onCreate(Bundle icicle) { @@ -172,8 +51,21 @@ public class BrowserActivity extends Activity Log.v(LOGTAG, this + " onStart"); } super.onCreate(icicle); - // test the browser in OpenGL - // requestWindowFeature(Window.FEATURE_OPENGL); + + // We load the first set of BrowserSettings from the db asynchronously + // but if it has not completed at this point, we have no choice but + // to block waiting for them to finish loading. :( + BrowserSettings.getInstance().waitForLoadFromDbToComplete(); + + // render the browser in OpenGL + if (BrowserSettings.getInstance().isHardwareAccelerated()) { + // Set the flag in the activity's window + this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, + WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); + } else { + // Clear the flag in the activity's window + this.getWindow().setFlags(0, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); + } // enable this to test the browser in 32bit if (false) { @@ -181,543 +73,41 @@ public class BrowserActivity extends Activity BitmapFactory.setDefaultConfig(Bitmap.Config.ARGB_8888); } - setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); - - mResolver = getContentResolver(); - - // Keep a settings instance handy. - mSettings = BrowserSettings.getInstance(); - // If this was a web search request, pass it on to the default web // search provider and finish this activity. - if (handleWebSearchIntent(getIntent())) { + if (IntentHandler.handleWebSearchIntent(this, null, getIntent())) { finish(); return; } - mSecLockIcon = Resources.getSystem().getDrawable( - android.R.drawable.ic_secure); - mMixLockIcon = Resources.getSystem().getDrawable( - android.R.drawable.ic_partial_secure); - - FrameLayout frameLayout = (FrameLayout) getWindow().getDecorView() - .findViewById(com.android.internal.R.id.content); - mBrowserFrameLayout = (FrameLayout) LayoutInflater.from(this) - .inflate(R.layout.custom_screen, null); - mContentView = (FrameLayout) mBrowserFrameLayout.findViewById( - R.id.main_content); - mErrorConsoleContainer = (LinearLayout) mBrowserFrameLayout - .findViewById(R.id.error_console); - mCustomViewContainer = (FrameLayout) mBrowserFrameLayout - .findViewById(R.id.fullscreen_custom_content); - frameLayout.addView(mBrowserFrameLayout, COVER_SCREEN_PARAMS); - mTitleBar = new TitleBar(this); - // mTitleBar will be always shown in the fully loaded mode - mTitleBar.setProgress(100); - mFakeTitleBar = new TitleBar(this); - - // Create the tab control and our initial tab - mTabControl = new TabControl(this); - - // Open the icon database and retain all the bookmark urls for favicons - retainIconsOnStartup(); - - mSettings.setTabControl(mTabControl); - - PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Browser"); - - // Find out if the network is currently up. - ConnectivityManager cm = (ConnectivityManager) getSystemService( - Context.CONNECTIVITY_SERVICE); - NetworkInfo info = cm.getActiveNetworkInfo(); - if (info != null) { - mIsNetworkUp = info.isAvailable(); - } - - /* enables registration for changes in network status from - http stack */ - mNetworkStateChangedFilter = new IntentFilter(); - mNetworkStateChangedFilter.addAction( - ConnectivityManager.CONNECTIVITY_ACTION); - mNetworkStateIntentReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals( - ConnectivityManager.CONNECTIVITY_ACTION)) { - - NetworkInfo info = intent.getParcelableExtra( - ConnectivityManager.EXTRA_NETWORK_INFO); - String typeName = info.getTypeName(); - String subtypeName = info.getSubtypeName(); - sendNetworkType(typeName.toLowerCase(), - (subtypeName != null ? subtypeName.toLowerCase() : "")); - - onNetworkToggle(info.isAvailable()); - } - } - }; - - IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); - filter.addAction(Intent.ACTION_PACKAGE_REMOVED); - filter.addDataScheme("package"); - mPackageInstallationReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - final String packageName = intent.getData() - .getSchemeSpecificPart(); - final boolean replacing = intent.getBooleanExtra( - Intent.EXTRA_REPLACING, false); - if (Intent.ACTION_PACKAGE_REMOVED.equals(action) && replacing) { - // if it is replacing, refreshPlugins() when adding - return; - } - - if (sGoogleApps.contains(packageName)) { - BrowserActivity.this.packageChanged(packageName, - Intent.ACTION_PACKAGE_ADDED.equals(action)); - } - - PackageManager pm = BrowserActivity.this.getPackageManager(); - PackageInfo pkgInfo = null; - try { - pkgInfo = pm.getPackageInfo(packageName, - PackageManager.GET_PERMISSIONS); - } catch (PackageManager.NameNotFoundException e) { - return; - } - if (pkgInfo != null) { - String permissions[] = pkgInfo.requestedPermissions; - if (permissions == null) { - return; - } - boolean permissionOk = false; - for (String permit : permissions) { - if (PluginManager.PLUGIN_PERMISSION.equals(permit)) { - permissionOk = true; - break; - } - } - if (permissionOk) { - PluginManager.getInstance(BrowserActivity.this) - .refreshPlugins( - Intent.ACTION_PACKAGE_ADDED - .equals(action)); - } - } - } - }; - registerReceiver(mPackageInstallationReceiver, filter); - - if (!mTabControl.restoreState(icicle)) { - // clear up the thumbnail directory if we can't restore the state as - // none of the files in the directory are referenced any more. - new ClearThumbnails().execute( - mTabControl.getThumbnailDir().listFiles()); - // there is no quit on Android. But if we can't restore the state, - // we can treat it as a new Browser, remove the old session cookies. - CookieManager.getInstance().removeSessionCookie(); - final Intent intent = getIntent(); - final Bundle extra = intent.getExtras(); - // Create an initial tab. - // If the intent is ACTION_VIEW and data is not null, the Browser is - // invoked to view the content by another application. In this case, - // the tab will be close when exit. - UrlData urlData = getUrlDataFromIntent(intent); - - String action = intent.getAction(); - final Tab t = mTabControl.createNewTab( - (Intent.ACTION_VIEW.equals(action) && - intent.getData() != null) - || RecognizerResultsIntent.ACTION_VOICE_SEARCH_RESULTS - .equals(action), - intent.getStringExtra(Browser.EXTRA_APPLICATION_ID), urlData.mUrl); - mTabControl.setCurrentTab(t); - attachTabToContentView(t); - WebView webView = t.getWebView(); - if (extra != null) { - int scale = extra.getInt(Browser.INITIAL_ZOOM_LEVEL, 0); - if (scale > 0 && scale <= 1000) { - webView.setInitialScale(scale); - } - } - - if (urlData.isEmpty()) { - loadUrl(webView, mSettings.getHomePage()); - } else { - loadUrlDataIn(t, urlData); - } + if (((AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE)) + .isEnabled()) { + setDefaultKeyMode(DEFAULT_KEYS_DISABLE); } else { - // TabControl.restoreState() will create a new tab even if - // restoring the state fails. - attachTabToContentView(mTabControl.getCurrentTab()); + setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); } - // Delete old thumbnails to save space - File dir = mTabControl.getThumbnailDir(); - if (dir.exists()) { - for (String child : dir.list()) { - File f = new File(dir, child); - f.delete(); - } - } - - // Read JavaScript flags if it exists. - String jsFlags = mSettings.getJsFlags(); - if (jsFlags.trim().length() != 0) { - mTabControl.getCurrentWebView().setJsFlags(jsFlags); - } - // Work out which packages are installed on the system. - getInstalledPackages(); + mController = new Controller(this); + mUi = new BaseUi(this, mController); + mController.setUi(mUi); + mController.setWebViewFactory((BaseUi) mUi); - // Start watching the default geolocation permissions - mSystemAllowGeolocationOrigins - = new SystemAllowGeolocationOrigins(getApplicationContext()); - mSystemAllowGeolocationOrigins.start(); + mController.start(icicle, getIntent()); } - /** - * Feed the previously stored results strings to the BrowserProvider so that - * the SearchDialog will show them instead of the standard searches. - * @param result String to show on the editable line of the SearchDialog. - */ - /* package */ void showVoiceSearchResults(String result) { - ContentProviderClient client = mResolver.acquireContentProviderClient( - Browser.BOOKMARKS_URI); - ContentProvider prov = client.getLocalContentProvider(); - BrowserProvider bp = (BrowserProvider) prov; - bp.setQueryResults(mTabControl.getCurrentTab().getVoiceSearchResults()); - client.release(); + Controller getController() { + return mController; + } - Bundle bundle = createGoogleSearchSourceBundle( - GOOGLE_SEARCH_SOURCE_SEARCHKEY); - bundle.putBoolean(SearchManager.CONTEXT_IS_VOICE, true); - startSearch(result, false, bundle, false); + // TODO: this is here for the test classes + // remove once tests are fixed + TabControl getTabControl() { + return mController.getTabControl(); } @Override protected void onNewIntent(Intent intent) { - Tab current = mTabControl.getCurrentTab(); - // When a tab is closed on exit, the current tab index is set to -1. - // Reset before proceed as Browser requires the current tab to be set. - if (current == null) { - // Try to reset the tab in case the index was incorrect. - current = mTabControl.getTab(0); - if (current == null) { - // No tabs at all so just ignore this intent. - return; - } - mTabControl.setCurrentTab(current); - attachTabToContentView(current); - resetTitleAndIcon(current.getWebView()); - } - final String action = intent.getAction(); - final int flags = intent.getFlags(); - if (Intent.ACTION_MAIN.equals(action) || - (flags & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) { - // just resume the browser - return; - } - // In case the SearchDialog is open. - ((SearchManager) getSystemService(Context.SEARCH_SERVICE)) - .stopSearch(); - boolean activateVoiceSearch = RecognizerResultsIntent - .ACTION_VOICE_SEARCH_RESULTS.equals(action); - if (Intent.ACTION_VIEW.equals(action) - || Intent.ACTION_SEARCH.equals(action) - || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action) - || Intent.ACTION_WEB_SEARCH.equals(action) - || activateVoiceSearch) { - if (current.isInVoiceSearchMode()) { - String title = current.getVoiceDisplayTitle(); - if (title != null && title.equals(intent.getStringExtra( - SearchManager.QUERY))) { - // The user submitted the same search as the last voice - // search, so do nothing. - return; - } - if (Intent.ACTION_SEARCH.equals(action) - && current.voiceSearchSourceIsGoogle()) { - Intent logIntent = new Intent( - LoggingEvents.ACTION_LOG_EVENT); - logIntent.putExtra(LoggingEvents.EXTRA_EVENT, - LoggingEvents.VoiceSearch.QUERY_UPDATED); - logIntent.putExtra( - LoggingEvents.VoiceSearch.EXTRA_QUERY_UPDATED_VALUE, - intent.getDataString()); - sendBroadcast(logIntent); - // Note, onPageStarted will revert the voice title bar - // When http://b/issue?id=2379215 is fixed, we should update - // the title bar here. - } - } - // If this was a search request (e.g. search query directly typed into the address bar), - // pass it on to the default web search provider. - if (handleWebSearchIntent(intent)) { - return; - } - - UrlData urlData = getUrlDataFromIntent(intent); - if (urlData.isEmpty()) { - urlData = new UrlData(mSettings.getHomePage()); - } - - final String appId = intent - .getStringExtra(Browser.EXTRA_APPLICATION_ID); - if ((Intent.ACTION_VIEW.equals(action) - // If a voice search has no appId, it means that it came - // from the browser. In that case, reuse the current tab. - || (activateVoiceSearch && appId != null)) - && !getPackageName().equals(appId) - && (flags & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) { - Tab appTab = mTabControl.getTabFromId(appId); - if (appTab != null) { - Log.i(LOGTAG, "Reusing tab for " + appId); - // Dismiss the subwindow if applicable. - dismissSubWindow(appTab); - // Since we might kill the WebView, remove it from the - // content view first. - removeTabFromContentView(appTab); - // Recreate the main WebView after destroying the old one. - // If the WebView has the same original url and is on that - // page, it can be reused. - boolean needsLoad = - mTabControl.recreateWebView(appTab, urlData); - - if (current != appTab) { - switchToTab(mTabControl.getTabIndex(appTab)); - if (needsLoad) { - loadUrlDataIn(appTab, urlData); - } - } else { - // If the tab was the current tab, we have to attach - // it to the view system again. - attachTabToContentView(appTab); - if (needsLoad) { - loadUrlDataIn(appTab, urlData); - } - } - return; - } else { - // No matching application tab, try to find a regular tab - // with a matching url. - appTab = mTabControl.findUnusedTabWithUrl(urlData.mUrl); - if (appTab != null) { - if (current != appTab) { - switchToTab(mTabControl.getTabIndex(appTab)); - } - // Otherwise, we are already viewing the correct tab. - } else { - // if FLAG_ACTIVITY_BROUGHT_TO_FRONT flag is on, the url - // will be opened in a new tab unless we have reached - // MAX_TABS. Then the url will be opened in the current - // tab. If a new tab is created, it will have "true" for - // exit on close. - openTabAndShow(urlData, true, appId); - } - } - } else { - if (!urlData.isEmpty() - && urlData.mUrl.startsWith("about:debug")) { - if ("about:debug.dom".equals(urlData.mUrl)) { - current.getWebView().dumpDomTree(false); - } else if ("about:debug.dom.file".equals(urlData.mUrl)) { - current.getWebView().dumpDomTree(true); - } else if ("about:debug.render".equals(urlData.mUrl)) { - current.getWebView().dumpRenderTree(false); - } else if ("about:debug.render.file".equals(urlData.mUrl)) { - current.getWebView().dumpRenderTree(true); - } else if ("about:debug.display".equals(urlData.mUrl)) { - current.getWebView().dumpDisplayTree(); - } else if (urlData.mUrl.startsWith("about:debug.drag")) { - int index = urlData.mUrl.codePointAt(16) - '0'; - if (index <= 0 || index > 9) { - current.getWebView().setDragTracker(null); - } else { - current.getWebView().setDragTracker(new MeshTracker(index)); - } - } else { - mSettings.toggleDebugSettings(); - } - return; - } - // Get rid of the subwindow if it exists - dismissSubWindow(current); - // If the current Tab is being used as an application tab, - // remove the association, since the new Intent means that it is - // no longer associated with that application. - current.setAppId(null); - loadUrlDataIn(current, urlData); - } - } - } - - /** - * Launches the default web search activity with the query parameters if the given intent's data - * are identified as plain search terms and not URLs/shortcuts. - * @return true if the intent was handled and web search activity was launched, false if not. - */ - private boolean handleWebSearchIntent(Intent intent) { - if (intent == null) return false; - - String url = null; - final String action = intent.getAction(); - if (RecognizerResultsIntent.ACTION_VOICE_SEARCH_RESULTS.equals( - action)) { - return false; - } - if (Intent.ACTION_VIEW.equals(action)) { - Uri data = intent.getData(); - if (data != null) url = data.toString(); - } else if (Intent.ACTION_SEARCH.equals(action) - || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action) - || Intent.ACTION_WEB_SEARCH.equals(action)) { - url = intent.getStringExtra(SearchManager.QUERY); - } - return handleWebSearchRequest(url, intent.getBundleExtra(SearchManager.APP_DATA), - intent.getStringExtra(SearchManager.EXTRA_DATA_KEY)); - } - - /** - * Launches the default web search activity with the query parameters if the given url string - * was identified as plain search terms and not URL/shortcut. - * @return true if the request was handled and web search activity was launched, false if not. - */ - private boolean handleWebSearchRequest(String inUrl, Bundle appData, String extraData) { - if (inUrl == null) return false; - - // In general, we shouldn't modify URL from Intent. - // But currently, we get the user-typed URL from search box as well. - String url = fixUrl(inUrl).trim(); - - // URLs are handled by the regular flow of control, so - // return early. - if (Patterns.WEB_URL.matcher(url).matches() - || ACCEPTED_URI_SCHEMA.matcher(url).matches()) { - return false; - } - - final ContentResolver cr = mResolver; - final String newUrl = url; - new AsyncTask<Void, Void, Void>() { - protected Void doInBackground(Void... unused) { - Browser.updateVisitedHistory(cr, newUrl, false); - Browser.addSearchUrl(cr, newUrl); - return null; - } - }.execute(); - - SearchEngine searchEngine = mSettings.getSearchEngine(); - if (searchEngine == null) return false; - searchEngine.startSearch(this, url, appData, extraData); - - return true; - } - - private UrlData getUrlDataFromIntent(Intent intent) { - String url = ""; - Map<String, String> headers = null; - if (intent != null) { - final String action = intent.getAction(); - if (Intent.ACTION_VIEW.equals(action)) { - url = smartUrlFilter(intent.getData()); - if (url != null && url.startsWith("content:")) { - /* Append mimetype so webview knows how to display */ - String mimeType = intent.resolveType(getContentResolver()); - if (mimeType != null) { - url += "?" + mimeType; - } - } - if (url != null && url.startsWith("http")) { - final Bundle pairs = intent - .getBundleExtra(Browser.EXTRA_HEADERS); - if (pairs != null && !pairs.isEmpty()) { - Iterator<String> iter = pairs.keySet().iterator(); - headers = new HashMap<String, String>(); - while (iter.hasNext()) { - String key = iter.next(); - headers.put(key, pairs.getString(key)); - } - } - } - } else if (Intent.ACTION_SEARCH.equals(action) - || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action) - || Intent.ACTION_WEB_SEARCH.equals(action)) { - url = intent.getStringExtra(SearchManager.QUERY); - if (url != null) { - mLastEnteredUrl = url; - // In general, we shouldn't modify URL from Intent. - // But currently, we get the user-typed URL from search box as well. - url = fixUrl(url); - url = smartUrlFilter(url); - final ContentResolver cr = mResolver; - final String newUrl = url; - new AsyncTask<Void, Void, Void>() { - protected Void doInBackground(Void... unused) { - Browser.updateVisitedHistory(cr, newUrl, false); - return null; - } - }.execute(); - String searchSource = "&source=android-" + GOOGLE_SEARCH_SOURCE_SUGGEST + "&"; - if (url.contains(searchSource)) { - String source = null; - final Bundle appData = intent.getBundleExtra(SearchManager.APP_DATA); - if (appData != null) { - source = appData.getString(Search.SOURCE); - } - if (TextUtils.isEmpty(source)) { - source = GOOGLE_SEARCH_SOURCE_UNKNOWN; - } - url = url.replace(searchSource, "&source=android-"+source+"&"); - } - } - } - } - return new UrlData(url, headers, intent); - } - /* package */ void showVoiceTitleBar(String title) { - mTitleBar.setInVoiceMode(true); - mFakeTitleBar.setInVoiceMode(true); - - mTitleBar.setDisplayTitle(title); - mFakeTitleBar.setDisplayTitle(title); - } - /* package */ void revertVoiceTitleBar() { - mTitleBar.setInVoiceMode(false); - mFakeTitleBar.setInVoiceMode(false); - - mTitleBar.setDisplayTitle(mUrl); - mFakeTitleBar.setDisplayTitle(mUrl); - } - /* package */ static String fixUrl(String inUrl) { - // FIXME: Converting the url to lower case - // duplicates functionality in smartUrlFilter(). - // However, changing all current callers of fixUrl to - // call smartUrlFilter in addition may have unwanted - // consequences, and is deferred for now. - int colon = inUrl.indexOf(':'); - boolean allLower = true; - for (int index = 0; index < colon; index++) { - char ch = inUrl.charAt(index); - if (!Character.isLetter(ch)) { - break; - } - allLower &= Character.isLowerCase(ch); - if (index == colon - 1 && !allLower) { - inUrl = inUrl.substring(0, colon).toLowerCase() - + inUrl.substring(colon); - } - } - if (inUrl.startsWith("http://") || inUrl.startsWith("https://")) - return inUrl; - if (inUrl.startsWith("http:") || - inUrl.startsWith("https:")) { - if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) { - inUrl = inUrl.replaceFirst("/", "//"); - } else inUrl = inUrl.replaceFirst(":", "://"); - } - return inUrl; + mController.handleNewIntent(intent); } @Override @@ -726,170 +116,26 @@ public class BrowserActivity extends Activity if (LOGV_ENABLED) { Log.v(LOGTAG, "BrowserActivity.onResume: this=" + this); } - - if (!mActivityInPause) { - Log.e(LOGTAG, "BrowserActivity is already resumed."); - return; - } - - mTabControl.resumeCurrentTab(); - mActivityInPause = false; - resumeWebViewTimers(); - - if (mWakeLock.isHeld()) { - mHandler.removeMessages(RELEASE_WAKELOCK); - mWakeLock.release(); - } - - registerReceiver(mNetworkStateIntentReceiver, - mNetworkStateChangedFilter); - WebView.enablePlatformNotifications(); + mController.onResume(); } - /** - * Since the actual title bar is embedded in the WebView, and removing it - * would change its appearance, use a different TitleBar to show overlayed - * at the top of the screen, when the menu is open or the page is loading. - */ - private TitleBar mFakeTitleBar; - - /** - * Keeps track of whether the options menu is open. This is important in - * determining whether to show or hide the title bar overlay. - */ - private boolean mOptionsMenuOpen; - - /** - * Only meaningful when mOptionsMenuOpen is true. This variable keeps track - * of whether the configuration has changed. The first onMenuOpened call - * after a configuration change is simply a reopening of the same menu - * (i.e. mIconView did not change). - */ - private boolean mConfigChanged; - - /** - * Whether or not the options menu is in its smaller, icon menu form. When - * true, we want the title bar overlay to be up. When false, we do not. - * Only meaningful if mOptionsMenuOpen is true. - */ - private boolean mIconView; - @Override public boolean onMenuOpened(int featureId, Menu menu) { if (Window.FEATURE_OPTIONS_PANEL == featureId) { - if (mOptionsMenuOpen) { - if (mConfigChanged) { - // We do not need to make any changes to the state of the - // title bar, since the only thing that happened was a - // change in orientation - mConfigChanged = false; - } else { - if (mIconView) { - // Switching the menu to expanded view, so hide the - // title bar. - hideFakeTitleBar(); - mIconView = false; - } else { - // Switching the menu back to icon view, so show the - // title bar once again. - showFakeTitleBar(); - mIconView = true; - } - } - } else { - // The options menu is closed, so open it, and show the title - showFakeTitleBar(); - mOptionsMenuOpen = true; - mConfigChanged = false; - mIconView = true; - } + mController.onMenuOpened(featureId, menu); } return true; } - private void showFakeTitleBar() { - if (mFakeTitleBar.getParent() == null && mActiveTabsPage == null - && !mActivityInPause) { - WebView mainView = mTabControl.getCurrentWebView(); - // if there is no current WebView, don't show the faked title bar; - if (mainView == null) { - return; - } - // Do not need to check for null, since the current tab will have - // at least a main WebView, or we would have returned above. - if (dialogIsUp()) { - // Do not show the fake title bar, which would cover up the - // find or select dialog. - return; - } - - WindowManager manager - = (WindowManager) getSystemService(Context.WINDOW_SERVICE); - - // Add the title bar to the window manager so it can receive touches - // while the menu is up - WindowManager.LayoutParams params - = new WindowManager.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT, - WindowManager.LayoutParams.TYPE_APPLICATION, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, - PixelFormat.TRANSLUCENT); - params.gravity = Gravity.TOP; - boolean atTop = mainView.getScrollY() == 0; - params.windowAnimations = atTop ? 0 : R.style.TitleBar; - manager.addView(mFakeTitleBar, params); - } - } - @Override public void onOptionsMenuClosed(Menu menu) { - mOptionsMenuOpen = false; - if (!mInLoad) { - hideFakeTitleBar(); - } else if (!mIconView) { - // The page is currently loading, and we are in expanded mode, so - // we were not showing the menu. Show it once again. It will be - // removed when the page finishes. - showFakeTitleBar(); - } - } - - private void hideFakeTitleBar() { - if (mFakeTitleBar.getParent() == null) return; - WindowManager.LayoutParams params = (WindowManager.LayoutParams) - mFakeTitleBar.getLayoutParams(); - WebView mainView = mTabControl.getCurrentWebView(); - // Although we decided whether or not to animate based on the current - // scroll position, the scroll position may have changed since the - // fake title bar was displayed. Make sure it has the appropriate - // animation/lack thereof before removing. - params.windowAnimations = mainView != null && mainView.getScrollY() == 0 - ? 0 : R.style.TitleBar; - WindowManager manager - = (WindowManager) getSystemService(Context.WINDOW_SERVICE); - manager.updateViewLayout(mFakeTitleBar, params); - manager.removeView(mFakeTitleBar); - } - - /** - * Special method for the fake title bar to call when displaying its context - * menu, since it is in its own Window, and its parent does not show a - * context menu. - */ - /* package */ void showTitleBarContextMenu() { - if (null == mTitleBar.getParent()) { - return; - } - openContextMenu(mTitleBar); + mController.onOptionsMenuClosed(menu); } @Override public void onContextMenuClosed(Menu menu) { super.onContextMenuClosed(menu); - if (mInLoad) { - showFakeTitleBar(); - } + mController.onContextMenuClosed(menu); } /** @@ -902,46 +148,13 @@ public class BrowserActivity extends Activity if (LOGV_ENABLED) { Log.v(LOGTAG, "BrowserActivity.onSaveInstanceState: this=" + this); } - // the default implementation requires each view to have an id. As the - // browser handles the state itself and it doesn't use id for the views, - // don't call the default implementation. Otherwise it will trigger the - // warning like this, "couldn't save which view has focus because the - // focused view XXX has no id". - - // Save all the tabs - mTabControl.saveState(outState); + mController.onSaveInstanceState(outState); } @Override protected void onPause() { + mController.onPause(); super.onPause(); - - if (mActivityInPause) { - Log.e(LOGTAG, "BrowserActivity is already paused."); - return; - } - - mTabControl.pauseCurrentTab(); - mActivityInPause = true; - if (mTabControl.getCurrentIndex() >= 0 && !pauseWebViewTimers()) { - mWakeLock.acquire(); - mHandler.sendMessageDelayed(mHandler - .obtainMessage(RELEASE_WAKELOCK), WAKELOCK_TIMEOUT); - } - - // FIXME: This removes the active tabs page and resets the menu to - // MAIN_MENU. A better solution might be to do this work in onNewIntent - // but then we would need to save it in onSaveInstanceState and restore - // it in onCreate/onRestoreInstanceState - if (mActiveTabsPage != null) { - removeActiveTabPage(true); - } - - cancelStopToast(); - - // unregister network state listener - unregisterReceiver(mNetworkStateIntentReceiver); - WebView.disablePlatformNotifications(); } @Override @@ -950,3070 +163,83 @@ public class BrowserActivity extends Activity Log.v(LOGTAG, "BrowserActivity.onDestroy: this=" + this); } super.onDestroy(); - - if (mUploadMessage != null) { - mUploadMessage.onReceiveValue(null); - mUploadMessage = null; - } - - if (mTabControl == null) return; - - // Remove the fake title bar if it is there - hideFakeTitleBar(); - - // Remove the current tab and sub window - Tab t = mTabControl.getCurrentTab(); - if (t != null) { - dismissSubWindow(t); - removeTabFromContentView(t); - } - // Destroy all the tabs - mTabControl.destroy(); - WebIconDatabase.getInstance().close(); - - unregisterReceiver(mPackageInstallationReceiver); - - // Stop watching the default geolocation permissions - mSystemAllowGeolocationOrigins.stop(); - mSystemAllowGeolocationOrigins = null; + mController.onDestroy(); + mUi = null; + mController = null; } @Override public void onConfigurationChanged(Configuration newConfig) { - mConfigChanged = true; super.onConfigurationChanged(newConfig); - - if (mPageInfoDialog != null) { - mPageInfoDialog.dismiss(); - showPageInfo( - mPageInfoView, - mPageInfoFromShowSSLCertificateOnError); - } - if (mSSLCertificateDialog != null) { - mSSLCertificateDialog.dismiss(); - showSSLCertificate( - mSSLCertificateView); - } - if (mSSLCertificateOnErrorDialog != null) { - mSSLCertificateOnErrorDialog.dismiss(); - showSSLCertificateOnError( - mSSLCertificateOnErrorView, - mSSLCertificateOnErrorHandler, - mSSLCertificateOnErrorError); - } - if (mHttpAuthenticationDialog != null) { - String title = ((TextView) mHttpAuthenticationDialog - .findViewById(com.android.internal.R.id.alertTitle)).getText() - .toString(); - String name = ((TextView) mHttpAuthenticationDialog - .findViewById(R.id.username_edit)).getText().toString(); - String password = ((TextView) mHttpAuthenticationDialog - .findViewById(R.id.password_edit)).getText().toString(); - int focusId = mHttpAuthenticationDialog.getCurrentFocus() - .getId(); - mHttpAuthenticationDialog.dismiss(); - showHttpAuthentication(mHttpAuthHandler, null, null, title, - name, password, focusId); - } + mController.onConfgurationChanged(newConfig); } @Override public void onLowMemory() { super.onLowMemory(); - mTabControl.freeMemory(); - } - - private void resumeWebViewTimers() { - Tab tab = mTabControl.getCurrentTab(); - if (tab == null) return; // monkey can trigger this - boolean inLoad = tab.inLoad(); - if ((!mActivityInPause && !inLoad) || (mActivityInPause && inLoad)) { - CookieSyncManager.getInstance().startSync(); - WebView w = tab.getWebView(); - if (w != null) { - w.resumeTimers(); - } - } - } - - private boolean pauseWebViewTimers() { - Tab tab = mTabControl.getCurrentTab(); - boolean inLoad = tab.inLoad(); - if (mActivityInPause && !inLoad) { - CookieSyncManager.getInstance().stopSync(); - WebView w = mTabControl.getCurrentWebView(); - if (w != null) { - w.pauseTimers(); - } - return true; - } else { - return false; - } - } - - // Open the icon database and retain all the icons for visited sites. - private void retainIconsOnStartup() { - final WebIconDatabase db = WebIconDatabase.getInstance(); - db.open(getDir("icons", 0).getPath()); - Cursor c = null; - try { - c = Browser.getAllBookmarks(mResolver); - if (c.moveToFirst()) { - int urlIndex = c.getColumnIndex(Browser.BookmarkColumns.URL); - do { - String url = c.getString(urlIndex); - db.retainIconForPageUrl(url); - } while (c.moveToNext()); - } - } catch (IllegalStateException e) { - Log.e(LOGTAG, "retainIconsOnStartup", e); - } finally { - if (c!= null) c.close(); - } - } - - // Helper method for getting the top window. - WebView getTopWindow() { - return mTabControl.getCurrentTopWebView(); - } - - TabControl getTabControl() { - return mTabControl; + mController.onLowMemory(); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); - - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.browser, menu); - mMenu = menu; - updateInLoadMenuItems(); - return true; - } - - /** - * As the menu can be open when loading state changes - * we must manually update the state of the stop/reload menu - * item - */ - private void updateInLoadMenuItems() { - if (mMenu == null) { - return; - } - MenuItem src = mInLoad ? - mMenu.findItem(R.id.stop_menu_id): - mMenu.findItem(R.id.reload_menu_id); - MenuItem dest = mMenu.findItem(R.id.stop_reload_menu_id); - dest.setIcon(src.getIcon()); - dest.setTitle(src.getTitle()); - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - // chording is not an issue with context menus, but we use the same - // options selector, so set mCanChord to true so we can access them. - mCanChord = true; - int id = item.getItemId(); - boolean result = true; - switch (id) { - // For the context menu from the title bar - case R.id.title_bar_copy_page_url: - Tab currentTab = mTabControl.getCurrentTab(); - if (null == currentTab) { - result = false; - break; - } - WebView mainView = currentTab.getWebView(); - if (null == mainView) { - result = false; - break; - } - copy(mainView.getUrl()); - break; - // -- Browser context menu - case R.id.open_context_menu_id: - case R.id.open_newtab_context_menu_id: - case R.id.bookmark_context_menu_id: - case R.id.save_link_context_menu_id: - case R.id.share_link_context_menu_id: - case R.id.copy_link_context_menu_id: - final WebView webView = getTopWindow(); - if (null == webView) { - result = false; - break; - } - final HashMap hrefMap = new HashMap(); - hrefMap.put("webview", webView); - final Message msg = mHandler.obtainMessage( - FOCUS_NODE_HREF, id, 0, hrefMap); - webView.requestFocusNodeHref(msg); - break; - - default: - // For other context menus - result = onOptionsItemSelected(item); - } - mCanChord = false; - return result; - } - - private Bundle createGoogleSearchSourceBundle(String source) { - Bundle bundle = new Bundle(); - bundle.putString(Search.SOURCE, source); - return bundle; - } - - /* package */ void editUrl() { - if (mOptionsMenuOpen) closeOptionsMenu(); - String url = (getTopWindow() == null) ? null : getTopWindow().getUrl(); - startSearch(mSettings.getHomePage().equals(url) ? null : url, true, - null, false); + return mController.onCreateOptionsMenu(menu); } - /** - * Overriding this to insert a local information bundle - */ @Override - public void startSearch(String initialQuery, boolean selectInitialQuery, - Bundle appSearchData, boolean globalSearch) { - if (appSearchData == null) { - appSearchData = createGoogleSearchSourceBundle(GOOGLE_SEARCH_SOURCE_TYPE); - } - - SearchEngine searchEngine = mSettings.getSearchEngine(); - if (searchEngine != null && !searchEngine.supportsVoiceSearch()) { - appSearchData.putBoolean(SearchManager.DISABLE_VOICE_SEARCH, true); - } - - super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch); - } - - /** - * Switch tabs. Called by the TitleBarSet when sliding the title bar - * results in changing tabs. - * @param index Index of the tab to change to, as defined by - * mTabControl.getTabIndex(Tab t). - * @return boolean True if we successfully switched to a different tab. If - * the indexth tab is null, or if that tab is the same as - * the current one, return false. - */ - /* package */ boolean switchToTab(int index) { - Tab tab = mTabControl.getTab(index); - Tab currentTab = mTabControl.getCurrentTab(); - if (tab == null || tab == currentTab) { - return false; - } - if (currentTab != null) { - // currentTab may be null if it was just removed. In that case, - // we do not need to remove it - removeTabFromContentView(currentTab); - } - mTabControl.setCurrentTab(tab); - attachTabToContentView(tab); - resetTitleIconAndProgress(); - updateLockIconToLatest(); - return true; - } - - /* package */ Tab openTabToHomePage() { - return openTabAndShow(mSettings.getHomePage(), false, null); - } - - /* package */ void closeCurrentWindow() { - final Tab current = mTabControl.getCurrentTab(); - if (mTabControl.getTabCount() == 1) { - // This is the last tab. Open a new one, with the home - // page and close the current one. - openTabToHomePage(); - closeTab(current); - return; - } - final Tab parent = current.getParentTab(); - int indexToShow = -1; - if (parent != null) { - indexToShow = mTabControl.getTabIndex(parent); - } else { - final int currentIndex = mTabControl.getCurrentIndex(); - // Try to move to the tab to the right - indexToShow = currentIndex + 1; - if (indexToShow > mTabControl.getTabCount() - 1) { - // Try to move to the tab to the left - indexToShow = currentIndex - 1; - } - } - if (switchToTab(indexToShow)) { - // Close window - closeTab(current); - } - } - - private ActiveTabsPage mActiveTabsPage; - - /** - * Remove the active tabs page. - * @param needToAttach If true, the active tabs page did not attach a tab - * to the content view, so we need to do that here. - */ - /* package */ void removeActiveTabPage(boolean needToAttach) { - mContentView.removeView(mActiveTabsPage); - mActiveTabsPage = null; - mMenuState = R.id.MAIN_MENU; - if (needToAttach) { - attachTabToContentView(mTabControl.getCurrentTab()); - } - getTopWindow().requestFocus(); - } - - private WebView showDialog(WebDialog dialog) { - Tab tab = mTabControl.getCurrentTab(); - if (tab.getSubWebView() == null) { - // If the find or select is being performed on the main webview, - // remove the embedded title bar. - WebView mainView = tab.getWebView(); - if (mainView != null) { - mainView.setEmbeddedTitleBar(null); - } - } - hideFakeTitleBar(); - mMenuState = EMPTY_MENU; - return tab.showDialog(dialog); + public boolean onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + return mController.prepareOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { - if (!mCanChord) { - // The user has already fired a shortcut with this hold down of the - // menu key. - return false; - } - if (null == getTopWindow()) { - return false; - } - if (mMenuIsDown) { - // The shortcut action consumes the MENU. Even if it is still down, - // it won't trigger the next shortcut action. In the case of the - // shortcut action triggering a new activity, like Bookmarks, we - // won't get onKeyUp for MENU. So it is important to reset it here. - mMenuIsDown = false; - } - switch (item.getItemId()) { - // -- Main menu - case R.id.new_tab_menu_id: - openTabToHomePage(); - break; - - case R.id.goto_menu_id: - editUrl(); - break; - - case R.id.bookmarks_menu_id: - bookmarksOrHistoryPicker(false); - break; - - case R.id.active_tabs_menu_id: - mActiveTabsPage = new ActiveTabsPage(this, mTabControl); - removeTabFromContentView(mTabControl.getCurrentTab()); - hideFakeTitleBar(); - mContentView.addView(mActiveTabsPage, COVER_SCREEN_PARAMS); - mActiveTabsPage.requestFocus(); - mMenuState = EMPTY_MENU; - break; - - case R.id.add_bookmark_menu_id: - Intent i = new Intent(BrowserActivity.this, - AddBookmarkPage.class); - WebView w = getTopWindow(); - i.putExtra("url", w.getUrl()); - i.putExtra("title", w.getTitle()); - i.putExtra("touch_icon_url", w.getTouchIconUrl()); - i.putExtra("thumbnail", createScreenshot(w)); - startActivity(i); - break; - - case R.id.stop_reload_menu_id: - if (mInLoad) { - stopLoading(); - } else { - getTopWindow().reload(); - } - break; - - case R.id.back_menu_id: - getTopWindow().goBack(); - break; - - case R.id.forward_menu_id: - getTopWindow().goForward(); - break; - - case R.id.close_menu_id: - // Close the subwindow if it exists. - if (mTabControl.getCurrentSubWindow() != null) { - dismissSubWindow(mTabControl.getCurrentTab()); - break; - } - closeCurrentWindow(); - break; - - case R.id.homepage_menu_id: - Tab current = mTabControl.getCurrentTab(); - if (current != null) { - dismissSubWindow(current); - loadUrl(current.getWebView(), mSettings.getHomePage()); - } - break; - - case R.id.preferences_menu_id: - Intent intent = new Intent(this, - BrowserPreferencesPage.class); - intent.putExtra(BrowserPreferencesPage.CURRENT_PAGE, - getTopWindow().getUrl()); - startActivityForResult(intent, PREFERENCES_PAGE); - break; - - case R.id.find_menu_id: - showFindDialog(); - break; - - case R.id.select_text_id: - if (true) { - Tab currentTab = mTabControl.getCurrentTab(); - if (currentTab != null) { - currentTab.getWebView().setUpSelect(); - } - } else { - showSelectDialog(); - } - break; - - case R.id.page_info_menu_id: - showPageInfo(mTabControl.getCurrentTab(), false); - break; - - case R.id.classic_history_menu_id: - bookmarksOrHistoryPicker(true); - break; - - case R.id.title_bar_share_page_url: - case R.id.share_page_menu_id: - Tab currentTab = mTabControl.getCurrentTab(); - if (null == currentTab) { - mCanChord = false; - return false; - } - currentTab.populatePickerData(); - sharePage(this, currentTab.getTitle(), - currentTab.getUrl(), currentTab.getFavicon(), - createScreenshot(currentTab.getWebView())); - break; - - case R.id.dump_nav_menu_id: - getTopWindow().debugDump(); - break; - - case R.id.dump_counters_menu_id: - getTopWindow().dumpV8Counters(); - break; - - case R.id.zoom_in_menu_id: - getTopWindow().zoomIn(); - break; - - case R.id.zoom_out_menu_id: - getTopWindow().zoomOut(); - break; - - case R.id.view_downloads_menu_id: - viewDownloads(); - break; - - case R.id.window_one_menu_id: - case R.id.window_two_menu_id: - case R.id.window_three_menu_id: - case R.id.window_four_menu_id: - case R.id.window_five_menu_id: - case R.id.window_six_menu_id: - case R.id.window_seven_menu_id: - case R.id.window_eight_menu_id: - { - int menuid = item.getItemId(); - for (int id = 0; id < WINDOW_SHORTCUT_ID_ARRAY.length; id++) { - if (WINDOW_SHORTCUT_ID_ARRAY[id] == menuid) { - Tab desiredTab = mTabControl.getTab(id); - if (desiredTab != null && - desiredTab != mTabControl.getCurrentTab()) { - switchToTab(id); - } - break; - } - } - } - break; - - default: - if (!super.onOptionsItemSelected(item)) { - return false; - } - // Otherwise fall through. - } - mCanChord = false; - return true; - } - - private boolean dialogIsUp() { - return null != mFindDialog && mFindDialog.isVisible() || - null != mSelectDialog && mSelectDialog.isVisible(); - } - - private boolean closeDialog(WebDialog dialog) { - if (null == dialog || !dialog.isVisible()) return false; - Tab currentTab = mTabControl.getCurrentTab(); - currentTab.closeDialog(dialog); - dialog.dismiss(); - return true; - } - - /* - * Remove the find dialog or select dialog. - */ - public void closeDialogs() { - if (!(closeDialog(mFindDialog) || closeDialog(mSelectDialog))) return; - // If the Find was being performed in the main WebView, replace the - // embedded title bar. - Tab currentTab = mTabControl.getCurrentTab(); - if (currentTab.getSubWebView() == null) { - WebView mainView = currentTab.getWebView(); - if (mainView != null) { - mainView.setEmbeddedTitleBar(mTitleBar); - } - } - mMenuState = R.id.MAIN_MENU; - if (mInLoad) { - // The title bar was hidden, because otherwise it would cover up the - // find or select dialog. Now that the dialog has been removed, - // show the fake title bar once again. - showFakeTitleBar(); - } - } - - public void showFindDialog() { - if (null == mFindDialog) { - mFindDialog = new FindDialog(this); - } - showDialog(mFindDialog).setFindIsUp(true); - } - - public void setFindDialogText(String text) { - mFindDialog.setText(text); - } - - public void showSelectDialog() { - if (null == mSelectDialog) { - mSelectDialog = new SelectDialog(this); - } - showDialog(mSelectDialog).setUpSelect(); - mSelectDialog.hideSoftInput(); - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - // This happens when the user begins to hold down the menu key, so - // allow them to chord to get a shortcut. - mCanChord = true; - // Note: setVisible will decide whether an item is visible; while - // setEnabled() will decide whether an item is enabled, which also means - // whether the matching shortcut key will function. - super.onPrepareOptionsMenu(menu); - switch (mMenuState) { - case EMPTY_MENU: - if (mCurrentMenuState != mMenuState) { - menu.setGroupVisible(R.id.MAIN_MENU, false); - menu.setGroupEnabled(R.id.MAIN_MENU, false); - menu.setGroupEnabled(R.id.MAIN_SHORTCUT_MENU, false); - } - break; - default: - if (mCurrentMenuState != mMenuState) { - menu.setGroupVisible(R.id.MAIN_MENU, true); - menu.setGroupEnabled(R.id.MAIN_MENU, true); - menu.setGroupEnabled(R.id.MAIN_SHORTCUT_MENU, true); - } - final WebView w = getTopWindow(); - boolean canGoBack = false; - boolean canGoForward = false; - boolean isHome = false; - if (w != null) { - canGoBack = w.canGoBack(); - canGoForward = w.canGoForward(); - isHome = mSettings.getHomePage().equals(w.getUrl()); - } - final MenuItem back = menu.findItem(R.id.back_menu_id); - back.setEnabled(canGoBack); - - final MenuItem home = menu.findItem(R.id.homepage_menu_id); - home.setEnabled(!isHome); - - menu.findItem(R.id.forward_menu_id) - .setEnabled(canGoForward); - - menu.findItem(R.id.new_tab_menu_id).setEnabled( - mTabControl.canCreateNewTab()); - - // decide whether to show the share link option - PackageManager pm = getPackageManager(); - Intent send = new Intent(Intent.ACTION_SEND); - send.setType("text/plain"); - ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY); - menu.findItem(R.id.share_page_menu_id).setVisible(ri != null); - - boolean isNavDump = mSettings.isNavDump(); - final MenuItem nav = menu.findItem(R.id.dump_nav_menu_id); - nav.setVisible(isNavDump); - nav.setEnabled(isNavDump); - - boolean showDebugSettings = mSettings.showDebugSettings(); - final MenuItem counter = menu.findItem(R.id.dump_counters_menu_id); - counter.setVisible(showDebugSettings); - counter.setEnabled(showDebugSettings); - - break; + if (!mController.onOptionsItemSelected(item)) { + return super.onOptionsItemSelected(item); } - mCurrentMenuState = mMenuState; return true; } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { - if (v instanceof TitleBar) { - return; - } - WebView webview = (WebView) v; - WebView.HitTestResult result = webview.getHitTestResult(); - if (result == null) { - return; - } - - int type = result.getType(); - if (type == WebView.HitTestResult.UNKNOWN_TYPE) { - Log.w(LOGTAG, - "We should not show context menu when nothing is touched"); - return; - } - if (type == WebView.HitTestResult.EDIT_TEXT_TYPE) { - // let TextView handles context menu - return; - } - - // Note, http://b/issue?id=1106666 is requesting that - // an inflated menu can be used again. This is not available - // yet, so inflate each time (yuk!) - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.browsercontext, menu); - - // Show the correct menu group - String extra = result.getExtra(); - menu.setGroupVisible(R.id.PHONE_MENU, - type == WebView.HitTestResult.PHONE_TYPE); - menu.setGroupVisible(R.id.EMAIL_MENU, - type == WebView.HitTestResult.EMAIL_TYPE); - menu.setGroupVisible(R.id.GEO_MENU, - type == WebView.HitTestResult.GEO_TYPE); - menu.setGroupVisible(R.id.IMAGE_MENU, - type == WebView.HitTestResult.IMAGE_TYPE - || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE); - menu.setGroupVisible(R.id.ANCHOR_MENU, - type == WebView.HitTestResult.SRC_ANCHOR_TYPE - || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE); - - // Setup custom handling depending on the type - switch (type) { - case WebView.HitTestResult.PHONE_TYPE: - menu.setHeaderTitle(Uri.decode(extra)); - menu.findItem(R.id.dial_context_menu_id).setIntent( - new Intent(Intent.ACTION_VIEW, Uri - .parse(WebView.SCHEME_TEL + extra))); - Intent addIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT); - addIntent.putExtra(Insert.PHONE, Uri.decode(extra)); - addIntent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE); - menu.findItem(R.id.add_contact_context_menu_id).setIntent( - addIntent); - menu.findItem(R.id.copy_phone_context_menu_id).setOnMenuItemClickListener( - new Copy(extra)); - break; - - case WebView.HitTestResult.EMAIL_TYPE: - menu.setHeaderTitle(extra); - menu.findItem(R.id.email_context_menu_id).setIntent( - new Intent(Intent.ACTION_VIEW, Uri - .parse(WebView.SCHEME_MAILTO + extra))); - menu.findItem(R.id.copy_mail_context_menu_id).setOnMenuItemClickListener( - new Copy(extra)); - break; - - case WebView.HitTestResult.GEO_TYPE: - menu.setHeaderTitle(extra); - menu.findItem(R.id.map_context_menu_id).setIntent( - new Intent(Intent.ACTION_VIEW, Uri - .parse(WebView.SCHEME_GEO - + URLEncoder.encode(extra)))); - menu.findItem(R.id.copy_geo_context_menu_id).setOnMenuItemClickListener( - new Copy(extra)); - break; - - case WebView.HitTestResult.SRC_ANCHOR_TYPE: - case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE: - TextView titleView = (TextView) LayoutInflater.from(this) - .inflate(android.R.layout.browser_link_context_header, - null); - titleView.setText(extra); - menu.setHeaderView(titleView); - // decide whether to show the open link in new tab option - menu.findItem(R.id.open_newtab_context_menu_id).setVisible( - mTabControl.canCreateNewTab()); - menu.findItem(R.id.bookmark_context_menu_id).setVisible( - Bookmarks.urlHasAcceptableScheme(extra)); - PackageManager pm = getPackageManager(); - Intent send = new Intent(Intent.ACTION_SEND); - send.setType("text/plain"); - ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY); - menu.findItem(R.id.share_link_context_menu_id).setVisible(ri != null); - if (type == WebView.HitTestResult.SRC_ANCHOR_TYPE) { - break; - } - // otherwise fall through to handle image part - case WebView.HitTestResult.IMAGE_TYPE: - if (type == WebView.HitTestResult.IMAGE_TYPE) { - menu.setHeaderTitle(extra); - } - menu.findItem(R.id.view_image_context_menu_id).setIntent( - new Intent(Intent.ACTION_VIEW, Uri.parse(extra))); - menu.findItem(R.id.download_context_menu_id). - setOnMenuItemClickListener(new Download(extra)); - menu.findItem(R.id.set_wallpaper_context_menu_id). - setOnMenuItemClickListener(new SetAsWallpaper(extra)); - break; - - default: - Log.w(LOGTAG, "We should not get here."); - break; - } - hideFakeTitleBar(); - } - - // Attach the given tab to the content view. - // this should only be called for the current tab. - private void attachTabToContentView(Tab t) { - // Attach the container that contains the main WebView and any other UI - // associated with the tab. - t.attachTabToContentView(mContentView); - - if (mShouldShowErrorConsole) { - ErrorConsoleView errorConsole = t.getErrorConsole(true); - if (errorConsole.numberOfErrors() == 0) { - errorConsole.showConsole(ErrorConsoleView.SHOW_NONE); - } else { - errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED); - } - - mErrorConsoleContainer.addView(errorConsole, - new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT)); - } - - WebView view = t.getWebView(); - view.setEmbeddedTitleBar(mTitleBar); - if (t.isInVoiceSearchMode()) { - showVoiceTitleBar(t.getVoiceDisplayTitle()); - } else { - revertVoiceTitleBar(); - } - // Request focus on the top window. - t.getTopWindow().requestFocus(); - } - - // Attach a sub window to the main WebView of the given tab. - void attachSubWindow(Tab t) { - t.attachSubWindow(mContentView); - getTopWindow().requestFocus(); + mController.onCreateContextMenu(menu, v, menuInfo); } - // Remove the given tab from the content view. - private void removeTabFromContentView(Tab t) { - // Remove the container that contains the main WebView. - t.removeTabFromContentView(mContentView); - - ErrorConsoleView errorConsole = t.getErrorConsole(false); - if (errorConsole != null) { - mErrorConsoleContainer.removeView(errorConsole); - } - - WebView view = t.getWebView(); - if (view != null) { - view.setEmbeddedTitleBar(null); - } - } - - // Remove the sub window if it exists. Also called by TabControl when the - // user clicks the 'X' to dismiss a sub window. - /* package */ void dismissSubWindow(Tab t) { - t.removeSubWindow(mContentView); - // dismiss the subwindow. This will destroy the WebView. - t.dismissSubWindow(); - getTopWindow().requestFocus(); - } - - // A wrapper function of {@link #openTabAndShow(UrlData, boolean, String)} - // that accepts url as string. - private Tab openTabAndShow(String url, boolean closeOnExit, String appId) { - return openTabAndShow(new UrlData(url), closeOnExit, appId); - } - - // This method does a ton of stuff. It will attempt to create a new tab - // if we haven't reached MAX_TABS. Otherwise it uses the current tab. If - // url isn't null, it will load the given url. - /* package */Tab openTabAndShow(UrlData urlData, boolean closeOnExit, - String appId) { - final Tab currentTab = mTabControl.getCurrentTab(); - if (mTabControl.canCreateNewTab()) { - final Tab tab = mTabControl.createNewTab(closeOnExit, appId, - urlData.mUrl); - WebView webview = tab.getWebView(); - // If the last tab was removed from the active tabs page, currentTab - // will be null. - if (currentTab != null) { - removeTabFromContentView(currentTab); - } - // We must set the new tab as the current tab to reflect the old - // animation behavior. - mTabControl.setCurrentTab(tab); - attachTabToContentView(tab); - if (!urlData.isEmpty()) { - loadUrlDataIn(tab, urlData); - } - return tab; - } else { - // Get rid of the subwindow if it exists - dismissSubWindow(currentTab); - if (!urlData.isEmpty()) { - // Load the given url. - loadUrlDataIn(currentTab, urlData); - } - return currentTab; - } - } - - private Tab openTab(String url) { - if (mSettings.openInBackground()) { - Tab t = mTabControl.createNewTab(); - if (t != null) { - WebView view = t.getWebView(); - loadUrl(view, url); - } - return t; - } else { - return openTabAndShow(url, false, null); - } - } - - private class Copy implements OnMenuItemClickListener { - private CharSequence mText; - - public boolean onMenuItemClick(MenuItem item) { - copy(mText); - return true; - } - - public Copy(CharSequence toCopy) { - mText = toCopy; - } - } - - private class Download implements OnMenuItemClickListener { - private String mText; - - public boolean onMenuItemClick(MenuItem item) { - onDownloadStartNoStream(mText, null, null, null, -1); - return true; - } - - public Download(String toDownload) { - mText = toDownload; - } - } - - private class SetAsWallpaper extends Thread implements - OnMenuItemClickListener, DialogInterface.OnCancelListener { - private URL mUrl; - private ProgressDialog mWallpaperProgress; - private boolean mCanceled = false; - - public SetAsWallpaper(String url) { - try { - mUrl = new URL(url); - } catch (MalformedURLException e) { - mUrl = null; - } - } - - public void onCancel(DialogInterface dialog) { - mCanceled = true; - } - - public boolean onMenuItemClick(MenuItem item) { - if (mUrl != null) { - // The user may have tried to set a image with a large file size as their - // background so it may take a few moments to perform the operation. Display - // a progress spinner while it is working. - mWallpaperProgress = new ProgressDialog(BrowserActivity.this); - mWallpaperProgress.setIndeterminate(true); - mWallpaperProgress.setMessage(getText(R.string.progress_dialog_setting_wallpaper)); - mWallpaperProgress.setCancelable(true); - mWallpaperProgress.setOnCancelListener(this); - mWallpaperProgress.show(); - start(); - } - return true; - } - - public void run() { - Drawable oldWallpaper = BrowserActivity.this.getWallpaper(); - try { - // TODO: This will cause the resource to be downloaded again, when we - // should in most cases be able to grab it from the cache. To fix this - // we should query WebCore to see if we can access a cached version and - // instead open an input stream on that. This pattern could also be used - // in the download manager where the same problem exists. - InputStream inputstream = mUrl.openStream(); - if (inputstream != null) { - setWallpaper(inputstream); - } - } catch (IOException e) { - Log.e(LOGTAG, "Unable to set new wallpaper"); - // Act as though the user canceled the operation so we try to - // restore the old wallpaper. - mCanceled = true; - } - - if (mCanceled) { - // Restore the old wallpaper if the user cancelled whilst we were setting - // the new wallpaper. - int width = oldWallpaper.getIntrinsicWidth(); - int height = oldWallpaper.getIntrinsicHeight(); - Bitmap bm = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565); - Canvas canvas = new Canvas(bm); - oldWallpaper.setBounds(0, 0, width, height); - oldWallpaper.draw(canvas); - try { - setWallpaper(bm); - } catch (IOException e) { - Log.e(LOGTAG, "Unable to restore old wallpaper."); - } - mCanceled = false; - } - - if (mWallpaperProgress.isShowing()) { - mWallpaperProgress.dismiss(); - } - } - } - - 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); - } - } - - /** - * Resets the browser title-view to whatever it must be - * (for example, if we had a loading error) - * When we have a new page, we call resetTitle, when we - * have to reset the titlebar to whatever it used to be - * (for example, if the user chose to stop loading), we - * call resetTitleAndRevertLockIcon. - */ - /* package */ void resetTitleAndRevertLockIcon() { - mTabControl.getCurrentTab().revertLockIcon(); - updateLockIconToLatest(); - resetTitleIconAndProgress(); - } - - /** - * Reset the title, favicon, and progress. - */ - private void resetTitleIconAndProgress() { - WebView current = mTabControl.getCurrentWebView(); - if (current == null) { - return; - } - resetTitleAndIcon(current); - int progress = current.getProgress(); - current.getWebChromeClient().onProgressChanged(current, progress); - } - - // Reset the title and the icon based on the given item. - private void resetTitleAndIcon(WebView view) { - WebHistoryItem item = view.copyBackForwardList().getCurrentItem(); - if (item != null) { - setUrlTitle(item.getUrl(), item.getTitle()); - setFavicon(item.getFavicon()); - } else { - setUrlTitle(null, null); - setFavicon(null); - } - } - - /** - * Sets a title composed of the URL and the title string. - * @param url The URL of the site being loaded. - * @param title The title of the site being loaded. - */ - void setUrlTitle(String url, String title) { - mUrl = url; - mTitle = title; - - // If we are in voice search mode, the title has already been set. - if (mTabControl.getCurrentTab().isInVoiceSearchMode()) return; - mTitleBar.setDisplayTitle(url); - mFakeTitleBar.setDisplayTitle(url); - } - - /** - * @param url The URL to build a title version of the URL from. - * @return The title version of the URL or null if fails. - * The title version of the URL can be either the URL hostname, - * or the hostname with an "https://" prefix (for secure URLs), - * or an empty string if, for example, the URL in question is a - * file:// URL with no hostname. - */ - /* package */ static String buildTitleUrl(String url) { - String titleUrl = null; - - if (url != null) { - try { - // parse the url string - URL urlObj = new URL(url); - if (urlObj != null) { - titleUrl = ""; - - String protocol = urlObj.getProtocol(); - String host = urlObj.getHost(); - - if (host != null && 0 < host.length()) { - titleUrl = host; - if (protocol != null) { - // if a secure site, add an "https://" prefix! - if (protocol.equalsIgnoreCase("https")) { - titleUrl = protocol + "://" + host; - } - } - } - } - } catch (MalformedURLException e) {} - } - - return titleUrl; - } - - // Set the favicon in the title bar. - void setFavicon(Bitmap icon) { - mTitleBar.setFavicon(icon); - mFakeTitleBar.setFavicon(icon); - } - - /** - * Close the tab, remove its associated title bar, and adjust mTabControl's - * current tab to a valid value. - */ - /* package */ void closeTab(Tab t) { - int currentIndex = mTabControl.getCurrentIndex(); - int removeIndex = mTabControl.getTabIndex(t); - mTabControl.removeTab(t); - if (currentIndex >= removeIndex && currentIndex != 0) { - currentIndex--; - } - mTabControl.setCurrentTab(mTabControl.getTab(currentIndex)); - resetTitleIconAndProgress(); - } - - /* package */ void goBackOnePageOrQuit() { - Tab current = mTabControl.getCurrentTab(); - if (current == null) { - /* - * Instead of finishing the activity, simply push this to the back - * of the stack and let ActivityManager to choose the foreground - * activity. As BrowserActivity is singleTask, it will be always the - * root of the task. So we can use either true or false for - * moveTaskToBack(). - */ - moveTaskToBack(true); - return; - } - WebView w = current.getWebView(); - if (w.canGoBack()) { - w.goBack(); - } else { - // Check to see if we are closing a window that was created by - // another window. If so, we switch back to that window. - Tab parent = current.getParentTab(); - if (parent != null) { - switchToTab(mTabControl.getTabIndex(parent)); - // Now we close the other tab - closeTab(current); - } else { - if (current.closeOnExit()) { - // force the tab's inLoad() to be false as we are going to - // either finish the activity or remove the tab. This will - // ensure pauseWebViewTimers() taking action. - mTabControl.getCurrentTab().clearInLoad(); - if (mTabControl.getTabCount() == 1) { - finish(); - return; - } - // call pauseWebViewTimers() now, we won't be able to call - // it in onPause() as the WebView won't be valid. - // Temporarily change mActivityInPause to be true as - // pauseWebViewTimers() will do nothing if mActivityInPause - // is false. - boolean savedState = mActivityInPause; - if (savedState) { - Log.e(LOGTAG, "BrowserActivity is already paused " - + "while handing goBackOnePageOrQuit."); - } - mActivityInPause = true; - pauseWebViewTimers(); - mActivityInPause = savedState; - removeTabFromContentView(current); - mTabControl.removeTab(current); - } - /* - * Instead of finishing the activity, simply push this to the back - * of the stack and let ActivityManager to choose the foreground - * activity. As BrowserActivity is singleTask, it will be always the - * root of the task. So we can use either true or false for - * moveTaskToBack(). - */ - moveTaskToBack(true); - } - } - } - - boolean isMenuDown() { - return mMenuIsDown; + @Override + public boolean onContextItemSelected(MenuItem item) { + return mController.onContextItemSelected(item); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - // Even if MENU is already held down, we need to call to super to open - // the IME on long press. - if (KeyEvent.KEYCODE_MENU == keyCode) { - mMenuIsDown = true; - return super.onKeyDown(keyCode, event); - } - // The default key mode is DEFAULT_KEYS_SEARCH_LOCAL. As the MENU is - // still down, we don't want to trigger the search. Pretend to consume - // the key and do nothing. - if (mMenuIsDown) return true; - - switch(keyCode) { - case KeyEvent.KEYCODE_SPACE: - // WebView/WebTextView handle the keys in the KeyDown. As - // the Activity's shortcut keys are only handled when WebView - // doesn't, have to do it in onKeyDown instead of onKeyUp. - if (event.isShiftPressed()) { - getTopWindow().pageUp(false); - } else { - getTopWindow().pageDown(false); - } - return true; - case KeyEvent.KEYCODE_BACK: - if (event.getRepeatCount() == 0) { - event.startTracking(); - return true; - } else if (mCustomView == null && mActiveTabsPage == null - && event.isLongPress()) { - bookmarksOrHistoryPicker(true); - return true; - } - break; - } - return super.onKeyDown(keyCode, event); + return mController.onKeyDown(keyCode, event) || + super.onKeyDown(keyCode, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { - switch(keyCode) { - case KeyEvent.KEYCODE_MENU: - mMenuIsDown = false; - break; - case KeyEvent.KEYCODE_BACK: - if (event.isTracking() && !event.isCanceled()) { - if (mCustomView != null) { - // if a custom view is showing, hide it - mTabControl.getCurrentWebView().getWebChromeClient() - .onHideCustomView(); - } else if (mActiveTabsPage != null) { - // if tab page is showing, hide it - removeActiveTabPage(true); - } else { - WebView subwindow = mTabControl.getCurrentSubWindow(); - if (subwindow != null) { - if (subwindow.canGoBack()) { - subwindow.goBack(); - } else { - dismissSubWindow(mTabControl.getCurrentTab()); - } - } else { - goBackOnePageOrQuit(); - } - } - return true; - } - break; - } - return super.onKeyUp(keyCode, event); - } - - /* package */ void stopLoading() { - mDidStopLoad = true; - resetTitleAndRevertLockIcon(); - WebView w = getTopWindow(); - w.stopLoading(); - // FIXME: before refactor, it is using mWebViewClient. So I keep the - // same logic here. But for subwindow case, should we call into the main - // WebView's onPageFinished as we never call its onPageStarted and if - // the page finishes itself, we don't call onPageFinished. - mTabControl.getCurrentWebView().getWebViewClient().onPageFinished(w, - w.getUrl()); - - cancelStopToast(); - mStopToast = Toast - .makeText(this, R.string.stopping, Toast.LENGTH_SHORT); - mStopToast.show(); - } - - boolean didUserStopLoading() { - return mDidStopLoad; - } - - private void cancelStopToast() { - if (mStopToast != null) { - mStopToast.cancel(); - mStopToast = null; - } + return mController.onKeyUp(keyCode, event) || + super.onKeyUp(keyCode, event); } - // called by a UI or non-UI thread to post the message - public void postMessage(int what, int arg1, int arg2, Object obj, - long delayMillis) { - mHandler.sendMessageDelayed(mHandler.obtainMessage(what, arg1, arg2, - obj), delayMillis); - } - - // called by a UI or non-UI thread to remove the message - void removeMessages(int what, Object object) { - mHandler.removeMessages(what, object); - } - - // public message ids - public final static int LOAD_URL = 1001; - public final static int STOP_LOAD = 1002; - - // Message Ids - private static final int FOCUS_NODE_HREF = 102; - private static final int RELEASE_WAKELOCK = 107; - - static final int UPDATE_BOOKMARK_THUMBNAIL = 108; - - // Private handler for handling javascript and saving passwords - private Handler mHandler = new Handler() { - - public void handleMessage(Message msg) { - switch (msg.what) { - case FOCUS_NODE_HREF: - { - String url = (String) msg.getData().get("url"); - String title = (String) msg.getData().get("title"); - if (url == null || url.length() == 0) { - break; - } - HashMap focusNodeMap = (HashMap) msg.obj; - WebView view = (WebView) focusNodeMap.get("webview"); - // Only apply the action if the top window did not change. - if (getTopWindow() != view) { - break; - } - switch (msg.arg1) { - case R.id.open_context_menu_id: - case R.id.view_image_context_menu_id: - loadUrlFromContext(getTopWindow(), url); - break; - case R.id.open_newtab_context_menu_id: - final Tab parent = mTabControl.getCurrentTab(); - final Tab newTab = openTab(url); - if (newTab != parent) { - parent.addChildTab(newTab); - } - break; - case R.id.bookmark_context_menu_id: - Intent intent = new Intent(BrowserActivity.this, - AddBookmarkPage.class); - intent.putExtra("url", url); - intent.putExtra("title", title); - startActivity(intent); - break; - case R.id.share_link_context_menu_id: - // See if this site has been visited before - StringBuilder sb = new StringBuilder( - Browser.BookmarkColumns.URL + " = "); - DatabaseUtils.appendEscapedSQLString(sb, url); - Cursor c = null; - try { - c = mResolver.query(Browser.BOOKMARKS_URI, - Browser.HISTORY_PROJECTION, - sb.toString(), - null, - null); - if (c.moveToFirst()) { - // The site has been visited before, so grab the - // info from the database. - Bitmap favicon = null; - Bitmap thumbnail = null; - String linkTitle = c.getString(Browser. - HISTORY_PROJECTION_TITLE_INDEX); - byte[] data = c.getBlob(Browser. - HISTORY_PROJECTION_FAVICON_INDEX); - if (data != null) { - favicon = BitmapFactory.decodeByteArray( - data, 0, data.length); - } - data = c.getBlob(Browser. - HISTORY_PROJECTION_THUMBNAIL_INDEX); - if (data != null) { - thumbnail = BitmapFactory.decodeByteArray( - data, 0, data.length); - } - sharePage(BrowserActivity.this, - linkTitle, url, favicon, thumbnail); - } else { - Browser.sendString(BrowserActivity.this, url, - getString( - R.string.choosertitle_sharevia)); - } - } finally { - if (c != null) c.close(); - } - // close the cursor after its used - if (c != null) { - c.close(); - } - break; - case R.id.copy_link_context_menu_id: - copy(url); - break; - case R.id.save_link_context_menu_id: - case R.id.download_context_menu_id: - onDownloadStartNoStream(url, null, null, null, -1); - break; - } - break; - } - - case LOAD_URL: - loadUrlFromContext(getTopWindow(), (String) msg.obj); - break; - - case STOP_LOAD: - stopLoading(); - break; - - case RELEASE_WAKELOCK: - if (mWakeLock.isHeld()) { - mWakeLock.release(); - // if we reach here, Browser should be still in the - // background loading after WAKELOCK_TIMEOUT (5-min). - // To avoid burning the battery, stop loading. - mTabControl.stopAllLoading(); - } - break; - - case UPDATE_BOOKMARK_THUMBNAIL: - WebView view = (WebView) msg.obj; - if (view != null) { - updateScreenshot(view); - } - break; - } - } - }; - - /** - * Share a page, providing the title, url, favicon, and a screenshot. Uses - * an {@link Intent} to launch the Activity chooser. - * @param c Context used to launch a new Activity. - * @param title Title of the page. Stored in the Intent with - * {@link Intent#EXTRA_SUBJECT} - * @param url URL of the page. Stored in the Intent with - * {@link Intent#EXTRA_TEXT} - * @param favicon Bitmap of the favicon for the page. Stored in the Intent - * with {@link Browser#EXTRA_SHARE_FAVICON} - * @param screenshot Bitmap of a screenshot of the page. Stored in the - * Intent with {@link Browser#EXTRA_SHARE_SCREENSHOT} - */ - public static final void sharePage(Context c, String title, String url, - Bitmap favicon, Bitmap screenshot) { - Intent send = new Intent(Intent.ACTION_SEND); - send.setType("text/plain"); - send.putExtra(Intent.EXTRA_TEXT, url); - send.putExtra(Intent.EXTRA_SUBJECT, title); - send.putExtra(Browser.EXTRA_SHARE_FAVICON, favicon); - send.putExtra(Browser.EXTRA_SHARE_SCREENSHOT, screenshot); - try { - c.startActivity(Intent.createChooser(send, c.getString( - R.string.choosertitle_sharevia))); - } catch(android.content.ActivityNotFoundException ex) { - // if no app handles it, do nothing - } - } - - private void updateScreenshot(WebView view) { - // If this is a bookmarked site, add a screenshot to the database. - // FIXME: When should we update? Every time? - // FIXME: Would like to make sure there is actually something to - // draw, but the API for that (WebViewCore.pictureReady()) is not - // currently accessible here. - - final Bitmap bm = createScreenshot(view); - if (bm == null) { - return; - } - - final ContentResolver cr = getContentResolver(); - final String url = view.getUrl(); - final String originalUrl = view.getOriginalUrl(); - - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... unused) { - Cursor c = null; - try { - c = BrowserBookmarksAdapter.queryBookmarksForUrl( - cr, originalUrl, url, true); - if (c != null) { - if (c.moveToFirst()) { - ContentValues values = new ContentValues(); - final ByteArrayOutputStream os - = new ByteArrayOutputStream(); - bm.compress(Bitmap.CompressFormat.PNG, 100, os); - values.put(Browser.BookmarkColumns.THUMBNAIL, - os.toByteArray()); - do { - cr.update(ContentUris.withAppendedId( - Browser.BOOKMARKS_URI, c.getInt(0)), - values, null, null); - } while (c.moveToNext()); - } - } - } catch (IllegalStateException e) { - // Ignore - } finally { - if (c != null) c.close(); - } - return null; - } - }.execute(); - } - - /** - * Values for the size of the thumbnail created when taking a screenshot. - * Lazily initialized. Instead of using these directly, use - * getDesiredThumbnailWidth() or getDesiredThumbnailHeight(). - */ - private static int THUMBNAIL_WIDTH = 0; - private static int THUMBNAIL_HEIGHT = 0; - - /** - * Return the desired width for thumbnail screenshots, which are stored in - * the database, and used on the bookmarks screen. - * @param context Context for finding out the density of the screen. - * @return int desired width for thumbnail screenshot. - */ - /* package */ static int getDesiredThumbnailWidth(Context context) { - if (THUMBNAIL_WIDTH == 0) { - float density = context.getResources().getDisplayMetrics().density; - THUMBNAIL_WIDTH = (int) (90 * density); - THUMBNAIL_HEIGHT = (int) (80 * density); - } - return THUMBNAIL_WIDTH; - } - - /** - * Return the desired height for thumbnail screenshots, which are stored in - * the database, and used on the bookmarks screen. - * @param context Context for finding out the density of the screen. - * @return int desired height for thumbnail screenshot. - */ - /* package */ static int getDesiredThumbnailHeight(Context context) { - // To ensure that they are both initialized. - getDesiredThumbnailWidth(context); - return THUMBNAIL_HEIGHT; - } - - private Bitmap createScreenshot(WebView view) { - Picture thumbnail = view.capturePicture(); - if (thumbnail == null) { - return null; - } - Bitmap bm = Bitmap.createBitmap(getDesiredThumbnailWidth(this), - getDesiredThumbnailHeight(this), Bitmap.Config.RGB_565); - Canvas canvas = new Canvas(bm); - // May need to tweak these values to determine what is the - // best scale factor - int thumbnailWidth = thumbnail.getWidth(); - int thumbnailHeight = thumbnail.getHeight(); - float scaleFactorX = 1.0f; - float scaleFactorY = 1.0f; - if (thumbnailWidth > 0) { - scaleFactorX = (float) getDesiredThumbnailWidth(this) / - (float)thumbnailWidth; - } else { - return null; - } - - if (view.getWidth() > view.getHeight() && - thumbnailHeight < view.getHeight() && thumbnailHeight > 0) { - // If the device is in landscape and the page is shorter - // than the height of the view, stretch the thumbnail to fill the - // space. - scaleFactorY = (float) getDesiredThumbnailHeight(this) / - (float)thumbnailHeight; - } else { - // In the portrait case, this looks nice. - scaleFactorY = scaleFactorX; - } - - canvas.scale(scaleFactorX, scaleFactorY); - - thumbnail.draw(canvas); - return bm; - } - - // ------------------------------------------------------------------------- - // Helper function for WebViewClient. - //------------------------------------------------------------------------- - - // Use in overrideUrlLoading - /* package */ final static String SCHEME_WTAI = "wtai://wp/"; - /* package */ final static String SCHEME_WTAI_MC = "wtai://wp/mc;"; - /* package */ final static String SCHEME_WTAI_SD = "wtai://wp/sd;"; - /* package */ final static String SCHEME_WTAI_AP = "wtai://wp/ap;"; - - // Keep this initial progress in sync with initialProgressValue (* 100) - // in ProgressTracker.cpp - private final static int INITIAL_PROGRESS = 10; - - void onPageStarted(WebView view, String url, Bitmap favicon) { - // when BrowserActivity just starts, onPageStarted may be called before - // onResume as it is triggered from onCreate. Call resumeWebViewTimers - // to start the timer. As we won't switch tabs while an activity is in - // pause state, we can ensure calling resume and pause in pair. - if (mActivityInPause) resumeWebViewTimers(); - - resetLockIcon(url); - setUrlTitle(url, null); - setFavicon(favicon); - // Show some progress so that the user knows the page is beginning to - // load - onProgressChanged(view, INITIAL_PROGRESS); - mDidStopLoad = false; - if (!mIsNetworkUp) createAndShowNetworkDialog(); - closeDialogs(); - if (mSettings.isTracing()) { - String host; - try { - WebAddress uri = new WebAddress(url); - host = uri.mHost; - } catch (android.net.ParseException ex) { - host = "browser"; - } - host = host.replace('.', '_'); - host += ".trace"; - mInTrace = true; - Debug.startMethodTracing(host, 20 * 1024 * 1024); - } - - // Performance probe - if (false) { - mStart = SystemClock.uptimeMillis(); - mProcessStart = Process.getElapsedCpuTime(); - long[] sysCpu = new long[7]; - if (Process.readProcFile("/proc/stat", SYSTEM_CPU_FORMAT, null, - sysCpu, null)) { - mUserStart = sysCpu[0] + sysCpu[1]; - mSystemStart = sysCpu[2]; - mIdleStart = sysCpu[3]; - mIrqStart = sysCpu[4] + sysCpu[5] + sysCpu[6]; - } - mUiStart = SystemClock.currentThreadTimeMillis(); - } - } - - void onPageFinished(WebView view, String url) { - // Reset the title and icon in case we stopped a provisional load. - resetTitleAndIcon(view); - // Update the lock icon image only once we are done loading - updateLockIconToLatest(); - // pause the WebView timer and release the wake lock if it is finished - // while BrowserActivity is in pause state. - if (mActivityInPause && pauseWebViewTimers()) { - if (mWakeLock.isHeld()) { - mHandler.removeMessages(RELEASE_WAKELOCK); - mWakeLock.release(); - } - } - - // Performance probe - if (false) { - long[] sysCpu = new long[7]; - if (Process.readProcFile("/proc/stat", SYSTEM_CPU_FORMAT, null, - sysCpu, null)) { - String uiInfo = "UI thread used " - + (SystemClock.currentThreadTimeMillis() - mUiStart) - + " ms"; - if (LOGD_ENABLED) { - Log.d(LOGTAG, uiInfo); - } - //The string that gets written to the log - String performanceString = "It took total " - + (SystemClock.uptimeMillis() - mStart) - + " ms clock time to load the page." - + "\nbrowser process used " - + (Process.getElapsedCpuTime() - mProcessStart) - + " ms, user processes used " - + (sysCpu[0] + sysCpu[1] - mUserStart) * 10 - + " ms, kernel used " - + (sysCpu[2] - mSystemStart) * 10 - + " ms, idle took " + (sysCpu[3] - mIdleStart) * 10 - + " ms and irq took " - + (sysCpu[4] + sysCpu[5] + sysCpu[6] - mIrqStart) - * 10 + " ms, " + uiInfo; - if (LOGD_ENABLED) { - Log.d(LOGTAG, performanceString + "\nWebpage: " + url); - } - if (url != null) { - // strip the url to maintain consistency - String newUrl = new String(url); - if (newUrl.startsWith("http://www.")) { - newUrl = newUrl.substring(11); - } else if (newUrl.startsWith("http://")) { - newUrl = newUrl.substring(7); - } else if (newUrl.startsWith("https://www.")) { - newUrl = newUrl.substring(12); - } else if (newUrl.startsWith("https://")) { - newUrl = newUrl.substring(8); - } - if (LOGD_ENABLED) { - Log.d(LOGTAG, newUrl + " loaded"); - } - } - } - } - - if (mInTrace) { - mInTrace = false; - Debug.stopMethodTracing(); - } - } - - boolean shouldOverrideUrlLoading(WebView view, String url) { - if (url.startsWith(SCHEME_WTAI)) { - // wtai://wp/mc;number - // number=string(phone-number) - if (url.startsWith(SCHEME_WTAI_MC)) { - Intent intent = new Intent(Intent.ACTION_VIEW, - Uri.parse(WebView.SCHEME_TEL + - url.substring(SCHEME_WTAI_MC.length()))); - startActivity(intent); - return true; - } - // wtai://wp/sd;dtmf - // dtmf=string(dialstring) - if (url.startsWith(SCHEME_WTAI_SD)) { - // TODO: only send when there is active voice connection - return false; - } - // wtai://wp/ap;number;name - // number=string(phone-number) - // name=string - if (url.startsWith(SCHEME_WTAI_AP)) { - // TODO - return false; - } - } - - // The "about:" schemes are internal to the browser; don't want these to - // be dispatched to other apps. - if (url.startsWith("about:")) { - return false; - } - - Intent intent; - // perform generic parsing of the URI to turn it into an Intent. - try { - intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); - } catch (URISyntaxException ex) { - Log.w("Browser", "Bad URI " + url + ": " + ex.getMessage()); - return false; - } - - // check whether the intent can be resolved. If not, we will see - // whether we can download it from the Market. - if (getPackageManager().resolveActivity(intent, 0) == null) { - String packagename = intent.getPackage(); - if (packagename != null) { - intent = new Intent(Intent.ACTION_VIEW, Uri - .parse("market://search?q=pname:" + packagename)); - intent.addCategory(Intent.CATEGORY_BROWSABLE); - startActivity(intent); - return true; - } else { - return false; - } - } - - // sanitize the Intent, ensuring web pages can not bypass browser - // security (only access to BROWSABLE activities). - intent.addCategory(Intent.CATEGORY_BROWSABLE); - intent.setComponent(null); - try { - if (startActivityIfNeeded(intent, -1)) { - return true; - } - } catch (ActivityNotFoundException ex) { - // ignore the error. If no application can handle the URL, - // eg about:blank, assume the browser can handle it. - } - - if (mMenuIsDown) { - openTab(url); - closeOptionsMenu(); - return true; - } - return false; - } - - // ------------------------------------------------------------------------- - // Helper function for WebChromeClient - // ------------------------------------------------------------------------- - - void onProgressChanged(WebView view, int newProgress) { - mFakeTitleBar.setProgress(newProgress); - - if (newProgress == 100) { - // onProgressChanged() may continue to be called after the main - // frame has finished loading, as any remaining sub frames continue - // to load. We'll only get called once though with newProgress as - // 100 when everything is loaded. (onPageFinished is called once - // when the main frame completes loading regardless of the state of - // any sub frames so calls to onProgressChanges may continue after - // onPageFinished has executed) - if (mInLoad) { - mInLoad = false; - updateInLoadMenuItems(); - // If the options menu is open, leave the title bar - if (!mOptionsMenuOpen || !mIconView) { - hideFakeTitleBar(); - } - } - } else { - if (!mInLoad) { - // onPageFinished may have already been called but a subframe is - // still loading and updating the progress. Reset mInLoad and - // update the menu items. - mInLoad = true; - updateInLoadMenuItems(); - } - // When the page first begins to load, the Activity may still be - // paused, in which case showFakeTitleBar will do nothing. Call - // again as the page continues to load so that it will be shown. - // (Calling it will the fake title bar is already showing will also - // do nothing. - if (!mOptionsMenuOpen || mIconView) { - // This page has begun to load, so show the title bar - showFakeTitleBar(); - } - } - } - - void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) { - // if a view already exists then immediately terminate the new one - if (mCustomView != null) { - callback.onCustomViewHidden(); - return; - } - - // Add the custom view to its container. - mCustomViewContainer.addView(view, COVER_SCREEN_GRAVITY_CENTER); - mCustomView = view; - mCustomViewCallback = callback; - // Save the menu state and set it to empty while the custom - // view is showing. - mOldMenuState = mMenuState; - mMenuState = EMPTY_MENU; - // Hide the content view. - mContentView.setVisibility(View.GONE); - // Finally show the custom view container. - setStatusBarVisibility(false); - mCustomViewContainer.setVisibility(View.VISIBLE); - mCustomViewContainer.bringToFront(); - } - - void onHideCustomView() { - if (mCustomView == null) - return; - - // Hide the custom view. - mCustomView.setVisibility(View.GONE); - // Remove the custom view from its container. - mCustomViewContainer.removeView(mCustomView); - mCustomView = null; - // Reset the old menu state. - mMenuState = mOldMenuState; - mOldMenuState = EMPTY_MENU; - mCustomViewContainer.setVisibility(View.GONE); - mCustomViewCallback.onCustomViewHidden(); - // Show the content view. - setStatusBarVisibility(true); - mContentView.setVisibility(View.VISIBLE); - } - - Bitmap getDefaultVideoPoster() { - if (mDefaultVideoPoster == null) { - mDefaultVideoPoster = BitmapFactory.decodeResource( - getResources(), R.drawable.default_video_poster); - } - return mDefaultVideoPoster; - } - - View getVideoLoadingProgressView() { - if (mVideoProgressView == null) { - LayoutInflater inflater = LayoutInflater.from(BrowserActivity.this); - mVideoProgressView = inflater.inflate( - R.layout.video_loading_progress, null); - } - return mVideoProgressView; - } - - /* - * The Object used to inform the WebView of the file to upload. - */ - private ValueCallback<Uri> mUploadMessage; - - void openFileChooser(ValueCallback<Uri> uploadMsg) { - if (mUploadMessage != null) return; - mUploadMessage = uploadMsg; - Intent i = new Intent(Intent.ACTION_GET_CONTENT); - i.addCategory(Intent.CATEGORY_OPENABLE); - i.setType("*/*"); - BrowserActivity.this.startActivityForResult(Intent.createChooser(i, - getString(R.string.choose_upload)), FILE_SELECTED); - } - - // ------------------------------------------------------------------------- - // Implement functions for DownloadListener - // ------------------------------------------------------------------------- - - /** - * Notify the host application a download should be done, or that - * the data should be streamed if a streaming viewer is available. - * @param url The full url to the content that should be downloaded - * @param contentDisposition Content-disposition http header, if - * present. - * @param mimetype The mimetype of the content reported by the server - * @param contentLength The file size reported by the server - */ - public void onDownloadStart(String url, String userAgent, - String contentDisposition, String mimetype, long contentLength) { - // if we're dealing wih A/V content that's not explicitly marked - // for download, check if it's streamable. - if (contentDisposition == null - || !contentDisposition.regionMatches( - true, 0, "attachment", 0, 10)) { - // query the package manager to see if there's a registered handler - // that matches. - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(Uri.parse(url), mimetype); - ResolveInfo info = getPackageManager().resolveActivity(intent, - PackageManager.MATCH_DEFAULT_ONLY); - if (info != null) { - ComponentName myName = getComponentName(); - // If we resolved to ourselves, we don't want to attempt to - // load the url only to try and download it again. - if (!myName.getPackageName().equals( - info.activityInfo.packageName) - || !myName.getClassName().equals( - info.activityInfo.name)) { - // someone (other than us) knows how to handle this mime - // type with this scheme, don't download. - try { - startActivity(intent); - return; - } catch (ActivityNotFoundException ex) { - if (LOGD_ENABLED) { - Log.d(LOGTAG, "activity not found for " + mimetype - + " over " + Uri.parse(url).getScheme(), - ex); - } - // Best behavior is to fall back to a download in this - // case - } - } - } - } - onDownloadStartNoStream(url, userAgent, contentDisposition, mimetype, contentLength); - } - - // This is to work around the fact that java.net.URI throws Exceptions - // instead of just encoding URL's properly - // Helper method for onDownloadStartNoStream - private static String encodePath(String path) { - char[] chars = path.toCharArray(); - - boolean needed = false; - for (char c : chars) { - if (c == '[' || c == ']') { - needed = true; - break; - } - } - if (needed == false) { - return path; - } - - StringBuilder sb = new StringBuilder(""); - for (char c : chars) { - if (c == '[' || c == ']') { - sb.append('%'); - sb.append(Integer.toHexString(c)); - } else { - sb.append(c); - } - } - - return sb.toString(); - } - - /** - * Notify the host application a download should be done, even if there - * is a streaming viewer available for thise type. - * @param url The full url to the content that should be downloaded - * @param contentDisposition Content-disposition http header, if - * present. - * @param mimetype The mimetype of the content reported by the server - * @param contentLength The file size reported by the server - */ - /*package */ void onDownloadStartNoStream(String url, String userAgent, - String contentDisposition, String mimetype, long contentLength) { - - String filename = URLUtil.guessFileName(url, - contentDisposition, mimetype); - - // Check to see if we have an SDCard - String status = Environment.getExternalStorageState(); - if (!status.equals(Environment.MEDIA_MOUNTED)) { - int title; - String msg; - - // Check to see if the SDCard is busy, same as the music app - if (status.equals(Environment.MEDIA_SHARED)) { - msg = getString(R.string.download_sdcard_busy_dlg_msg); - title = R.string.download_sdcard_busy_dlg_title; - } else { - msg = getString(R.string.download_no_sdcard_dlg_msg, filename); - title = R.string.download_no_sdcard_dlg_title; - } - - new AlertDialog.Builder(this) - .setTitle(title) - .setIcon(android.R.drawable.ic_dialog_alert) - .setMessage(msg) - .setPositiveButton(R.string.ok, null) - .show(); - return; - } - - // java.net.URI is a lot stricter than KURL so we have to encode some - // extra characters. Fix for b 2538060 and b 1634719 - WebAddress webAddress; - try { - webAddress = new WebAddress(url); - webAddress.mPath = encodePath(webAddress.mPath); - } catch (Exception e) { - // This only happens for very bad urls, we want to chatch the - // exception here - Log.e(LOGTAG, "Exception trying to parse url:" + url); - return; - } - - // XXX: Have to use the old url since the cookies were stored using the - // old percent-encoded url. - String cookies = CookieManager.getInstance().getCookie(url); - - ContentValues values = new ContentValues(); - values.put(Downloads.Impl.COLUMN_URI, webAddress.toString()); - values.put(Downloads.Impl.COLUMN_COOKIE_DATA, cookies); - values.put(Downloads.Impl.COLUMN_USER_AGENT, userAgent); - values.put(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, - getPackageName()); - values.put(Downloads.Impl.COLUMN_NOTIFICATION_CLASS, - OpenDownloadReceiver.class.getCanonicalName()); - values.put(Downloads.Impl.COLUMN_VISIBILITY, - Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); - values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimetype); - values.put(Downloads.Impl.COLUMN_FILE_NAME_HINT, filename); - values.put(Downloads.Impl.COLUMN_DESCRIPTION, webAddress.mHost); - if (contentLength > 0) { - values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, contentLength); - } - if (mimetype == null) { - // We must have long pressed on a link or image to download it. We - // are not sure of the mimetype in this case, so do a head request - new FetchUrlMimeType(this).execute(values); - } else { - final Uri contentUri = - getContentResolver().insert(Downloads.Impl.CONTENT_URI, values); - } - Toast.makeText(this, R.string.download_pending, Toast.LENGTH_SHORT) - .show(); - } - - // ------------------------------------------------------------------------- - - /** - * Resets the lock icon. This method is called when we start a new load and - * know the url to be loaded. - */ - private void resetLockIcon(String url) { - // Save the lock-icon state (we revert to it if the load gets cancelled) - mTabControl.getCurrentTab().resetLockIcon(url); - updateLockIconImage(LOCK_ICON_UNSECURE); - } - - /** - * Update the lock icon to correspond to our latest state. - */ - private void updateLockIconToLatest() { - updateLockIconImage(mTabControl.getCurrentTab().getLockIconType()); - } - - /** - * Updates the lock-icon image in the title-bar. - */ - private void updateLockIconImage(int lockIconType) { - Drawable d = null; - if (lockIconType == LOCK_ICON_SECURE) { - d = mSecLockIcon; - } else if (lockIconType == LOCK_ICON_MIXED) { - d = mMixLockIcon; - } - mTitleBar.setLock(d); - mFakeTitleBar.setLock(d); - } - - /** - * Displays a page-info dialog. - * @param tab The tab to show info about - * @param fromShowSSLCertificateOnError The flag that indicates whether - * this dialog was opened from the SSL-certificate-on-error dialog or - * not. This is important, since we need to know whether to return to - * the parent dialog or simply dismiss. - */ - private void showPageInfo(final Tab tab, - final boolean fromShowSSLCertificateOnError) { - final LayoutInflater factory = LayoutInflater - .from(this); - - final View pageInfoView = factory.inflate(R.layout.page_info, null); - - final WebView view = tab.getWebView(); - - String url = null; - String title = null; - - if (view == null) { - url = tab.getUrl(); - title = tab.getTitle(); - } else if (view == mTabControl.getCurrentWebView()) { - // Use the cached title and url if this is the current WebView - url = mUrl; - title = mTitle; - } else { - url = view.getUrl(); - title = view.getTitle(); - } - - if (url == null) { - url = ""; - } - if (title == null) { - title = ""; - } - - ((TextView) pageInfoView.findViewById(R.id.address)).setText(url); - ((TextView) pageInfoView.findViewById(R.id.title)).setText(title); - - mPageInfoView = tab; - mPageInfoFromShowSSLCertificateOnError = fromShowSSLCertificateOnError; - - AlertDialog.Builder alertDialogBuilder = - new AlertDialog.Builder(this) - .setTitle(R.string.page_info).setIcon(android.R.drawable.ic_dialog_info) - .setView(pageInfoView) - .setPositiveButton( - R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, - int whichButton) { - mPageInfoDialog = null; - mPageInfoView = null; - - // if we came here from the SSL error dialog - if (fromShowSSLCertificateOnError) { - // go back to the SSL error dialog - showSSLCertificateOnError( - mSSLCertificateOnErrorView, - mSSLCertificateOnErrorHandler, - mSSLCertificateOnErrorError); - } - } - }) - .setOnCancelListener( - new DialogInterface.OnCancelListener() { - public void onCancel(DialogInterface dialog) { - mPageInfoDialog = null; - mPageInfoView = null; - - // if we came here from the SSL error dialog - if (fromShowSSLCertificateOnError) { - // go back to the SSL error dialog - showSSLCertificateOnError( - mSSLCertificateOnErrorView, - mSSLCertificateOnErrorHandler, - mSSLCertificateOnErrorError); - } - } - }); - - // if we have a main top-level page SSL certificate set or a certificate - // error - if (fromShowSSLCertificateOnError || - (view != null && view.getCertificate() != null)) { - // add a 'View Certificate' button - alertDialogBuilder.setNeutralButton( - R.string.view_certificate, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, - int whichButton) { - mPageInfoDialog = null; - mPageInfoView = null; - - // if we came here from the SSL error dialog - if (fromShowSSLCertificateOnError) { - // go back to the SSL error dialog - showSSLCertificateOnError( - mSSLCertificateOnErrorView, - mSSLCertificateOnErrorHandler, - mSSLCertificateOnErrorError); - } else { - // otherwise, display the top-most certificate from - // the chain - if (view.getCertificate() != null) { - showSSLCertificate(tab); - } - } - } - }); - } - - mPageInfoDialog = alertDialogBuilder.show(); - } - - /** - * Displays the main top-level page SSL certificate dialog - * (accessible from the Page-Info dialog). - * @param tab The tab to show certificate for. - */ - private void showSSLCertificate(final Tab tab) { - final View certificateView = - inflateCertificateView(tab.getWebView().getCertificate()); - if (certificateView == null) { - return; - } - - LayoutInflater factory = LayoutInflater.from(this); - - final LinearLayout placeholder = - (LinearLayout)certificateView.findViewById(R.id.placeholder); - - LinearLayout ll = (LinearLayout) factory.inflate( - R.layout.ssl_success, placeholder); - ((TextView)ll.findViewById(R.id.success)) - .setText(R.string.ssl_certificate_is_valid); - - mSSLCertificateView = tab; - mSSLCertificateDialog = - new AlertDialog.Builder(this) - .setTitle(R.string.ssl_certificate).setIcon( - R.drawable.ic_dialog_browser_certificate_secure) - .setView(certificateView) - .setPositiveButton(R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, - int whichButton) { - mSSLCertificateDialog = null; - mSSLCertificateView = null; - - showPageInfo(tab, false); - } - }) - .setOnCancelListener( - new DialogInterface.OnCancelListener() { - public void onCancel(DialogInterface dialog) { - mSSLCertificateDialog = null; - mSSLCertificateView = null; - - showPageInfo(tab, false); - } - }) - .show(); - } - - /** - * Displays the SSL error certificate dialog. - * @param view The target web-view. - * @param handler The SSL error handler responsible for cancelling the - * connection that resulted in an SSL error or proceeding per user request. - * @param error The SSL error object. - */ - void showSSLCertificateOnError( - final WebView view, final SslErrorHandler handler, final SslError error) { - - final View certificateView = - inflateCertificateView(error.getCertificate()); - if (certificateView == null) { - return; - } - - LayoutInflater factory = LayoutInflater.from(this); - - final LinearLayout placeholder = - (LinearLayout)certificateView.findViewById(R.id.placeholder); - - if (error.hasError(SslError.SSL_UNTRUSTED)) { - LinearLayout ll = (LinearLayout)factory - .inflate(R.layout.ssl_warning, placeholder); - ((TextView)ll.findViewById(R.id.warning)) - .setText(R.string.ssl_untrusted); - } - - if (error.hasError(SslError.SSL_IDMISMATCH)) { - LinearLayout ll = (LinearLayout)factory - .inflate(R.layout.ssl_warning, placeholder); - ((TextView)ll.findViewById(R.id.warning)) - .setText(R.string.ssl_mismatch); - } - - if (error.hasError(SslError.SSL_EXPIRED)) { - LinearLayout ll = (LinearLayout)factory - .inflate(R.layout.ssl_warning, placeholder); - ((TextView)ll.findViewById(R.id.warning)) - .setText(R.string.ssl_expired); - } - - if (error.hasError(SslError.SSL_NOTYETVALID)) { - LinearLayout ll = (LinearLayout)factory - .inflate(R.layout.ssl_warning, placeholder); - ((TextView)ll.findViewById(R.id.warning)) - .setText(R.string.ssl_not_yet_valid); - } - - mSSLCertificateOnErrorHandler = handler; - mSSLCertificateOnErrorView = view; - mSSLCertificateOnErrorError = error; - mSSLCertificateOnErrorDialog = - new AlertDialog.Builder(this) - .setTitle(R.string.ssl_certificate).setIcon( - R.drawable.ic_dialog_browser_certificate_partially_secure) - .setView(certificateView) - .setPositiveButton(R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, - int whichButton) { - mSSLCertificateOnErrorDialog = null; - mSSLCertificateOnErrorView = null; - mSSLCertificateOnErrorHandler = null; - mSSLCertificateOnErrorError = null; - - view.getWebViewClient().onReceivedSslError( - view, handler, error); - } - }) - .setNeutralButton(R.string.page_info_view, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, - int whichButton) { - mSSLCertificateOnErrorDialog = null; - - // do not clear the dialog state: we will - // need to show the dialog again once the - // user is done exploring the page-info details - - showPageInfo(mTabControl.getTabFromView(view), - true); - } - }) - .setOnCancelListener( - new DialogInterface.OnCancelListener() { - public void onCancel(DialogInterface dialog) { - mSSLCertificateOnErrorDialog = null; - mSSLCertificateOnErrorView = null; - mSSLCertificateOnErrorHandler = null; - mSSLCertificateOnErrorError = null; - - view.getWebViewClient().onReceivedSslError( - view, handler, error); - } - }) - .show(); - } - - /** - * Inflates the SSL certificate view (helper method). - * @param certificate The SSL certificate. - * @return The resultant certificate view with issued-to, issued-by, - * issued-on, expires-on, and possibly other fields set. - * If the input certificate is null, returns null. - */ - private View inflateCertificateView(SslCertificate certificate) { - if (certificate == null) { - return null; - } - - LayoutInflater factory = LayoutInflater.from(this); - - View certificateView = factory.inflate( - R.layout.ssl_certificate, null); - - // issued to: - SslCertificate.DName issuedTo = certificate.getIssuedTo(); - if (issuedTo != null) { - ((TextView) certificateView.findViewById(R.id.to_common)) - .setText(issuedTo.getCName()); - ((TextView) certificateView.findViewById(R.id.to_org)) - .setText(issuedTo.getOName()); - ((TextView) certificateView.findViewById(R.id.to_org_unit)) - .setText(issuedTo.getUName()); - } - - // issued by: - SslCertificate.DName issuedBy = certificate.getIssuedBy(); - if (issuedBy != null) { - ((TextView) certificateView.findViewById(R.id.by_common)) - .setText(issuedBy.getCName()); - ((TextView) certificateView.findViewById(R.id.by_org)) - .setText(issuedBy.getOName()); - ((TextView) certificateView.findViewById(R.id.by_org_unit)) - .setText(issuedBy.getUName()); - } - - // issued on: - String issuedOn = formatCertificateDate( - certificate.getValidNotBeforeDate()); - ((TextView) certificateView.findViewById(R.id.issued_on)) - .setText(issuedOn); - - // expires on: - String expiresOn = formatCertificateDate( - certificate.getValidNotAfterDate()); - ((TextView) certificateView.findViewById(R.id.expires_on)) - .setText(expiresOn); - - return certificateView; - } - - /** - * Formats the certificate date to a properly localized date string. - * @return Properly localized version of the certificate date string and - * the "" if it fails to localize. - */ - private String formatCertificateDate(Date certificateDate) { - if (certificateDate == null) { - return ""; - } - String formattedDate = DateFormat.getDateFormat(this).format(certificateDate); - if (formattedDate == null) { - return ""; - } - return formattedDate; - } - - /** - * Displays an http-authentication dialog. - */ - void showHttpAuthentication(final HttpAuthHandler handler, - final String host, final String realm, final String title, - final String name, final String password, int focusId) { - LayoutInflater factory = LayoutInflater.from(this); - final View v = factory - .inflate(R.layout.http_authentication, null); - if (name != null) { - ((EditText) v.findViewById(R.id.username_edit)).setText(name); - } - if (password != null) { - ((EditText) v.findViewById(R.id.password_edit)).setText(password); - } - - String titleText = title; - if (titleText == null) { - titleText = getText(R.string.sign_in_to).toString().replace( - "%s1", host).replace("%s2", realm); - } - - mHttpAuthHandler = handler; - AlertDialog dialog = new AlertDialog.Builder(this) - .setTitle(titleText) - .setIcon(android.R.drawable.ic_dialog_alert) - .setView(v) - .setPositiveButton(R.string.action, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, - int whichButton) { - String nm = ((EditText) v - .findViewById(R.id.username_edit)) - .getText().toString(); - String pw = ((EditText) v - .findViewById(R.id.password_edit)) - .getText().toString(); - BrowserActivity.this.setHttpAuthUsernamePassword - (host, realm, nm, pw); - handler.proceed(nm, pw); - mHttpAuthenticationDialog = null; - mHttpAuthHandler = null; - }}) - .setNegativeButton(R.string.cancel, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, - int whichButton) { - handler.cancel(); - BrowserActivity.this.resetTitleAndRevertLockIcon(); - mHttpAuthenticationDialog = null; - mHttpAuthHandler = null; - }}) - .setOnCancelListener(new DialogInterface.OnCancelListener() { - public void onCancel(DialogInterface dialog) { - handler.cancel(); - BrowserActivity.this.resetTitleAndRevertLockIcon(); - mHttpAuthenticationDialog = null; - mHttpAuthHandler = null; - }}) - .create(); - // Make the IME appear when the dialog is displayed if applicable. - dialog.getWindow().setSoftInputMode( - WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); - dialog.show(); - if (focusId != 0) { - dialog.findViewById(focusId).requestFocus(); - } else { - v.findViewById(R.id.username_edit).requestFocus(); - } - mHttpAuthenticationDialog = dialog; - } - - public int getProgress() { - WebView w = mTabControl.getCurrentWebView(); - if (w != null) { - return w.getProgress(); - } else { - return 100; - } - } - - /** - * Set HTTP authentication password. - * - * @param host The host for the password - * @param realm The realm for the password - * @param username The username for the password. If it is null, it means - * password can't be saved. - * @param password The password - */ - public void setHttpAuthUsernamePassword(String host, String realm, - String username, - String password) { - WebView w = getTopWindow(); - if (w != null) { - w.setHttpAuthUsernamePassword(host, realm, username, password); - } - } - - /** - * connectivity manager says net has come or gone... inform the user - * @param up true if net has come up, false if net has gone down - */ - public void onNetworkToggle(boolean up) { - if (up == mIsNetworkUp) { - return; - } else if (up) { - mIsNetworkUp = true; - if (mAlertDialog != null) { - mAlertDialog.cancel(); - mAlertDialog = null; - } - } else { - mIsNetworkUp = false; - if (mInLoad) { - createAndShowNetworkDialog(); - } - } - WebView w = mTabControl.getCurrentWebView(); - if (w != null) { - w.setNetworkAvailable(up); - } - } - - boolean isNetworkUp() { - return mIsNetworkUp; + @Override + public void onActionModeStarted(ActionMode mode) { + super.onActionModeStarted(mode); + mController.onActionModeStarted(mode); } - // This method shows the network dialog alerting the user that the net is - // down. It will only show the dialog if mAlertDialog is null. - private void createAndShowNetworkDialog() { - if (mAlertDialog == null) { - mAlertDialog = new AlertDialog.Builder(this) - .setTitle(R.string.loadSuspendedTitle) - .setMessage(R.string.loadSuspended) - .setPositiveButton(R.string.ok, null) - .show(); - } + @Override + public void onActionModeFinished(ActionMode mode) { + super.onActionModeFinished(mode); + mController.onActionModeFinished(mode); } @Override protected void onActivityResult(int requestCode, int resultCode, - Intent intent) { - if (getTopWindow() == null) return; - - switch (requestCode) { - case COMBO_PAGE: - if (resultCode == RESULT_OK && intent != null) { - String data = intent.getAction(); - Bundle extras = intent.getExtras(); - if (extras != null && extras.getBoolean("new_window", false)) { - openTab(data); - } else { - final Tab currentTab = - mTabControl.getCurrentTab(); - dismissSubWindow(currentTab); - if (data != null && data.length() != 0) { - loadUrl(getTopWindow(), data); - } - } - } - // Deliberately fall through to PREFERENCES_PAGE, since the - // same extra may be attached to the COMBO_PAGE - case PREFERENCES_PAGE: - if (resultCode == RESULT_OK && intent != null) { - String action = intent.getStringExtra(Intent.EXTRA_TEXT); - if (BrowserSettings.PREF_CLEAR_HISTORY.equals(action)) { - mTabControl.removeParentChildRelationShips(); - } - } - break; - // Choose a file from the file picker. - case FILE_SELECTED: - if (null == mUploadMessage) break; - Uri result = intent == null || resultCode != RESULT_OK ? null - : intent.getData(); - mUploadMessage.onReceiveValue(result); - mUploadMessage = null; - break; - default: - break; - } - getTopWindow().requestFocus(); - } - - /* - * This method is called as a result of the user selecting the options - * menu to see the download window. It shows the download window on top of - * the current window. - */ - private void viewDownloads() { - Intent intent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS); - startActivity(intent); - } - - /** - * Open the Go page. - * @param startWithHistory If true, open starting on the history tab. - * Otherwise, start with the bookmarks tab. - */ - /* package */ void bookmarksOrHistoryPicker(boolean startWithHistory) { - WebView current = mTabControl.getCurrentWebView(); - if (current == null) { - return; - } - Intent intent = new Intent(this, - CombinedBookmarkHistoryActivity.class); - String title = current.getTitle(); - String url = current.getUrl(); - Bitmap thumbnail = createScreenshot(current); - - // Just in case the user opens bookmarks before a page finishes loading - // so the current history item, and therefore the page, is null. - if (null == url) { - url = mLastEnteredUrl; - // This can happen. - if (null == url) { - url = mSettings.getHomePage(); - } - } - // In case the web page has not yet received its associated title. - if (title == null) { - title = url; - } - intent.putExtra("title", title); - intent.putExtra("url", url); - intent.putExtra("thumbnail", thumbnail); - // Disable opening in a new window if we have maxed out the windows - intent.putExtra("disable_new_window", !mTabControl.canCreateNewTab()); - intent.putExtra("touch_icon_url", current.getTouchIconUrl()); - if (startWithHistory) { - intent.putExtra(CombinedBookmarkHistoryActivity.STARTING_TAB, - CombinedBookmarkHistoryActivity.HISTORY_TAB); - } - startActivityForResult(intent, COMBO_PAGE); - } - - // Called when loading from context menu or LOAD_URL message - private void loadUrlFromContext(WebView view, String url) { - // In case the user enters nothing. - if (url != null && url.length() != 0 && view != null) { - url = smartUrlFilter(url); - if (!view.getWebViewClient().shouldOverrideUrlLoading(view, url)) { - loadUrl(view, url); - } - } - } - - /** - * Load the URL into the given WebView and update the title bar - * to reflect the new load. Call this instead of WebView.loadUrl - * directly. - * @param view The WebView used to load url. - * @param url The URL to load. - */ - private void loadUrl(WebView view, String url) { - updateTitleBarForNewLoad(view, url); - view.loadUrl(url); - } - - /** - * Load UrlData into a Tab and update the title bar to reflect the new - * load. Call this instead of UrlData.loadIn directly. - * @param t The Tab used to load. - * @param data The UrlData being loaded. - */ - private void loadUrlDataIn(Tab t, UrlData data) { - updateTitleBarForNewLoad(t.getWebView(), data.mUrl); - data.loadIn(t); - } - - /** - * If the WebView is the top window, update the title bar to reflect - * loading the new URL. i.e. set its text, clear the favicon (which - * will be set once the page begins loading), and set the progress to - * INITIAL_PROGRESS to show that the page has begun to load. Called - * by loadUrl and loadUrlDataIn. - * @param view The WebView that is starting a load. - * @param url The URL that is being loaded. - */ - private void updateTitleBarForNewLoad(WebView view, String url) { - if (view == getTopWindow()) { - setUrlTitle(url, null); - setFavicon(null); - onProgressChanged(view, INITIAL_PROGRESS); - } - } - - private String smartUrlFilter(Uri inUri) { - if (inUri != null) { - return smartUrlFilter(inUri.toString()); - } - return null; - } - - protected static final Pattern ACCEPTED_URI_SCHEMA = Pattern.compile( - "(?i)" + // switch on case insensitive matching - "(" + // begin group for schema - "(?:http|https|file):\\/\\/" + - "|(?:inline|data|about|content|javascript):" + - ")" + - "(.*)" ); - - /** - * Attempts to determine whether user input is a URL or search - * terms. Anything with a space is passed to search. - * - * Converts to lowercase any mistakenly uppercased schema (i.e., - * "Http://" converts to "http://" - * - * @return Original or modified URL - * - */ - String smartUrlFilter(String url) { - - String inUrl = url.trim(); - boolean hasSpace = inUrl.indexOf(' ') != -1; - - Matcher matcher = ACCEPTED_URI_SCHEMA.matcher(inUrl); - if (matcher.matches()) { - // force scheme to lowercase - String scheme = matcher.group(1); - String lcScheme = scheme.toLowerCase(); - if (!lcScheme.equals(scheme)) { - inUrl = lcScheme + matcher.group(2); - } - if (hasSpace) { - inUrl = inUrl.replace(" ", "%20"); - } - return inUrl; - } - if (!hasSpace) { - if (Patterns.WEB_URL.matcher(inUrl).matches()) { - return URLUtil.guessUrl(inUrl); - } - } - - // FIXME: Is this the correct place to add to searches? - // what if someone else calls this function? - - Browser.addSearchUrl(mResolver, inUrl); - return URLUtil.composeSearchUrl(inUrl, QuickSearch_G, QUERY_PLACE_HOLDER); - } - - /* package */ void setShouldShowErrorConsole(boolean flag) { - if (flag == mShouldShowErrorConsole) { - // Nothing to do. - return; - } - Tab t = mTabControl.getCurrentTab(); - if (t == null) { - // There is no current tab so we cannot toggle the error console - return; - } - - mShouldShowErrorConsole = flag; - - ErrorConsoleView errorConsole = t.getErrorConsole(true); - - if (flag) { - // Setting the show state of the console will cause it's the layout to be inflated. - if (errorConsole.numberOfErrors() > 0) { - errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED); - } else { - errorConsole.showConsole(ErrorConsoleView.SHOW_NONE); - } - - // Now we can add it to the main view. - mErrorConsoleContainer.addView(errorConsole, - new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT)); - } else { - mErrorConsoleContainer.removeView(errorConsole); - } - - } - - boolean shouldShowErrorConsole() { - return mShouldShowErrorConsole; - } - - private void setStatusBarVisibility(boolean visible) { - int flag = visible ? 0 : WindowManager.LayoutParams.FLAG_FULLSCREEN; - getWindow().setFlags(flag, WindowManager.LayoutParams.FLAG_FULLSCREEN); - } - - - private void sendNetworkType(String type, String subtype) { - WebView w = mTabControl.getCurrentWebView(); - if (w != null) { - w.setNetworkType(type, subtype); - } - } - - private void packageChanged(String packageName, boolean wasAdded) { - WebView w = mTabControl.getCurrentWebView(); - if (w == null) { - return; - } - - if (wasAdded) { - w.addPackageName(packageName); - } else { - w.removePackageName(packageName); - } - } - - private void addPackageNames(Set<String> packageNames) { - WebView w = mTabControl.getCurrentWebView(); - if (w == null) { - return; - } - - w.addPackageNames(packageNames); + Intent intent) { + mController.onActivityResult(requestCode, resultCode, intent); } - private void getInstalledPackages() { - AsyncTask<Void, Void, Set<String> > task = - new AsyncTask<Void, Void, Set<String> >() { - protected Set<String> doInBackground(Void... unused) { - Set<String> installedPackages = new HashSet<String>(); - PackageManager pm = BrowserActivity.this.getPackageManager(); - if (pm != null) { - List<PackageInfo> packages = pm.getInstalledPackages(0); - for (PackageInfo p : packages) { - if (BrowserActivity.this.sGoogleApps.contains(p.packageName)) { - installedPackages.add(p.packageName); - } - } - } - - return installedPackages; - } - - // Executes on the UI thread - protected void onPostExecute(Set<String> installedPackages) { - addPackageNames(installedPackages); - } - }; - task.execute(); - } - - final static int LOCK_ICON_UNSECURE = 0; - final static int LOCK_ICON_SECURE = 1; - final static int LOCK_ICON_MIXED = 2; - - private BrowserSettings mSettings; - private TabControl mTabControl; - private ContentResolver mResolver; - private FrameLayout mContentView; - private View mCustomView; - private FrameLayout mCustomViewContainer; - private WebChromeClient.CustomViewCallback mCustomViewCallback; - - // FIXME, temp address onPrepareMenu performance problem. When we move everything out of - // view, we should rewrite this. - private int mCurrentMenuState = 0; - private int mMenuState = R.id.MAIN_MENU; - private int mOldMenuState = EMPTY_MENU; - private static final int EMPTY_MENU = -1; - private Menu mMenu; - - private FindDialog mFindDialog; - private SelectDialog mSelectDialog; - // Used to prevent chording to result in firing two shortcuts immediately - // one after another. Fixes bug 1211714. - boolean mCanChord; - - private boolean mInLoad; - private boolean mIsNetworkUp; - private boolean mDidStopLoad; - - /* package */ boolean mActivityInPause = true; - - private boolean mMenuIsDown; - - private static boolean mInTrace; - - // Performance probe - private static final int[] SYSTEM_CPU_FORMAT = new int[] { - Process.PROC_SPACE_TERM | Process.PROC_COMBINE, - Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 1: user time - Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 2: nice time - Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 3: sys time - Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 4: idle time - Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 5: iowait time - Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 6: irq time - Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG // 7: softirq time - }; - - private long mStart; - private long mProcessStart; - private long mUserStart; - private long mSystemStart; - private long mIdleStart; - private long mIrqStart; - - private long mUiStart; - - private Drawable mMixLockIcon; - private Drawable mSecLockIcon; - - /* hold a ref so we can auto-cancel if necessary */ - private AlertDialog mAlertDialog; - - // The up-to-date URL and title (these can be different from those stored - // in WebView, since it takes some time for the information in WebView to - // get updated) - private String mUrl; - private String mTitle; - - // As PageInfo has different style for landscape / portrait, we have - // to re-open it when configuration changed - private AlertDialog mPageInfoDialog; - private Tab mPageInfoView; - // If the Page-Info dialog is launched from the SSL-certificate-on-error - // dialog, we should not just dismiss it, but should get back to the - // SSL-certificate-on-error dialog. This flag is used to store this state - private boolean mPageInfoFromShowSSLCertificateOnError; - - // as SSLCertificateOnError has different style for landscape / portrait, - // we have to re-open it when configuration changed - private AlertDialog mSSLCertificateOnErrorDialog; - private WebView mSSLCertificateOnErrorView; - private SslErrorHandler mSSLCertificateOnErrorHandler; - private SslError mSSLCertificateOnErrorError; - - // as SSLCertificate has different style for landscape / portrait, we - // have to re-open it when configuration changed - private AlertDialog mSSLCertificateDialog; - private Tab mSSLCertificateView; - - // as HttpAuthentication has different style for landscape / portrait, we - // have to re-open it when configuration changed - private AlertDialog mHttpAuthenticationDialog; - private HttpAuthHandler mHttpAuthHandler; - - /*package*/ static final FrameLayout.LayoutParams COVER_SCREEN_PARAMS = - new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT); - /*package*/ static final FrameLayout.LayoutParams COVER_SCREEN_GRAVITY_CENTER = - new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - Gravity.CENTER); - // Google search - final static String QuickSearch_G = "http://www.google.com/m?q=%s"; - - final static String QUERY_PLACE_HOLDER = "%s"; - - // "source" parameter for Google search through search key - final static String GOOGLE_SEARCH_SOURCE_SEARCHKEY = "browser-key"; - // "source" parameter for Google search through goto menu - final static String GOOGLE_SEARCH_SOURCE_GOTO = "browser-goto"; - // "source" parameter for Google search through simplily type - final static String GOOGLE_SEARCH_SOURCE_TYPE = "browser-type"; - // "source" parameter for Google search suggested by the browser - final static String GOOGLE_SEARCH_SOURCE_SUGGEST = "browser-suggest"; - // "source" parameter for Google search from unknown source - final static String GOOGLE_SEARCH_SOURCE_UNKNOWN = "unknown"; - - private final static String LOGTAG = "browser"; - - private String mLastEnteredUrl; - - private PowerManager.WakeLock mWakeLock; - private final static int WAKELOCK_TIMEOUT = 5 * 60 * 1000; // 5 minutes - - private Toast mStopToast; - - private TitleBar mTitleBar; - - private LinearLayout mErrorConsoleContainer = null; - private boolean mShouldShowErrorConsole = false; - - // As the ids are dynamically created, we can't guarantee that they will - // be in sequence, so this static array maps ids to a window number. - final static private int[] WINDOW_SHORTCUT_ID_ARRAY = - { R.id.window_one_menu_id, R.id.window_two_menu_id, R.id.window_three_menu_id, - R.id.window_four_menu_id, R.id.window_five_menu_id, R.id.window_six_menu_id, - R.id.window_seven_menu_id, R.id.window_eight_menu_id }; - - // monitor platform changes - private IntentFilter mNetworkStateChangedFilter; - private BroadcastReceiver mNetworkStateIntentReceiver; - - private BroadcastReceiver mPackageInstallationReceiver; - - private SystemAllowGeolocationOrigins mSystemAllowGeolocationOrigins; - - // activity requestCode - final static int COMBO_PAGE = 1; - final static int PREFERENCES_PAGE = 3; - final static int FILE_SELECTED = 4; - - // the default <video> poster - private Bitmap mDefaultVideoPoster; - // the video progress view - private View mVideoProgressView; - - // The Google packages we monitor for the navigator.isApplicationInstalled() - // API. Add as needed. - private static Set<String> sGoogleApps; - static { - sGoogleApps = new HashSet<String>(); - sGoogleApps.add("com.google.android.youtube"); - } - - /** - * A UrlData class to abstract how the content will be set to WebView. - * This base class uses loadUrl to show the content. - */ - /* package */ static class UrlData { - final String mUrl; - final Map<String, String> mHeaders; - final Intent mVoiceIntent; - - UrlData(String url) { - this.mUrl = url; - this.mHeaders = null; - this.mVoiceIntent = null; - } - - UrlData(String url, Map<String, String> headers, Intent intent) { - this.mUrl = url; - this.mHeaders = headers; - if (RecognizerResultsIntent.ACTION_VOICE_SEARCH_RESULTS - .equals(intent.getAction())) { - this.mVoiceIntent = intent; - } else { - this.mVoiceIntent = null; - } - } - - boolean isEmpty() { - return mVoiceIntent == null && (mUrl == null || mUrl.length() == 0); - } - - /** - * Load this UrlData into the given Tab. Use loadUrlDataIn to update - * the title bar as well. - */ - public void loadIn(Tab t) { - if (mVoiceIntent != null) { - t.activateVoiceSearchMode(mVoiceIntent); - } else { - t.getWebView().loadUrl(mUrl, mHeaders); - } - } - }; - /* package */ static final UrlData EMPTY_URL_DATA = new UrlData(null); } diff --git a/src/com/android/browser/BrowserBackupAgent.java b/src/com/android/browser/BrowserBackupAgent.java index c968ce5d7..9c5d65b4c 100644 --- a/src/com/android/browser/BrowserBackupAgent.java +++ b/src/com/android/browser/BrowserBackupAgent.java @@ -166,8 +166,11 @@ public class BrowserBackupAgent extends BackupAgent { if (DEBUG) Log.v(TAG, "Did not see url: " + mark.url); // Right now we do not reconstruct the db entry in its // entirety; we just add a new bookmark with the same data - Bookmarks.addBookmark(null, getContentResolver(), - mark.url, mark.title, null, false); + // FIXME: This file needs to be reworked + // anyway For now, add the bookmark at + // the root level. + Bookmarks.addBookmark(this, false, + mark.url, mark.title, null, false, 0); nUnique++; } else { if (DEBUG) Log.v(TAG, "Skipping extant url: " + mark.url); diff --git a/src/com/android/browser/BrowserBookmarksAdapter.java b/src/com/android/browser/BrowserBookmarksAdapter.java index 241b33b75..f587f0189 100644 --- a/src/com/android/browser/BrowserBookmarksAdapter.java +++ b/src/com/android/browser/BrowserBookmarksAdapter.java @@ -16,567 +16,106 @@ package com.android.browser; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.database.ContentObserver; +import android.content.Context; import android.database.Cursor; -import android.database.DataSetObserver; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.provider.Browser; -import android.provider.Browser.BookmarkColumns; -import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.webkit.WebIconDatabase; -import android.webkit.WebView; -import android.widget.BaseAdapter; +import android.widget.CursorAdapter; import android.widget.ImageView; import android.widget.TextView; -import java.io.ByteArrayOutputStream; - -class BrowserBookmarksAdapter extends BaseAdapter { - - private String mCurrentPage; - private String mCurrentTitle; - private Bitmap mCurrentThumbnail; - private Cursor mCursor; - private int mCount; - private BrowserBookmarksPage mBookmarksPage; - private ContentResolver mContentResolver; - private boolean mDataValid; - private BookmarkViewMode mViewMode; - private boolean mMostVisited; - private boolean mNeedsOffset; - private int mExtraOffset; +class BrowserBookmarksAdapter extends CursorAdapter { + LayoutInflater mInflater; + int mCurrentView; /** * Create a new BrowserBookmarksAdapter. - * @param b BrowserBookmarksPage that instantiated this. - * Necessary so it will adjust its focus - * appropriately after a search. */ - public BrowserBookmarksAdapter(BrowserBookmarksPage b, String curPage, - String curTitle, Bitmap curThumbnail, boolean createShortcut, - boolean mostVisited) { - mNeedsOffset = !(createShortcut || mostVisited); - mMostVisited = mostVisited; - mExtraOffset = mNeedsOffset ? 1 : 0; - mBookmarksPage = b; - mCurrentPage = b.getResources().getString(R.string.current_page) - + curPage; - mCurrentTitle = curTitle; - mCurrentThumbnail = curThumbnail; - mContentResolver = b.getContentResolver(); - mViewMode = BookmarkViewMode.LIST; + public BrowserBookmarksAdapter(Context context, int defaultView) { + // Make sure to tell the CursorAdapter to avoid the observer and auto-requery + // since the Loader will do that for us. + super(context, null); + mInflater = LayoutInflater.from(context); + selectView(defaultView); + } - String whereClause; - // FIXME: Should have a default sort order that the user selects. - String orderBy = Browser.BookmarkColumns.VISITS + " DESC"; - if (mostVisited) { - whereClause = Browser.BookmarkColumns.VISITS + " != 0"; + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (mCurrentView == BrowserBookmarksPage.VIEW_LIST) { + bindListView(view, context, cursor); } else { - whereClause = Browser.BookmarkColumns.BOOKMARK + " = 1"; + bindGridView(view, context, cursor); } - mCursor = b.managedQuery(Browser.BOOKMARKS_URI, - Browser.HISTORY_PROJECTION, whereClause, null, orderBy); - mCursor.registerContentObserver(new ChangeObserver()); - mCursor.registerDataSetObserver(new MyDataSetObserver()); - - mDataValid = true; - notifyDataSetChanged(); - - mCount = mCursor.getCount() + mExtraOffset; - } - - /** - * Return a hashmap with one row's Title, Url, and favicon. - * @param position Position in the list. - * @return Bundle Stores title, url of row position, favicon, and id - * for the url. Return a blank map if position is out of - * range. - */ - public Bundle getRow(int position) { - Bundle map = new Bundle(); - if (position < mExtraOffset || position >= mCount) { - return map; - } - mCursor.moveToPosition(position- mExtraOffset); - String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX); - map.putString(Browser.BookmarkColumns.TITLE, - mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX)); - map.putString(Browser.BookmarkColumns.URL, url); - byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX); - if (data != null) { - map.putParcelable(Browser.BookmarkColumns.FAVICON, - BitmapFactory.decodeByteArray(data, 0, data.length)); - } - map.putInt("id", mCursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX)); - return map; } - /** - * Update a row in the database with new information. - * Requeries the database if the information has changed. - * @param map Bundle storing id, title and url of new information - */ - public void updateRow(Bundle map) { + void bindGridView(View view, Context context, Cursor cursor) { + ImageView thumb = (ImageView) view.findViewById(R.id.thumb); + TextView tv = (TextView) view.findViewById(R.id.label); - // Find the record - int id = map.getInt("id"); - int position = -1; - for (mCursor.moveToFirst(); !mCursor.isAfterLast(); mCursor.moveToNext()) { - if (mCursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX) == id) { - position = mCursor.getPosition(); - break; + tv.setText(cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE)); + if (cursor.getInt(BookmarksLoader.COLUMN_INDEX_IS_FOLDER) != 0) { + // folder + thumb.setImageResource(R.drawable.ic_folder); + } else { + byte[] thumbData = cursor.getBlob(BookmarksLoader.COLUMN_INDEX_THUMBNAIL); + Bitmap thumbBitmap = null; + if (thumbData != null) { + thumbBitmap = BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length); } - } - if (position < 0) { - return; - } - mCursor.moveToPosition(position); - ContentValues values = new ContentValues(); - String title = map.getString(Browser.BookmarkColumns.TITLE); - if (!title.equals(mCursor - .getString(Browser.HISTORY_PROJECTION_TITLE_INDEX))) { - values.put(Browser.BookmarkColumns.TITLE, title); - } - String url = map.getString(Browser.BookmarkColumns.URL); - if (!url.equals(mCursor. - getString(Browser.HISTORY_PROJECTION_URL_INDEX))) { - values.put(Browser.BookmarkColumns.URL, url); - } - - if (map.getBoolean("invalidateThumbnail") == true) { - values.put(Browser.BookmarkColumns.THUMBNAIL, new byte[0]); - } - if (values.size() > 0 - && mContentResolver.update(Browser.BOOKMARKS_URI, values, - "_id = " + id, null) != -1) { - refreshList(); - } - } - - /** - * Delete a row from the database. Requeries the database. - * Does nothing if the provided position is out of range. - * @param position Position in the list. - */ - public void deleteRow(int position) { - if (position < mExtraOffset || position >= getCount()) { - return; - } - mCursor.moveToPosition(position- mExtraOffset); - String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX); - String title = mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX); - Bookmarks.removeFromBookmarks(null, mContentResolver, url, title); - refreshList(); - } - - /** - * Delete all bookmarks from the db. Requeries the database. - * All bookmarks with become visited URLs or if never visited - * are removed - */ - public void deleteAllRows() { - StringBuilder deleteIds = null; - StringBuilder convertIds = null; - - for (mCursor.moveToFirst(); !mCursor.isAfterLast(); mCursor.moveToNext()) { - String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX); - WebIconDatabase.getInstance().releaseIconForPageUrl(url); - int id = mCursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX); - int numVisits = mCursor.getInt(Browser.HISTORY_PROJECTION_VISITS_INDEX); - if (0 == numVisits) { - if (deleteIds == null) { - deleteIds = new StringBuilder(); - deleteIds.append("( "); - } else { - deleteIds.append(" OR ( "); - } - deleteIds.append(BookmarkColumns._ID); - deleteIds.append(" = "); - deleteIds.append(id); - deleteIds.append(" )"); + if (thumbBitmap == null) { + thumb.setImageResource(R.drawable.browser_thumbnail); } else { - // It is no longer a bookmark, but it is still a visited site. - if (convertIds == null) { - convertIds = new StringBuilder(); - convertIds.append("( "); - } else { - convertIds.append(" OR ( "); - } - convertIds.append(BookmarkColumns._ID); - convertIds.append(" = "); - convertIds.append(id); - convertIds.append(" )"); + thumb.setImageBitmap(thumbBitmap); } } - - if (deleteIds != null) { - mContentResolver.delete(Browser.BOOKMARKS_URI, deleteIds.toString(), - null); - } - if (convertIds != null) { - ContentValues values = new ContentValues(); - values.put(Browser.BookmarkColumns.BOOKMARK, 0); - mContentResolver.update(Browser.BOOKMARKS_URI, values, - convertIds.toString(), null); - } - refreshList(); - } - - /** - * Refresh list to recognize a change in the database. - */ - public void refreshList() { - mCursor.requery(); - mCount = mCursor.getCount() + mExtraOffset; - notifyDataSetChanged(); - } - - /** - * Update the bookmark's favicon. This is a convenience method for updating - * a bookmark favicon for the originalUrl and url of the passed in WebView. - * @param cr The ContentResolver to use. - * @param originalUrl The original url before any redirects. - * @param url The current url. - * @param favicon The favicon bitmap to write to the db. - */ - /* package */ static void updateBookmarkFavicon(final ContentResolver cr, - final String originalUrl, final String url, final Bitmap favicon) { - new AsyncTask<Void, Void, Void>() { - protected Void doInBackground(Void... unused) { - final Cursor c = - queryBookmarksForUrl(cr, originalUrl, url, true); - if (c == null) { - return null; - } - if (c.moveToFirst()) { - ContentValues values = new ContentValues(); - final ByteArrayOutputStream os = - new ByteArrayOutputStream(); - favicon.compress(Bitmap.CompressFormat.PNG, 100, os); - values.put(Browser.BookmarkColumns.FAVICON, - os.toByteArray()); - do { - cr.update(ContentUris.withAppendedId( - Browser.BOOKMARKS_URI, c.getInt(0)), - values, null, null); - } while (c.moveToNext()); - } - c.close(); - return null; - } - }.execute(); - } - - /* package */ static Cursor queryBookmarksForUrl(ContentResolver cr, - String originalUrl, String url, boolean onlyBookmarks) { - if (cr == null || url == null) { - return null; - } - - // If originalUrl is null, just set it to url. - if (originalUrl == null) { - originalUrl = url; - } - - // Look for both the original url and the actual url. This takes in to - // account redirects. - String originalUrlNoQuery = removeQuery(originalUrl); - String urlNoQuery = removeQuery(url); - originalUrl = originalUrlNoQuery + '?'; - url = urlNoQuery + '?'; - - // Use NoQuery to search for the base url (i.e. if the url is - // http://www.yahoo.com/?rs=1, search for http://www.yahoo.com) - // Use url to match the base url with other queries (i.e. if the url is - // http://www.google.com/m, search for - // http://www.google.com/m?some_query) - final String[] selArgs = new String[] { - originalUrlNoQuery, urlNoQuery, originalUrl, url }; - String where = BookmarkColumns.URL + " == ? OR " - + BookmarkColumns.URL + " == ? OR " - + BookmarkColumns.URL + " LIKE ? || '%' OR " - + BookmarkColumns.URL + " LIKE ? || '%'"; - if (onlyBookmarks) { - where = "(" + where + ") AND " + BookmarkColumns.BOOKMARK + " == 1"; - } - final String[] projection = - new String[] { Browser.BookmarkColumns._ID }; - return cr.query(Browser.BOOKMARKS_URI, projection, where, selArgs, - null); - } - - // Strip the query from the given url. - private static String removeQuery(String url) { - if (url == null) { - return null; - } - int query = url.indexOf('?'); - String noQuery = url; - if (query != -1) { - noQuery = url.substring(0, query); - } - return noQuery; - } - - /** - * How many items should be displayed in the list. - * @return Count of items. - */ - public int getCount() { - if (mDataValid) { - return mCount; - } else { - return 0; - } - } - - public boolean areAllItemsEnabled() { - return true; - } - - public boolean isEnabled(int position) { - return true; - } - - /** - * Get the data associated with the specified position in the list. - * @param position Index of the item whose data we want. - * @return The data at the specified position. - */ - public Object getItem(int position) { - return null; } - /** - * Get the row id associated with the specified position in the list. - * @param position Index of the item whose row id we want. - * @return The id of the item at the specified position. - */ - public long getItemId(int position) { - return position; - } + void bindListView(View view, Context context, Cursor cursor) { + ImageView favicon = (ImageView) view.findViewById(R.id.favicon); + TextView tv = (TextView) view.findViewById(R.id.label); - /* package */ void switchViewMode(BookmarkViewMode viewMode) { - mViewMode = viewMode; - } - - /* package */ void populateBookmarkItem(BookmarkItem b, int position) { - mCursor.moveToPosition(position - mExtraOffset); - String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX); - b.setUrl(url); - b.setName(mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX)); - byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX); - Bitmap bitmap = null; - if (data == null) { - bitmap = CombinedBookmarkHistoryActivity.getIconListenerSet() - .getFavicon(url); + tv.setText(cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE)); + if (cursor.getInt(BookmarksLoader.COLUMN_INDEX_IS_FOLDER) != 0) { + // folder + favicon.setImageResource(R.drawable.ic_folder); } else { - bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); - } - b.setFavicon(bitmap); - } - - /** - * Get a View that displays the data at the specified position - * in the list. - * @param position Index of the item whose view we want. - * @return A View corresponding to the data at the specified position. - */ - public View getView(int position, View convertView, ViewGroup parent) { - if (!mDataValid) { - throw new IllegalStateException( - "this should only be called when the cursor is valid"); - } - if (position < 0 || position > mCount) { - throw new AssertionError( - "BrowserBookmarksAdapter tried to get a view out of range"); - } - if (mViewMode == BookmarkViewMode.GRID) { - if (convertView == null || convertView instanceof AddNewBookmark - || convertView instanceof BookmarkItem) { - LayoutInflater factory = LayoutInflater.from(mBookmarksPage); - convertView - = factory.inflate(R.layout.bookmark_thumbnail, null); + byte[] faviconData = cursor.getBlob(BookmarksLoader.COLUMN_INDEX_FAVICON); + Bitmap faviconBitmap = null; + if (faviconData != null) { + faviconBitmap = BitmapFactory.decodeByteArray(faviconData, 0, faviconData.length); } - View holder = convertView.findViewById(R.id.holder); - ImageView thumb = (ImageView) convertView.findViewById(R.id.thumb); - TextView tv = (TextView) convertView.findViewById(R.id.label); - if (0 == position && mNeedsOffset) { - // This is to create a bookmark for the current page. - holder.setVisibility(View.VISIBLE); - tv.setText(mCurrentTitle); - - if (mCurrentThumbnail != null) { - thumb.setImageBitmap(mCurrentThumbnail); - } else { - thumb.setImageResource( - R.drawable.browser_thumbnail); - } - return convertView; - } - holder.setVisibility(View.GONE); - mCursor.moveToPosition(position - mExtraOffset); - tv.setText(mCursor.getString( - Browser.HISTORY_PROJECTION_TITLE_INDEX)); - Bitmap thumbnail = getScreenshot(position); - if (thumbnail == null) { - thumb.setImageResource(R.drawable.browser_thumbnail); + if (faviconBitmap == null) { + favicon.setImageResource(R.drawable.app_web_browser_sm); } else { - thumb.setImageBitmap(thumbnail); - } - - return convertView; - - } - if (position == 0 && mNeedsOffset) { - AddNewBookmark b; - if (convertView instanceof AddNewBookmark) { - b = (AddNewBookmark) convertView; - } else { - b = new AddNewBookmark(mBookmarksPage); - } - b.setUrl(mCurrentPage); - return b; - } - if (mMostVisited) { - if (convertView == null || !(convertView instanceof HistoryItem)) { - convertView = new HistoryItem(mBookmarksPage); - } - } else { - if (convertView == null || !(convertView instanceof BookmarkItem)) { - convertView = new BookmarkItem(mBookmarksPage); + favicon.setImageBitmap(faviconBitmap); } } - bind((BookmarkItem) convertView, position); - if (mMostVisited) { - ((HistoryItem) convertView).setIsBookmark( - getIsBookmark(position)); - } - return convertView; - } - - /** - * Return the title for this item in the list. - */ - public String getTitle(int position) { - return getString(Browser.HISTORY_PROJECTION_TITLE_INDEX, position); } - /** - * Return the Url for this item in the list. - */ - public String getUrl(int position) { - return getString(Browser.HISTORY_PROJECTION_URL_INDEX, position); - } - - /** - * Return the screenshot for this item in the list. - */ - public Bitmap getScreenshot(int position) { - return getBitmap(Browser.HISTORY_PROJECTION_THUMBNAIL_INDEX, position); - } - - /** - * Return the favicon for this item in the list. - */ - public Bitmap getFavicon(int position) { - return getBitmap(Browser.HISTORY_PROJECTION_FAVICON_INDEX, position); - } - - public Bitmap getTouchIcon(int position) { - return getBitmap(Browser.HISTORY_PROJECTION_TOUCH_ICON_INDEX, position); - } - - private Bitmap getBitmap(int cursorIndex, int position) { - if (position < mExtraOffset || position > mCount) { - return null; - } - mCursor.moveToPosition(position - mExtraOffset); - byte[] data = mCursor.getBlob(cursorIndex); - if (data == null) { - return null; - } - return BitmapFactory.decodeByteArray(data, 0, data.length); - } - - /** - * Return whether or not this item represents a bookmarked site. - */ - public boolean getIsBookmark(int position) { - if (position < mExtraOffset || position > mCount) { - return false; - } - mCursor.moveToPosition(position - mExtraOffset); - return (1 == mCursor.getInt(Browser.HISTORY_PROJECTION_BOOKMARK_INDEX)); - } - - /** - * Private helper function to return the title or url. - */ - private String getString(int cursorIndex, int position) { - if (position < mExtraOffset || position > mCount) { - return ""; - } - mCursor.moveToPosition(position- mExtraOffset); - return mCursor.getString(cursorIndex); - } - - private void bind(BookmarkItem b, int position) { - mCursor.moveToPosition(position- mExtraOffset); - - b.setName(mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX)); - String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX); - b.setUrl(url); - byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX); - if (data != null) { - b.setFavicon(BitmapFactory.decodeByteArray(data, 0, data.length)); + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + if (mCurrentView == BrowserBookmarksPage.VIEW_LIST) { + return mInflater.inflate(R.layout.bookmark_list, parent, false); } else { - b.setFavicon(CombinedBookmarkHistoryActivity.getIconListenerSet() - .getFavicon(url)); + return mInflater.inflate(R.layout.bookmark_thumbnail, parent, false); } } - private class ChangeObserver extends ContentObserver { - public ChangeObserver() { - super(new Handler(Looper.getMainLooper())); - } - - @Override - public boolean deliverSelfNotifications() { - return true; - } - - @Override - public void onChange(boolean selfChange) { - refreshList(); + public void selectView(int view) { + if (view != BrowserBookmarksPage.VIEW_LIST + && view != BrowserBookmarksPage.VIEW_THUMBNAILS) { + throw new IllegalArgumentException("Unknown view specified: " + view); } + mCurrentView = view; } - - private class MyDataSetObserver extends DataSetObserver { - @Override - public void onChanged() { - mDataValid = true; - notifyDataSetChanged(); - } - @Override - public void onInvalidated() { - mDataValid = false; - notifyDataSetInvalidated(); - } + @Override + public Cursor getItem(int position) { + return (Cursor) super.getItem(position); } } diff --git a/src/com/android/browser/BrowserBookmarksPage.java b/src/com/android/browser/BrowserBookmarksPage.java index dd01009f8..4887f3f49 100644 --- a/src/com/android/browser/BrowserBookmarksPage.java +++ b/src/com/android/browser/BrowserBookmarksPage.java @@ -18,89 +18,260 @@ package com.android.browser; import android.app.Activity; import android.app.AlertDialog; +import android.app.Fragment; +import android.app.LoaderManager; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.content.CursorLoader; import android.content.DialogInterface; import android.content.Intent; +import android.content.Loader; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; +import android.database.Cursor; 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.preference.PreferenceManager; +import android.provider.BrowserContract; +import android.provider.BrowserContract.Accounts; +import android.text.TextUtils; import android.view.ContextMenu; -import android.view.Menu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; -import android.view.ContextMenu.ContextMenuInfo; import android.webkit.WebIconDatabase.IconListener; +import android.widget.Adapter; import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemSelectedListener; import android.widget.GridView; import android.widget.ListView; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; +import android.widget.TextView; import android.widget.Toast; -/*package*/ enum BookmarkViewMode { NONE, GRID, LIST } +interface BookmarksPageCallbacks { + // Return true if handled + boolean onBookmarkSelected(Cursor c, boolean isFolder); + // Return true if handled + boolean onOpenInNewWindow(Cursor c); +} + /** * 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"; +public class BrowserBookmarksPage extends Fragment implements View.OnCreateContextMenuListener, + LoaderManager.LoaderCallbacks<Cursor>, OnItemClickListener, IconListener, + OnItemSelectedListener, BreadCrumbView.Controller, OnClickListener, OnMenuItemClickListener { + + static final String LOGTAG = "browser"; + + static final int LOADER_BOOKMARKS = 1; + static final int LOADER_ACCOUNTS_THEN_BOOKMARKS = 2; + + static final String EXTRA_DISABLE_WINDOW = "disable_new_window"; + + static final String ACCOUNT_NAME_UNSYNCED = "Unsynced"; + + public static final String PREF_ACCOUNT_TYPE = "acct_type"; + public static final String PREF_ACCOUNT_NAME = "acct_name"; + + static final String DEFAULT_ACCOUNT = "local"; + static final int VIEW_THUMBNAILS = 1; + static final int VIEW_LIST = 2; + static final String PREF_SELECTED_VIEW = "bookmarks_view"; + + BookmarksPageCallbacks mCallbacks; + GridView mGrid; + ListView mList; + BrowserBookmarksAdapter mAdapter; + boolean mDisableNewWindow; + boolean mCanceled = false; + boolean mEnableContextMenu = true; + boolean mShowRootFolder = false; + View mEmptyView; + int mCurrentView; + View mHeader; + View mRootFolderView; + ViewGroup mHeaderContainer; + BreadCrumbView mCrumbs; + TextView mSelectBookmarkView; + + static BrowserBookmarksPage newInstance(BookmarksPageCallbacks cb, + Bundle args, ViewGroup headerContainer) { + BrowserBookmarksPage bbp = new BrowserBookmarksPage(); + bbp.mCallbacks = cb; + bbp.mHeaderContainer = headerContainer; + bbp.setArguments(args); + return bbp; + } + + @Override + public Loader<Cursor> onCreateLoader(int id, Bundle args) { + switch (id) { + case LOADER_BOOKMARKS: { + String accountType = null; + String accountName = null; + if (args != null) { + accountType = args.getString(BookmarksLoader.ARG_ACCOUNT_TYPE); + accountName = args.getString(BookmarksLoader.ARG_ACCOUNT_NAME); + } + BookmarksLoader bl = new BookmarksLoader(getActivity(), accountType, accountName); + if (mCrumbs != null) { + Uri uri = (Uri) mCrumbs.getTopData(); + if (uri != null) { + bl.setUri(uri); + } + } + return bl; + } + case LOADER_ACCOUNTS_THEN_BOOKMARKS: { + return new CursorLoader(getActivity(), Accounts.CONTENT_URI, + new String[] { Accounts.ACCOUNT_TYPE, Accounts.ACCOUNT_NAME }, null, null, + null); + } + } + throw new UnsupportedOperationException("Unknown loader id " + id); + } + + @Override + public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { + switch (loader.getId()) { + case LOADER_BOOKMARKS: { + // Set the visibility of the empty vs. content views + if (cursor == null || cursor.getCount() == 0) { + mEmptyView.setVisibility(View.VISIBLE); + mGrid.setVisibility(View.GONE); + mList.setVisibility(View.GONE); + } else { + mEmptyView.setVisibility(View.GONE); + setupBookmarkView(); + } + + // Give the new data to the adapter + mAdapter.changeCursor(cursor); + break; + } + + case LOADER_ACCOUNTS_THEN_BOOKMARKS: { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences( + getActivity()); + String storedAccountType = prefs.getString(PREF_ACCOUNT_TYPE, null); + String storedAccountName = prefs.getString(PREF_ACCOUNT_NAME, null); + String accountType = + TextUtils.isEmpty(storedAccountType) ? DEFAULT_ACCOUNT : storedAccountType; + String accountName = + TextUtils.isEmpty(storedAccountName) ? DEFAULT_ACCOUNT : storedAccountName; + + Bundle args = null; + if (cursor == null || !cursor.moveToFirst()) { + // No accounts, set the prefs to the default + accountType = DEFAULT_ACCOUNT; + accountName = DEFAULT_ACCOUNT; + } else { + int accountPosition = -1; + + if (!DEFAULT_ACCOUNT.equals(accountType) && + !DEFAULT_ACCOUNT.equals(accountName)) { + // Check to see if the account in prefs still exists + cursor.moveToFirst(); + do { + if (accountType.equals(cursor.getString(0)) + && accountName.equals(cursor.getString(1))) { + accountPosition = cursor.getPosition(); + break; + } + } while (cursor.moveToNext()); + } + + if (accountPosition == -1) { + if (!(DEFAULT_ACCOUNT.equals(accountType) + && DEFAULT_ACCOUNT.equals(accountName))) { + // No account is set in prefs and there is at least one, + // so pick the first one as the default + cursor.moveToFirst(); + accountType = cursor.getString(0); + accountName = cursor.getString(1); + } + } + + args = new Bundle(); + args.putString(BookmarksLoader.ARG_ACCOUNT_TYPE, accountType); + args.putString(BookmarksLoader.ARG_ACCOUNT_NAME, accountName); + } + + // The stored account name wasn't found, update the stored account with a valid one + if (!accountType.equals(storedAccountType) + || !accountName.equals(storedAccountName)) { + prefs.edit() + .putString(PREF_ACCOUNT_TYPE, accountType) + .putString(PREF_ACCOUNT_NAME, accountName) + .apply(); + } + getLoaderManager().initLoader(LOADER_BOOKMARKS, args, this); + + break; + } + } + } + + long getFolderId() { + LoaderManager manager = getLoaderManager(); + BookmarksLoader loader = + (BookmarksLoader) ((Loader<?>)manager.getLoader(LOADER_BOOKMARKS)); + + Uri uri = loader.getUri(); + if (uri != null) { + try { + return ContentUris.parseId(uri); + } catch (NumberFormatException nfx) { + return -1; + } + } + return -1; + } + + public void onFolderChange(int level, Object data) { + Uri uri = (Uri) data; + if (uri == null) { + // top level + uri = BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER; + } + LoaderManager manager = getLoaderManager(); + BookmarksLoader loader = + (BookmarksLoader) ((Loader<?>) manager.getLoader(LOADER_BOOKMARKS)); + loader.setUri(uri); + loader.forceLoad(); + + } @Override public boolean onContextItemSelected(MenuItem item) { + final Activity activity = getActivity(); // 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; + return false; } 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; + return false; } switch (item.getItemId()) { - case R.id.new_context_menu_id: - saveCurrentPage(); - break; case R.id.open_context_menu_id: loadUrl(i.position); break; @@ -108,652 +279,493 @@ public class BrowserBookmarksPage extends Activity implements editBookmark(i.position); break; case R.id.shortcut_context_menu_id: - final Intent send = createShortcutIntent(i.position); - send.setAction(INSTALL_SHORTCUT); - sendBroadcast(send); + Cursor c = mAdapter.getItem(i.position); + activity.sendBroadcast(createShortcutIntent(getActivity(), c)); break; case R.id.delete_context_menu_id: - if (mMostVisited) { - Browser.deleteFromHistory(getContentResolver(), - getUrl(i.position)); - refreshList(); - } else { - displayRemoveBookmarkDialog(i.position); - } + 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)); + case R.id.share_link_context_menu_id: { + Cursor cursor = mAdapter.getItem(i.position); + Controller.sharePage(activity, + cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE), + cursor.getString(BookmarksLoader.COLUMN_INDEX_URL), + getBitmap(cursor, BookmarksLoader.COLUMN_INDEX_FAVICON), + getBitmap(cursor, BookmarksLoader.COLUMN_INDEX_THUMBNAIL)); 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(); + case R.id.homepage_context_menu_id: { + BrowserSettings.getInstance().setHomePage(activity, getUrl(i.position)); + Toast.makeText(activity, 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(); - } + case R.id.save_to_bookmarks_menu_id: { + Cursor cursor = mAdapter.getItem(i.position); + String name = cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE); + String url = cursor.getString(BookmarksLoader.COLUMN_INDEX_URL); // 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); - } + Bookmarks.removeFromBookmarks(activity, activity.getContentResolver(), url, name); 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); - } + static Bitmap getBitmap(Cursor cursor, int columnIndex) { + byte[] data = cursor.getBlob(columnIndex); + if (data == null) { + return null; + } + return BitmapFactory.decodeByteArray(data, 0, data.length); + } - 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); - } + private MenuItem.OnMenuItemClickListener mContextItemClickListener = + new MenuItem.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + return onContextItemSelected(item); + } + }; + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; + Cursor cursor = mAdapter.getItem(info.position); + boolean isFolder + = cursor.getInt(BookmarksLoader.COLUMN_INDEX_IS_FOLDER) != 0; + + final Activity activity = getActivity(); + MenuInflater inflater = activity.getMenuInflater(); + inflater.inflate(R.menu.bookmarkscontext, menu); + if (isFolder) { + menu.setGroupVisible(R.id.FOLDER_CONTEXT_MENU, true); + } else { + menu.setGroupVisible(R.id.BOOKMARK_CONTEXT_MENU, true); 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); + menu.findItem(R.id.new_window_context_menu_id).setVisible(false); } - if (mViewMode == BookmarkViewMode.GRID) { - mBookmarksAdapter.populateBookmarkItem(mContextHeader, - i.position); - } else { - BookmarkItem b = (BookmarkItem) i.targetView; - b.copyTo(mContextHeader); + } + BookmarkItem header = new BookmarkItem(activity); + populateBookmarkItem(cursor, header, isFolder); + new LookupBookmarkCount(getActivity(), header) + .execute(cursor.getLong(BookmarksLoader.COLUMN_INDEX_ID)); + menu.setHeaderView(header); + + int count = menu.size(); + for (int i = 0; i < count; i++) { + menu.getItem(i).setOnMenuItemClickListener(mContextItemClickListener); + } + } + + private void populateBookmarkItem(Cursor cursor, BookmarkItem item, boolean isFolder) { + item.setName(cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE)); + if (isFolder) { + item.setUrl(null); + Bitmap bitmap = + BitmapFactory.decodeResource(getResources(), R.drawable.ic_folder); + item.setFavicon(bitmap); + } else { + String url = cursor.getString(BookmarksLoader.COLUMN_INDEX_URL); + item.setUrl(url); + Bitmap bitmap = getBitmap(cursor, BookmarksLoader.COLUMN_INDEX_FAVICON); + if (bitmap == null) { + bitmap = CombinedBookmarkHistoryView.getIconListenerSet().getFavicon(url); } - menu.setHeaderView(mContextHeader); + item.setFavicon(bitmap); } + } /** * Create a new BrowserBookmarksPage. */ @Override - protected void onCreate(Bundle icicle) { + public 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(); + Bundle args = getArguments(); + mDisableNewWindow = args == null ? false : args.getBoolean(EXTRA_DISABLE_WINDOW, false); } @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; + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + Context context = getActivity(); + + View root = inflater.inflate(R.layout.bookmarks, container, false); + mEmptyView = root.findViewById(android.R.id.empty); + + mGrid = (GridView) root.findViewById(R.id.grid); + mGrid.setOnItemClickListener(this); + mGrid.setColumnWidth(Controller.getDesiredThumbnailWidth(getActivity())); + mList = (ListView) root.findViewById(R.id.list); + mList.setOnItemClickListener(this); + setEnableContextMenu(mEnableContextMenu); + + // Prep the header + ViewGroup hc = mHeaderContainer; + if (hc == null) { + hc = (ViewGroup) root.findViewById(R.id.header_container); + hc.setVisibility(View.VISIBLE); } - - 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()); + mHeader = inflater.inflate(R.layout.bookmarks_header, hc, true); + mCrumbs = (BreadCrumbView) mHeader.findViewById(R.id.crumbs); + mCrumbs.setController(this); + mSelectBookmarkView = (TextView) mHeader.findViewById(R.id.select_bookmark_view); + mSelectBookmarkView.setOnClickListener(this); + mRootFolderView = mHeader.findViewById(R.id.root_folder); + mRootFolderView.setOnClickListener(this); + setShowRootFolder(mShowRootFolder); + + // Start the loaders + LoaderManager lm = getLoaderManager(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + mCurrentView = + prefs.getInt(PREF_SELECTED_VIEW, BrowserBookmarksPage.VIEW_THUMBNAILS); + if (mCurrentView == BrowserBookmarksPage.VIEW_THUMBNAILS) { + mSelectBookmarkView.setText(R.string.bookmark_thumbnail_view); } else { - ed.putInt(PREF_BOOKMARK_VIEW_MODE, mViewMode.ordinal()); + mSelectBookmarkView.setText(R.string.bookmark_list_view); } - 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); - } + mAdapter = new BrowserBookmarksAdapter(getActivity(), mCurrentView); + String accountType = prefs.getString(PREF_ACCOUNT_TYPE, DEFAULT_ACCOUNT); + String accountName = prefs.getString(PREF_ACCOUNT_NAME, DEFAULT_ACCOUNT); + if (!TextUtils.isEmpty(accountType) && !TextUtils.isEmpty(accountName)) { + // There is an account set, load up that one + Bundle args = null; + if (!DEFAULT_ACCOUNT.equals(accountType) && !DEFAULT_ACCOUNT.equals(accountName)) { + args = new Bundle(); + args.putString(BookmarksLoader.ARG_ACCOUNT_TYPE, accountType); + args.putString(BookmarksLoader.ARG_ACCOUNT_NAME, accountName); } + lm.restartLoader(LOADER_BOOKMARKS, args, this); } 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); - } - } + // No account set, load the account list first + lm.restartLoader(LOADER_ACCOUNTS_THEN_BOOKMARKS, null, this); } - } - private static final ViewGroup.LayoutParams FULL_SCREEN_PARAMS - = new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT); + // Add our own listener in case there are favicons that have yet to be loaded. + CombinedBookmarkHistoryView.getIconListenerSet().addListener(this); - 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(); - } - } - }; + return root; + } - 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)); + public void setShowRootFolder(boolean show) { + mShowRootFolder = show; + if (mRootFolderView != null) { + if (mShowRootFolder) { + mRootFolderView.setVisibility(View.VISIBLE); } 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); + mRootFolderView.setVisibility(View.GONE); } } - // 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); + @Override + public void onDestroyView() { + super.onDestroyView(); + if (mHeaderContainer != null) { + mHeaderContainer.removeView(mHeader); + } + mCrumbs.setController(null); + mCrumbs = null; } - private void loadUrl(int position) { - Intent intent = (new Intent()).setAction(getUrl(position)); - setResultToParent(RESULT_OK, intent); - finish(); + @Override + public void onReceivedIcon(String url, Bitmap icon) { + // A new favicon has been loaded, so let anything attached to the adapter know about it + // so new icons will be loaded. + mAdapter.notifyDataSetChanged(); } @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; + 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; + } + + Cursor cursor = mAdapter.getItem(position); + boolean isFolder = cursor.getInt(BookmarksLoader.COLUMN_INDEX_IS_FOLDER) != 0; + if (mCallbacks != null && + mCallbacks.onBookmarkSelected(cursor, isFolder)) { + return; + } + + if (isFolder) { + String title = cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE); + LoaderManager manager = getLoaderManager(); + BookmarksLoader loader = + (BookmarksLoader) ((Loader<?>) manager.getLoader(LOADER_BOOKMARKS)); + Uri uri = ContentUris.withAppendedId( + BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER, id); + if (mCrumbs != null) { + // update crumbs + mCrumbs.pushView(title, uri); + } + loader.setUri(uri); + loader.forceLoad(); } - 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; + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + Adapter adapter = parent.getAdapter(); + String accountType = "com.google"; + String accountName = adapter.getItem(position).toString(); + + Bundle args = null; + if (ACCOUNT_NAME_UNSYNCED.equals(accountName)) { + accountType = DEFAULT_ACCOUNT; + accountName = DEFAULT_ACCOUNT; } else { - titleResId = R.string.switch_to_thumbnails; - iconResId = R.drawable.ic_menu_thumbnail; + args = new Bundle(); + args.putString(BookmarksLoader.ARG_ACCOUNT_TYPE, accountType); + args.putString(BookmarksLoader.ARG_ACCOUNT_NAME, accountName); } - switchItem.setTitle(titleResId); - switchItem.setIcon(iconResId); - return true; + + // Remember the selection for later + PreferenceManager.getDefaultSharedPreferences(getActivity()).edit() + .putString(PREF_ACCOUNT_TYPE, accountType) + .putString(PREF_ACCOUNT_NAME, accountName) + .apply(); + + getLoaderManager().restartLoader(LOADER_BOOKMARKS, args, this); } @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.new_context_menu_id: - saveCurrentPage(); - break; + public void onNothingSelected(AdapterView<?> parent) { + // Do nothing + } - case R.id.switch_mode_menu_id: - if (mViewMode == BookmarkViewMode.GRID) { - switchViewMode(BookmarkViewMode.LIST); - } else { - switchViewMode(BookmarkViewMode.GRID); - } - break; + /* package */ static Intent createShortcutIntent(Context context, Cursor cursor) { + String url = cursor.getString(BookmarksLoader.COLUMN_INDEX_URL); + String title = cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE); + Bitmap touchIcon = getBitmap(cursor, BookmarksLoader.COLUMN_INDEX_TOUCH_ICON); + Bitmap favicon = getBitmap(cursor, BookmarksLoader.COLUMN_INDEX_FAVICON); + return BookmarkUtils.createAddToHomeIntent(context, url, title, touchIcon, favicon); + } - default: - return super.onOptionsItemSelected(item); + private void loadUrl(int position) { + if (mCallbacks != null) { + mCallbacks.onBookmarkSelected(mAdapter.getItem(position), false); } - 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(); + if (mCallbacks != null) { + mCallbacks.onOpenInNewWindow(mAdapter.getItem(position)); + } } - 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; + Intent intent = new Intent(getActivity(), AddBookmarkPage.class); + Cursor cursor = mAdapter.getItem(position); + Bundle item = new Bundle(); + item.putString(BrowserContract.Bookmarks.TITLE, + cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE)); + item.putString(BrowserContract.Bookmarks.URL, + cursor.getString(BookmarksLoader.COLUMN_INDEX_URL)); + byte[] data = cursor.getBlob(BookmarksLoader.COLUMN_INDEX_FAVICON); + if (data != null) { + item.putParcelable(BrowserContract.Bookmarks.FAVICON, + BitmapFactory.decodeByteArray(data, 0, data.length)); } + item.putLong(BrowserContract.Bookmarks._ID, + cursor.getLong(BookmarksLoader.COLUMN_INDEX_ID)); + item.putLong(BrowserContract.Bookmarks.PARENT, + cursor.getLong(BookmarksLoader.COLUMN_INDEX_PARENT)); + intent.putExtra(AddBookmarkPage.EXTRA_EDIT_BOOKMARK, item); + intent.putExtra(AddBookmarkPage.EXTRA_IS_FOLDER, + cursor.getInt(BookmarksLoader.COLUMN_INDEX_IS_FOLDER) == 1); + startActivity(intent); } - private void displayRemoveBookmarkDialog(int position) { + private void displayRemoveBookmarkDialog(final int position) { // Put up a dialog asking if the user really wants to // delete the bookmark - final int deletePos = position; - new AlertDialog.Builder(this) + Cursor cursor = mAdapter.getItem(position); + Context context = getActivity(); + final ContentResolver resolver = context.getContentResolver(); + final Uri uri = ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, + cursor.getLong(BookmarksLoader.COLUMN_INDEX_ID)); + + new AlertDialog.Builder(context) .setTitle(R.string.delete_bookmark) .setIcon(android.R.drawable.ic_dialog_alert) - .setMessage(getText(R.string.delete_bookmark_warning).toString().replace( - "%s", getBookmarkTitle(deletePos))) + .setMessage(context.getString(R.string.delete_bookmark_warning, + cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE))) .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int whichButton) { - deleteBookmark(deletePos); + resolver.delete(uri, null, null); } }) .setNegativeButton(R.string.cancel, null) .show(); } - /** - * Refresh the shown list after the database has changed. - */ - private void refreshList() { - if (mBookmarksAdapter == null) return; - mBookmarksAdapter.refreshList(); + private String getUrl(int position) { + return getUrl(mAdapter.getItem(position)); } - /** - * Return a hashmap representing the currently highlighted row. - */ - public Bundle getRow(int position) { - return mBookmarksAdapter == null ? null - : mBookmarksAdapter.getRow(position); + /* package */ static String getUrl(Cursor c) { + return c.getString(BookmarksLoader.COLUMN_INDEX_URL); } - /** - * Return the url of the currently highlighted row. - */ - public String getUrl(int position) { - return mBookmarksAdapter == null ? null - : mBookmarksAdapter.getUrl(position); + private void copy(CharSequence text) { + ClipboardManager cm = (ClipboardManager) getActivity().getSystemService( + Context.CLIPBOARD_SERVICE); + cm.setPrimaryClip(ClipData.newRawUri(null, null, Uri.parse(text.toString()))); + } + + void selectView(int view) { + if (view == mCurrentView) { + return; + } + mCurrentView = view; + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); + Editor edit = prefs.edit(); + edit.putInt(PREF_SELECTED_VIEW, mCurrentView); + edit.apply(); + if (mEmptyView.getVisibility() == View.VISIBLE) { + return; + } + setupBookmarkView(); + } + + private void setupBookmarkView() { + mAdapter.selectView(mCurrentView); + switch (mCurrentView) { + case VIEW_THUMBNAILS: + mList.setAdapter(null); + mGrid.setAdapter(mAdapter); + mGrid.setVisibility(View.VISIBLE); + mList.setVisibility(View.GONE); + break; + case VIEW_LIST: + mGrid.setAdapter(null); + mList.setAdapter(mAdapter); + mGrid.setVisibility(View.GONE); + mList.setVisibility(View.VISIBLE); + break; + } + } + + public BreadCrumbView getBreadCrumb() { + return mCrumbs; } /** - * Return the favicon of the currently highlighted row. + * BreadCrumb controller callback */ - public Bitmap getFavicon(int position) { - return mBookmarksAdapter == null ? null - : mBookmarksAdapter.getFavicon(position); + @Override + public void onTop(int level, Object data) { + onFolderChange(level, data); } - private Bitmap getTouchIcon(int position) { - return mBookmarksAdapter == null ? null - : mBookmarksAdapter.getTouchIcon(position); + @Override + public void onClick(View view) { + if (mSelectBookmarkView == view) { + PopupMenu popup = new PopupMenu(getActivity(), mSelectBookmarkView); + popup.getMenuInflater().inflate(R.menu.bookmark_view, + popup.getMenu()); + popup.setOnMenuItemClickListener(this); + popup.show(); + } else if (mRootFolderView == view) { + mCrumbs.clear(); + } } - 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); + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.list_view: + mSelectBookmarkView.setText(R.string.bookmark_list_view); + selectView(BrowserBookmarksPage.VIEW_LIST); + return true; + case R.id.thumbnail_view: + mSelectBookmarkView.setText(R.string.bookmark_thumbnail_view); + selectView(BrowserBookmarksPage.VIEW_THUMBNAILS); + return true; } + return false; } - public String getBookmarkTitle(int position) { - return mBookmarksAdapter == null ? null - : mBookmarksAdapter.getTitle(position); + public boolean onBackPressed() { + if (mCrumbs != null && + mCrumbs.size() > 0) { + mCrumbs.popView(); + return true; + } + return false; } - /** - * Delete the currently highlighted row. - */ - public void deleteBookmark(int position) { - if (mBookmarksAdapter == null) return; - mBookmarksAdapter.deleteRow(position); + public void setCallbackListener(BookmarksPageCallbacks callbackListener) { + mCallbacks = callbackListener; } - @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); + public void setEnableContextMenu(boolean enable) { + mEnableContextMenu = enable; + if (mGrid != null) { + if (mEnableContextMenu) { + registerForContextMenu(mGrid); + } else { + unregisterForContextMenu(mGrid); + mGrid.setLongClickable(false); + } + } + if (mList != null) { + if (mEnableContextMenu) { + registerForContextMenu(mList); + } else { + unregisterForContextMenu(mList); + mList.setLongClickable(false); + } + } + } + + private static class LookupBookmarkCount extends AsyncTask<Long, Void, Integer> { + Context mContext; + BookmarkItem mHeader; + + public LookupBookmarkCount(Context context, BookmarkItem header) { + mContext = context; + mHeader = header; + } + + @Override + protected Integer doInBackground(Long... params) { + if (params.length != 1) { + throw new IllegalArgumentException("Missing folder id!"); + } + Uri uri = BookmarkUtils.getBookmarksUri(mContext); + Cursor c = mContext.getContentResolver().query(uri, + null, BrowserContract.Bookmarks.PARENT + "=?", + new String[] {params[0].toString()}, null); + return c.getCount(); + } + + @Override + protected void onPostExecute(Integer result) { + if (result > 0) { + mHeader.setUrl(mContext.getString(R.string.contextheader_folder_bookmarkcount, + result)); + } else if (result == 0) { + mHeader.setUrl(mContext.getString(R.string.contextheader_folder_empty)); + } } } } diff --git a/src/com/android/browser/BrowserHistoryPage.java b/src/com/android/browser/BrowserHistoryPage.java index 23080f86b..a3ce0b446 100644 --- a/src/com/android/browser/BrowserHistoryPage.java +++ b/src/com/android/browser/BrowserHistoryPage.java @@ -17,198 +17,252 @@ package com.android.browser; import android.app.Activity; -import android.app.ExpandableListActivity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.Fragment; +import android.app.LoaderManager.LoaderCallbacks; +import android.content.ClipboardManager; import android.content.Context; +import android.content.CursorLoader; +import android.content.DialogInterface; import android.content.Intent; +import android.content.Loader; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.net.Uri; import android.os.Bundle; -import android.os.ServiceManager; import android.provider.Browser; -import android.text.IClipboard; -import android.util.Log; +import android.provider.BrowserContract; +import android.provider.BrowserContract.History; import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.ViewStub; import android.webkit.WebIconDatabase.IconListener; -import android.widget.ExpandableListAdapter; import android.widget.ExpandableListView; import android.widget.ExpandableListView.ExpandableListContextMenuInfo; +import android.widget.ExpandableListView.OnChildClickListener; +import android.widget.TextView; import android.widget.Toast; /** * Activity for displaying the browser's history, divided into * days of viewing. */ -public class BrowserHistoryPage extends ExpandableListActivity { - private HistoryAdapter mAdapter; - private boolean mDisableNewWindow; - private HistoryItem mContextHeader; +public class BrowserHistoryPage extends Fragment + implements LoaderCallbacks<Cursor>, OnChildClickListener { + + static final int LOADER_HISTORY = 1; + static final int LOADER_MOST_VISITED = 2; - private final static String LOGTAG = "browser"; + BookmarksHistoryCallbacks mCallbacks; + ExpandableListView mList; + View mEmptyView; + HistoryAdapter mAdapter; + boolean mDisableNewWindow; + HistoryItem mContextHeader; + String mMostVisitsLimit; // Implementation of WebIconDatabase.IconListener - private class IconReceiver implements IconListener { + class IconReceiver implements IconListener { + @Override public void onReceivedIcon(String url, Bitmap icon) { - setListAdapter(mAdapter); + mAdapter.notifyDataSetChanged(); } } + // Instance of IconReceiver - private final IconReceiver mIconReceiver = new IconReceiver(); - - /** - * Report back to the calling activity to load a site. - * @param url Site to load. - * @param newWindow True if the URL should be loaded in a new window - */ - private void loadUrl(String url, boolean newWindow) { - Intent intent = new Intent().setAction(url); - if (newWindow) { - Bundle b = new Bundle(); - b.putBoolean("new_window", true); - intent.putExtras(b); - } - setResultToParent(RESULT_OK, intent); - finish(); + final IconReceiver mIconReceiver = new IconReceiver(); + + static interface HistoryQuery { + static final String[] PROJECTION = new String[] { + History._ID, // 0 + History.DATE_LAST_VISITED, // 1 + History.TITLE, // 2 + History.URL, // 3 + History.FAVICON, // 4 + History.VISITS // 5 + }; + + static final int INDEX_ID = 0; + static final int INDEX_DATE_LAST_VISITED = 1; + static final int INDEX_TITE = 2; + static final int INDEX_URL = 3; + static final int INDEX_FAVICON = 4; + static final int INDEX_VISITS = 5; } - + private void copy(CharSequence text) { - try { - IClipboard clip = IClipboard.Stub.asInterface(ServiceManager.getService("clipboard")); - if (clip != null) { - clip.setClipboardText(text); + ClipboardManager cm = (ClipboardManager) getActivity().getSystemService( + Context.CLIPBOARD_SERVICE); + cm.setText(text); + } + + static BrowserHistoryPage newInstance(BookmarksHistoryCallbacks cb, Bundle args) { + BrowserHistoryPage bhp = new BrowserHistoryPage(); + bhp.mCallbacks = cb; + bhp.setArguments(args); + return bhp; + } + + @Override + public Loader<Cursor> onCreateLoader(int id, Bundle args) { + switch (id) { + case LOADER_HISTORY: { + CursorLoader loader = new CursorLoader(getActivity(), History.CONTENT_URI, + HistoryQuery.PROJECTION, null, null, null); + return loader; + } + + case LOADER_MOST_VISITED: { + Uri uri = History.CONTENT_URI + .buildUpon() + .appendQueryParameter(BrowserContract.PARAM_LIMIT, mMostVisitsLimit) + .build(); + CursorLoader loader = new CursorLoader(getActivity(), uri, + HistoryQuery.PROJECTION, null, null, History.VISITS + " DESC"); + return loader; + } + + default: { + throw new IllegalArgumentException(); } - } catch (android.os.RemoteException e) { - Log.e(LOGTAG, "Copy failed", e); } } @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - setTitle(R.string.browser_history); - - final String whereClause = Browser.BookmarkColumns.VISITS + " > 0" - // In AddBookmarkPage, where we save new bookmarks, we add - // three visits to newly created bookmarks, so that - // bookmarks that have not been visited will show up in the - // most visited, and higher in the goto search box. - // However, this puts the site in the history, unless we - // ignore sites with a DATE of 0, which the next line does. - + " AND " + Browser.BookmarkColumns.DATE + " > 0"; - final String orderBy = Browser.BookmarkColumns.DATE + " DESC"; - - Cursor cursor = managedQuery( - Browser.BOOKMARKS_URI, - Browser.HISTORY_PROJECTION, - whereClause, null, orderBy); - - mAdapter = new HistoryAdapter(this, cursor, - Browser.HISTORY_PROJECTION_DATE_INDEX); - setListAdapter(mAdapter); - final ExpandableListView list = getExpandableListView(); - list.setOnCreateContextMenuListener(this); - View v = new ViewStub(this, R.layout.empty_history); - addContentView(v, new LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT)); - list.setEmptyView(v); - // Do not post the runnable if there is nothing in the list. - if (list.getExpandableListAdapter().getGroupCount() > 0) { - list.post(new Runnable() { - public void run() { - // In case the history gets cleared before this event - // happens. - if (list.getExpandableListAdapter().getGroupCount() > 0) { - list.expandGroup(0); - } - } - }); + public void onLoadFinished(Loader<Cursor> loader, Cursor data) { + switch (loader.getId()) { + case LOADER_HISTORY: { + mAdapter.changeCursor(data); + + // Add an empty view late, so it does not claim an empty + // history before the adapter is present + mList.setEmptyView(mEmptyView); + break; + } + + case LOADER_MOST_VISITED: { + mAdapter.changeMostVisitedCursor(data); + + // Add an empty view late, so it does not claim an empty + // history before the adapter is present + mList.setEmptyView(mEmptyView); + break; + } + + default: { + throw new IllegalArgumentException(); + } } - mDisableNewWindow = getIntent().getBooleanExtra("disable_new_window", - false); + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + setHasOptionsMenu(true); + + Bundle args = getArguments(); + mDisableNewWindow = args.getBoolean(BrowserBookmarksPage.EXTRA_DISABLE_WINDOW, false); + int mvlimit = getResources().getInteger(R.integer.most_visits_limit); + mMostVisitsLimit = Integer.toString(mvlimit); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.history, container, false); + mList = (ExpandableListView) root.findViewById(android.R.id.list); + mList.setCacheColorHint(0); + mList.setOnCreateContextMenuListener(this); + mList.setOnChildClickListener(this); + mAdapter = new HistoryAdapter(getActivity()); + mList.setAdapter(mAdapter); + + mEmptyView = root.findViewById(android.R.id.empty); + + // Start the loader + getLoaderManager().initLoader(LOADER_HISTORY, null, this); + getLoaderManager().initLoader(LOADER_MOST_VISITED, null, this); // Register to receive icons in case they haven't all been loaded. - CombinedBookmarkHistoryActivity.getIconListenerSet() - .addListener(mIconReceiver); - - Activity parent = getParent(); - if (null == parent - || !(parent instanceof CombinedBookmarkHistoryActivity)) { - throw new AssertionError("history page can only be viewed as a tab" - + "in CombinedBookmarkHistoryActivity"); - } - // initialize the result to canceled, so that if the user just presses - // back then it will have the correct result - setResultToParent(RESULT_CANCELED, null); + CombinedBookmarkHistoryView.getIconListenerSet().addListener(mIconReceiver); + return root; } @Override - protected void onDestroy() { + public void onDestroy() { super.onDestroy(); - CombinedBookmarkHistoryActivity.getIconListenerSet() - .removeListener(mIconReceiver); + CombinedBookmarkHistoryView.getIconListenerSet().removeListener(mIconReceiver); + getLoaderManager().stopLoader(LOADER_HISTORY); + getLoaderManager().stopLoader(LOADER_MOST_VISITED); } @Override - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - MenuInflater inflater = getMenuInflater(); + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.history, menu); - return true; } @Override - public boolean onPrepareOptionsMenu(Menu menu) { - menu.findItem(R.id.clear_history_menu_id).setVisible(Browser.canClearHistory(this.getContentResolver())); - return true; + public void onPrepareOptionsMenu(Menu menu) { + menu.findItem(R.id.clear_history_menu_id).setVisible( + Browser.canClearHistory(getActivity().getContentResolver())); } - + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.clear_history_menu_id: - Browser.clearHistory(getContentResolver()); - // BrowserHistoryPage is always a child of - // CombinedBookmarkHistoryActivity - ((CombinedBookmarkHistoryActivity) getParent()) - .removeParentChildRelationShips(); - mAdapter.refreshData(); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) + .setTitle(R.string.clear) + .setMessage(R.string.pref_privacy_clear_history_dlg) + .setIcon(android.R.drawable.ic_dialog_alert) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + Browser.clearHistory(getActivity().getContentResolver()); + mCallbacks.onRemoveParentChildRelationships(); + } + } + }); + final Dialog dialog = builder.create(); + dialog.show(); return true; - + default: break; - } + } return super.onOptionsItemSelected(item); } @Override - public void onCreateContextMenu(ContextMenu menu, View v, - ContextMenuInfo menuInfo) { - ExpandableListContextMenuInfo i = - (ExpandableListContextMenuInfo) menuInfo; + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + ExpandableListContextMenuInfo i = (ExpandableListContextMenuInfo) menuInfo; // Do not allow a context menu to come up from the group views. if (!(i.targetView instanceof HistoryItem)) { return; } // Inflate the menu - MenuInflater inflater = getMenuInflater(); + Activity parent = getActivity(); + MenuInflater inflater = parent.getMenuInflater(); inflater.inflate(R.menu.historycontext, menu); HistoryItem historyItem = (HistoryItem) i.targetView; // Setup the header if (mContextHeader == null) { - mContextHeader = new HistoryItem(this); + mContextHeader = new HistoryItem(parent); } else if (mContextHeader.getParent() != null) { ((ViewGroup) mContextHeader.getParent()).removeView(mContextHeader); } @@ -225,88 +279,173 @@ public class BrowserHistoryPage extends ExpandableListActivity { item.setTitle(R.string.remove_from_bookmarks); } // decide whether to show the share link option - PackageManager pm = getPackageManager(); + PackageManager pm = parent.getPackageManager(); Intent send = new Intent(Intent.ACTION_SEND); send.setType("text/plain"); ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY); menu.findItem(R.id.share_link_context_menu_id).setVisible(ri != null); - + super.onCreateContextMenu(menu, v, menuInfo); } - + @Override public boolean onContextItemSelected(MenuItem item) { - ExpandableListContextMenuInfo i = + ExpandableListContextMenuInfo i = (ExpandableListContextMenuInfo) item.getMenuInfo(); + if (i == null) { + return false; + } HistoryItem historyItem = (HistoryItem) i.targetView; String url = historyItem.getUrl(); String title = historyItem.getName(); + Activity activity = getActivity(); switch (item.getItemId()) { case R.id.open_context_menu_id: - loadUrl(url, false); + mCallbacks.onUrlSelected(url, false); return true; case R.id.new_window_context_menu_id: - loadUrl(url, true); + mCallbacks.onUrlSelected(url, true); return true; case R.id.save_to_bookmarks_menu_id: if (historyItem.isBookmark()) { - Bookmarks.removeFromBookmarks(this, getContentResolver(), + Bookmarks.removeFromBookmarks(activity, activity.getContentResolver(), url, title); } else { - Browser.saveBookmark(this, title, url); + Browser.saveBookmark(activity, title, url); } return true; case R.id.share_link_context_menu_id: - Browser.sendString(this, url, - getText(R.string.choosertitle_sharevia).toString()); + Browser.sendString(activity, url, + activity.getText(R.string.choosertitle_sharevia).toString()); return true; case R.id.copy_url_context_menu_id: copy(url); return true; case R.id.delete_context_menu_id: - Browser.deleteFromHistory(getContentResolver(), url); - mAdapter.refreshData(); + Browser.deleteFromHistory(activity.getContentResolver(), url); return true; case R.id.homepage_context_menu_id: - BrowserSettings.getInstance().setHomePage(this, url); - Toast.makeText(this, R.string.homepage_set, - Toast.LENGTH_LONG).show(); + BrowserSettings.getInstance().setHomePage(activity, url); + Toast.makeText(activity, R.string.homepage_set, Toast.LENGTH_LONG).show(); return true; default: break; } return super.onContextItemSelected(item); } - + @Override public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { if (v instanceof HistoryItem) { - loadUrl(((HistoryItem) v).getUrl(), false); + mCallbacks.onUrlSelected(((HistoryItem) v).getUrl(), false); return true; } return false; } - // This Activity is always a sub-Activity of - // CombinedBookmarkHistoryActivity. Therefore, we need to pass our - // result code up to our parent. - private void setResultToParent(int resultCode, Intent data) { - ((CombinedBookmarkHistoryActivity) getParent()).setResultFromChild( - resultCode, data); - } - private class HistoryAdapter extends DateSortedExpandableListAdapter { - HistoryAdapter(Context context, Cursor cursor, int index) { - super(context, cursor, index); - + private Cursor mMostVisited, mHistoryCursor; + + HistoryAdapter(Context context) { + super(context, HistoryQuery.INDEX_DATE_LAST_VISITED); } + @Override + public void changeCursor(Cursor cursor) { + mHistoryCursor = cursor; + super.changeCursor(cursor); + } + + void changeMostVisitedCursor(Cursor cursor) { + if (mMostVisited == cursor) { + return; + } + if (mMostVisited != null) { + mMostVisited.unregisterDataSetObserver(mDataSetObserver); + mMostVisited.close(); + } + mMostVisited = cursor; + mMostVisited.registerDataSetObserver(mDataSetObserver); + } + + @Override + public long getChildId(int groupPosition, int childPosition) { + if (!mDataValid) return 0; + if (moveCursorToChildPosition(groupPosition, childPosition)) { + Cursor cursor = getCursor(groupPosition); + return cursor.getLong(HistoryQuery.INDEX_ID); + } + return 0; + } + + @Override + public int getGroupCount() { + return super.getGroupCount() + (mMostVisited != null ? 1 : 0); + } + + @Override + public int getChildrenCount(int groupPosition) { + if (groupPosition >= super.getGroupCount()) { + return mMostVisited.getCount(); + } + return super.getChildrenCount(groupPosition); + } + + @Override + public boolean isEmpty() { + if (!super.isEmpty()) { + return false; + } + return mMostVisited == null + || mMostVisited.isClosed() + || mMostVisited.getCount() == 0; + } + + Cursor getCursor(int groupPosition) { + if (groupPosition >= super.getGroupCount()) { + return mMostVisited; + } + return mHistoryCursor; + } + + @Override + public View getGroupView(int groupPosition, boolean isExpanded, + View convertView, ViewGroup parent) { + if (groupPosition >= super.getGroupCount()) { + if (!mDataValid) throw new IllegalStateException("Data is not valid"); + TextView item; + if (null == convertView || !(convertView instanceof TextView)) { + LayoutInflater factory = LayoutInflater.from(getContext()); + item = (TextView) factory.inflate(R.layout.history_header, null); + } else { + item = (TextView) convertView; + } + item.setText(R.string.tab_most_visited); + return item; + } + return super.getGroupView(groupPosition, isExpanded, convertView, parent); + } + + @Override + boolean moveCursorToChildPosition( + int groupPosition, int childPosition) { + if (groupPosition >= super.getGroupCount()) { + if (mDataValid && !mMostVisited.isClosed()) { + mMostVisited.moveToPosition(childPosition); + return true; + } + return false; + } + return super.moveCursorToChildPosition(groupPosition, childPosition); + } + + @Override public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { HistoryItem item; if (null == convertView || !(convertView instanceof HistoryItem)) { - item = new HistoryItem(BrowserHistoryPage.this); + item = new HistoryItem(getContext()); // Add padding on the left so it will be indented from the // arrows on the group views. item.setPadding(item.getPaddingLeft() + 10, @@ -316,23 +455,24 @@ public class BrowserHistoryPage extends ExpandableListActivity { } else { item = (HistoryItem) convertView; } + // Bail early if the Cursor is closed. if (!moveCursorToChildPosition(groupPosition, childPosition)) { return item; } - item.setName(getString(Browser.HISTORY_PROJECTION_TITLE_INDEX)); - String url = getString(Browser.HISTORY_PROJECTION_URL_INDEX); + + Cursor cursor = getCursor(groupPosition); + item.setName(cursor.getString(HistoryQuery.INDEX_TITE)); + String url = cursor.getString(HistoryQuery.INDEX_URL); item.setUrl(url); - byte[] data = getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX); + byte[] data = cursor.getBlob(HistoryQuery.INDEX_FAVICON); if (data != null) { item.setFavicon(BitmapFactory.decodeByteArray(data, 0, data.length)); } else { - item.setFavicon(CombinedBookmarkHistoryActivity + item.setFavicon(CombinedBookmarkHistoryView .getIconListenerSet().getFavicon(url)); } - item.setIsBookmark(1 == - getInt(Browser.HISTORY_PROJECTION_BOOKMARK_INDEX)); return item; } } diff --git a/src/com/android/browser/BrowserHomepagePreference.java b/src/com/android/browser/BrowserHomepagePreference.java index 4f18bd5db..988605320 100644 --- a/src/com/android/browser/BrowserHomepagePreference.java +++ b/src/com/android/browser/BrowserHomepagePreference.java @@ -18,32 +18,38 @@ package com.android.browser; import android.app.AlertDialog; import android.content.Context; +import android.content.DialogInterface; import android.os.Bundle; import android.preference.EditTextPreference; -import android.widget.Button; -import android.widget.EditText; -import android.widget.LinearLayout; +import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; -import android.util.AttributeSet; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.Toast; public class BrowserHomepagePreference extends EditTextPreference { private String mCurrentPage; + private AlertDialog mSetHomepageTo; public BrowserHomepagePreference(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + createSetHomepageToDialog(); } public BrowserHomepagePreference(Context context, AttributeSet attrs) { super(context, attrs); + createSetHomepageToDialog(); } public BrowserHomepagePreference(Context context) { super(context); + createSetHomepageToDialog(); } @Override @@ -54,10 +60,10 @@ public class BrowserHomepagePreference extends EditTextPreference { // page. ViewGroup parent = (ViewGroup) editText.getParent(); Button button = new Button(getContext()); - button.setText(R.string.pref_use_current); + button.setText(R.string.pref_set_homepage_to); button.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { - getEditText().setText(mCurrentPage); + mSetHomepageTo.show(); } }); if (parent instanceof LinearLayout) { @@ -67,24 +73,48 @@ public class BrowserHomepagePreference extends EditTextPreference { ViewGroup.LayoutParams.WRAP_CONTENT); } + private void createSetHomepageToDialog() { + Context context = getContext(); + CharSequence[] setToChoices = new CharSequence[] { + context.getText(R.string.pref_use_current), + context.getText(R.string.pref_use_blank), + context.getText(R.string.pref_use_default), + }; + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.pref_set_homepage_to); + builder.setItems(setToChoices, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == 0) { + getEditText().setText(mCurrentPage); + } else if (which == 1) { + getEditText().setText("about:blank"); + } else if (which == 2) { + getEditText().setText(BrowserSettings + .getFactoryResetHomeUrl(getContext())); + } + } + }); + mSetHomepageTo = builder.create(); + } + @Override protected void onDialogClosed(boolean positiveResult) { if (positiveResult) { String url = getEditText().getText().toString(); if (url.length() > 0 - && !BrowserActivity.ACCEPTED_URI_SCHEMA.matcher(url) + && !UrlUtils.ACCEPTED_URI_SCHEMA.matcher(url) .matches()) { int colon = url.indexOf(':'); int space = url.indexOf(' '); - if (colon == -1 && space == -1) { + if (colon == -1 && space == -1 && url.length() > 0) { // if no colon, no space, add "http://" to make it a url getEditText().setText("http://" + url); } else { - // show an error dialog and change the positiveResult to + // show an error toast and change the positiveResult to // false so that the bad url will not override the old url - new AlertDialog.Builder(this.getContext()).setMessage( - R.string.bookmark_url_not_valid).setPositiveButton( - R.string.ok, null).show(); + Toast.makeText(getContext(), R.string.bookmark_url_not_valid, + Toast.LENGTH_SHORT).show(); positiveResult = false; } } @@ -97,7 +127,7 @@ public class BrowserHomepagePreference extends EditTextPreference { * @param currentPage This String will replace the text in the EditText * when the user clicks the "Use current page" button. */ - /* package */ void setCurrentPage(String currentPage) { + public void setCurrentPage(String currentPage) { mCurrentPage = currentPage; } diff --git a/src/com/android/browser/BrowserPreferencesPage.java b/src/com/android/browser/BrowserPreferencesPage.java index 9af66f1fa..d93e70f24 100644 --- a/src/com/android/browser/BrowserPreferencesPage.java +++ b/src/com/android/browser/BrowserPreferencesPage.java @@ -16,96 +16,30 @@ package com.android.browser; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.preference.EditTextPreference; -import android.preference.Preference; +import com.android.browser.preferences.DebugPreferencesFragment; + import android.preference.PreferenceActivity; -import android.preference.PreferenceScreen; -import android.webkit.GeolocationPermissions; -import android.webkit.ValueCallback; -import android.webkit.WebStorage; +import android.preference.PreferenceManager; -import java.util.Map; -import java.util.Set; +import java.util.List; -public class BrowserPreferencesPage extends PreferenceActivity - implements Preference.OnPreferenceChangeListener { +public class BrowserPreferencesPage extends PreferenceActivity { - private String LOGTAG = "BrowserPreferencesPage"; - /* package */ static final String CURRENT_PAGE = "currentPage"; + public static final String CURRENT_PAGE = "currentPage"; + /** + * Populate the activity with the top-level headers. + */ @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // Load the XML preferences file - addPreferencesFromResource(R.xml.browser_preferences); - - Preference e = findPreference(BrowserSettings.PREF_HOMEPAGE); - e.setOnPreferenceChangeListener(this); - e.setSummary(getPreferenceScreen().getSharedPreferences() - .getString(BrowserSettings.PREF_HOMEPAGE, null)); - ((BrowserHomepagePreference) e).setCurrentPage( - getIntent().getStringExtra(CURRENT_PAGE)); - - e = findPreference(BrowserSettings.PREF_EXTRAS_RESET_DEFAULTS); - e.setOnPreferenceChangeListener(this); - - e = findPreference(BrowserSettings.PREF_TEXT_SIZE); - e.setOnPreferenceChangeListener(this); - e.setSummary(getVisualTextSizeName( - getPreferenceScreen().getSharedPreferences() - .getString(BrowserSettings.PREF_TEXT_SIZE, null)) ); - - e = findPreference(BrowserSettings.PREF_DEFAULT_ZOOM); - e.setOnPreferenceChangeListener(this); - e.setSummary(getVisualDefaultZoomName( - getPreferenceScreen().getSharedPreferences() - .getString(BrowserSettings.PREF_DEFAULT_ZOOM, null)) ); - - e = findPreference(BrowserSettings.PREF_DEFAULT_TEXT_ENCODING); - e.setOnPreferenceChangeListener(this); - - e = findPreference(BrowserSettings.PREF_CLEAR_HISTORY); - e.setOnPreferenceChangeListener(this); + public void onBuildHeaders(List<Header> target) { + loadHeadersFromResource(R.xml.preference_headers, target); if (BrowserSettings.getInstance().showDebugSettings()) { - addPreferencesFromResource(R.xml.debug_preferences); + Header debug = new Header(); + debug.title = getText(R.string.pref_development_title); + debug.fragment = DebugPreferencesFragment.class.getName(); + target.add(debug); } - - PreferenceScreen websiteSettings = (PreferenceScreen) - findPreference(BrowserSettings.PREF_WEBSITE_SETTINGS); - Intent intent = new Intent(this, WebsiteSettingsActivity.class); - websiteSettings.setIntent(intent); - } - - /* - * We need to set the PreferenceScreen state in onResume(), as the number of - * origins with active features (WebStorage, Geolocation etc) could have - * changed after calling the WebsiteSettingsActivity. - */ - @Override - protected void onResume() { - super.onResume(); - final PreferenceScreen websiteSettings = (PreferenceScreen) - findPreference(BrowserSettings.PREF_WEBSITE_SETTINGS); - websiteSettings.setEnabled(false); - WebStorage.getInstance().getOrigins(new ValueCallback<Map>() { - public void onReceiveValue(Map webStorageOrigins) { - if ((webStorageOrigins != null) && !webStorageOrigins.isEmpty()) { - websiteSettings.setEnabled(true); - } - } - }); - GeolocationPermissions.getInstance().getOrigins(new ValueCallback<Set<String> >() { - public void onReceiveValue(Set<String> geolocationOrigins) { - if ((geolocationOrigins != null) && !geolocationOrigins.isEmpty()) { - websiteSettings.setEnabled(true); - } - } - }); } @Override @@ -115,98 +49,6 @@ public class BrowserPreferencesPage extends PreferenceActivity // sync the shared preferences back to BrowserSettings BrowserSettings.getInstance().syncSharedPreferences( getApplicationContext(), - getPreferenceScreen().getSharedPreferences()); - } - - public boolean onPreferenceChange(Preference pref, Object objValue) { - if (pref.getKey().equals(BrowserSettings.PREF_EXTRAS_RESET_DEFAULTS)) { - Boolean value = (Boolean) objValue; - if (value.booleanValue() == true) { - finish(); - } - } else if (pref.getKey().equals(BrowserSettings.PREF_HOMEPAGE)) { - String value = (String) objValue; - boolean needUpdate = value.indexOf(' ') != -1; - if (needUpdate) { - value = value.trim().replace(" ", "%20"); - } - if (value.length() != 0 && Uri.parse(value).getScheme() == null) { - value = "http://" + value; - needUpdate = true; - } - // Set the summary value. - pref.setSummary(value); - if (needUpdate) { - // Update through the EditText control as it has a cached copy - // of the string and it will handle persisting the value - ((EditTextPreference) pref).setText(value); - - // as we update the value above, we need to return false - // here so that setText() is not called by EditTextPref - // with the old value. - return false; - } else { - return true; - } - } else if (pref.getKey().equals(BrowserSettings.PREF_TEXT_SIZE)) { - pref.setSummary(getVisualTextSizeName((String) objValue)); - return true; - } else if (pref.getKey().equals(BrowserSettings.PREF_DEFAULT_ZOOM)) { - pref.setSummary(getVisualDefaultZoomName((String) objValue)); - return true; - } else if (pref.getKey().equals( - BrowserSettings.PREF_DEFAULT_TEXT_ENCODING)) { - pref.setSummary((String) objValue); - return true; - } else if (pref.getKey().equals(BrowserSettings.PREF_CLEAR_HISTORY) - && ((Boolean) objValue).booleanValue() == true) { - // Need to tell the browser to remove the parent/child relationship - // between tabs - setResult(RESULT_OK, (new Intent()).putExtra(Intent.EXTRA_TEXT, - pref.getKey())); - return true; - } - - return false; - } - - private CharSequence getVisualTextSizeName(String enumName) { - CharSequence[] visualNames = getResources().getTextArray( - R.array.pref_text_size_choices); - CharSequence[] enumNames = getResources().getTextArray( - R.array.pref_text_size_values); - - // Sanity check - if (visualNames.length != enumNames.length) { - return ""; - } - - for (int i = 0; i < enumNames.length; i++) { - if (enumNames[i].equals(enumName)) { - return visualNames[i]; - } - } - - return ""; - } - - private CharSequence getVisualDefaultZoomName(String enumName) { - CharSequence[] visualNames = getResources().getTextArray( - R.array.pref_default_zoom_choices); - CharSequence[] enumNames = getResources().getTextArray( - R.array.pref_default_zoom_values); - - // Sanity check - if (visualNames.length != enumNames.length) { - return ""; - } - - for (int i = 0; i < enumNames.length; i++) { - if (enumNames[i].equals(enumName)) { - return visualNames[i]; - } - } - - return ""; + PreferenceManager.getDefaultSharedPreferences(this)); } } diff --git a/src/com/android/browser/BrowserProvider.java b/src/com/android/browser/BrowserProvider.java index 72ec81937..cba16a00c 100644 --- a/src/com/android/browser/BrowserProvider.java +++ b/src/com/android/browser/BrowserProvider.java @@ -29,8 +29,10 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.UriMatcher; +import android.content.res.Configuration; import android.database.AbstractCursor; import android.database.Cursor; +import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; @@ -38,6 +40,7 @@ import android.os.Process; import android.preference.PreferenceManager; import android.provider.Browser; import android.provider.Browser.BookmarkColumns; +import android.provider.Settings; import android.speech.RecognizerResultsIntent; import android.text.TextUtils; import android.util.Log; @@ -84,6 +87,17 @@ public class BrowserProvider extends ContentProvider { private static final int SUGGEST_COLUMN_QUERY_ID = 8; private static final int SUGGEST_COLUMN_INTENT_EXTRA_DATA = 9; + // how many suggestions will be shown in dropdown + // 0..SHORT: filled by browser db + private static final int MAX_SUGGEST_SHORT_SMALL = 3; + // SHORT..LONG: filled by search suggestions + private static final int MAX_SUGGEST_LONG_SMALL = 6; + + // large screen size shows more + private static final int MAX_SUGGEST_SHORT_LARGE = 6; + private static final int MAX_SUGGEST_LONG_LARGE = 9; + + // shared suggestion columns private static final String[] COLUMNS = new String[] { "_id", @@ -97,10 +111,6 @@ public class BrowserProvider extends ContentProvider { SearchManager.SUGGEST_COLUMN_QUERY, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA}; - private static final int MAX_SUGGESTION_SHORT_ENTRIES = 3; - private static final int MAX_SUGGESTION_LONG_ENTRIES = 6; - private static final String MAX_SUGGESTION_LONG_ENTRIES_STRING = - Integer.valueOf(MAX_SUGGESTION_LONG_ENTRIES).toString(); // make sure that these match the index of TABLE_NAMES private static final int URI_MATCH_BOOKMARKS = 0; @@ -161,6 +171,9 @@ public class BrowserProvider extends ContentProvider { private BrowserSettings mSettings; + private int mMaxSuggestionShortSize; + private int mMaxSuggestionLongSize; + public BrowserProvider() { } @@ -365,6 +378,20 @@ public class BrowserProvider extends ContentProvider { @Override public boolean onCreate() { final Context context = getContext(); + boolean xlargeScreenSize = (context.getResources().getConfiguration().screenLayout + & Configuration.SCREENLAYOUT_SIZE_MASK) + == Configuration.SCREENLAYOUT_SIZE_XLARGE; + boolean isPortrait = (context.getResources().getConfiguration().orientation + == Configuration.ORIENTATION_PORTRAIT); + + + if (xlargeScreenSize && isPortrait) { + mMaxSuggestionLongSize = MAX_SUGGEST_LONG_LARGE; + mMaxSuggestionShortSize = MAX_SUGGEST_SHORT_LARGE; + } else { + mMaxSuggestionLongSize = MAX_SUGGEST_LONG_SMALL; + mMaxSuggestionShortSize = MAX_SUGGEST_SHORT_SMALL; + } mOpenHelper = new DatabaseHelper(context); mBackupManager = new BackupManager(context); // we added "picasa web album" into default bookmarks for version 19. @@ -431,10 +458,10 @@ public class BrowserProvider extends ContentProvider { public MySuggestionCursor(Cursor hc, Cursor sc, String string) { mHistoryCursor = hc; mSuggestCursor = sc; - mHistoryCount = hc.getCount(); + mHistoryCount = hc != null ? hc.getCount() : 0; mSuggestionCount = sc != null ? sc.getCount() : 0; - if (mSuggestionCount > (MAX_SUGGESTION_LONG_ENTRIES - mHistoryCount)) { - mSuggestionCount = MAX_SUGGESTION_LONG_ENTRIES - mHistoryCount; + if (mSuggestionCount > (mMaxSuggestionLongSize - mHistoryCount)) { + mSuggestionCount = mMaxSuggestionLongSize - mHistoryCount; } mString = string; mIncludeWebSearch = string.length() > 0; @@ -648,6 +675,7 @@ public class BrowserProvider extends ContentProvider { } // TODO Temporary change, finalize after jq's changes go in + @Override public void deactivate() { if (mHistoryCursor != null) { mHistoryCursor.deactivate(); @@ -658,12 +686,14 @@ public class BrowserProvider extends ContentProvider { super.deactivate(); } + @Override public boolean requery() { return (mHistoryCursor != null ? mHistoryCursor.requery() : false) | (mSuggestCursor != null ? mSuggestCursor.requery() : false); } // TODO Temporary change, finalize after jq's changes go in + @Override public void close() { super.close(); if (mHistoryCursor != null) { @@ -728,12 +758,15 @@ public class BrowserProvider extends ContentProvider { public ResultsCursor(ArrayList<String> results) { mResults = results; } + @Override public int getCount() { return mResults.size(); } + @Override public String[] getColumnNames() { return RESULTS_COLUMNS; } + @Override public String getString(int column) { switch (column) { case RESULT_ACTION_ID: @@ -755,30 +788,39 @@ public class BrowserProvider extends ContentProvider { return null; } } + @Override public short getShort(int column) { throw new UnsupportedOperationException(); } + @Override public int getInt(int column) { throw new UnsupportedOperationException(); } + @Override public long getLong(int column) { if ((mPos != -1) && column == 0) { return mPos; // use row# as the _id } throw new UnsupportedOperationException(); } + @Override public float getFloat(int column) { throw new UnsupportedOperationException(); } + @Override public double getDouble(int column) { throw new UnsupportedOperationException(); } + @Override public boolean isNull(int column) { throw new UnsupportedOperationException(); } } + /** Contains custom suggestions results set by the UI */ private ResultsCursor mResultsCursor; + /** Locks access to {@link #mResultsCursor} */ + private Object mResultsCursorLock = new Object(); /** * Provide a set of results to be returned to query, intended to be used @@ -786,10 +828,12 @@ public class BrowserProvider extends ContentProvider { * @param results Strings to display in the dropdown from the SearchDialog */ /* package */ void setQueryResults(ArrayList<String> results) { - if (results == null) { - mResultsCursor = null; - } else { - mResultsCursor = new ResultsCursor(results); + synchronized (mResultsCursorLock) { + if (results == null) { + mResultsCursor = null; + } else { + mResultsCursor = new ResultsCursor(results); + } } } @@ -801,57 +845,19 @@ public class BrowserProvider extends ContentProvider { if (match == -1) { throw new IllegalArgumentException("Unknown URL"); } - if (match == URI_MATCH_SUGGEST && mResultsCursor != null) { - Cursor results = mResultsCursor; - mResultsCursor = null; - return results; - } - SQLiteDatabase db = mOpenHelper.getReadableDatabase(); - if (match == URI_MATCH_SUGGEST || match == URI_MATCH_BOOKMARKS_SUGGEST) { - String suggestSelection; - String [] myArgs; - if (selectionArgs[0] == null || selectionArgs[0].equals("")) { - suggestSelection = null; - myArgs = null; - } else { - String like = selectionArgs[0] + "%"; - if (selectionArgs[0].startsWith("http") - || selectionArgs[0].startsWith("file")) { - myArgs = new String[1]; - myArgs[0] = like; - suggestSelection = selection; - } else { - SUGGEST_ARGS[0] = "http://" + like; - SUGGEST_ARGS[1] = "http://www." + like; - SUGGEST_ARGS[2] = "https://" + like; - SUGGEST_ARGS[3] = "https://www." + like; - // To match against titles. - SUGGEST_ARGS[4] = like; - myArgs = SUGGEST_ARGS; - suggestSelection = SUGGEST_SELECTION; - } + // If results for the suggestion are already ready just return them directly + synchronized (mResultsCursorLock) { + if (match == URI_MATCH_SUGGEST && mResultsCursor != null) { + Cursor results = mResultsCursor; + mResultsCursor = null; + return results; } + } - Cursor c = db.query(TABLE_NAMES[URI_MATCH_BOOKMARKS], - SUGGEST_PROJECTION, suggestSelection, myArgs, null, null, - ORDER_BY, MAX_SUGGESTION_LONG_ENTRIES_STRING); - - if (match == URI_MATCH_BOOKMARKS_SUGGEST - || Patterns.WEB_URL.matcher(selectionArgs[0]).matches()) { - return new MySuggestionCursor(c, null, ""); - } else { - // get search suggestions if there is still space in the list - if (myArgs != null && myArgs.length > 1 - && c.getCount() < (MAX_SUGGESTION_SHORT_ENTRIES - 1)) { - SearchEngine searchEngine = mSettings.getSearchEngine(); - if (searchEngine != null && searchEngine.supportsSuggestions()) { - Cursor sc = searchEngine.getSuggestions(getContext(), selectionArgs[0]); - return new MySuggestionCursor(c, sc, selectionArgs[0]); - } - } - return new MySuggestionCursor(c, null, selectionArgs[0]); - } + if (match == URI_MATCH_SUGGEST || match == URI_MATCH_BOOKMARKS_SUGGEST) { + // Handle suggestions + return doSuggestQuery(selection, selectionArgs, match == URI_MATCH_BOOKMARKS_SUGGEST); } String[] projection = null; @@ -861,27 +867,60 @@ public class BrowserProvider extends ContentProvider { projection[projectionIn.length] = "_id AS _id"; } - StringBuilder whereClause = new StringBuilder(256); + String whereClause = null; if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) { - whereClause.append("(_id = ").append(url.getPathSegments().get(1)) - .append(")"); + whereClause = "_id = " + url.getPathSegments().get(1); } - // Tack on the user's selection, if present - if (selection != null && selection.length() > 0) { - if (whereClause.length() > 0) { - whereClause.append(" AND "); + Cursor c = mOpenHelper.getReadableDatabase().query(TABLE_NAMES[match % 10], projection, + DatabaseUtils.concatenateWhere(whereClause, selection), selectionArgs, + null, null, sortOrder, null); + c.setNotificationUri(getContext().getContentResolver(), url); + return c; + } + + private Cursor doSuggestQuery(String selection, String[] selectionArgs, boolean bookmarksOnly) { + String suggestSelection; + String [] myArgs; + if (selectionArgs[0] == null || selectionArgs[0].equals("")) { + return new MySuggestionCursor(null, null, ""); + } else { + String like = selectionArgs[0] + "%"; + if (selectionArgs[0].startsWith("http") + || selectionArgs[0].startsWith("file")) { + myArgs = new String[1]; + myArgs[0] = like; + suggestSelection = selection; + } else { + SUGGEST_ARGS[0] = "http://" + like; + SUGGEST_ARGS[1] = "http://www." + like; + SUGGEST_ARGS[2] = "https://" + like; + SUGGEST_ARGS[3] = "https://www." + like; + // To match against titles. + SUGGEST_ARGS[4] = like; + myArgs = SUGGEST_ARGS; + suggestSelection = SUGGEST_SELECTION; } + } + + Cursor c = mOpenHelper.getReadableDatabase().query(TABLE_NAMES[URI_MATCH_BOOKMARKS], + SUGGEST_PROJECTION, suggestSelection, myArgs, null, null, + ORDER_BY, Integer.toString(mMaxSuggestionLongSize)); - whereClause.append('('); - whereClause.append(selection); - whereClause.append(')'); + if (bookmarksOnly || Patterns.WEB_URL.matcher(selectionArgs[0]).matches()) { + return new MySuggestionCursor(c, null, ""); + } else { + // get search suggestions if there is still space in the list + if (myArgs != null && myArgs.length > 1 + && c.getCount() < (MAX_SUGGEST_SHORT_SMALL - 1)) { + SearchEngine searchEngine = mSettings.getSearchEngine(); + if (searchEngine != null && searchEngine.supportsSuggestions()) { + Cursor sc = searchEngine.getSuggestions(getContext(), selectionArgs[0]); + return new MySuggestionCursor(c, sc, selectionArgs[0]); + } + } + return new MySuggestionCursor(c, null, selectionArgs[0]); } - Cursor c = db.query(TABLE_NAMES[match % 10], projection, - whereClause.toString(), selectionArgs, null, null, sortOrder, - null); - c.setNotificationUri(getContext().getContentResolver(), url); - return c; } @Override @@ -1089,4 +1128,10 @@ public class BrowserProvider extends ContentProvider { } } + public static Cursor getBookmarksSuggestions(ContentResolver cr, String constraint) { + Uri uri = Uri.parse("content://browser/" + SearchManager.SUGGEST_URI_PATH_QUERY); + return cr.query(uri, SUGGEST_PROJECTION, SUGGEST_SELECTION, + new String[] { constraint }, ORDER_BY); + } + } diff --git a/src/com/android/browser/BrowserSettings.java b/src/com/android/browser/BrowserSettings.java index 3791eb07f..ba2f3fe85 100644 --- a/src/com/android/browser/BrowserSettings.java +++ b/src/com/android/browser/BrowserSettings.java @@ -28,9 +28,15 @@ import android.content.pm.ActivityInfo; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.database.ContentObserver; +import android.database.Cursor; +import android.net.Uri; +import android.os.AsyncTask; import android.os.Handler; +import android.os.Message; import android.preference.PreferenceActivity; +import android.preference.PreferenceManager; import android.preference.PreferenceScreen; +import android.provider.Browser; import android.provider.Settings; import android.util.Log; import android.webkit.CookieManager; @@ -40,9 +46,9 @@ import android.webkit.WebView; import android.webkit.WebViewDatabase; import android.webkit.WebIconDatabase; import android.webkit.WebSettings; +import android.webkit.WebSettings.AutoFillProfile; import android.webkit.WebStorage; -import android.preference.PreferenceManager; -import android.provider.Browser; +import android.widget.Toast; import java.util.HashMap; import java.util.Map; @@ -62,7 +68,7 @@ import java.util.Observable; * To remove an observer: * s.deleteObserver(webView.getSettings()); */ -class BrowserSettings extends Observable { +public class BrowserSettings extends Observable { // Private variables for settings // NOTE: these defaults need to be kept in sync with the XML @@ -76,12 +82,12 @@ class BrowserSettings extends Observable { private boolean showSecurityWarnings; private boolean rememberPasswords; private boolean saveFormData; + private boolean autoFillEnabled; private boolean openInBackground; private String defaultTextEncodingName; private String homeUrl = ""; private SearchEngine searchEngine; private boolean autoFitPage; - private boolean landscapeOnly; private boolean loadsPageInOverviewMode; private boolean showDebugSettings; // HTML5 API flags @@ -109,14 +115,15 @@ class BrowserSettings extends Observable { private boolean tracing = false; private boolean lightTouch = false; private boolean navDump = false; + private boolean hardwareAccelerated = true; // By default the error console is shown once the user navigates to about:debug. // The setting can be then toggled from the settings menu. private boolean showConsole = true; // Private preconfigured values - private static int minimumFontSize = 8; - private static int minimumLogicalFontSize = 8; + private static int minimumFontSize = 1; + private static int minimumLogicalFontSize = 1; private static int defaultFontSize = 16; private static int defaultFixedFontSize = 13; private static WebSettings.TextSize textSize = @@ -125,6 +132,15 @@ class BrowserSettings extends Observable { WebSettings.ZoomDensity.MEDIUM; private static int pageCacheCapacity; + + private AutoFillProfile autoFillProfile; + // Default to zero. In the case no profile is set up, the initial + // value will come from the AutoFillSettingsFragment when the user + // creates a profile. Otherwise, we'll read the ID of the last used + // profile from the prefs db. + private int autoFillActiveProfileId; + private static final int NO_AUTOFILL_PROFILE_SET = 0; + // Preference keys that are used outside this class public final static String PREF_CLEAR_CACHE = "privacy_clear_cache"; public final static String PREF_CLEAR_COOKIES = "privacy_clear_cookies"; @@ -145,6 +161,9 @@ class BrowserSettings extends Observable { "default_text_encoding"; public final static String PREF_CLEAR_GEOLOCATION_ACCESS = "privacy_clear_geolocation_access"; + public final static String PREF_AUTOFILL_ENABLED = "autofill_enabled"; + public final static String PREF_AUTOFILL_PROFILE = "autofill_profile"; + public final static String PREF_AUTOFILL_ACTIVE_PROFILE_ID = "autofill_active_profile_id"; private static final String DESKTOP_USERAGENT = "Mozilla/5.0 (Macintosh; " + "U; Intel Mac OS X 10_6_3; en-us) AppleWebKit/533.16 (KHTML, " + @@ -166,7 +185,11 @@ class BrowserSettings extends Observable { // a ListView public final static int MAX_TEXTVIEW_LEN = 80; - private TabControl mTabControl; + public static final String RLZ_PROVIDER = "com.google.android.partnersetup.rlzappprovider"; + + public static final Uri RLZ_PROVIDER_URI = Uri.parse("content://" + RLZ_PROVIDER + "/"); + + private Controller mController; // Single instance of the BrowserSettings for use in the Browser app. private static BrowserSettings sSingleton; @@ -176,6 +199,18 @@ class BrowserSettings extends Observable { private HashMap<WebSettings,Observer> mWebSettingsToObservers = new HashMap<WebSettings,Observer>(); + private boolean mLoadFromDbComplete; + + public void waitForLoadFromDbToComplete() { + synchronized (sSingleton) { + while (!mLoadFromDbComplete) { + try { + sSingleton.wait(); + } catch (InterruptedException e) { } + } + } + } + /* * An observer wrapper for updating a WebSettings object with the new * settings after a call to BrowserSettings.update(). @@ -221,6 +256,7 @@ class BrowserSettings extends Observable { s.setDefaultZoom(b.zoomDensity); s.setLightTouchEnabled(b.lightTouch); s.setSaveFormData(b.saveFormData); + s.setAutoFillEnabled(b.autoFillEnabled); s.setSavePassword(b.rememberPasswords); s.setLoadWithOverviewMode(b.loadsPageInOverviewMode); s.setPageCacheCapacity(pageCacheCapacity); @@ -229,6 +265,9 @@ class BrowserSettings extends Observable { s.setNeedInitialFocus(false); // Browser supports multiple windows s.setSupportMultipleWindows(true); + // enable smooth transition for better performance during panning or + // zooming + s.setEnableSmoothTransition(true); // HTML5 API flags s.setAppCacheEnabled(b.appCacheEnabled); @@ -243,56 +282,122 @@ class BrowserSettings extends Observable { s.setDatabasePath(b.databasePath); s.setGeolocationDatabasePath(b.geolocationDatabasePath); + // Active AutoFill profile data. + s.setAutoFillProfile(b.autoFillProfile); + b.updateTabControlSettings(); } } /** - * Load settings from the browser app's database. + * Load settings from the browser app's database. It is performed in + * an AsyncTask as it involves plenty of slow disk IO. * NOTE: Strings used for the preferences must match those specified - * in the browser_preferences.xml + * in the various preference XML files. * @param ctx A Context object used to query the browser's settings * database. If the database exists, the saved settings will be * stored in this BrowserSettings object. This will update all * observers of this object. */ - public void loadFromDb(final Context ctx) { - SharedPreferences p = - PreferenceManager.getDefaultSharedPreferences(ctx); - // Set the default value for the Application Caches path. - appCachePath = ctx.getDir("appcache", 0).getPath(); - // Determine the maximum size of the application cache. - webStorageSizeManager = new WebStorageSizeManager( - ctx, - new WebStorageSizeManager.StatFsDiskInfo(appCachePath), - new WebStorageSizeManager.WebKitAppCacheInfo(appCachePath)); - appCacheMaxSize = webStorageSizeManager.getAppCacheMaxSize(); - // Set the default value for the Database path. - databasePath = ctx.getDir("databases", 0).getPath(); - // Set the default value for the Geolocation database path. - geolocationDatabasePath = ctx.getDir("geolocation", 0).getPath(); - - if (p.getString(PREF_HOMEPAGE, "") == "") { - // No home page preferences is set, set it to default. - setHomePage(ctx, getFactoryResetHomeUrl(ctx)); - } + public void asyncLoadFromDb(final Context ctx) { + mLoadFromDbComplete = false; + // Run the initial settings load in an AsyncTask as it hits the + // disk multiple times through SharedPreferences and SQLite. We + // need to be certain though that this has completed before we start + // to load pages though, so in the worst case we will block waiting + // for it to finish in BrowserActivity.onCreate(). + new LoadFromDbTask(ctx).execute(); + } - // the cost of one cached page is ~3M (measured using nytimes.com). For - // low end devices, we only cache one page. For high end devices, we try - // to cache more pages, currently choose 5. - ActivityManager am = (ActivityManager) ctx - .getSystemService(Context.ACTIVITY_SERVICE); - if (am.getMemoryClass() > 16) { - pageCacheCapacity = 5; - } else { - pageCacheCapacity = 1; + private class LoadFromDbTask extends AsyncTask<Void, Void, Void> { + private Context mContext; + + public LoadFromDbTask(Context context) { + mContext = context; } - // Load the defaults from the xml - // This call is TOO SLOW, need to manually keep the defaults - // in sync - //PreferenceManager.setDefaultValues(ctx, R.xml.browser_preferences); - syncSharedPreferences(ctx, p); + protected Void doInBackground(Void... unused) { + SharedPreferences p = + PreferenceManager.getDefaultSharedPreferences(mContext); + // Set the default value for the Application Caches path. + appCachePath = mContext.getDir("appcache", 0).getPath(); + // Determine the maximum size of the application cache. + webStorageSizeManager = new WebStorageSizeManager( + mContext, + new WebStorageSizeManager.StatFsDiskInfo(appCachePath), + new WebStorageSizeManager.WebKitAppCacheInfo(appCachePath)); + appCacheMaxSize = webStorageSizeManager.getAppCacheMaxSize(); + // Set the default value for the Database path. + databasePath = mContext.getDir("databases", 0).getPath(); + // Set the default value for the Geolocation database path. + geolocationDatabasePath = mContext.getDir("geolocation", 0).getPath(); + + if (p.getString(PREF_HOMEPAGE, "") == "") { + // No home page preferences is set, set it to default. + setHomePage(mContext, getFactoryResetHomeUrl(mContext)); + } + + // the cost of one cached page is ~3M (measured using nytimes.com). For + // low end devices, we only cache one page. For high end devices, we try + // to cache more pages, currently choose 5. + ActivityManager am = (ActivityManager) mContext + .getSystemService(Context.ACTIVITY_SERVICE); + if (am.getMemoryClass() > 16) { + pageCacheCapacity = 5; + } else { + pageCacheCapacity = 1; + } + + // Read the last active AutoFill profile id. + autoFillActiveProfileId = p.getInt( + PREF_AUTOFILL_ACTIVE_PROFILE_ID, autoFillActiveProfileId); + + // Load the autofill profile data from the database. We use a database separate + // to the browser preference DB to make it easier to support multiple profiles + // and switching between them. + AutoFillProfileDatabase autoFillDb = AutoFillProfileDatabase.getInstance(mContext); + Cursor c = autoFillDb.getProfile(autoFillActiveProfileId); + + if (c.getCount() > 0) { + c.moveToFirst(); + + String fullName = c.getString(c.getColumnIndex( + AutoFillProfileDatabase.Profiles.FULL_NAME)); + String email = c.getString(c.getColumnIndex( + AutoFillProfileDatabase.Profiles.EMAIL_ADDRESS)); + String company = c.getString(c.getColumnIndex( + AutoFillProfileDatabase.Profiles.COMPANY_NAME)); + String addressLine1 = c.getString(c.getColumnIndex( + AutoFillProfileDatabase.Profiles.ADDRESS_LINE_1)); + String addressLine2 = c.getString(c.getColumnIndex( + AutoFillProfileDatabase.Profiles.ADDRESS_LINE_2)); + String city = c.getString(c.getColumnIndex( + AutoFillProfileDatabase.Profiles.CITY)); + String state = c.getString(c.getColumnIndex( + AutoFillProfileDatabase.Profiles.STATE)); + String zip = c.getString(c.getColumnIndex( + AutoFillProfileDatabase.Profiles.ZIP_CODE)); + String country = c.getString(c.getColumnIndex( + AutoFillProfileDatabase.Profiles.COUNTRY)); + String phone = c.getString(c.getColumnIndex( + AutoFillProfileDatabase.Profiles.PHONE_NUMBER)); + autoFillProfile = new AutoFillProfile(autoFillActiveProfileId, + fullName, email, company, addressLine1, addressLine2, city, + state, zip, country, phone); + } + c.close(); + autoFillDb.close(); + + // PreferenceManager.setDefaultValues is TOO SLOW, need to manually keep + // the defaults in sync + syncSharedPreferences(mContext, p); + + synchronized (sSingleton) { + mLoadFromDbComplete = true; + sSingleton.notify(); + } + return null; + } } /* package */ void syncSharedPreferences(Context ctx, SharedPreferences p) { @@ -307,8 +412,8 @@ class BrowserSettings extends Observable { // One or more tabs could have been in voice search mode. // Clear it, since the new SearchEngine may not support // it, or may handle it differently. - for (int i = 0; i < mTabControl.getTabCount(); i++) { - mTabControl.getTab(i).revertVoiceSearchMode(); + for (int i = 0; i < mController.getTabControl().getTabCount(); i++) { + mController.getTabControl().getTab(i).revertVoiceSearchMode(); } } searchEngine.close(); @@ -332,6 +437,7 @@ class BrowserSettings extends Observable { rememberPasswords); saveFormData = p.getBoolean("save_formdata", saveFormData); + autoFillEnabled = p.getBoolean("autofill_enabled", autoFillEnabled); boolean accept_cookies = p.getBoolean("accept_cookies", CookieManager.getInstance().acceptCookie()); CookieManager.getInstance().setAcceptCookie(accept_cookies); @@ -343,11 +449,6 @@ class BrowserSettings extends Observable { autoFitPage = p.getBoolean("autofit_pages", autoFitPage); loadsPageInOverviewMode = p.getBoolean("load_page", loadsPageInOverviewMode); - boolean landscapeOnlyTemp = - p.getBoolean("landscape_only", landscapeOnly); - if (landscapeOnlyTemp != landscapeOnly) { - landscapeOnly = landscapeOnlyTemp; - } useWideViewPort = true; // use wide view port for either setting if (autoFitPage) { layoutAlgorithm = WebSettings.LayoutAlgorithm.NARROW_COLUMNS; @@ -383,6 +484,12 @@ class BrowserSettings extends Observable { navDump = p.getBoolean("enable_nav_dump", navDump); userAgent = Integer.parseInt(p.getString("user_agent", "0")); } + + // This setting can only be modified when the debug settings have been + // enabled but it is read and used by the browser at startup so we must + // initialize it regardless of the status of the debug settings. + hardwareAccelerated = p.getBoolean("enable_hardware_accel", hardwareAccelerated); + // JS flags is loaded from DB even if showDebugSettings is false, // so that it can be set once and be effective all the time. jsFlags = p.getString("js_engine_flags", ""); @@ -456,6 +563,10 @@ class BrowserSettings extends Observable { return navDump; } + public boolean isHardwareAccelerated() { + return hardwareAccelerated; + } + public boolean showDebugSettings() { return showDebugSettings; } @@ -466,6 +577,40 @@ class BrowserSettings extends Observable { update(); } + public void setAutoFillProfile(Context ctx, AutoFillProfile profile, Message msg) { + if (profile != null) { + setActiveAutoFillProfileId(ctx, profile.getUniqueId()); + // Update the AutoFill DB with the new profile. + new SaveProfileToDbTask(ctx, msg).execute(profile); + } else { + // Delete the current profile. + if (autoFillProfile != null) { + new DeleteProfileFromDbTask(ctx, msg).execute(autoFillProfile.getUniqueId()); + setActiveAutoFillProfileId(ctx, NO_AUTOFILL_PROFILE_SET); + } + } + autoFillProfile = profile; + } + + public AutoFillProfile getAutoFillProfile() { + return autoFillProfile; + } + + private void setActiveAutoFillProfileId(Context context, int activeProfileId) { + autoFillActiveProfileId = activeProfileId; + Editor ed = PreferenceManager. + getDefaultSharedPreferences(context).edit(); + ed.putInt(PREF_AUTOFILL_ACTIVE_PROFILE_ID, activeProfileId); + ed.apply(); + } + + /* package */ void disableAutoFill(Context ctx) { + autoFillEnabled = false; + Editor ed = PreferenceManager.getDefaultSharedPreferences(ctx).edit(); + ed.putBoolean(PREF_AUTOFILL_ENABLED, false); + ed.apply(); + } + /** * Add a WebSettings object to the list of observers that will be updated * when update() is called. @@ -510,8 +655,8 @@ class BrowserSettings extends Observable { /* * Package level method for associating the BrowserSettings with TabControl */ - /* package */void setTabControl(TabControl tabControl) { - mTabControl = tabControl; + /* package */void setController(Controller ctrl) { + mController = ctrl; updateTabControlSettings(); } @@ -525,8 +670,8 @@ class BrowserSettings extends Observable { /*package*/ void clearCache(Context context) { WebIconDatabase.getInstance().removeAllIcons(); - if (mTabControl != null) { - WebView current = mTabControl.getCurrentWebView(); + if (mController != null) { + WebView current = mController.getCurrentWebView(); if (current != null) { current.clearCache(true); } @@ -545,8 +690,8 @@ class BrowserSettings extends Observable { /* package */ void clearFormData(Context context) { WebViewDatabase.getInstance(context).clearFormData(); - if (mTabControl != null) { - WebView currentTopView = mTabControl.getCurrentTopWebView(); + if (mController!= null) { + WebView currentTopView = mController.getCurrentTopWebView(); if (currentTopView != null) { currentTopView.clearFormData(); } @@ -561,59 +706,35 @@ class BrowserSettings extends Observable { private void updateTabControlSettings() { // Enable/disable the error console. - mTabControl.getBrowserActivity().setShouldShowErrorConsole( + mController.setShouldShowErrorConsole( showDebugSettings && showConsole); - mTabControl.getBrowserActivity().setRequestedOrientation( - landscapeOnly ? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE - : ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); - } - - private void maybeDisableWebsiteSettings(Context context) { - PreferenceActivity activity = (PreferenceActivity) context; - final PreferenceScreen screen = (PreferenceScreen) - activity.findPreference(BrowserSettings.PREF_WEBSITE_SETTINGS); - screen.setEnabled(false); - WebStorage.getInstance().getOrigins(new ValueCallback<Map>() { - public void onReceiveValue(Map webStorageOrigins) { - if ((webStorageOrigins != null) && !webStorageOrigins.isEmpty()) { - screen.setEnabled(true); - } - } - }); - - GeolocationPermissions.getInstance().getOrigins(new ValueCallback<Set<String> >() { - public void onReceiveValue(Set<String> geolocationOrigins) { - if ((geolocationOrigins != null) && !geolocationOrigins.isEmpty()) { - screen.setEnabled(true); - } - } - }); } /*package*/ void clearDatabases(Context context) { WebStorage.getInstance().deleteAllData(); - maybeDisableWebsiteSettings(context); } /*package*/ void clearLocationAccess(Context context) { GeolocationPermissions.getInstance().clearAll(); - maybeDisableWebsiteSettings(context); } /*package*/ void resetDefaultPreferences(Context ctx) { reset(); - SharedPreferences p = - PreferenceManager.getDefaultSharedPreferences(ctx); + SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(ctx); p.edit().clear().apply(); - PreferenceManager.setDefaultValues(ctx, R.xml.browser_preferences, - true); + PreferenceManager.setDefaultValues(ctx, R.xml.page_content_preferences, true); + PreferenceManager.setDefaultValues(ctx, R.xml.personal_preferences, true); + PreferenceManager.setDefaultValues(ctx, R.xml.privacy_preferences, true); + PreferenceManager.setDefaultValues(ctx, R.xml.security_preferences, true); + PreferenceManager.setDefaultValues(ctx, R.xml.advanced_preferences, true); // reset homeUrl setHomePage(ctx, getFactoryResetHomeUrl(ctx)); // reset appcache max size appCacheMaxSize = webStorageSizeManager.getAppCacheMaxSize(); + setActiveAutoFillProfileId(ctx, NO_AUTOFILL_PROFILE_SET); } - private String getFactoryResetHomeUrl(Context context) { + /*package*/ static String getFactoryResetHomeUrl(Context context) { String url = context.getResources().getString(R.string.homepage_base); if (url.indexOf("{CID}") != -1) { url = url.replace("{CID}", @@ -639,9 +760,9 @@ class BrowserSettings extends Observable { showSecurityWarnings = true; rememberPasswords = true; saveFormData = true; + autoFillEnabled = true; openInBackground = false; autoFitPage = true; - landscapeOnly = false; loadsPageInOverviewMode = true; showDebugSettings = false; // HTML5 API flags @@ -651,4 +772,53 @@ class BrowserSettings extends Observable { geolocationEnabled = true; workersEnabled = true; // only affects V8. JSC does not have a similar setting } + + private abstract class AutoFillProfileDbTask<T> extends AsyncTask<T, Void, Void> { + Context mContext; + AutoFillProfileDatabase mAutoFillProfileDb; + Message mCompleteMessage; + + public AutoFillProfileDbTask(Context ctx, Message msg) { + mContext = ctx; + mCompleteMessage = msg; + } + + protected void onPostExecute(Void result) { + if (mCompleteMessage != null) { + mCompleteMessage.sendToTarget(); + } + mAutoFillProfileDb.close(); + } + + abstract protected Void doInBackground(T... values); + } + + + private class SaveProfileToDbTask extends AutoFillProfileDbTask<AutoFillProfile> { + public SaveProfileToDbTask(Context ctx, Message msg) { + super(ctx, msg); + } + + protected Void doInBackground(AutoFillProfile... values) { + mAutoFillProfileDb = AutoFillProfileDatabase.getInstance(mContext); + assert autoFillActiveProfileId != NO_AUTOFILL_PROFILE_SET; + AutoFillProfile newProfile = values[0]; + mAutoFillProfileDb.addOrUpdateProfile(autoFillActiveProfileId, newProfile); + return null; + } + } + + private class DeleteProfileFromDbTask extends AutoFillProfileDbTask<Integer> { + public DeleteProfileFromDbTask(Context ctx, Message msg) { + super(ctx, msg); + } + + protected Void doInBackground(Integer... values) { + mAutoFillProfileDb = AutoFillProfileDatabase.getInstance(mContext); + int id = values[0]; + assert id > 0; + mAutoFillProfileDb.dropProfile(id); + return null; + } + } } diff --git a/src/com/android/browser/CombinedBookmarkHistoryActivity.java b/src/com/android/browser/CombinedBookmarkHistoryActivity.java deleted file mode 100644 index 194956f14..000000000 --- a/src/com/android/browser/CombinedBookmarkHistoryActivity.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright (C) 2009 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.TabActivity; -import android.content.Intent; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.os.AsyncTask; -import android.os.Bundle; -import android.provider.Browser; -import android.webkit.WebIconDatabase; -import android.webkit.WebIconDatabase.IconListener; -import android.widget.TabHost; - -import java.util.HashMap; -import java.util.Vector; - -public class CombinedBookmarkHistoryActivity extends TabActivity - implements TabHost.OnTabChangeListener { - /** - * Used to inform BrowserActivity to remove the parent/child relationships - * from all the tabs. - */ - private String mExtraData; - /** - * Intent to be passed to calling Activity when finished. Keep a pointer to - * it locally so mExtraData can be added. - */ - private Intent mResultData; - /** - * Result code to pass back to calling Activity when finished. - */ - private int mResultCode; - - /* package */ static String BOOKMARKS_TAB = "bookmark"; - /* package */ static String VISITED_TAB = "visited"; - /* package */ static String HISTORY_TAB = "history"; - /* package */ static String STARTING_TAB = "tab"; - - static class IconListenerSet implements IconListener { - // Used to store favicons as we get them from the database - // FIXME: We use a different method to get the Favicons in - // BrowserBookmarksAdapter. They should probably be unified. - private HashMap<String, Bitmap> mUrlsToIcons; - private Vector<IconListener> mListeners; - - public IconListenerSet() { - mUrlsToIcons = new HashMap<String, Bitmap>(); - mListeners = new Vector<IconListener>(); - } - public void onReceivedIcon(String url, Bitmap icon) { - mUrlsToIcons.put(url, icon); - for (IconListener listener : mListeners) { - listener.onReceivedIcon(url, icon); - } - } - public void addListener(IconListener listener) { - mListeners.add(listener); - } - public void removeListener(IconListener listener) { - mListeners.remove(listener); - } - public Bitmap getFavicon(String url) { - return (Bitmap) mUrlsToIcons.get(url); - } - } - private static IconListenerSet sIconListenerSet; - static IconListenerSet getIconListenerSet() { - if (null == sIconListenerSet) { - sIconListenerSet = new IconListenerSet(); - } - return sIconListenerSet; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.tabs); - - setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); - - getTabHost().setOnTabChangedListener(this); - - Bundle extras = getIntent().getExtras(); - - Intent bookmarksIntent = new Intent(this, BrowserBookmarksPage.class); - if (extras != null) { - bookmarksIntent.putExtras(extras); - } - createTab(bookmarksIntent, R.string.tab_bookmarks, - R.drawable.browser_bookmark_tab, BOOKMARKS_TAB); - - Intent visitedIntent = new Intent(this, BrowserBookmarksPage.class); - // Need to copy extras so the bookmarks activity and this one will be - // different - Bundle visitedExtras = extras == null ? new Bundle() : new Bundle(extras); - visitedExtras.putBoolean("mostVisited", true); - visitedIntent.putExtras(visitedExtras); - createTab(visitedIntent, R.string.tab_most_visited, - R.drawable.browser_visited_tab, VISITED_TAB); - - Intent historyIntent = new Intent(this, BrowserHistoryPage.class); - String defaultTab = null; - if (extras != null) { - historyIntent.putExtras(extras); - defaultTab = extras.getString(STARTING_TAB); - } - createTab(historyIntent, R.string.tab_history, - R.drawable.browser_history_tab, HISTORY_TAB); - - if (defaultTab != null) { - getTabHost().setCurrentTab(2); - } - - // XXX: Must do this before launching the AsyncTask to avoid a - // potential crash if the icon database has not been created. - WebIconDatabase.getInstance(); - // Do this every time we launch the activity in case a new favicon was - // added to the webkit db. - (new AsyncTask<Void, Void, Void>() { - public Void doInBackground(Void... v) { - Browser.requestAllIcons(getContentResolver(), - Browser.BookmarkColumns.FAVICON + " is NULL", - getIconListenerSet()); - return null; - } - }).execute(); - } - - private void createTab(Intent intent, int labelResId, int iconResId, - String tab) { - Resources resources = getResources(); - TabHost tabHost = getTabHost(); - tabHost.addTab(tabHost.newTabSpec(tab).setIndicator( - resources.getText(labelResId), resources.getDrawable(iconResId)) - .setContent(intent)); - } - // Copied from DialTacts Activity - /** {@inheritDoc} */ - public void onTabChanged(String tabId) { - Activity activity = getLocalActivityManager().getActivity(tabId); - if (activity != null) { - activity.onWindowFocusChanged(true); - } - } - - /** - * Store extra data in the Intent to return to the calling Activity to tell - * it to clear the parent/child relationships from all tabs. - */ - /* package */ void removeParentChildRelationShips() { - mExtraData = BrowserSettings.PREF_CLEAR_HISTORY; - } - - /** - * Custom setResult() method so that the Intent can have extra data attached - * if necessary. - * @param resultCode Uses same codes as Activity.setResult - * @param data Intent returned to onActivityResult. - */ - /* package */ void setResultFromChild(int resultCode, Intent data) { - mResultCode = resultCode; - mResultData = data; - } - - @Override - public void finish() { - if (mExtraData != null) { - mResultCode = RESULT_OK; - if (mResultData == null) mResultData = new Intent(); - mResultData.putExtra(Intent.EXTRA_TEXT, mExtraData); - } - setResult(mResultCode, mResultData); - super.finish(); - } -} diff --git a/src/com/android/browser/CombinedBookmarkHistoryView.java b/src/com/android/browser/CombinedBookmarkHistoryView.java new file mode 100644 index 000000000..15f31f603 --- /dev/null +++ b/src/com/android/browser/CombinedBookmarkHistoryView.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2009 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.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.os.AsyncTask; +import android.os.Bundle; +import android.provider.Browser; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.webkit.WebIconDatabase; +import android.webkit.WebIconDatabase.IconListener; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.HashMap; +import java.util.Vector; + +interface BookmarksHistoryCallbacks { + public void onUrlSelected(String url, boolean newWindow); + public void onRemoveParentChildRelationships(); + public void onComboCanceled(); +} + +public class CombinedBookmarkHistoryView extends LinearLayout + implements OnClickListener { + + final static String STARTING_FRAGMENT = "fragment"; + + final static int FRAGMENT_ID_BOOKMARKS = 1; + final static int FRAGMENT_ID_HISTORY = 2; + + private UiController mUiController; + private Activity mActivity; + + private Bundle mExtras; + + long mCurrentFragment; + + View mTabs; + TextView mTabBookmarks; + TextView mTabHistory; + TextView mAddBookmark; + View mSeperateSelectAdd; + ViewGroup mBookmarksHeader; + + BrowserBookmarksPage mBookmarks; + BrowserHistoryPage mHistory; + + static class IconListenerSet implements IconListener { + // Used to store favicons as we get them from the database + // FIXME: We use a different method to get the Favicons in + // BrowserBookmarksAdapter. They should probably be unified. + private HashMap<String, Bitmap> mUrlsToIcons; + private Vector<IconListener> mListeners; + + public IconListenerSet() { + mUrlsToIcons = new HashMap<String, Bitmap>(); + mListeners = new Vector<IconListener>(); + } + @Override + public void onReceivedIcon(String url, Bitmap icon) { + mUrlsToIcons.put(url, icon); + for (IconListener listener : mListeners) { + listener.onReceivedIcon(url, icon); + } + } + public void addListener(IconListener listener) { + mListeners.add(listener); + } + public void removeListener(IconListener listener) { + mListeners.remove(listener); + } + public Bitmap getFavicon(String url) { + return mUrlsToIcons.get(url); + } + } + + private static IconListenerSet sIconListenerSet; + static IconListenerSet getIconListenerSet() { + if (null == sIconListenerSet) { + sIconListenerSet = new IconListenerSet(); + } + return sIconListenerSet; + } + + public CombinedBookmarkHistoryView(Activity activity, UiController controller, + int startingFragment, Bundle extras) { + super(activity); + mUiController = controller; + mActivity = activity; + mExtras = extras; + View v = LayoutInflater.from(activity).inflate(R.layout.bookmarks_history, this); + Resources res = activity.getResources(); + +// setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); + + mTabs = findViewById(R.id.tabs); + mBookmarksHeader = (ViewGroup) findViewById(R.id.header_container); + + mTabBookmarks = (TextView) findViewById(R.id.bmtab); + mTabHistory = (TextView) findViewById(R.id.historytab); + mAddBookmark = (TextView) findViewById(R.id.addbm); + mSeperateSelectAdd = findViewById(R.id.seperate_select_add); + mAddBookmark.setOnClickListener(this); + mTabHistory.setOnClickListener(this); + mTabBookmarks.setOnClickListener(this); + // Start up the default fragment + initFragments(mExtras); + loadFragment(startingFragment, mExtras, false); + + // XXX: Must do this before launching the AsyncTask to avoid a + // potential crash if the icon database has not been created. + WebIconDatabase.getInstance(); + + // Do this every time the view is created in case a new favicon was + // added to the webkit db. + (new AsyncTask<Void, Void, Void>() { + @Override + public Void doInBackground(Void... v) { + Browser.requestAllIcons(mActivity.getContentResolver(), + Browser.BookmarkColumns.FAVICON + " is NULL", getIconListenerSet()); + return null; + } + }).execute(); + + } + + private BookmarksPageCallbacks mBookmarkCallbackWrapper = new BookmarksPageCallbacks() { + @Override + public boolean onOpenInNewWindow(Cursor c) { + mUiController.onUrlSelected(BrowserBookmarksPage.getUrl(c), true); + return true; + } + + @Override + public boolean onBookmarkSelected(Cursor c, boolean isFolder) { + if (isFolder) { + return false; + } + mUiController.onUrlSelected(BrowserBookmarksPage.getUrl(c), false); + return true; + } + }; + + private void initFragments(Bundle extras) { + mBookmarks = BrowserBookmarksPage.newInstance(mBookmarkCallbackWrapper, + extras, mBookmarksHeader); + mHistory = BrowserHistoryPage.newInstance(mUiController, extras); + } + + private void loadFragment(int id, Bundle extras, boolean notify) { + String fragmentClassName; + Fragment fragment = null; + switch (id) { + case FRAGMENT_ID_BOOKMARKS: + fragment = mBookmarks; + mSeperateSelectAdd.setVisibility(View.VISIBLE); + mBookmarksHeader.setVisibility(View.VISIBLE); + break; + case FRAGMENT_ID_HISTORY: + fragment = mHistory; + mBookmarksHeader.setVisibility(View.INVISIBLE); + mSeperateSelectAdd.setVisibility(View.INVISIBLE); + break; + default: + throw new IllegalArgumentException(); + } + mCurrentFragment = id; + + FragmentManager fm = mActivity.getFragmentManager(); + FragmentTransaction transaction = fm.openTransaction(); + transaction.replace(R.id.fragment, fragment); + transaction.commit(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + FragmentManager fm = mActivity.getFragmentManager(); + FragmentTransaction transaction = fm.openTransaction(); + if (mCurrentFragment == FRAGMENT_ID_BOOKMARKS) { + transaction.remove(mBookmarks); + } else if (mCurrentFragment == FRAGMENT_ID_HISTORY) { + transaction.remove(mHistory); + } + transaction.commit(); + } + + @Override + public void onClick(View view) { + if ((mTabHistory == view) && (mCurrentFragment != FRAGMENT_ID_HISTORY)) { + loadFragment(FRAGMENT_ID_HISTORY, mExtras, false); + } else if (mTabBookmarks == view) { + if (mCurrentFragment != FRAGMENT_ID_BOOKMARKS) { + loadFragment(FRAGMENT_ID_BOOKMARKS, mExtras, true); + } else { + BreadCrumbView crumbs = mBookmarks.getBreadCrumb(); + if (crumbs != null) { + crumbs.clear(); + } + } + } else if (mAddBookmark == view) { + mUiController.bookmarkCurrentPage(mBookmarks.getFolderId()); + } + } + + /** + * callback for back key presses + */ + boolean onBackPressed() { + if (mCurrentFragment == FRAGMENT_ID_BOOKMARKS) { + return mBookmarks.onBackPressed(); + } + return false; + } +} diff --git a/src/com/android/browser/Controller.java b/src/com/android/browser/Controller.java new file mode 100644 index 000000000..acd76ddf7 --- /dev/null +++ b/src/com/android/browser/Controller.java @@ -0,0 +1,2453 @@ +/* + * Copyright (C) 2010 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 com.android.browser.IntentHandler.UrlData; +import com.android.browser.search.SearchEngine; +import com.android.common.Search; + +import android.app.Activity; +import android.app.DownloadManager; +import android.app.SearchManager; +import android.content.ClipboardManager; +import android.content.ContentProvider; +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Configuration; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Picture; +import android.net.Uri; +import android.net.http.SslError; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.preference.PreferenceActivity; +import android.provider.Browser; +import android.provider.BrowserContract; +import android.provider.BrowserContract.History; +import android.provider.BrowserContract.Images; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Intents.Insert; +import android.speech.RecognizerResultsIntent; +import android.text.TextUtils; +import android.util.Log; +import android.view.ActionMode; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.View; +import android.webkit.CookieManager; +import android.webkit.CookieSyncManager; +import android.webkit.HttpAuthHandler; +import android.webkit.SslErrorHandler; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; +import android.webkit.WebIconDatabase; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.widget.TextView; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.net.URLEncoder; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; + +/** + * Controller for browser + */ +public class Controller + implements WebViewController, UiController { + + private static final String LOGTAG = "Controller"; + + // public message ids + public final static int LOAD_URL = 1001; + public final static int STOP_LOAD = 1002; + + // Message Ids + private static final int FOCUS_NODE_HREF = 102; + private static final int RELEASE_WAKELOCK = 107; + + static final int UPDATE_BOOKMARK_THUMBNAIL = 108; + + private static final int OPEN_BOOKMARKS = 201; + + private static final int EMPTY_MENU = -1; + + // Keep this initial progress in sync with initialProgressValue (* 100) + // in ProgressTracker.cpp + private final static int INITIAL_PROGRESS = 10; + + // activity requestCode + final static int PREFERENCES_PAGE = 3; + final static int FILE_SELECTED = 4; + final static int AUTOFILL_SETUP = 5; + + private final static int WAKELOCK_TIMEOUT = 5 * 60 * 1000; // 5 minutes + + // As the ids are dynamically created, we can't guarantee that they will + // be in sequence, so this static array maps ids to a window number. + final static private int[] WINDOW_SHORTCUT_ID_ARRAY = + { R.id.window_one_menu_id, R.id.window_two_menu_id, + R.id.window_three_menu_id, R.id.window_four_menu_id, + R.id.window_five_menu_id, R.id.window_six_menu_id, + R.id.window_seven_menu_id, R.id.window_eight_menu_id }; + + // "source" parameter for Google search through search key + final static String GOOGLE_SEARCH_SOURCE_SEARCHKEY = "browser-key"; + // "source" parameter for Google search through simplily type + final static String GOOGLE_SEARCH_SOURCE_TYPE = "browser-type"; + + private Activity mActivity; + private UI mUi; + private TabControl mTabControl; + private BrowserSettings mSettings; + private WebViewFactory mFactory; + + private WakeLock mWakeLock; + + private UrlHandler mUrlHandler; + private UploadHandler mUploadHandler; + private IntentHandler mIntentHandler; + private PageDialogsHandler mPageDialogsHandler; + private NetworkStateHandler mNetworkHandler; + + private Message mAutoFillSetupMessage; + + private boolean mShouldShowErrorConsole; + + private SystemAllowGeolocationOrigins mSystemAllowGeolocationOrigins; + + // FIXME, temp address onPrepareMenu performance problem. + // When we move everything out of view, we should rewrite this. + private int mCurrentMenuState = 0; + private int mMenuState = R.id.MAIN_MENU; + private int mOldMenuState = EMPTY_MENU; + private Menu mCachedMenu; + + // Used to prevent chording to result in firing two shortcuts immediately + // one after another. Fixes bug 1211714. + boolean mCanChord; + private boolean mMenuIsDown; + + // For select and find, we keep track of the ActionMode so that + // finish() can be called as desired. + private ActionMode mActionMode; + + /** + * Only meaningful when mOptionsMenuOpen is true. This variable keeps track + * of whether the configuration has changed. The first onMenuOpened call + * after a configuration change is simply a reopening of the same menu + * (i.e. mIconView did not change). + */ + private boolean mConfigChanged; + + /** + * Keeps track of whether the options menu is open. This is important in + * determining whether to show or hide the title bar overlay + */ + private boolean mOptionsMenuOpen; + + /** + * Whether or not the options menu is in its bigger, popup menu form. When + * true, we want the title bar overlay to be gone. When false, we do not. + * Only meaningful if mOptionsMenuOpen is true. + */ + private boolean mExtendedMenuOpen; + + private boolean mInLoad; + + private boolean mActivityPaused = true; + private boolean mLoadStopped; + + private Handler mHandler; + + private static class ClearThumbnails extends AsyncTask<File, Void, Void> { + @Override + public Void doInBackground(File... files) { + if (files != null) { + for (File f : files) { + if (!f.delete()) { + Log.e(LOGTAG, f.getPath() + " was not deleted"); + } + } + } + return null; + } + } + + public Controller(Activity browser) { + mActivity = browser; + mSettings = BrowserSettings.getInstance(); + mTabControl = new TabControl(this); + mSettings.setController(this); + + mUrlHandler = new UrlHandler(this); + mIntentHandler = new IntentHandler(mActivity, this); + mPageDialogsHandler = new PageDialogsHandler(mActivity, this); + + PowerManager pm = (PowerManager) mActivity + .getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Browser"); + + startHandler(); + + mNetworkHandler = new NetworkStateHandler(mActivity, this); + // Start watching the default geolocation permissions + mSystemAllowGeolocationOrigins = + new SystemAllowGeolocationOrigins(mActivity.getApplicationContext()); + mSystemAllowGeolocationOrigins.start(); + + retainIconsOnStartup(); + } + + void start(Bundle icicle, Intent intent) { + // Unless the last browser usage was within 24 hours, destroy any + // remaining incognito tabs. + + Calendar lastActiveDate = icicle != null ? + (Calendar) icicle.getSerializable("lastActiveDate") : null; + Calendar today = Calendar.getInstance(); + Calendar yesterday = Calendar.getInstance(); + yesterday.add(Calendar.DATE, -1); + + boolean restoreIncognitoTabs = !(lastActiveDate == null + || lastActiveDate.before(yesterday) + || lastActiveDate.after(today)); + + if (!mTabControl.restoreState(icicle, restoreIncognitoTabs, + mUi.needsRestoreAllTabs())) { + // there is no quit on Android. But if we can't restore the state, + // we can treat it as a new Browser, remove the old session cookies. + CookieManager.getInstance().removeSessionCookie(); + // remove any incognito files + WebView.cleanupPrivateBrowsingFiles(); + final Bundle extra = intent.getExtras(); + // Create an initial tab. + // If the intent is ACTION_VIEW and data is not null, the Browser is + // invoked to view the content by another application. In this case, + // the tab will be close when exit. + UrlData urlData = mIntentHandler.getUrlDataFromIntent(intent); + + String action = intent.getAction(); + final Tab t = mTabControl.createNewTab( + (Intent.ACTION_VIEW.equals(action) && + intent.getData() != null) + || RecognizerResultsIntent.ACTION_VOICE_SEARCH_RESULTS + .equals(action), + intent.getStringExtra(Browser.EXTRA_APPLICATION_ID), + urlData.mUrl, false); + addTab(t); + setActiveTab(t); + WebView webView = t.getWebView(); + if (extra != null) { + int scale = extra.getInt(Browser.INITIAL_ZOOM_LEVEL, 0); + if (scale > 0 && scale <= 1000) { + webView.setInitialScale(scale); + } + } + + if (urlData.isEmpty()) { + loadUrl(webView, mSettings.getHomePage()); + } else { + loadUrlDataIn(t, urlData); + } + } else { + mUi.updateTabs(mTabControl.getTabs()); + if (!restoreIncognitoTabs) { + WebView.cleanupPrivateBrowsingFiles(); + } + // TabControl.restoreState() will create a new tab even if + // restoring the state fails. + setActiveTab(mTabControl.getCurrentTab()); + } + // clear up the thumbnail directory, which is no longer used; + // ideally this should only be run once after an upgrade from + // a previous version of the browser + new ClearThumbnails().execute(mTabControl.getThumbnailDir() + .listFiles()); + // Read JavaScript flags if it exists. + String jsFlags = getSettings().getJsFlags(); + if (jsFlags.trim().length() != 0) { + getCurrentWebView().setJsFlags(jsFlags); + } + } + + void setWebViewFactory(WebViewFactory factory) { + mFactory = factory; + } + + @Override + public WebViewFactory getWebViewFactory() { + return mFactory; + } + + @Override + public void createSubWindow(Tab tab) { + endActionMode(); + WebView mainView = tab.getWebView(); + WebView subView = mFactory.createWebView((mainView == null) + ? false + : mainView.isPrivateBrowsingEnabled()); + mUi.createSubWindow(tab, subView); + } + + @Override + public Activity getActivity() { + return mActivity; + } + + void setUi(UI ui) { + mUi = ui; + } + + BrowserSettings getSettings() { + return mSettings; + } + + IntentHandler getIntentHandler() { + return mIntentHandler; + } + + @Override + public UI getUi() { + return mUi; + } + + int getMaxTabs() { + return mActivity.getResources().getInteger(R.integer.max_tabs); + } + + @Override + public TabControl getTabControl() { + return mTabControl; + } + + @Override + public List<Tab> getTabs() { + return mTabControl.getTabs(); + } + + // Open the icon database and retain all the icons for visited sites. + // This is done on a background thread so as not to stall startup. + private void retainIconsOnStartup() { + // WebIconDatabase needs to be retrieved on the UI thread so that if + // it has not been created successfully yet the Handler is started on the + // UI thread. + new RetainIconsOnStartupTask(WebIconDatabase.getInstance()).execute(); + } + + private class RetainIconsOnStartupTask extends AsyncTask<Void, Void, Void> { + private WebIconDatabase mDb; + + public RetainIconsOnStartupTask(WebIconDatabase db) { + mDb = db; + } + + protected Void doInBackground(Void... unused) { + mDb.open(mActivity.getDir("icons", 0).getPath()); + Cursor c = null; + try { + c = Browser.getAllBookmarks(mActivity.getContentResolver()); + if (c.moveToFirst()) { + int urlIndex = c.getColumnIndex(Browser.BookmarkColumns.URL); + do { + String url = c.getString(urlIndex); + mDb.retainIconForPageUrl(url); + } while (c.moveToNext()); + } + } catch (IllegalStateException e) { + Log.e(LOGTAG, "retainIconsOnStartup", e); + } finally { + if (c != null) c.close(); + } + + return null; + } + } + + private void startHandler() { + mHandler = new Handler() { + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case OPEN_BOOKMARKS: + bookmarksOrHistoryPicker(false); + break; + case FOCUS_NODE_HREF: + { + String url = (String) msg.getData().get("url"); + String title = (String) msg.getData().get("title"); + if (TextUtils.isEmpty(url)) { + break; + } + HashMap focusNodeMap = (HashMap) msg.obj; + WebView view = (WebView) focusNodeMap.get("webview"); + // Only apply the action if the top window did not change. + if (getCurrentTopWebView() != view) { + break; + } + switch (msg.arg1) { + case R.id.open_context_menu_id: + case R.id.view_image_context_menu_id: + loadUrlFromContext(getCurrentTopWebView(), url); + break; + case R.id.open_newtab_context_menu_id: + final Tab parent = mTabControl.getCurrentTab(); + final Tab newTab = openTab(url, false); + if (newTab != null && newTab != parent) { + parent.addChildTab(newTab); + } + break; + case R.id.bookmark_context_menu_id: + Intent intent = new Intent(mActivity, + AddBookmarkPage.class); + intent.putExtra(BrowserContract.Bookmarks.URL, url); + intent.putExtra(BrowserContract.Bookmarks.TITLE, + title); + mActivity.startActivity(intent); + break; + case R.id.share_link_context_menu_id: + sharePage(mActivity, title, url, null, + null); + break; + case R.id.copy_link_context_menu_id: + copy(url); + break; + case R.id.save_link_context_menu_id: + case R.id.download_context_menu_id: + DownloadHandler.onDownloadStartNoStream( + mActivity, url, null, null, null); + break; + } + break; + } + + case LOAD_URL: + loadUrlFromContext(getCurrentTopWebView(), (String) msg.obj); + break; + + case STOP_LOAD: + stopLoading(); + break; + + case RELEASE_WAKELOCK: + if (mWakeLock.isHeld()) { + mWakeLock.release(); + // if we reach here, Browser should be still in the + // background loading after WAKELOCK_TIMEOUT (5-min). + // To avoid burning the battery, stop loading. + mTabControl.stopAllLoading(); + } + break; + + case UPDATE_BOOKMARK_THUMBNAIL: + WebView view = (WebView) msg.obj; + if (view != null) { + updateScreenshot(view); + } + break; + } + } + }; + + } + + /** + * Share a page, providing the title, url, favicon, and a screenshot. Uses + * an {@link Intent} to launch the Activity chooser. + * @param c Context used to launch a new Activity. + * @param title Title of the page. Stored in the Intent with + * {@link Intent#EXTRA_SUBJECT} + * @param url URL of the page. Stored in the Intent with + * {@link Intent#EXTRA_TEXT} + * @param favicon Bitmap of the favicon for the page. Stored in the Intent + * with {@link Browser#EXTRA_SHARE_FAVICON} + * @param screenshot Bitmap of a screenshot of the page. Stored in the + * Intent with {@link Browser#EXTRA_SHARE_SCREENSHOT} + */ + static final void sharePage(Context c, String title, String url, + Bitmap favicon, Bitmap screenshot) { + Intent send = new Intent(Intent.ACTION_SEND); + send.setType("text/plain"); + send.putExtra(Intent.EXTRA_TEXT, url); + send.putExtra(Intent.EXTRA_SUBJECT, title); + send.putExtra(Browser.EXTRA_SHARE_FAVICON, favicon); + send.putExtra(Browser.EXTRA_SHARE_SCREENSHOT, screenshot); + try { + c.startActivity(Intent.createChooser(send, c.getString( + R.string.choosertitle_sharevia))); + } catch(android.content.ActivityNotFoundException ex) { + // if no app handles it, do nothing + } + } + + private void copy(CharSequence text) { + ClipboardManager cm = (ClipboardManager) mActivity + .getSystemService(Context.CLIPBOARD_SERVICE); + cm.setText(text); + } + + // lifecycle + + protected void onConfgurationChanged(Configuration config) { + mConfigChanged = true; + if (mPageDialogsHandler != null) { + mPageDialogsHandler.onConfigurationChanged(config); + } + mUi.onConfigurationChanged(config); + } + + @Override + public void handleNewIntent(Intent intent) { + mIntentHandler.onNewIntent(intent); + } + + protected void onPause() { + if (mActivityPaused) { + Log.e(LOGTAG, "BrowserActivity is already paused."); + return; + } + mTabControl.pauseCurrentTab(); + mActivityPaused = true; + if (mTabControl.getCurrentIndex() >= 0 && + !pauseWebViewTimers(mActivityPaused)) { + mWakeLock.acquire(); + mHandler.sendMessageDelayed(mHandler + .obtainMessage(RELEASE_WAKELOCK), WAKELOCK_TIMEOUT); + } + mUi.onPause(); + mNetworkHandler.onPause(); + + WebView.disablePlatformNotifications(); + } + + void onSaveInstanceState(Bundle outState) { + // the default implementation requires each view to have an id. As the + // browser handles the state itself and it doesn't use id for the views, + // don't call the default implementation. Otherwise it will trigger the + // warning like this, "couldn't save which view has focus because the + // focused view XXX has no id". + + // Save all the tabs + mTabControl.saveState(outState); + // Save time so that we know how old incognito tabs (if any) are. + outState.putSerializable("lastActiveDate", Calendar.getInstance()); + } + + void onResume() { + if (!mActivityPaused) { + Log.e(LOGTAG, "BrowserActivity is already resumed."); + return; + } + mTabControl.resumeCurrentTab(); + mActivityPaused = false; + resumeWebViewTimers(); + + if (mWakeLock.isHeld()) { + mHandler.removeMessages(RELEASE_WAKELOCK); + mWakeLock.release(); + } + mUi.onResume(); + mNetworkHandler.onResume(); + WebView.enablePlatformNotifications(); + } + + private void resumeWebViewTimers() { + Tab tab = mTabControl.getCurrentTab(); + if (tab == null) return; // monkey can trigger this + boolean inLoad = tab.inPageLoad(); + if ((!mActivityPaused && !inLoad) || (mActivityPaused && inLoad)) { + CookieSyncManager.getInstance().startSync(); + WebView w = tab.getWebView(); + if (w != null) { + w.resumeTimers(); + } + } + } + + private boolean pauseWebViewTimers(boolean activityPaused) { + Tab tab = mTabControl.getCurrentTab(); + boolean inLoad = tab.inPageLoad(); + if (activityPaused && !inLoad) { + CookieSyncManager.getInstance().stopSync(); + WebView w = getCurrentWebView(); + if (w != null) { + w.pauseTimers(); + } + return true; + } else { + return false; + } + } + + void onDestroy() { + if (mUploadHandler != null) { + mUploadHandler.onResult(Activity.RESULT_CANCELED, null); + mUploadHandler = null; + } + if (mTabControl == null) return; + mUi.onDestroy(); + // Remove the current tab and sub window + Tab t = mTabControl.getCurrentTab(); + if (t != null) { + dismissSubWindow(t); + removeTab(t); + } + // Destroy all the tabs + mTabControl.destroy(); + WebIconDatabase.getInstance().close(); + // Stop watching the default geolocation permissions + mSystemAllowGeolocationOrigins.stop(); + mSystemAllowGeolocationOrigins = null; + } + + protected boolean isActivityPaused() { + return mActivityPaused; + } + + protected void onLowMemory() { + mTabControl.freeMemory(); + } + + @Override + public boolean shouldShowErrorConsole() { + return mShouldShowErrorConsole; + } + + protected void setShouldShowErrorConsole(boolean show) { + if (show == mShouldShowErrorConsole) { + // Nothing to do. + return; + } + mShouldShowErrorConsole = show; + Tab t = mTabControl.getCurrentTab(); + if (t == null) { + // There is no current tab so we cannot toggle the error console + return; + } + mUi.setShouldShowErrorConsole(t, show); + } + + @Override + public void stopLoading() { + mLoadStopped = true; + Tab tab = mTabControl.getCurrentTab(); + resetTitleAndRevertLockIcon(tab); + WebView w = getCurrentTopWebView(); + w.stopLoading(); + // FIXME: before refactor, it is using mWebViewClient. So I keep the + // same logic here. But for subwindow case, should we call into the main + // WebView's onPageFinished as we never call its onPageStarted and if + // the page finishes itself, we don't call onPageFinished. + mTabControl.getCurrentWebView().getWebViewClient().onPageFinished(w, + w.getUrl()); + mUi.onPageStopped(tab); + } + + boolean didUserStopLoading() { + return mLoadStopped; + } + + // WebViewController + + @Override + public void onPageStarted(Tab tab, WebView view, String url, Bitmap favicon) { + + // We've started to load a new page. If there was a pending message + // to save a screenshot then we will now take the new page and save + // an incorrect screenshot. Therefore, remove any pending thumbnail + // messages from the queue. + mHandler.removeMessages(Controller.UPDATE_BOOKMARK_THUMBNAIL, + view); + + // reset sync timer to avoid sync starts during loading a page + CookieSyncManager.getInstance().resetSync(); + + if (!mNetworkHandler.isNetworkUp()) { + view.setNetworkAvailable(false); + } + + // when BrowserActivity just starts, onPageStarted may be called before + // onResume as it is triggered from onCreate. Call resumeWebViewTimers + // to start the timer. As we won't switch tabs while an activity is in + // pause state, we can ensure calling resume and pause in pair. + if (mActivityPaused) { + resumeWebViewTimers(); + } + mLoadStopped = false; + if (!mNetworkHandler.isNetworkUp()) { + mNetworkHandler.createAndShowNetworkDialog(); + } + endActionMode(); + + mUi.onPageStarted(tab, url, favicon); + + // Show some progress so that the user knows the page is beginning to + // load + onProgressChanged(tab, INITIAL_PROGRESS); + + // update the bookmark database for favicon + maybeUpdateFavicon(tab, null, url, favicon); + + Performance.tracePageStart(url); + + // Performance probe + if (false) { + Performance.onPageStarted(); + } + + } + + @Override + public void onPageFinished(Tab tab, String url) { + mUi.onPageFinished(tab, url); + if (!tab.isPrivateBrowsingEnabled()) { + if (tab.inForeground() && !didUserStopLoading() + || !tab.inForeground()) { + // Only update the bookmark screenshot if the user did not + // cancel the load early. + mHandler.sendMessageDelayed(mHandler.obtainMessage( + UPDATE_BOOKMARK_THUMBNAIL, 0, 0, tab.getWebView()), + 500); + } + } + // pause the WebView timer and release the wake lock if it is finished + // while BrowserActivity is in pause state. + if (mActivityPaused && pauseWebViewTimers(mActivityPaused)) { + if (mWakeLock.isHeld()) { + mHandler.removeMessages(RELEASE_WAKELOCK); + mWakeLock.release(); + } + } + // Performance probe + if (false) { + Performance.onPageFinished(url); + } + + Performance.tracePageFinished(); + } + + @Override + public void onProgressChanged(Tab tab, int newProgress) { + + if (newProgress == 100) { + CookieSyncManager.getInstance().sync(); + // onProgressChanged() may continue to be called after the main + // frame has finished loading, as any remaining sub frames continue + // to load. We'll only get called once though with newProgress as + // 100 when everything is loaded. (onPageFinished is called once + // when the main frame completes loading regardless of the state of + // any sub frames so calls to onProgressChanges may continue after + // onPageFinished has executed) + if (mInLoad) { + mInLoad = false; + updateInLoadMenuItems(mCachedMenu); + } + } else { + if (!mInLoad) { + // onPageFinished may have already been called but a subframe is + // still loading and updating the progress. Reset mInLoad and + // update the menu items. + mInLoad = true; + updateInLoadMenuItems(mCachedMenu); + } + } + mUi.onProgressChanged(tab, newProgress); + } + + @Override + public void onReceivedTitle(Tab tab, final String title) { + final String pageUrl = tab.getWebView().getUrl(); + setUrlTitle(tab, pageUrl, title); + if (pageUrl == null || pageUrl.length() + >= SQLiteDatabase.SQLITE_MAX_LIKE_PATTERN_LENGTH) { + return; + } + // Update the title in the history database if not in private browsing mode + if (!tab.isPrivateBrowsingEnabled()) { + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... unused) { + // See if we can find the current url in our history + // database and add the new title to it. + String url = pageUrl; + if (url.startsWith("http://www.")) { + url = url.substring(11); + } else if (url.startsWith("http://")) { + url = url.substring(4); + } + // Escape wildcards for LIKE operator. + url = url.replace("\\", "\\\\").replace("%", "\\%") + .replace("_", "\\_"); + Cursor c = null; + try { + final ContentResolver cr = + getActivity().getContentResolver(); + String selection = History.URL + " LIKE ? ESCAPE '\\'"; + String [] selectionArgs = new String[] { "%" + url }; + ContentValues values = new ContentValues(); + values.put(History.TITLE, title); + cr.update(History.CONTENT_URI, values, selection, + selectionArgs); + } catch (IllegalStateException e) { + Log.e(LOGTAG, "Tab onReceived title", e); + } catch (SQLiteException ex) { + Log.e(LOGTAG, + "onReceivedTitle() caught SQLiteException: ", + ex); + } finally { + if (c != null) c.close(); + } + return null; + } + }.execute(); + } + } + + @Override + public void onFavicon(Tab tab, WebView view, Bitmap icon) { + mUi.setFavicon(tab, icon); + maybeUpdateFavicon(tab, view.getOriginalUrl(), view.getUrl(), icon); + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + return mUrlHandler.shouldOverrideUrlLoading(view, url); + } + + @Override + public boolean shouldOverrideKeyEvent(KeyEvent event) { + if (mMenuIsDown) { + // only check shortcut key when MENU is held + return mActivity.getWindow().isShortcutKey(event.getKeyCode(), + event); + } else { + return false; + } + } + + @Override + public void onUnhandledKeyEvent(KeyEvent event) { + if (!isActivityPaused()) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + mActivity.onKeyDown(event.getKeyCode(), event); + } else { + mActivity.onKeyUp(event.getKeyCode(), event); + } + } + } + + @Override + public void doUpdateVisitedHistory(Tab tab, String url, + boolean isReload) { + // Don't save anything in private browsing mode + if (tab.isPrivateBrowsingEnabled()) return; + + if (url.regionMatches(true, 0, "about:", 0, 6)) { + return; + } + // remove "client" before updating it to the history so that it wont + // show up in the auto-complete list. + int index = url.indexOf("client=ms-"); + if (index > 0 && url.contains(".google.")) { + int end = url.indexOf('&', index); + if (end > 0) { + url = url.substring(0, index) + .concat(url.substring(end + 1)); + } else { + // the url.charAt(index-1) should be either '?' or '&' + url = url.substring(0, index-1); + } + } + final ContentResolver cr = getActivity().getContentResolver(); + final String newUrl = url; + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... unused) { + Browser.updateVisitedHistory(cr, newUrl, true); + return null; + } + }.execute(); + WebIconDatabase.getInstance().retainIconForPageUrl(url); + } + + @Override + public void getVisitedHistory(final ValueCallback<String[]> callback) { + AsyncTask<Void, Void, String[]> task = + new AsyncTask<Void, Void, String[]>() { + @Override + public String[] doInBackground(Void... unused) { + return Browser.getVisitedHistory(mActivity.getContentResolver()); + } + @Override + public void onPostExecute(String[] result) { + callback.onReceiveValue(result); + } + }; + task.execute(); + } + + @Override + public void onReceivedHttpAuthRequest(Tab tab, WebView view, + final HttpAuthHandler handler, final String host, + final String realm) { + String username = null; + String password = null; + + boolean reuseHttpAuthUsernamePassword + = handler.useHttpAuthUsernamePassword(); + + if (reuseHttpAuthUsernamePassword && view != null) { + String[] credentials = view.getHttpAuthUsernamePassword(host, realm); + if (credentials != null && credentials.length == 2) { + username = credentials[0]; + password = credentials[1]; + } + } + + if (username != null && password != null) { + handler.proceed(username, password); + } else { + if (tab.inForeground()) { + mPageDialogsHandler.showHttpAuthentication(tab, handler, host, realm); + } else { + handler.cancel(); + } + } + } + + @Override + public void onDownloadStart(Tab tab, String url, String userAgent, + String contentDisposition, String mimetype, long contentLength) { + DownloadHandler.onDownloadStart(mActivity, url, userAgent, + contentDisposition, mimetype); + if (tab.getWebView().copyBackForwardList().getSize() == 0) { + // This Tab was opened for the sole purpose of downloading a + // file. Remove it. + if (tab == mTabControl.getCurrentTab()) { + // In this case, the Tab is still on top. + goBackOnePageOrQuit(); + } else { + // In this case, it is not. + closeTab(tab); + } + } + } + + @Override + public Bitmap getDefaultVideoPoster() { + return mUi.getDefaultVideoPoster(); + } + + @Override + public View getVideoLoadingProgressView() { + return mUi.getVideoLoadingProgressView(); + } + + @Override + public void showSslCertificateOnError(WebView view, SslErrorHandler handler, + SslError error) { + mPageDialogsHandler.showSSLCertificateOnError(view, handler, error); + } + + // helper method + + /* + * Update the favorites icon if the private browsing isn't enabled and the + * icon is valid. + */ + private void maybeUpdateFavicon(Tab tab, final String originalUrl, + final String url, Bitmap favicon) { + if (favicon == null) { + return; + } + if (!tab.isPrivateBrowsingEnabled()) { + Bookmarks.updateFavicon(mActivity + .getContentResolver(), originalUrl, url, favicon); + } + } + + // end WebViewController + + protected void pageUp() { + getCurrentTopWebView().pageUp(false); + } + + protected void pageDown() { + getCurrentTopWebView().pageDown(false); + } + + // callback from phone title bar + public void editUrl() { + if (mOptionsMenuOpen) mActivity.closeOptionsMenu(); + String url = (getCurrentTopWebView() == null) ? null : getCurrentTopWebView().getUrl(); + startSearch(mSettings.getHomePage().equals(url) ? null : url, true, + null, false); + } + + public void activateVoiceSearchMode(String title) { + mUi.showVoiceTitleBar(title); + } + + public void revertVoiceSearchMode(Tab tab) { + mUi.revertVoiceTitleBar(tab); + } + + public void showCustomView(Tab tab, View view, + WebChromeClient.CustomViewCallback callback) { + if (tab.inForeground()) { + if (mUi.isCustomViewShowing()) { + callback.onCustomViewHidden(); + return; + } + mUi.showCustomView(view, callback); + // Save the menu state and set it to empty while the custom + // view is showing. + mOldMenuState = mMenuState; + mMenuState = EMPTY_MENU; + } + } + + @Override + public void hideCustomView() { + if (mUi.isCustomViewShowing()) { + mUi.onHideCustomView(); + // Reset the old menu state. + mMenuState = mOldMenuState; + mOldMenuState = EMPTY_MENU; + } + } + + protected void onActivityResult(int requestCode, int resultCode, + Intent intent) { + if (getCurrentTopWebView() == null) return; + switch (requestCode) { + case PREFERENCES_PAGE: + if (resultCode == Activity.RESULT_OK && intent != null) { + String action = intent.getStringExtra(Intent.EXTRA_TEXT); + if (BrowserSettings.PREF_CLEAR_HISTORY.equals(action)) { + mTabControl.removeParentChildRelationShips(); + } + } + break; + case FILE_SELECTED: + // Choose a file from the file picker. + if (null == mUploadHandler) break; + mUploadHandler.onResult(resultCode, intent); + mUploadHandler = null; + break; + case AUTOFILL_SETUP: + // Determine whether a profile was actually set up or not + // and if so, send the message back to the WebTextView to + // fill the form with the new profile. + if (getSettings().getAutoFillProfile() != null) { + mAutoFillSetupMessage.sendToTarget(); + mAutoFillSetupMessage = null; + } + break; + default: + break; + } + getCurrentTopWebView().requestFocus(); + } + + /** + * Open the Go page. + * @param startWithHistory If true, open starting on the history tab. + * Otherwise, start with the bookmarks tab. + */ + @Override + public void bookmarksOrHistoryPicker(boolean startWithHistory) { + if (mTabControl.getCurrentWebView() == null) { + return; + } + Bundle extras = new Bundle(); + // Disable opening in a new window if we have maxed out the windows + extras.putBoolean(BrowserBookmarksPage.EXTRA_DISABLE_WINDOW, + !mTabControl.canCreateNewTab()); + mUi.showComboView(startWithHistory, extras); + } + + // combo view callbacks + + /** + * callback from ComboPage when clear history is requested + */ + public void onRemoveParentChildRelationships() { + mTabControl.removeParentChildRelationShips(); + } + + /** + * callback from ComboPage when bookmark/history selection + */ + @Override + public void onUrlSelected(String url, boolean newTab) { + removeComboView(); + if (!TextUtils.isEmpty(url)) { + if (newTab) { + openTab(url, false); + } else { + final Tab currentTab = mTabControl.getCurrentTab(); + dismissSubWindow(currentTab); + loadUrl(getCurrentTopWebView(), url); + } + } + } + + /** + * callback from ComboPage when dismissed + */ + @Override + public void onComboCanceled() { + removeComboView(); + } + + /** + * dismiss the ComboPage + */ + @Override + public void removeComboView() { + mUi.hideComboView(); + } + + // active tabs page handling + + protected void showActiveTabsPage() { + mMenuState = EMPTY_MENU; + mUi.showActiveTabsPage(); + } + + /** + * Remove the active tabs page. + * @param needToAttach If true, the active tabs page did not attach a tab + * to the content view, so we need to do that here. + */ + @Override + public void removeActiveTabsPage(boolean needToAttach) { + mMenuState = R.id.MAIN_MENU; + mUi.removeActiveTabsPage(); + if (needToAttach) { + setActiveTab(mTabControl.getCurrentTab()); + } + getCurrentTopWebView().requestFocus(); + } + + // key handling + protected void onBackKey() { + if (!mUi.onBackKey()) { + WebView subwindow = mTabControl.getCurrentSubWindow(); + if (subwindow != null) { + if (subwindow.canGoBack()) { + subwindow.goBack(); + } else { + dismissSubWindow(mTabControl.getCurrentTab()); + } + } else { + goBackOnePageOrQuit(); + } + } + } + + // menu handling and state + // TODO: maybe put into separate handler + + protected boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = mActivity.getMenuInflater(); + inflater.inflate(R.menu.browser, menu); + updateInLoadMenuItems(menu); + // hold on to the menu reference here; it is used by the page callbacks + // to update the menu based on loading state + mCachedMenu = menu; + return true; + } + + protected void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + if (v instanceof TitleBarBase) { + return; + } + if (!(v instanceof WebView)) { + return; + } + final WebView webview = (WebView) v; + WebView.HitTestResult result = webview.getHitTestResult(); + if (result == null) { + return; + } + + int type = result.getType(); + if (type == WebView.HitTestResult.UNKNOWN_TYPE) { + Log.w(LOGTAG, + "We should not show context menu when nothing is touched"); + return; + } + if (type == WebView.HitTestResult.EDIT_TEXT_TYPE) { + // let TextView handles context menu + return; + } + + // Note, http://b/issue?id=1106666 is requesting that + // an inflated menu can be used again. This is not available + // yet, so inflate each time (yuk!) + MenuInflater inflater = mActivity.getMenuInflater(); + inflater.inflate(R.menu.browsercontext, menu); + + // Show the correct menu group + final String extra = result.getExtra(); + menu.setGroupVisible(R.id.PHONE_MENU, + type == WebView.HitTestResult.PHONE_TYPE); + menu.setGroupVisible(R.id.EMAIL_MENU, + type == WebView.HitTestResult.EMAIL_TYPE); + menu.setGroupVisible(R.id.GEO_MENU, + type == WebView.HitTestResult.GEO_TYPE); + menu.setGroupVisible(R.id.IMAGE_MENU, + type == WebView.HitTestResult.IMAGE_TYPE + || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE); + menu.setGroupVisible(R.id.ANCHOR_MENU, + type == WebView.HitTestResult.SRC_ANCHOR_TYPE + || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE); + boolean hitText = type == WebView.HitTestResult.SRC_ANCHOR_TYPE + || type == WebView.HitTestResult.PHONE_TYPE + || type == WebView.HitTestResult.EMAIL_TYPE + || type == WebView.HitTestResult.GEO_TYPE; + menu.setGroupVisible(R.id.SELECT_TEXT_MENU, hitText); + if (hitText) { + menu.findItem(R.id.select_text_menu_id) + .setOnMenuItemClickListener(new SelectText(webview)); + } + // Setup custom handling depending on the type + switch (type) { + case WebView.HitTestResult.PHONE_TYPE: + menu.setHeaderTitle(Uri.decode(extra)); + menu.findItem(R.id.dial_context_menu_id).setIntent( + new Intent(Intent.ACTION_VIEW, Uri + .parse(WebView.SCHEME_TEL + extra))); + Intent addIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT); + addIntent.putExtra(Insert.PHONE, Uri.decode(extra)); + addIntent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE); + menu.findItem(R.id.add_contact_context_menu_id).setIntent( + addIntent); + menu.findItem(R.id.copy_phone_context_menu_id) + .setOnMenuItemClickListener( + new Copy(extra)); + break; + + case WebView.HitTestResult.EMAIL_TYPE: + menu.setHeaderTitle(extra); + menu.findItem(R.id.email_context_menu_id).setIntent( + new Intent(Intent.ACTION_VIEW, Uri + .parse(WebView.SCHEME_MAILTO + extra))); + menu.findItem(R.id.copy_mail_context_menu_id) + .setOnMenuItemClickListener( + new Copy(extra)); + break; + + case WebView.HitTestResult.GEO_TYPE: + menu.setHeaderTitle(extra); + menu.findItem(R.id.map_context_menu_id).setIntent( + new Intent(Intent.ACTION_VIEW, Uri + .parse(WebView.SCHEME_GEO + + URLEncoder.encode(extra)))); + menu.findItem(R.id.copy_geo_context_menu_id) + .setOnMenuItemClickListener( + new Copy(extra)); + break; + + case WebView.HitTestResult.SRC_ANCHOR_TYPE: + case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE: + TextView titleView = (TextView) LayoutInflater.from(mActivity) + .inflate(android.R.layout.browser_link_context_header, + null); + titleView.setText(extra); + menu.setHeaderView(titleView); + // decide whether to show the open link in new tab option + boolean showNewTab = mTabControl.canCreateNewTab(); + MenuItem newTabItem + = menu.findItem(R.id.open_newtab_context_menu_id); + newTabItem.setVisible(showNewTab); + if (showNewTab) { + if (WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE == type) { + newTabItem.setOnMenuItemClickListener( + new MenuItem.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + final HashMap<String, WebView> hrefMap = + new HashMap<String, WebView>(); + hrefMap.put("webview", webview); + final Message msg = mHandler.obtainMessage( + FOCUS_NODE_HREF, + R.id.open_newtab_context_menu_id, + 0, hrefMap); + webview.requestFocusNodeHref(msg); + return true; + } + }); + } else { + newTabItem.setOnMenuItemClickListener( + new MenuItem.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + final Tab parent = mTabControl.getCurrentTab(); + final Tab newTab = openTab(extra, false); + if (newTab != parent) { + parent.addChildTab(newTab); + } + return true; + } + }); + } + } + menu.findItem(R.id.bookmark_context_menu_id).setVisible( + Bookmarks.urlHasAcceptableScheme(extra)); + PackageManager pm = mActivity.getPackageManager(); + Intent send = new Intent(Intent.ACTION_SEND); + send.setType("text/plain"); + ResolveInfo ri = pm.resolveActivity(send, + PackageManager.MATCH_DEFAULT_ONLY); + menu.findItem(R.id.share_link_context_menu_id) + .setVisible(ri != null); + if (type == WebView.HitTestResult.SRC_ANCHOR_TYPE) { + break; + } + // otherwise fall through to handle image part + case WebView.HitTestResult.IMAGE_TYPE: + if (type == WebView.HitTestResult.IMAGE_TYPE) { + menu.setHeaderTitle(extra); + } + menu.findItem(R.id.view_image_context_menu_id).setIntent( + new Intent(Intent.ACTION_VIEW, Uri.parse(extra))); + menu.findItem(R.id.download_context_menu_id). + setOnMenuItemClickListener(new Download(mActivity, extra)); + menu.findItem(R.id.set_wallpaper_context_menu_id). + setOnMenuItemClickListener(new WallpaperHandler(mActivity, + extra)); + break; + + default: + Log.w(LOGTAG, "We should not get here."); + break; + } + //update the ui + mUi.onContextMenuCreated(menu); + } + + /** + * As the menu can be open when loading state changes + * we must manually update the state of the stop/reload menu + * item + */ + private void updateInLoadMenuItems(Menu menu) { + if (menu == null) { + return; + } + MenuItem dest = menu.findItem(R.id.stop_reload_menu_id); + MenuItem src = mInLoad ? + menu.findItem(R.id.stop_menu_id): + menu.findItem(R.id.reload_menu_id); + if (src != null) { + dest.setIcon(src.getIcon()); + dest.setTitle(src.getTitle()); + } + } + + boolean prepareOptionsMenu(Menu menu) { + // This happens when the user begins to hold down the menu key, so + // allow them to chord to get a shortcut. + mCanChord = true; + // Note: setVisible will decide whether an item is visible; while + // setEnabled() will decide whether an item is enabled, which also means + // whether the matching shortcut key will function. + switch (mMenuState) { + case EMPTY_MENU: + if (mCurrentMenuState != mMenuState) { + menu.setGroupVisible(R.id.MAIN_MENU, false); + menu.setGroupEnabled(R.id.MAIN_MENU, false); + menu.setGroupEnabled(R.id.MAIN_SHORTCUT_MENU, false); + } + break; + default: + if (mCurrentMenuState != mMenuState) { + menu.setGroupVisible(R.id.MAIN_MENU, true); + menu.setGroupEnabled(R.id.MAIN_MENU, true); + menu.setGroupEnabled(R.id.MAIN_SHORTCUT_MENU, true); + } + final WebView w = getCurrentTopWebView(); + boolean canGoBack = false; + boolean canGoForward = false; + boolean isHome = false; + if (w != null) { + canGoBack = w.canGoBack(); + canGoForward = w.canGoForward(); + isHome = mSettings.getHomePage().equals(w.getUrl()); + } + final MenuItem back = menu.findItem(R.id.back_menu_id); + back.setEnabled(canGoBack); + + final MenuItem home = menu.findItem(R.id.homepage_menu_id); + home.setEnabled(!isHome); + + final MenuItem forward = menu.findItem(R.id.forward_menu_id); + forward.setEnabled(canGoForward); + + // decide whether to show the share link option + PackageManager pm = mActivity.getPackageManager(); + Intent send = new Intent(Intent.ACTION_SEND); + send.setType("text/plain"); + ResolveInfo ri = pm.resolveActivity(send, + PackageManager.MATCH_DEFAULT_ONLY); + menu.findItem(R.id.share_page_menu_id).setVisible(ri != null); + + boolean isNavDump = mSettings.isNavDump(); + final MenuItem nav = menu.findItem(R.id.dump_nav_menu_id); + nav.setVisible(isNavDump); + nav.setEnabled(isNavDump); + + boolean showDebugSettings = mSettings.showDebugSettings(); + final MenuItem counter = menu.findItem(R.id.dump_counters_menu_id); + counter.setVisible(showDebugSettings); + counter.setEnabled(showDebugSettings); + + // allow the ui to adjust state based settings + mUi.onPrepareOptionsMenu(menu); + + break; + } + mCurrentMenuState = mMenuState; + return true; + } + + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getGroupId() != R.id.CONTEXT_MENU) { + // menu remains active, so ensure comboview is dismissed + // if main menu option is selected + removeComboView(); + } + // check the action bar button before mCanChord check, as the prepare call + // doesn't come for action bar buttons + if (item.getItemId() == R.id.newtab) { + openTabToHomePage(); + return true; + } + if (!mCanChord) { + // The user has already fired a shortcut with this hold down of the + // menu key. + return false; + } + if (null == getCurrentTopWebView()) { + return false; + } + if (mMenuIsDown) { + // The shortcut action consumes the MENU. Even if it is still down, + // it won't trigger the next shortcut action. In the case of the + // shortcut action triggering a new activity, like Bookmarks, we + // won't get onKeyUp for MENU. So it is important to reset it here. + mMenuIsDown = false; + } + switch (item.getItemId()) { + // -- Main menu + case R.id.new_tab_menu_id: + openTabToHomePage(); + break; + + case R.id.incognito_menu_id: + openIncognitoTab(); + break; + + case R.id.goto_menu_id: + editUrl(); + break; + + case R.id.bookmarks_menu_id: + bookmarksOrHistoryPicker(false); + break; + + case R.id.active_tabs_menu_id: + showActiveTabsPage(); + break; + + case R.id.add_bookmark_menu_id: + bookmarkCurrentPage(AddBookmarkPage.DEFAULT_FOLDER_ID); + break; + + case R.id.stop_reload_menu_id: + if (mInLoad) { + stopLoading(); + } else { + getCurrentTopWebView().reload(); + } + break; + + case R.id.back_menu_id: + getCurrentTopWebView().goBack(); + break; + + case R.id.forward_menu_id: + getCurrentTopWebView().goForward(); + break; + + case R.id.close_menu_id: + // Close the subwindow if it exists. + if (mTabControl.getCurrentSubWindow() != null) { + dismissSubWindow(mTabControl.getCurrentTab()); + break; + } + closeCurrentTab(); + break; + + case R.id.homepage_menu_id: + Tab current = mTabControl.getCurrentTab(); + if (current != null) { + dismissSubWindow(current); + loadUrl(current.getWebView(), mSettings.getHomePage()); + } + break; + + case R.id.preferences_menu_id: + Intent intent = new Intent(mActivity, BrowserPreferencesPage.class); + intent.putExtra(BrowserPreferencesPage.CURRENT_PAGE, + getCurrentTopWebView().getUrl()); + mActivity.startActivityForResult(intent, PREFERENCES_PAGE); + break; + + case R.id.find_menu_id: + getCurrentTopWebView().showFindDialog(null); + break; + + case R.id.page_info_menu_id: + mPageDialogsHandler.showPageInfo(mTabControl.getCurrentTab(), + false); + break; + + case R.id.classic_history_menu_id: + bookmarksOrHistoryPicker(true); + break; + + case R.id.title_bar_share_page_url: + case R.id.share_page_menu_id: + Tab currentTab = mTabControl.getCurrentTab(); + if (null == currentTab) { + mCanChord = false; + return false; + } + currentTab.populatePickerData(); + sharePage(mActivity, currentTab.getTitle(), + currentTab.getUrl(), currentTab.getFavicon(), + createScreenshot(currentTab.getWebView(), + getDesiredThumbnailWidth(mActivity), + getDesiredThumbnailHeight(mActivity))); + break; + + case R.id.dump_nav_menu_id: + getCurrentTopWebView().debugDump(); + break; + + case R.id.dump_counters_menu_id: + getCurrentTopWebView().dumpV8Counters(); + break; + + case R.id.zoom_in_menu_id: + getCurrentTopWebView().zoomIn(); + break; + + case R.id.zoom_out_menu_id: + getCurrentTopWebView().zoomOut(); + break; + + case R.id.view_downloads_menu_id: + viewDownloads(); + break; + + case R.id.window_one_menu_id: + case R.id.window_two_menu_id: + case R.id.window_three_menu_id: + case R.id.window_four_menu_id: + case R.id.window_five_menu_id: + case R.id.window_six_menu_id: + case R.id.window_seven_menu_id: + case R.id.window_eight_menu_id: + { + int menuid = item.getItemId(); + for (int id = 0; id < WINDOW_SHORTCUT_ID_ARRAY.length; id++) { + if (WINDOW_SHORTCUT_ID_ARRAY[id] == menuid) { + Tab desiredTab = mTabControl.getTab(id); + if (desiredTab != null && + desiredTab != mTabControl.getCurrentTab()) { + switchToTab(id); + } + break; + } + } + } + break; + + default: + return false; + } + mCanChord = false; + return true; + } + + public boolean onContextItemSelected(MenuItem item) { + // Let the History and Bookmark fragments handle menus they created. + if (item.getGroupId() == R.id.CONTEXT_MENU) { + return false; + } + + // chording is not an issue with context menus, but we use the same + // options selector, so set mCanChord to true so we can access them. + mCanChord = true; + int id = item.getItemId(); + boolean result = true; + switch (id) { + // For the context menu from the title bar + case R.id.title_bar_copy_page_url: + Tab currentTab = mTabControl.getCurrentTab(); + if (null == currentTab) { + result = false; + break; + } + WebView mainView = currentTab.getWebView(); + if (null == mainView) { + result = false; + break; + } + copy(mainView.getUrl()); + break; + // -- Browser context menu + case R.id.open_context_menu_id: + case R.id.bookmark_context_menu_id: + case R.id.save_link_context_menu_id: + case R.id.share_link_context_menu_id: + case R.id.copy_link_context_menu_id: + final WebView webView = getCurrentTopWebView(); + if (null == webView) { + result = false; + break; + } + final HashMap<String, WebView> hrefMap = + new HashMap<String, WebView>(); + hrefMap.put("webview", webView); + final Message msg = mHandler.obtainMessage( + FOCUS_NODE_HREF, id, 0, hrefMap); + webView.requestFocusNodeHref(msg); + break; + + default: + // For other context menus + result = onOptionsItemSelected(item); + } + mCanChord = false; + return result; + } + + /** + * support programmatically opening the context menu + */ + public void openContextMenu(View view) { + mActivity.openContextMenu(view); + } + + /** + * programmatically open the options menu + */ + public void openOptionsMenu() { + mActivity.openOptionsMenu(); + } + + public boolean onMenuOpened(int featureId, Menu menu) { + if (mOptionsMenuOpen) { + if (mConfigChanged) { + // We do not need to make any changes to the state of the + // title bar, since the only thing that happened was a + // change in orientation + mConfigChanged = false; + } else { + if (!mExtendedMenuOpen) { + mExtendedMenuOpen = true; + mUi.onExtendedMenuOpened(); + } else { + // Switching the menu back to icon view, so show the + // title bar once again. + mExtendedMenuOpen = false; + mUi.onExtendedMenuClosed(mInLoad); + mUi.onOptionsMenuOpened(); + } + } + } else { + // The options menu is closed, so open it, and show the title + mOptionsMenuOpen = true; + mConfigChanged = false; + mExtendedMenuOpen = false; + mUi.onOptionsMenuOpened(); + } + return true; + } + + public void onOptionsMenuClosed(Menu menu) { + mOptionsMenuOpen = false; + mUi.onOptionsMenuClosed(mInLoad); + } + + public void onContextMenuClosed(Menu menu) { + mUi.onContextMenuClosed(menu, mInLoad); + } + + // Helper method for getting the top window. + @Override + public WebView getCurrentTopWebView() { + return mTabControl.getCurrentTopWebView(); + } + + @Override + public WebView getCurrentWebView() { + return mTabControl.getCurrentWebView(); + } + + /* + * This method is called as a result of the user selecting the options + * menu to see the download window. It shows the download window on top of + * the current window. + */ + void viewDownloads() { + Intent intent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS); + mActivity.startActivity(intent); + } + + // action mode + + void onActionModeStarted(ActionMode mode) { + mUi.onActionModeStarted(mode); + mActionMode = mode; + } + + /* + * True if a custom ActionMode (i.e. find or select) is in use. + */ + @Override + public boolean isInCustomActionMode() { + return mActionMode != null; + } + + /* + * End the current ActionMode. + */ + @Override + public void endActionMode() { + if (mActionMode != null) { + mActionMode.finish(); + } + } + + /* + * Called by find and select when they are finished. Replace title bars + * as necessary. + */ + public void onActionModeFinished(ActionMode mode) { + if (!isInCustomActionMode()) return; + mUi.onActionModeFinished(mInLoad); + mActionMode = null; + } + + boolean isInLoad() { + return mInLoad; + } + + // bookmark handling + + /** + * add the current page as a bookmark to the given folder id + * @param folderId use -1 for the default folder + */ + @Override + public void bookmarkCurrentPage(long folderId) { + Intent i = new Intent(mActivity, + AddBookmarkPage.class); + WebView w = getCurrentTopWebView(); + i.putExtra(BrowserContract.Bookmarks.URL, w.getUrl()); + i.putExtra(BrowserContract.Bookmarks.TITLE, w.getTitle()); + String touchIconUrl = w.getTouchIconUrl(); + if (touchIconUrl != null) { + i.putExtra(AddBookmarkPage.TOUCH_ICON_URL, touchIconUrl); + WebSettings settings = w.getSettings(); + if (settings != null) { + i.putExtra(AddBookmarkPage.USER_AGENT, + settings.getUserAgentString()); + } + } + i.putExtra(BrowserContract.Bookmarks.THUMBNAIL, + createScreenshot(w, getDesiredThumbnailWidth(mActivity), + getDesiredThumbnailHeight(mActivity))); + i.putExtra(BrowserContract.Bookmarks.FAVICON, w.getFavicon()); + i.putExtra(BrowserContract.Bookmarks.PARENT, + folderId); + // Put the dialog at the upper right of the screen, covering the + // star on the title bar. + i.putExtra("gravity", Gravity.RIGHT | Gravity.TOP); + mActivity.startActivity(i); + } + + // file chooser + public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) { + mUploadHandler = new UploadHandler(this); + mUploadHandler.openFileChooser(uploadMsg, acceptType); + } + + // thumbnails + + /** + * Return the desired width for thumbnail screenshots, which are stored in + * the database, and used on the bookmarks screen. + * @param context Context for finding out the density of the screen. + * @return desired width for thumbnail screenshot. + */ + static int getDesiredThumbnailWidth(Context context) { + return context.getResources().getDimensionPixelOffset( + R.dimen.bookmarkThumbnailWidth); + } + + /** + * Return the desired height for thumbnail screenshots, which are stored in + * the database, and used on the bookmarks screen. + * @param context Context for finding out the density of the screen. + * @return desired height for thumbnail screenshot. + */ + static int getDesiredThumbnailHeight(Context context) { + return context.getResources().getDimensionPixelOffset( + R.dimen.bookmarkThumbnailHeight); + } + + private static Bitmap createScreenshot(WebView view, int width, int height) { + Picture thumbnail = view.capturePicture(); + if (thumbnail == null) { + return null; + } + Bitmap bm = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565); + Canvas canvas = new Canvas(bm); + // May need to tweak these values to determine what is the + // best scale factor + int thumbnailWidth = thumbnail.getWidth(); + int thumbnailHeight = thumbnail.getHeight(); + float scaleFactor = 1.0f; + if (thumbnailWidth > 0) { + scaleFactor = (float) width / (float)thumbnailWidth; + } else { + return null; + } + + if (view.getWidth() > view.getHeight() && + thumbnailHeight < view.getHeight() && thumbnailHeight > 0) { + // If the device is in landscape and the page is shorter + // than the height of the view, center the thumnail and crop the sides + scaleFactor = (float) height / (float)thumbnailHeight; + float wx = (thumbnailWidth * scaleFactor) - width; + canvas.translate((int) -(wx / 2), 0); + } + + canvas.scale(scaleFactor, scaleFactor); + + thumbnail.draw(canvas); + return bm; + } + + private void updateScreenshot(WebView view) { + // If this is a bookmarked site, add a screenshot to the database. + // FIXME: When should we update? Every time? + // FIXME: Would like to make sure there is actually something to + // draw, but the API for that (WebViewCore.pictureReady()) is not + // currently accessible here. + + final Bitmap bm = createScreenshot(view, getDesiredThumbnailWidth(mActivity), + getDesiredThumbnailHeight(mActivity)); + if (bm == null) { + return; + } + + final ContentResolver cr = mActivity.getContentResolver(); + final String url = view.getUrl(); + final String originalUrl = view.getOriginalUrl(); + + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... unused) { + Cursor cursor = null; + try { + cursor = Bookmarks.queryCombinedForUrl(cr, originalUrl, url); + if (cursor != null && cursor.moveToFirst()) { + final ByteArrayOutputStream os = + new ByteArrayOutputStream(); + bm.compress(Bitmap.CompressFormat.PNG, 100, os); + + ContentValues values = new ContentValues(); + values.put(Images.THUMBNAIL, os.toByteArray()); + values.put(Images.URL, cursor.getString(0)); + + do { + cr.update(Images.CONTENT_URI, values, null, null); + } while (cursor.moveToNext()); + } + } catch (IllegalStateException e) { + // Ignore + } finally { + if (cursor != null) cursor.close(); + } + return null; + } + }.execute(); + } + + private class Copy implements OnMenuItemClickListener { + private CharSequence mText; + + public boolean onMenuItemClick(MenuItem item) { + copy(mText); + return true; + } + + public Copy(CharSequence toCopy) { + mText = toCopy; + } + } + + private static class Download implements OnMenuItemClickListener { + private Activity mActivity; + private String mText; + + public boolean onMenuItemClick(MenuItem item) { + DownloadHandler.onDownloadStartNoStream(mActivity, mText, null, + null, null); + return true; + } + + public Download(Activity activity, String toDownload) { + mActivity = activity; + mText = toDownload; + } + } + + private static class SelectText implements OnMenuItemClickListener { + private WebView mWebView; + + public boolean onMenuItemClick(MenuItem item) { + if (mWebView != null) { + return mWebView.selectText(); + } + return false; + } + + public SelectText(WebView webView) { + mWebView = webView; + } + + } + + /********************** TODO: UI stuff *****************************/ + + // these methods have been copied, they still need to be cleaned up + + /****************** tabs ***************************************************/ + + // basic tab interactions: + + // it is assumed that tabcontrol already knows about the tab + protected void addTab(Tab tab) { + mUi.addTab(tab); + } + + protected void removeTab(Tab tab) { + mUi.removeTab(tab); + mTabControl.removeTab(tab); + } + + protected void setActiveTab(Tab tab) { + mTabControl.setCurrentTab(tab); + // the tab is guaranteed to have a webview after setCurrentTab + mUi.setActiveTab(tab); + } + + protected void closeEmptyChildTab() { + Tab current = mTabControl.getCurrentTab(); + if (current != null + && current.getWebView().copyBackForwardList().getSize() == 0) { + Tab parent = current.getParentTab(); + if (parent != null) { + switchToTab(mTabControl.getTabIndex(parent)); + closeTab(current); + } + } + } + + protected void reuseTab(Tab appTab, String appId, UrlData urlData) { + Log.i(LOGTAG, "Reusing tab for " + appId); + // Dismiss the subwindow if applicable. + dismissSubWindow(appTab); + // Since we might kill the WebView, remove it from the + // content view first. + mUi.detachTab(appTab); + // Recreate the main WebView after destroying the old one. + // If the WebView has the same original url and is on that + // page, it can be reused. + boolean needsLoad = + mTabControl.recreateWebView(appTab, urlData); + // TODO: analyze why the remove and add are necessary + mUi.attachTab(appTab); + if (mTabControl.getCurrentTab() != appTab) { + switchToTab(mTabControl.getTabIndex(appTab)); + if (needsLoad) { + loadUrlDataIn(appTab, urlData); + } + } else { + // If the tab was the current tab, we have to attach + // it to the view system again. + setActiveTab(appTab); + if (needsLoad) { + loadUrlDataIn(appTab, urlData); + } + } + } + + // Remove the sub window if it exists. Also called by TabControl when the + // user clicks the 'X' to dismiss a sub window. + public void dismissSubWindow(Tab tab) { + removeSubWindow(tab); + // dismiss the subwindow. This will destroy the WebView. + tab.dismissSubWindow(); + getCurrentTopWebView().requestFocus(); + } + + @Override + public void removeSubWindow(Tab t) { + if (t.getSubWebView() != null) { + mUi.removeSubWindow(t.getSubViewContainer()); + } + } + + @Override + public void attachSubWindow(Tab tab) { + if (tab.getSubWebView() != null) { + mUi.attachSubWindow(tab.getSubViewContainer()); + getCurrentTopWebView().requestFocus(); + } + } + + // A wrapper function of {@link #openTabAndShow(UrlData, boolean, String)} + // that accepts url as string. + + protected Tab openTabAndShow(String url, boolean closeOnExit, String appId) { + return openTabAndShow(new UrlData(url), closeOnExit, appId); + } + + // This method does a ton of stuff. It will attempt to create a new tab + // if we haven't reached MAX_TABS. Otherwise it uses the current tab. If + // url isn't null, it will load the given url. + + public Tab openTabAndShow(UrlData urlData, boolean closeOnExit, + String appId) { + final Tab currentTab = mTabControl.getCurrentTab(); + if (mTabControl.canCreateNewTab()) { + final Tab tab = mTabControl.createNewTab(closeOnExit, appId, + urlData.mUrl, false); + WebView webview = tab.getWebView(); + // We must set the new tab as the current tab to reflect the old + // animation behavior. + addTab(tab); + setActiveTab(tab); + if (!urlData.isEmpty()) { + loadUrlDataIn(tab, urlData); + } + return tab; + } else { + // Get rid of the subwindow if it exists + dismissSubWindow(currentTab); + if (!urlData.isEmpty()) { + // Load the given url. + loadUrlDataIn(currentTab, urlData); + } + return currentTab; + } + } + + protected Tab openTab(String url, boolean forceForeground) { + if (mSettings.openInBackground() && !forceForeground) { + Tab tab = mTabControl.createNewTab(); + if (tab != null) { + addTab(tab); + WebView view = tab.getWebView(); + loadUrl(view, url); + } + return tab; + } else { + return openTabAndShow(url, false, null); + } + } + + @Override + public Tab openIncognitoTab() { + if (mTabControl.canCreateNewTab()) { + Tab currentTab = mTabControl.getCurrentTab(); + Tab tab = mTabControl.createNewTab(false, null, null, true); + addTab(tab); + setActiveTab(tab); + return tab; + } + return null; + } + + /** + * @param index Index of the tab to change to, as defined by + * mTabControl.getTabIndex(Tab t). + * @return boolean True if we successfully switched to a different tab. If + * the indexth tab is null, or if that tab is the same as + * the current one, return false. + */ + @Override + public boolean switchToTab(int index) { + Tab tab = mTabControl.getTab(index); + Tab currentTab = mTabControl.getCurrentTab(); + if (tab == null || tab == currentTab) { + return false; + } + setActiveTab(tab); + return true; + } + + @Override + public Tab openTabToHomePage() { + return openTabAndShow(mSettings.getHomePage(), false, null); + } + + @Override + public void closeCurrentTab() { + final Tab current = mTabControl.getCurrentTab(); + if (mTabControl.getTabCount() == 1) { + // This is the last tab. Open a new one, with the home + // page and close the current one. + openTabToHomePage(); + closeTab(current); + return; + } + final Tab parent = current.getParentTab(); + int indexToShow = -1; + if (parent != null) { + indexToShow = mTabControl.getTabIndex(parent); + } else { + final int currentIndex = mTabControl.getCurrentIndex(); + // Try to move to the tab to the right + indexToShow = currentIndex + 1; + if (indexToShow > mTabControl.getTabCount() - 1) { + // Try to move to the tab to the left + indexToShow = currentIndex - 1; + } + } + if (switchToTab(indexToShow)) { + // Close window + closeTab(current); + } + } + + /** + * Close the tab, remove its associated title bar, and adjust mTabControl's + * current tab to a valid value. + */ + @Override + public void closeTab(Tab tab) { + int currentIndex = mTabControl.getCurrentIndex(); + int removeIndex = mTabControl.getTabIndex(tab); + removeTab(tab); + if (currentIndex >= removeIndex && currentIndex != 0) { + currentIndex--; + } + Tab newtab = mTabControl.getTab(currentIndex); + setActiveTab(newtab); + if (!mTabControl.hasAnyOpenIncognitoTabs()) { + WebView.cleanupPrivateBrowsingFiles(); + } + } + + /**************** TODO: Url loading clean up *******************************/ + + // Called when loading from context menu or LOAD_URL message + protected void loadUrlFromContext(WebView view, String url) { + // In case the user enters nothing. + if (url != null && url.length() != 0 && view != null) { + url = UrlUtils.smartUrlFilter(url); + if (!view.getWebViewClient().shouldOverrideUrlLoading(view, url)) { + loadUrl(view, url); + } + } + } + + /** + * Load the URL into the given WebView and update the title bar + * to reflect the new load. Call this instead of WebView.loadUrl + * directly. + * @param view The WebView used to load url. + * @param url The URL to load. + */ + protected void loadUrl(WebView view, String url) { + updateTitleBarForNewLoad(view, url); + view.loadUrl(url); + } + + /** + * Load UrlData into a Tab and update the title bar to reflect the new + * load. Call this instead of UrlData.loadIn directly. + * @param t The Tab used to load. + * @param data The UrlData being loaded. + */ + protected void loadUrlDataIn(Tab t, UrlData data) { + updateTitleBarForNewLoad(t.getWebView(), data.mUrl); + data.loadIn(t); + } + + /** + * Resets the browser title-view to whatever it must be + * (for example, if we had a loading error) + * When we have a new page, we call resetTitle, when we + * have to reset the titlebar to whatever it used to be + * (for example, if the user chose to stop loading), we + * call resetTitleAndRevertLockIcon. + */ + public void resetTitleAndRevertLockIcon(Tab tab) { + mUi.resetTitleAndRevertLockIcon(tab); + } + + void resetTitleAndIcon(Tab tab) { + mUi.resetTitleAndIcon(tab); + } + + /** + * If the WebView is the top window, update the title bar to reflect + * loading the new URL. i.e. set its text, clear the favicon (which + * will be set once the page begins loading), and set the progress to + * INITIAL_PROGRESS to show that the page has begun to load. Called + * by loadUrl and loadUrlDataIn. + * @param view The WebView that is starting a load. + * @param url The URL that is being loaded. + */ + private void updateTitleBarForNewLoad(WebView view, String url) { + if (view == getCurrentTopWebView()) { + // TODO we should come with a tab and not with a view + Tab tab = mTabControl.getTabFromView(view); + setUrlTitle(tab, url, null); + mUi.setFavicon(tab, null); + onProgressChanged(tab, INITIAL_PROGRESS); + } + } + + /** + * Sets a title composed of the URL and the title string. + * @param url The URL of the site being loaded. + * @param title The title of the site being loaded. + */ + void setUrlTitle(Tab tab, String url, String title) { + tab.setCurrentUrl(url); + tab.setCurrentTitle(title); + // If we are in voice search mode, the title has already been set. + if (tab.isInVoiceSearchMode()) return; + mUi.setUrlTitle(tab, url, title); + } + + void goBackOnePageOrQuit() { + Tab current = mTabControl.getCurrentTab(); + if (current == null) { + /* + * Instead of finishing the activity, simply push this to the back + * of the stack and let ActivityManager to choose the foreground + * activity. As BrowserActivity is singleTask, it will be always the + * root of the task. So we can use either true or false for + * moveTaskToBack(). + */ + mActivity.moveTaskToBack(true); + return; + } + WebView w = current.getWebView(); + if (w.canGoBack()) { + w.goBack(); + } else { + // Check to see if we are closing a window that was created by + // another window. If so, we switch back to that window. + Tab parent = current.getParentTab(); + if (parent != null) { + switchToTab(mTabControl.getTabIndex(parent)); + // Now we close the other tab + closeTab(current); + } else { + if (current.closeOnExit()) { + // force the tab's inLoad() to be false as we are going to + // either finish the activity or remove the tab. This will + // ensure pauseWebViewTimers() taking action. + mTabControl.getCurrentTab().clearInPageLoad(); + if (mTabControl.getTabCount() == 1) { + mActivity.finish(); + return; + } + if (mActivityPaused) { + Log.e(LOGTAG, "BrowserActivity is already paused " + + "while handing goBackOnePageOrQuit."); + } + pauseWebViewTimers(true); + removeTab(current); + } + /* + * Instead of finishing the activity, simply push this to the back + * of the stack and let ActivityManager to choose the foreground + * activity. As BrowserActivity is singleTask, it will be always the + * root of the task. So we can use either true or false for + * moveTaskToBack(). + */ + mActivity.moveTaskToBack(true); + } + } + } + + /** + * Feed the previously stored results strings to the BrowserProvider so that + * the SearchDialog will show them instead of the standard searches. + * @param result String to show on the editable line of the SearchDialog. + */ + @Override + public void showVoiceSearchResults(String result) { + ContentProviderClient client = mActivity.getContentResolver() + .acquireContentProviderClient(Browser.BOOKMARKS_URI); + ContentProvider prov = client.getLocalContentProvider(); + BrowserProvider bp = (BrowserProvider) prov; + bp.setQueryResults(mTabControl.getCurrentTab().getVoiceSearchResults()); + client.release(); + + Bundle bundle = createGoogleSearchSourceBundle( + GOOGLE_SEARCH_SOURCE_SEARCHKEY); + bundle.putBoolean(SearchManager.CONTEXT_IS_VOICE, true); + startSearch(result, false, bundle, false); + } + + private void startSearch(String initialQuery, boolean selectInitialQuery, + Bundle appSearchData, boolean globalSearch) { + if (appSearchData == null) { + appSearchData = createGoogleSearchSourceBundle( + GOOGLE_SEARCH_SOURCE_TYPE); + } + + SearchEngine searchEngine = mSettings.getSearchEngine(); + if (searchEngine != null && !searchEngine.supportsVoiceSearch()) { + appSearchData.putBoolean(SearchManager.DISABLE_VOICE_SEARCH, true); + } + mActivity.startSearch(initialQuery, selectInitialQuery, appSearchData, + globalSearch); + } + + private Bundle createGoogleSearchSourceBundle(String source) { + Bundle bundle = new Bundle(); + bundle.putString(Search.SOURCE, source); + return bundle; + } + + /** + * handle key events in browser + * + * @param keyCode + * @param event + * @return true if handled, false to pass to super + */ + boolean onKeyDown(int keyCode, KeyEvent event) { + // Even if MENU is already held down, we need to call to super to open + // the IME on long press. + if (KeyEvent.KEYCODE_MENU == keyCode) { + mMenuIsDown = true; + return false; + } + // The default key mode is DEFAULT_KEYS_SEARCH_LOCAL. As the MENU is + // still down, we don't want to trigger the search. Pretend to consume + // the key and do nothing. + if (mMenuIsDown) return true; + + switch(keyCode) { + case KeyEvent.KEYCODE_SPACE: + // WebView/WebTextView handle the keys in the KeyDown. As + // the Activity's shortcut keys are only handled when WebView + // doesn't, have to do it in onKeyDown instead of onKeyUp. + if (event.isShiftPressed()) { + pageUp(); + } else { + pageDown(); + } + return true; + case KeyEvent.KEYCODE_BACK: + if (event.getRepeatCount() == 0) { + event.startTracking(); + return true; + } else if (mUi.showsWeb() + && event.isLongPress()) { + bookmarksOrHistoryPicker(true); + return true; + } + break; + } + return false; + } + + boolean onKeyUp(int keyCode, KeyEvent event) { + switch(keyCode) { + case KeyEvent.KEYCODE_MENU: + mMenuIsDown = false; + break; + case KeyEvent.KEYCODE_BACK: + if (event.isTracking() && !event.isCanceled()) { + onBackKey(); + return true; + } + break; + } + return false; + } + + public boolean isMenuDown() { + return mMenuIsDown; + } + + public void setupAutoFill(Message message) { + // Open the settings activity at the AutoFill profile fragment so that + // the user can create a new profile. When they return, we will dispatch + // the message so that we can autofill the form using their new profile. + Intent intent = new Intent(mActivity, BrowserPreferencesPage.class); + intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT, + AutoFillSettingsFragment.class.getName()); + mAutoFillSetupMessage = message; + mActivity.startActivityForResult(intent, AUTOFILL_SETUP); + } +} diff --git a/src/com/android/browser/DateSortedExpandableListAdapter.java b/src/com/android/browser/DateSortedExpandableListAdapter.java index 1d04493aa..a48efe679 100644 --- a/src/com/android/browser/DateSortedExpandableListAdapter.java +++ b/src/com/android/browser/DateSortedExpandableListAdapter.java @@ -17,65 +17,55 @@ package com.android.browser; import android.content.Context; -import android.database.ContentObserver; import android.database.Cursor; import android.database.DataSetObserver; -import android.os.Handler; -import android.provider.BaseColumns; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.webkit.DateSorter; -import android.widget.ExpandableListAdapter; +import android.widget.BaseExpandableListAdapter; import android.widget.ExpandableListView; import android.widget.TextView; -import java.util.Vector; - /** * ExpandableListAdapter which separates data into categories based on date. * Used for History and Downloads. */ -public class DateSortedExpandableListAdapter implements ExpandableListAdapter { +public class DateSortedExpandableListAdapter extends BaseExpandableListAdapter { // Array for each of our bins. Each entry represents how many items are // in that bin. private int mItemMap[]; // This is our GroupCount. We will have at most DateSorter.DAY_COUNT // bins, less if the user has no items in one or more bins. private int mNumberOfBins; - private Vector<DataSetObserver> mObservers; private Cursor mCursor; private DateSorter mDateSorter; private int mDateIndex; private int mIdIndex; private Context mContext; - private class ChangeObserver extends ContentObserver { - public ChangeObserver() { - super(new Handler()); - } + boolean mDataValid; + DataSetObserver mDataSetObserver = new DataSetObserver() { @Override - public boolean deliverSelfNotifications() { - return true; + public void onChanged() { + mDataValid = true; + notifyDataSetChanged(); } @Override - public void onChange(boolean selfChange) { - refreshData(); + public void onInvalidated() { + mDataValid = false; + notifyDataSetInvalidated(); } - } - - public DateSortedExpandableListAdapter(Context context, Cursor cursor, - int dateIndex) { + }; + + public DateSortedExpandableListAdapter(Context context, int dateIndex) { mContext = context; mDateSorter = new DateSorter(context); - mObservers = new Vector<DataSetObserver>(); - mCursor = cursor; - mIdIndex = cursor.getColumnIndexOrThrow(BaseColumns._ID); - cursor.registerContentObserver(new ChangeObserver()); mDateIndex = dateIndex; - buildMap(); + mDataValid = false; + mIdIndex = -1; } /** @@ -122,6 +112,7 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { * @return corresponding byte array from the Cursor. */ /* package */ byte[] getBlob(int cursorIndex) { + if (!mDataValid) return null; return mCursor.getBlob(cursorIndex); } @@ -138,6 +129,7 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { * @return corresponding integer from the Cursor. */ /* package */ int getInt(int cursorIndex) { + if (!mDataValid) return 0; return mCursor.getInt(cursorIndex); } @@ -146,6 +138,7 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { * already been moved to the correct position. */ /* package */ long getLong(int cursorIndex) { + if (!mDataValid) return 0; return mCursor.getLong(cursorIndex); } @@ -158,6 +151,7 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { * @return corresponding String from the Cursor. */ /* package */ String getString(int cursorIndex) { + if (!mDataValid) return null; return mCursor.getString(cursorIndex); } @@ -166,6 +160,7 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { * @param childId ID of the child view in question. * @return int Group position of the containing group. /* package */ int groupFromChildId(long childId) { + if (!mDataValid) return -1; int group = -1; for (mCursor.moveToFirst(); !mCursor.isAfterLast(); mCursor.moveToNext()) { @@ -173,11 +168,15 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { int bin = mDateSorter.getIndex(getLong(mDateIndex)); // bin is the same as the group if the number of bins is the // same as DateSorter - if (mDateSorter.DAY_COUNT == mNumberOfBins) return bin; + if (DateSorter.DAY_COUNT == mNumberOfBins) { + return bin; + } // There are some empty bins. Find the corresponding group. group = 0; for (int i = 0; i < bin; i++) { - if (mItemMap[i] != 0) group++; + if (mItemMap[i] != 0) { + group++; + } } break; } @@ -193,6 +192,7 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { * @return The corresponding bin that holds that group. */ private int groupPositionToBin(int groupPosition) { + if (!mDataValid) return -1; if (groupPosition < 0 || groupPosition >= DateSorter.DAY_COUNT) { throw new AssertionError("group position out of range"); } @@ -241,7 +241,9 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { */ /* package */ boolean moveCursorToChildPosition(int groupPosition, int childPosition) { - if (mCursor.isClosed()) return false; + if (!mDataValid || mCursor.isClosed()) { + return false; + } groupPosition = groupPositionToBin(groupPosition); int index = childPosition; for (int i = 0; i < groupPosition; i++) { @@ -250,19 +252,34 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { return mCursor.moveToPosition(index); } - /* package */ void refreshData() { - if (mCursor.isClosed()) { + public void changeCursor(Cursor cursor) { + if (cursor == mCursor) { return; } - mCursor.requery(); - buildMap(); - for (DataSetObserver o : mObservers) { - o.onChanged(); + if (mCursor != null) { + mCursor.unregisterDataSetObserver(mDataSetObserver); + mCursor.close(); + } + mCursor = cursor; + if (cursor != null) { + cursor.registerDataSetObserver(mDataSetObserver); + mIdIndex = cursor.getColumnIndexOrThrow("_id"); + mDataValid = true; + buildMap(); + // notify the observers about the new cursor + notifyDataSetChanged(); + } else { + mIdIndex = -1; + mDataValid = false; + // notify the observers about the lack of a data set + notifyDataSetInvalidated(); } } + @Override public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { + if (!mDataValid) throw new IllegalStateException("Data is not valid"); TextView item; if (null == convertView || !(convertView instanceof TextView)) { LayoutInflater factory = LayoutInflater.from(mContext); @@ -275,73 +292,87 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { return item; } + @Override public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { + if (!mDataValid) throw new IllegalStateException("Data is not valid"); return null; } + @Override public boolean areAllItemsEnabled() { return true; } + @Override public boolean isChildSelectable(int groupPosition, int childPosition) { return true; } + @Override public int getGroupCount() { + if (!mDataValid) return 0; return mNumberOfBins; } + @Override public int getChildrenCount(int groupPosition) { + if (!mDataValid) return 0; return mItemMap[groupPositionToBin(groupPosition)]; } + @Override public Object getGroup(int groupPosition) { return null; } + @Override public Object getChild(int groupPosition, int childPosition) { return null; } + @Override public long getGroupId(int groupPosition) { + if (!mDataValid) return 0; return groupPosition; } + @Override public long getChildId(int groupPosition, int childPosition) { + if (!mDataValid) return 0; if (moveCursorToChildPosition(groupPosition, childPosition)) { return getLong(mIdIndex); } return 0; } + @Override public boolean hasStableIds() { return true; } - public void registerDataSetObserver(DataSetObserver observer) { - mObservers.add(observer); - } - - public void unregisterDataSetObserver(DataSetObserver observer) { - mObservers.remove(observer); - } - + @Override public void onGroupExpanded(int groupPosition) { } + @Override public void onGroupCollapsed(int groupPosition) { } + @Override public long getCombinedChildId(long groupId, long childId) { + if (!mDataValid) return 0; return childId; } + @Override public long getCombinedGroupId(long groupId) { + if (!mDataValid) return 0; return groupId; } + @Override public boolean isEmpty() { - return mCursor.isClosed() || mCursor.getCount() == 0; + return !mDataValid || mCursor == null || mCursor.isClosed() || mCursor.getCount() == 0; } } diff --git a/src/com/android/browser/Dots.java b/src/com/android/browser/Dots.java deleted file mode 100644 index eb8d49347..000000000 --- a/src/com/android/browser/Dots.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (C) 2008 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.content.Context; -import android.util.AttributeSet; -import android.view.Gravity; -import android.widget.ImageView; -import android.widget.LinearLayout; - -import java.util.Map; - -/** - * Displays a series of dots. The selected one is highlighted. - * No animations yet. Nothing fancy. - */ -class Dots extends LinearLayout { - - private static final int MAX_DOTS = 8; - private int mSelected = -1; - - public Dots(Context context) { - this(context, null); - } - - public Dots(Context context, AttributeSet attrs) { - super(context, attrs); - - setGravity(Gravity.CENTER); - setPadding(0, 4, 0, 4); - - LayoutParams lp = - new LayoutParams(LayoutParams.WRAP_CONTENT, - LayoutParams.WRAP_CONTENT); - - for (int i = 0; i < MAX_DOTS; i++) { - ImageView dotView = new ImageView(mContext); - dotView.setImageResource(R.drawable.page_indicator_unselected2); - addView(dotView, lp); - } - } - - /** - * @param dotCount if less than 1 or greater than MAX_DOTS, Dots - * disappears - */ - public void setDotCount(int dotCount) { - if (dotCount > 1 && dotCount <= MAX_DOTS) { - setVisibility(VISIBLE); - for (int i = 0; i < MAX_DOTS; i++) { - getChildAt(i).setVisibility(i < dotCount? VISIBLE : GONE); - } - } else { - setVisibility(GONE); - } - } - - public void setSelected(int index) { - if (index < 0 || index >= MAX_DOTS) return; - - if (mSelected >= 0) { - // Unselect old - ((ImageView)getChildAt(mSelected)).setImageResource( - R.drawable.page_indicator_unselected2); - } - ((ImageView)getChildAt(index)).setImageResource(R.drawable.page_indicator); - mSelected = index; - } -} diff --git a/src/com/android/browser/DownloadHandler.java b/src/com/android/browser/DownloadHandler.java new file mode 100644 index 000000000..cbf26f40f --- /dev/null +++ b/src/com/android/browser/DownloadHandler.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2010 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.app.DownloadManager; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.net.WebAddress; +import android.os.Environment; +import android.text.TextUtils; +import android.util.Log; +import android.webkit.CookieManager; +import android.webkit.URLUtil; +import android.widget.Toast; + +/** + * Handle download requests + */ +public class DownloadHandler { + + private static final boolean LOGD_ENABLED = + com.android.browser.Browser.LOGD_ENABLED; + + private static final String LOGTAG = "DLHandler"; + + /** + * Notify the host application a download should be done, or that + * the data should be streamed if a streaming viewer is available. + * @param activity Activity requesting the download. + * @param url The full url to the content that should be downloaded + * @param userAgent User agent of the downloading application. + * @param contentDisposition Content-disposition http header, if present. + * @param mimetype The mimetype of the content reported by the server + */ + public static void onDownloadStart(Activity activity, String url, + String userAgent, String contentDisposition, String mimetype) { + // if we're dealing wih A/V content that's not explicitly marked + // for download, check if it's streamable. + if (contentDisposition == null + || !contentDisposition.regionMatches( + true, 0, "attachment", 0, 10)) { + // query the package manager to see if there's a registered handler + // that matches. + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(Uri.parse(url), mimetype); + ResolveInfo info = activity.getPackageManager().resolveActivity(intent, + PackageManager.MATCH_DEFAULT_ONLY); + if (info != null) { + ComponentName myName = activity.getComponentName(); + // If we resolved to ourselves, we don't want to attempt to + // load the url only to try and download it again. + if (!myName.getPackageName().equals( + info.activityInfo.packageName) + || !myName.getClassName().equals( + info.activityInfo.name)) { + // someone (other than us) knows how to handle this mime + // type with this scheme, don't download. + try { + activity.startActivity(intent); + return; + } catch (ActivityNotFoundException ex) { + if (LOGD_ENABLED) { + Log.d(LOGTAG, "activity not found for " + mimetype + + " over " + Uri.parse(url).getScheme(), + ex); + } + // Best behavior is to fall back to a download in this + // case + } + } + } + } + onDownloadStartNoStream(activity, url, userAgent, contentDisposition, + mimetype); + } + + // This is to work around the fact that java.net.URI throws Exceptions + // instead of just encoding URL's properly + // Helper method for onDownloadStartNoStream + private static String encodePath(String path) { + char[] chars = path.toCharArray(); + + boolean needed = false; + for (char c : chars) { + if (c == '[' || c == ']') { + needed = true; + break; + } + } + if (needed == false) { + return path; + } + + StringBuilder sb = new StringBuilder(""); + for (char c : chars) { + if (c == '[' || c == ']') { + sb.append('%'); + sb.append(Integer.toHexString(c)); + } else { + sb.append(c); + } + } + + return sb.toString(); + } + + /** + * Notify the host application a download should be done, even if there + * is a streaming viewer available for thise type. + * @param activity Activity requesting the download. + * @param url The full url to the content that should be downloaded + * @param userAgent User agent of the downloading application. + * @param contentDisposition Content-disposition http header, if present. + * @param mimetype The mimetype of the content reported by the server + */ + /*package */ static void onDownloadStartNoStream(Activity activity, + String url, String userAgent, String contentDisposition, + String mimetype) { + + String filename = URLUtil.guessFileName(url, + contentDisposition, mimetype); + + // Check to see if we have an SDCard + String status = Environment.getExternalStorageState(); + if (!status.equals(Environment.MEDIA_MOUNTED)) { + int title; + String msg; + + // Check to see if the SDCard is busy, same as the music app + if (status.equals(Environment.MEDIA_SHARED)) { + msg = activity.getString(R.string.download_sdcard_busy_dlg_msg); + title = R.string.download_sdcard_busy_dlg_title; + } else { + msg = activity.getString(R.string.download_no_sdcard_dlg_msg, filename); + title = R.string.download_no_sdcard_dlg_title; + } + + new AlertDialog.Builder(activity) + .setTitle(title) + .setIcon(android.R.drawable.ic_dialog_alert) + .setMessage(msg) + .setPositiveButton(R.string.ok, null) + .show(); + return; + } + + // java.net.URI is a lot stricter than KURL so we have to encode some + // extra characters. Fix for b 2538060 and b 1634719 + WebAddress webAddress; + try { + webAddress = new WebAddress(url); + webAddress.setPath(encodePath(webAddress.getPath())); + } catch (Exception e) { + // This only happens for very bad urls, we want to chatch the + // exception here + Log.e(LOGTAG, "Exception trying to parse url:" + url); + return; + } + + String addressString = webAddress.toString(); + Uri uri = Uri.parse(addressString); + final DownloadManager.Request request = new DownloadManager.Request(uri); + request.setMimeType(mimetype); + request.setDestinationInExternalFilesDir(activity, null, filename); + // let this downloaded file be scanned by MediaScanner - so that it can + // show up in Gallery app, for example. + request.allowScanningByMediaScanner(); + request.setDescription(webAddress.getHost()); + // XXX: Have to use the old url since the cookies were stored using the + // old percent-encoded url. + String cookies = CookieManager.getInstance().getCookie(url); + request.addRequestHeader("cookie", cookies); + request.setNotificationVisibility( + DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + if (mimetype == null) { + if (TextUtils.isEmpty(addressString)) { + return; + } + // We must have long pressed on a link or image to download it. We + // are not sure of the mimetype in this case, so do a head request + new FetchUrlMimeType(activity, request, addressString, cookies, + userAgent).start(); + } else { + final DownloadManager manager + = (DownloadManager) activity.getSystemService(Context.DOWNLOAD_SERVICE); + new Thread("Browser download") { + public void run() { + manager.enqueue(request); + } + }.start(); + } + Toast.makeText(activity, R.string.download_pending, Toast.LENGTH_SHORT) + .show(); + } + +} diff --git a/src/com/android/browser/DownloadTouchIcon.java b/src/com/android/browser/DownloadTouchIcon.java index e8a912cd8..768eab584 100644 --- a/src/com/android/browser/DownloadTouchIcon.java +++ b/src/com/android/browser/DownloadTouchIcon.java @@ -16,27 +16,28 @@ package com.android.browser; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.params.HttpClientParams; +import org.apache.http.conn.params.ConnRouteParams; + import android.content.ContentResolver; -import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.net.http.AndroidHttpClient; import android.net.Proxy; +import android.net.http.AndroidHttpClient; import android.os.AsyncTask; -import android.provider.Browser; +import android.os.Bundle; +import android.os.Message; +import android.provider.BrowserContract; +import android.provider.BrowserContract.Images; import android.webkit.WebView; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpHost; -import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.params.HttpClientParams; -import org.apache.http.conn.params.ConnRouteParams; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -46,10 +47,17 @@ class DownloadTouchIcon extends AsyncTask<String, Void, Void> { private Cursor mCursor; private final String mOriginalUrl; private final String mUrl; - private final String mUserAgent; + private final String mUserAgent; // Sites may serve a different icon to different UAs + private Message mMessage; + private final Context mContext; /* package */ Tab mTab; + /** + * Use this ctor to store the touch icon in the bookmarks database for + * the originalUrl so we take account of redirects. Used when the user + * bookmarks a page from outside the bookmarks activity. + */ public DownloadTouchIcon(Tab tab, Context ctx, ContentResolver cr, WebView view) { mTab = tab; mContext = ctx; @@ -60,6 +68,13 @@ class DownloadTouchIcon extends AsyncTask<String, Void, Void> { mUserAgent = view.getSettings().getUserAgentString(); } + /** + * Use this ctor to download the touch icon and update the bookmarks database + * entry for the given url. Used when the user creates a bookmark from + * within the bookmarks activity and there haven't been any redirects. + * TODO: Would be nice to set the user agent here so that there is no + * potential for the three different ctors here to return different icons. + */ public DownloadTouchIcon(Context ctx, ContentResolver cr, String url) { mTab = null; mContext = ctx; @@ -69,15 +84,33 @@ class DownloadTouchIcon extends AsyncTask<String, Void, Void> { mUserAgent = null; } + /** + * Use this ctor to not store the touch icon in a database, rather add it to + * the passed Message's data bundle with the key + * {@link BrowserContract.Bookmarks#TOUCH_ICON} and then send the message. + */ + public DownloadTouchIcon(Context context, Message msg, String userAgent) { + mMessage = msg; + mContext = context; + mContentResolver = null; + mOriginalUrl = null; + mUrl = null; + mUserAgent = userAgent; + } + @Override public Void doInBackground(String... values) { - mCursor = BrowserBookmarksAdapter.queryBookmarksForUrl(mContentResolver, - mOriginalUrl, mUrl, true); - if (mCursor != null && mCursor.getCount() > 0) { - String url = values[0]; + if (mContentResolver != null) { + mCursor = Bookmarks.queryCombinedForUrl(mContentResolver, + mOriginalUrl, mUrl); + } + + boolean inDatabase = mCursor != null && mCursor.getCount() > 0; + + String url = values[0]; - AndroidHttpClient client = AndroidHttpClient.newInstance( - mUserAgent); + if (inDatabase || mMessage != null) { + AndroidHttpClient client = AndroidHttpClient.newInstance(mUserAgent); HttpHost httpHost = Proxy.getPreferredHttpHost(mContext, url); if (httpHost != null) { ConnRouteParams.setDefaultProxy(client.getParams(), httpHost); @@ -90,7 +123,6 @@ class DownloadTouchIcon extends AsyncTask<String, Void, Void> { try { HttpResponse response = client.execute(request); - if (response.getStatusLine().getStatusCode() == 200) { HttpEntity entity = response.getEntity(); if (entity != null) { @@ -98,7 +130,12 @@ class DownloadTouchIcon extends AsyncTask<String, Void, Void> { if (content != null) { Bitmap icon = BitmapFactory.decodeStream( content, null, null); - storeIcon(icon); + if (inDatabase) { + storeIcon(icon); + } else if (mMessage != null) { + Bundle b = mMessage.getData(); + b.putParcelable(BrowserContract.Bookmarks.TOUCH_ICON, icon); + } } } } @@ -110,9 +147,15 @@ class DownloadTouchIcon extends AsyncTask<String, Void, Void> { client.close(); } } + if (mCursor != null) { mCursor.close(); } + + if (mMessage != null) { + mMessage.sendToTarget(); + } + return null; } @@ -134,17 +177,16 @@ class DownloadTouchIcon extends AsyncTask<String, Void, Void> { return; } - final ByteArrayOutputStream os = new ByteArrayOutputStream(); - icon.compress(Bitmap.CompressFormat.PNG, 100, os); - ContentValues values = new ContentValues(); - values.put(Browser.BookmarkColumns.TOUCH_ICON, - os.toByteArray()); - if (mCursor.moveToFirst()) { + final ByteArrayOutputStream os = new ByteArrayOutputStream(); + icon.compress(Bitmap.CompressFormat.PNG, 100, os); + + ContentValues values = new ContentValues(); + values.put(Images.TOUCH_ICON, os.toByteArray()); + values.put(Images.URL, mCursor.getString(0)); + do { - mContentResolver.update(ContentUris.withAppendedId( - Browser.BOOKMARKS_URI, mCursor.getInt(0)), - values, null, null); + mContentResolver.update(Images.CONTENT_URI, values, null, null); } while (mCursor.moveToNext()); } } diff --git a/src/com/android/browser/FetchUrlMimeType.java b/src/com/android/browser/FetchUrlMimeType.java index 62d877edf..2538d90ad 100644 --- a/src/com/android/browser/FetchUrlMimeType.java +++ b/src/com/android/browser/FetchUrlMimeType.java @@ -16,78 +16,67 @@ package com.android.browser; -import android.content.ContentValues; -import android.net.Proxy; -import android.net.Uri; -import android.net.http.AndroidHttpClient; - +import org.apache.http.Header; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; -import org.apache.http.Header; import org.apache.http.client.methods.HttpHead; import org.apache.http.conn.params.ConnRouteParams; -import java.io.IOException; - -import android.os.AsyncTask; -import android.provider.Downloads; +import android.app.Activity; +import android.app.DownloadManager; +import android.content.Context; +import android.net.Proxy; +import android.net.http.AndroidHttpClient; import android.webkit.MimeTypeMap; import android.webkit.URLUtil; +import java.io.IOException; + /** * This class is used to pull down the http headers of a given URL so that * we can analyse the mimetype and make any correction needed before we give - * the URL to the download manager. The ContentValues class holds the - * content that would be provided to the download manager, so that on - * completion of checking the mimetype, we can issue the download to - * the download manager. + * the URL to the download manager. * This operation is needed when the user long-clicks on a link or image and * we don't know the mimetype. If the user just clicks on the link, we will * do the same steps of correcting the mimetype down in * android.os.webkit.LoadListener rather than handling it here. * */ -class FetchUrlMimeType extends AsyncTask<ContentValues, String, ContentValues> { +class FetchUrlMimeType extends Thread { - BrowserActivity mActivity; - ContentValues mValues; + private Activity mActivity; + private DownloadManager.Request mRequest; + private String mUri; + private String mCookies; + private String mUserAgent; - public FetchUrlMimeType(BrowserActivity activity) { + public FetchUrlMimeType(Activity activity, DownloadManager.Request request, + String uri, String cookies, String userAgent) { mActivity = activity; + mRequest = request; + mUri = uri; + mCookies = cookies; + mUserAgent = userAgent; } @Override - public ContentValues doInBackground(ContentValues... values) { - mValues = values[0]; - - // Check to make sure we have a URI to download - String uri = mValues.getAsString(Downloads.Impl.COLUMN_URI); - if (uri == null || uri.length() == 0) { - return null; - } - + public void run() { // User agent is likely to be null, though the AndroidHttpClient // seems ok with that. - AndroidHttpClient client = AndroidHttpClient.newInstance( - mValues.getAsString(Downloads.Impl.COLUMN_USER_AGENT)); - HttpHost httpHost = Proxy.getPreferredHttpHost(mActivity, uri); + AndroidHttpClient client = AndroidHttpClient.newInstance(mUserAgent); + HttpHost httpHost = Proxy.getPreferredHttpHost(mActivity, mUri); if (httpHost != null) { ConnRouteParams.setDefaultProxy(client.getParams(), httpHost); } - HttpHead request = new HttpHead(uri); + HttpHead request = new HttpHead(mUri); - String cookie = mValues.getAsString(Downloads.Impl.COLUMN_COOKIE_DATA); - if (cookie != null && cookie.length() > 0) { - request.addHeader("Cookie", cookie); - } - - String referer = mValues.getAsString(Downloads.Impl.COLUMN_REFERER); - if (referer != null && referer.length() > 0) { - request.addHeader("Referer", referer); + if (mCookies != null && mCookies.length() > 0) { + request.addHeader("Cookie", mCookies); } HttpResponse response; - ContentValues result = new ContentValues(); + String mimeType = null; + String contentDisposition = null; try { response = client.execute(request); // We could get a redirect here, but if we do lets let @@ -96,16 +85,15 @@ class FetchUrlMimeType extends AsyncTask<ContentValues, String, ContentValues> { if (response.getStatusLine().getStatusCode() == 200) { Header header = response.getFirstHeader("Content-Type"); if (header != null) { - String mimeType = header.getValue(); + mimeType = header.getValue(); final int semicolonIndex = mimeType.indexOf(';'); if (semicolonIndex != -1) { mimeType = mimeType.substring(0, semicolonIndex); } - result.put("Content-Type", mimeType); } Header contentDispositionHeader = response.getFirstHeader("Content-Disposition"); if (contentDispositionHeader != null) { - result.put("Content-Disposition", contentDispositionHeader.getValue()); + contentDisposition = contentDispositionHeader.getValue(); } } } catch (IllegalArgumentException ex) { @@ -116,32 +104,25 @@ class FetchUrlMimeType extends AsyncTask<ContentValues, String, ContentValues> { client.close(); } - return result; - } - - @Override - public void onPostExecute(ContentValues values) { - final String mimeType = values.getAsString("Content-Type"); - final String contentDisposition = values.getAsString("Content-Disposition"); if (mimeType != null) { - String url = mValues.getAsString(Downloads.Impl.COLUMN_URI); if (mimeType.equalsIgnoreCase("text/plain") || mimeType.equalsIgnoreCase("application/octet-stream")) { String newMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( - MimeTypeMap.getFileExtensionFromUrl(url)); + MimeTypeMap.getFileExtensionFromUrl(mUri)); if (newMimeType != null) { - mValues.put(Downloads.Impl.COLUMN_MIME_TYPE, newMimeType); + mRequest.setMimeType(newMimeType); } } - String filename = URLUtil.guessFileName(url, - contentDisposition, mimeType); - mValues.put(Downloads.Impl.COLUMN_FILE_NAME_HINT, filename); + String filename = URLUtil.guessFileName(mUri, contentDisposition, + mimeType); + mRequest.setDestinationInExternalFilesDir(mActivity, null, filename); } // Start the download - final Uri contentUri = - mActivity.getContentResolver().insert(Downloads.Impl.CONTENT_URI, mValues); + DownloadManager manager = (DownloadManager) mActivity.getSystemService( + Context.DOWNLOAD_SERVICE); + manager.enqueue(mRequest); } } diff --git a/src/com/android/browser/FindDialog.java b/src/com/android/browser/FindDialog.java deleted file mode 100644 index 726138ef0..000000000 --- a/src/com/android/browser/FindDialog.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright (C) 2007 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.content.Context; -import android.text.Editable; -import android.text.Selection; -import android.text.Spannable; -import android.text.TextWatcher; -import android.view.Gravity; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.AnimationUtils; -import android.view.inputmethod.InputMethodManager; -import android.webkit.WebView; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.TextView; - -/* package */ class FindDialog extends WebDialog implements TextWatcher { - private TextView mMatches; - - // Views with which the user can interact. - private EditText mEditText; - private View mNextButton; - private View mPrevButton; - private View mMatchesView; - - // When the dialog is opened up with old text, enter needs to be pressed - // (or the text needs to be changed) before WebView.findAll can be called. - // Once it has been called, enter should move to the next match. - private boolean mMatchesFound; - private int mNumberOfMatches; - - private View.OnClickListener mFindListener = new View.OnClickListener() { - public void onClick(View v) { - findNext(); - } - }; - - private View.OnClickListener mFindPreviousListener = - new View.OnClickListener() { - public void onClick(View v) { - if (mWebView == null) { - throw new AssertionError("No WebView for FindDialog::onClick"); - } - mWebView.findNext(false); - updateMatchesString(); - hideSoftInput(); - } - }; - - private void disableButtons() { - mPrevButton.setEnabled(false); - mNextButton.setEnabled(false); - mPrevButton.setFocusable(false); - mNextButton.setFocusable(false); - } - - /* package */ FindDialog(BrowserActivity context) { - super(context); - - LayoutInflater factory = LayoutInflater.from(context); - factory.inflate(R.layout.browser_find, this); - - addCancel(); - mEditText = (EditText) findViewById(R.id.edit); - - View button = findViewById(R.id.next); - button.setOnClickListener(mFindListener); - mNextButton = button; - - button = findViewById(R.id.previous); - button.setOnClickListener(mFindPreviousListener); - mPrevButton = button; - - mMatches = (TextView) findViewById(R.id.matches); - mMatchesView = findViewById(R.id.matches_view); - disableButtons(); - - } - - /** - * Called by BrowserActivity.closeDialog. Start the animation to hide - * the dialog, inform the WebView that the dialog is being dismissed, - * and hide the soft keyboard. - */ - public void dismiss() { - super.dismiss(); - mWebView.notifyFindDialogDismissed(); - hideSoftInput(); - } - - @Override - public boolean dispatchKeyEventPreIme(KeyEvent event) { - if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { - KeyEvent.DispatcherState state = getKeyDispatcherState(); - if (state != null) { - int action = event.getAction(); - if (KeyEvent.ACTION_DOWN == action - && event.getRepeatCount() == 0) { - state.startTracking(event, this); - return true; - } else if (KeyEvent.ACTION_UP == action - && !event.isCanceled() && state.isTracking(event)) { - mBrowserActivity.closeDialogs(); - return true; - } - } - } - return super.dispatchKeyEventPreIme(event); - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - int keyCode = event.getKeyCode(); - if (event.getAction() == KeyEvent.ACTION_UP) { - if (keyCode == KeyEvent.KEYCODE_ENTER - && mEditText.hasFocus()) { - if (mMatchesFound) { - findNext(); - } else { - findAll(); - // Set the selection to the end. - Spannable span = (Spannable) mEditText.getText(); - Selection.setSelection(span, span.length()); - } - return true; - } - } - return super.dispatchKeyEvent(event); - } - - private void findNext() { - if (mWebView == null) { - throw new AssertionError("No WebView for FindDialog::findNext"); - } - mWebView.findNext(true); - updateMatchesString(); - hideSoftInput(); - } - - public void show() { - super.show(); - // In case the matches view is showing from a previous search - mMatchesView.setVisibility(View.INVISIBLE); - mMatchesFound = false; - // This text is only here to ensure that mMatches has a height. - mMatches.setText("0"); - mEditText.requestFocus(); - Spannable span = (Spannable) mEditText.getText(); - int length = span.length(); - Selection.setSelection(span, 0, length); - span.setSpan(this, 0, length, Spannable.SPAN_INCLUSIVE_INCLUSIVE); - disableButtons(); - InputMethodManager imm = (InputMethodManager) - mBrowserActivity.getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(mEditText, 0); - } - - // TextWatcher methods - public void beforeTextChanged(CharSequence s, - int start, - int count, - int after) { - } - - public void onTextChanged(CharSequence s, - int start, - int before, - int count) { - findAll(); - } - - private void findAll() { - if (mWebView == null) { - throw new AssertionError( - "No WebView for FindDialog::findAll"); - } - CharSequence find = mEditText.getText(); - if (0 == find.length()) { - disableButtons(); - mWebView.clearMatches(); - mMatchesView.setVisibility(View.INVISIBLE); - } else { - mMatchesView.setVisibility(View.VISIBLE); - int found = mWebView.findAll(find.toString()); - mMatchesFound = true; - setMatchesFound(found); - if (found < 2) { - disableButtons(); - if (found == 0) { - // Cannot use getQuantityString, which ignores the "zero" - // quantity. - // FIXME: is this fix is beyond the scope - // of adding touch selection to gingerbread? - // mMatches.setText(mBrowserActivity.getResources().getString( - // R.string.no_matches)); - } - } else { - mPrevButton.setFocusable(true); - mNextButton.setFocusable(true); - mPrevButton.setEnabled(true); - mNextButton.setEnabled(true); - } - } - } - - private void setMatchesFound(int found) { - mNumberOfMatches = found; - updateMatchesString(); - } - - public void setText(String text) { - mEditText.setText(text); - findAll(); - } - - private void updateMatchesString() { - // Note: updateMatchesString is only called by methods that have already - // checked mWebView for null. - String template = mBrowserActivity.getResources(). - getQuantityString(R.plurals.matches_found, mNumberOfMatches, - mWebView.findIndex() + 1, mNumberOfMatches); - - mMatches.setText(template); - } - - public void afterTextChanged(Editable s) { - } -} diff --git a/src/com/android/browser/HistoryItem.java b/src/com/android/browser/HistoryItem.java index 72e1b19e5..11198f04d 100644 --- a/src/com/android/browser/HistoryItem.java +++ b/src/com/android/browser/HistoryItem.java @@ -18,12 +18,8 @@ package com.android.browser; import android.content.Context; -import android.graphics.Bitmap; -import android.provider.Browser; import android.view.View; import android.widget.CompoundButton; -import android.widget.ImageView; -import android.widget.TextView; /** * Layout representing a history item in the classic history viewer. @@ -45,12 +41,13 @@ import android.widget.TextView; public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) { - Bookmarks.addBookmark(mContext, - mContext.getContentResolver(), mUrl, getName(), null, true); + // FIXME: For now, add at the root level. Should we + // open AddBookmark from here? + Bookmarks.addBookmark(getContext(), true, mUrl, getName(), null, true, 0); LogTag.logBookmarkAdded(mUrl, "history"); } else { - Bookmarks.removeFromBookmarks(mContext, - mContext.getContentResolver(), mUrl, getName()); + Bookmarks.removeFromBookmarks(getContext(), + getContext().getContentResolver(), mUrl, getName()); } } }; diff --git a/src/com/android/browser/HttpAuthenticationDialog.java b/src/com/android/browser/HttpAuthenticationDialog.java new file mode 100644 index 000000000..a9ba332e3 --- /dev/null +++ b/src/com/android/browser/HttpAuthenticationDialog.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2010 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.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.TextView; + +/** + * HTTP authentication dialog. + */ +public class HttpAuthenticationDialog { + + private final Context mContext; + + private final String mHost; + private final String mRealm; + + private AlertDialog mDialog; + private TextView mUsernameView; + private TextView mPasswordView; + + private OkListener mOkListener; + private CancelListener mCancelListener; + + /** + * Creates an HTTP authentication dialog. + */ + public HttpAuthenticationDialog(Context context, String host, String realm) { + mContext = context; + mHost = host; + mRealm = realm; + createDialog(); + } + + private String getUsername() { + return mUsernameView.getText().toString(); + } + + private String getPassword() { + return mPasswordView.getText().toString(); + } + + /** + * Sets the listener that will be notified when the user submits the credentials. + */ + public void setOkListener(OkListener okListener) { + mOkListener = okListener; + } + + /** + * Sets the listener that will be notified when the user cancels the authentication + * dialog. + */ + public void setCancelListener(CancelListener cancelListener) { + mCancelListener = cancelListener; + } + + /** + * Shows the dialog. + */ + public void show() { + mDialog.show(); + mUsernameView.requestFocus(); + } + + /** + * Hides, recreates, and shows the dialog. This can be used to handle configuration changes. + */ + public void reshow() { + String username = getUsername(); + String password = getPassword(); + int focusId = mDialog.getCurrentFocus().getId(); + mDialog.dismiss(); + createDialog(); + mDialog.show(); + if (username != null) { + mUsernameView.setText(username); + } + if (password != null) { + mPasswordView.setText(password); + } + if (focusId != 0) { + mDialog.findViewById(focusId).requestFocus(); + } else { + mUsernameView.requestFocus(); + } + } + + private void createDialog() { + LayoutInflater factory = LayoutInflater.from(mContext); + View v = factory.inflate(R.layout.http_authentication, null); + mUsernameView = (TextView) v.findViewById(R.id.username_edit); + mPasswordView = (TextView) v.findViewById(R.id.password_edit); + + String title = mContext.getText(R.string.sign_in_to).toString().replace( + "%s1", mHost).replace("%s2", mRealm); + + mDialog = new AlertDialog.Builder(mContext) + .setTitle(title) + .setIcon(android.R.drawable.ic_dialog_alert) + .setView(v) + .setPositiveButton(R.string.action, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + if (mOkListener != null) { + mOkListener.onOk(mHost, mRealm, getUsername(), getPassword()); + } + }}) + .setNegativeButton(R.string.cancel,new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + if (mCancelListener != null) mCancelListener.onCancel(); + }}) + .setOnCancelListener(new DialogInterface.OnCancelListener() { + public void onCancel(DialogInterface dialog) { + if (mCancelListener != null) mCancelListener.onCancel(); + }}) + .create(); + + // Make the IME appear when the dialog is displayed if applicable. + mDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + } + + /** + * Interface for listeners that are notified when the user submits the credentials. + */ + public interface OkListener { + void onOk(String host, String realm, String username, String password); + } + + /** + * Interface for listeners that are notified when the user cancels the dialog. + */ + public interface CancelListener { + void onCancel(); + } +} diff --git a/src/com/android/browser/IntentHandler.java b/src/com/android/browser/IntentHandler.java new file mode 100644 index 000000000..040af8178 --- /dev/null +++ b/src/com/android/browser/IntentHandler.java @@ -0,0 +1,371 @@ +/* + * Copyright (C) 2010 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 com.android.browser.search.SearchEngine; +import com.android.common.Search; +import com.android.common.speech.LoggingEvents; + +import android.app.Activity; +import android.app.SearchManager; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.provider.Browser; +import android.provider.MediaStore; +import android.speech.RecognizerResultsIntent; +import android.text.TextUtils; +import android.util.Patterns; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * Handle all browser related intents + */ +public class IntentHandler { + + // "source" parameter for Google search suggested by the browser + final static String GOOGLE_SEARCH_SOURCE_SUGGEST = "browser-suggest"; + // "source" parameter for Google search from unknown source + final static String GOOGLE_SEARCH_SOURCE_UNKNOWN = "unknown"; + + /* package */ static final UrlData EMPTY_URL_DATA = new UrlData(null); + + private Activity mActivity; + private Controller mController; + private TabControl mTabControl; + private BrowserSettings mSettings; + + public IntentHandler(Activity browser, Controller controller) { + mActivity = browser; + mController = controller; + mTabControl = mController.getTabControl(); + mSettings = controller.getSettings(); + } + + void onNewIntent(Intent intent) { + Tab current = mTabControl.getCurrentTab(); + // When a tab is closed on exit, the current tab index is set to -1. + // Reset before proceed as Browser requires the current tab to be set. + if (current == null) { + // Try to reset the tab in case the index was incorrect. + current = mTabControl.getTab(0); + if (current == null) { + // No tabs at all so just ignore this intent. + return; + } + mController.setActiveTab(current); + mController.resetTitleAndIcon(current); + } + final String action = intent.getAction(); + final int flags = intent.getFlags(); + if (Intent.ACTION_MAIN.equals(action) || + (flags & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) { + // just resume the browser + return; + } + // In case the SearchDialog is open. + ((SearchManager) mActivity.getSystemService(Context.SEARCH_SERVICE)) + .stopSearch(); + boolean activateVoiceSearch = RecognizerResultsIntent + .ACTION_VOICE_SEARCH_RESULTS.equals(action); + if (Intent.ACTION_VIEW.equals(action) + || Intent.ACTION_SEARCH.equals(action) + || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action) + || Intent.ACTION_WEB_SEARCH.equals(action) + || activateVoiceSearch) { + if (current.isInVoiceSearchMode()) { + String title = current.getVoiceDisplayTitle(); + if (title != null && title.equals(intent.getStringExtra( + SearchManager.QUERY))) { + // The user submitted the same search as the last voice + // search, so do nothing. + return; + } + if (Intent.ACTION_SEARCH.equals(action) + && current.voiceSearchSourceIsGoogle()) { + Intent logIntent = new Intent( + LoggingEvents.ACTION_LOG_EVENT); + logIntent.putExtra(LoggingEvents.EXTRA_EVENT, + LoggingEvents.VoiceSearch.QUERY_UPDATED); + logIntent.putExtra( + LoggingEvents.VoiceSearch.EXTRA_QUERY_UPDATED_VALUE, + intent.getDataString()); + mActivity.sendBroadcast(logIntent); + // Note, onPageStarted will revert the voice title bar + // When http://b/issue?id=2379215 is fixed, we should update + // the title bar here. + } + } + // If this was a search request (e.g. search query directly typed into the address bar), + // pass it on to the default web search provider. + if (handleWebSearchIntent(mActivity, mController, intent)) { + return; + } + + UrlData urlData = getUrlDataFromIntent(intent); + if (urlData.isEmpty()) { + urlData = new UrlData(mSettings.getHomePage()); + } + + final String appId = intent + .getStringExtra(Browser.EXTRA_APPLICATION_ID); + if ((Intent.ACTION_VIEW.equals(action) + // If a voice search has no appId, it means that it came + // from the browser. In that case, reuse the current tab. + || (activateVoiceSearch && appId != null)) + && !mActivity.getPackageName().equals(appId) + && (flags & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) { + Tab appTab = mTabControl.getTabFromId(appId); + if (appTab != null) { + mController.reuseTab(appTab, appId, urlData); + return; + } else { + // No matching application tab, try to find a regular tab + // with a matching url. + appTab = mTabControl.findUnusedTabWithUrl(urlData.mUrl); + if (appTab != null) { + if (current != appTab) { + mController.switchToTab(mTabControl.getTabIndex(appTab)); + } + // Otherwise, we are already viewing the correct tab. + } else { + // if FLAG_ACTIVITY_BROUGHT_TO_FRONT flag is on, the url + // will be opened in a new tab unless we have reached + // MAX_TABS. Then the url will be opened in the current + // tab. If a new tab is created, it will have "true" for + // exit on close. + mController.openTabAndShow(urlData, true, appId); + } + } + } else { + if (!urlData.isEmpty() + && urlData.mUrl.startsWith("about:debug")) { + if ("about:debug.dom".equals(urlData.mUrl)) { + current.getWebView().dumpDomTree(false); + } else if ("about:debug.dom.file".equals(urlData.mUrl)) { + current.getWebView().dumpDomTree(true); + } else if ("about:debug.render".equals(urlData.mUrl)) { + current.getWebView().dumpRenderTree(false); + } else if ("about:debug.render.file".equals(urlData.mUrl)) { + current.getWebView().dumpRenderTree(true); + } else if ("about:debug.display".equals(urlData.mUrl)) { + current.getWebView().dumpDisplayTree(); + } else { + mSettings.toggleDebugSettings(); + } + return; + } + // Get rid of the subwindow if it exists + mController.dismissSubWindow(current); + // If the current Tab is being used as an application tab, + // remove the association, since the new Intent means that it is + // no longer associated with that application. + current.setAppId(null); + mController.loadUrlDataIn(current, urlData); + } + } + } + + protected UrlData getUrlDataFromIntent(Intent intent) { + String url = ""; + Map<String, String> headers = null; + if (intent != null) { + final String action = intent.getAction(); + if (Intent.ACTION_VIEW.equals(action)) { + url = UrlUtils.smartUrlFilter(intent.getData()); + if (url != null && url.startsWith("content:")) { + /* Append mimetype so webview knows how to display */ + String mimeType = intent.resolveType(mActivity.getContentResolver()); + if (mimeType != null) { + url += "?" + mimeType; + } + } + if (url != null && url.startsWith("http")) { + final Bundle pairs = intent + .getBundleExtra(Browser.EXTRA_HEADERS); + if (pairs != null && !pairs.isEmpty()) { + Iterator<String> iter = pairs.keySet().iterator(); + headers = new HashMap<String, String>(); + while (iter.hasNext()) { + String key = iter.next(); + headers.put(key, pairs.getString(key)); + } + } + } + } else if (Intent.ACTION_SEARCH.equals(action) + || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action) + || Intent.ACTION_WEB_SEARCH.equals(action)) { + url = intent.getStringExtra(SearchManager.QUERY); + if (url != null) { + // In general, we shouldn't modify URL from Intent. + // But currently, we get the user-typed URL from search box as well. + url = UrlUtils.fixUrl(url); + url = UrlUtils.smartUrlFilter(url); + final ContentResolver cr = mActivity.getContentResolver(); + final String newUrl = url; + if (mTabControl == null + || mTabControl.getCurrentWebView() == null + || !mTabControl.getCurrentWebView().isPrivateBrowsingEnabled()) { + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... unused) { + Browser.updateVisitedHistory(cr, newUrl, false); + return null; + } + }.execute(); + } + String searchSource = "&source=android-" + GOOGLE_SEARCH_SOURCE_SUGGEST + "&"; + if (url.contains(searchSource)) { + String source = null; + final Bundle appData = intent.getBundleExtra(SearchManager.APP_DATA); + if (appData != null) { + source = appData.getString(Search.SOURCE); + } + if (TextUtils.isEmpty(source)) { + source = GOOGLE_SEARCH_SOURCE_UNKNOWN; + } + url = url.replace(searchSource, "&source=android-"+source+"&"); + } + } + } + } + return new UrlData(url, headers, intent); + } + + /** + * Launches the default web search activity with the query parameters if the given intent's data + * are identified as plain search terms and not URLs/shortcuts. + * @return true if the intent was handled and web search activity was launched, false if not. + */ + static boolean handleWebSearchIntent(Activity activity, + Controller controller, Intent intent) { + if (intent == null) return false; + + String url = null; + final String action = intent.getAction(); + if (RecognizerResultsIntent.ACTION_VOICE_SEARCH_RESULTS.equals( + action)) { + return false; + } + if (Intent.ACTION_VIEW.equals(action)) { + Uri data = intent.getData(); + if (data != null) url = data.toString(); + } else if (Intent.ACTION_SEARCH.equals(action) + || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action) + || Intent.ACTION_WEB_SEARCH.equals(action)) { + url = intent.getStringExtra(SearchManager.QUERY); + } + return handleWebSearchRequest(activity, controller, url, + intent.getBundleExtra(SearchManager.APP_DATA), + intent.getStringExtra(SearchManager.EXTRA_DATA_KEY)); + } + + /** + * Launches the default web search activity with the query parameters if the given url string + * was identified as plain search terms and not URL/shortcut. + * @return true if the request was handled and web search activity was launched, false if not. + */ + private static boolean handleWebSearchRequest(Activity activity, + Controller controller, String inUrl, Bundle appData, + String extraData) { + if (inUrl == null) return false; + + // In general, we shouldn't modify URL from Intent. + // But currently, we get the user-typed URL from search box as well. + String url = UrlUtils.fixUrl(inUrl).trim(); + + // URLs are handled by the regular flow of control, so + // return early. + if (Patterns.WEB_URL.matcher(url).matches() + || UrlUtils.ACCEPTED_URI_SCHEMA.matcher(url).matches()) { + return false; + } + + final ContentResolver cr = activity.getContentResolver(); + final String newUrl = url; + if (controller == null || controller.getTabControl() == null + || controller.getTabControl().getCurrentWebView() == null + || !controller.getTabControl().getCurrentWebView() + .isPrivateBrowsingEnabled()) { + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... unused) { + Browser.updateVisitedHistory(cr, newUrl, false); + Browser.addSearchUrl(cr, newUrl); + return null; + } + }.execute(); + } + + SearchEngine searchEngine = BrowserSettings.getInstance().getSearchEngine(); + if (searchEngine == null) return false; + searchEngine.startSearch(activity, url, appData, extraData); + + return true; + } + + /** + * A UrlData class to abstract how the content will be set to WebView. + * This base class uses loadUrl to show the content. + */ + static class UrlData { + final String mUrl; + final Map<String, String> mHeaders; + final Intent mVoiceIntent; + + UrlData(String url) { + this.mUrl = url; + this.mHeaders = null; + this.mVoiceIntent = null; + } + + UrlData(String url, Map<String, String> headers, Intent intent) { + this.mUrl = url; + this.mHeaders = headers; + if (RecognizerResultsIntent.ACTION_VOICE_SEARCH_RESULTS + .equals(intent.getAction())) { + this.mVoiceIntent = intent; + } else { + this.mVoiceIntent = null; + } + } + + boolean isEmpty() { + return mVoiceIntent == null && (mUrl == null || mUrl.length() == 0); + } + + /** + * Load this UrlData into the given Tab. Use loadUrlDataIn to update + * the title bar as well. + */ + public void loadIn(Tab t) { + if (mVoiceIntent != null) { + t.activateVoiceSearchMode(mVoiceIntent); + } else { + t.getWebView().loadUrl(mUrl, mHeaders); + } + } + } + +} diff --git a/src/com/android/browser/MeshTracker.java b/src/com/android/browser/MeshTracker.java deleted file mode 100644 index c4b63329f..000000000 --- a/src/com/android/browser/MeshTracker.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (C) 2009 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.graphics.Bitmap; -import android.graphics.utils.BoundaryPatch; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.webkit.WebView; - -/*package*/ class MeshTracker extends WebView.DragTracker { - - private static class Mesh { - private int mWhich; - private int mRows; - private int mCols; - private BoundaryPatch mPatch = new BoundaryPatch(); - private float[] mCubics = new float[24]; - private float[] mOrig = new float[24]; - private float mStretchX, mStretchY; - - Mesh(int which, int rows, int cols) { - mWhich = which; - mRows = rows; - mCols = cols; - } - - private void rebuildPatch() { - mPatch.setCubicBoundary(mCubics, 0, mRows, mCols); - } - - private void setSize(float w, float h) { - float[] pts = mCubics; - float x1 = w*0.3333f; - float y1 = h*0.3333f; - float x2 = w*0.6667f; - float y2 = h*0.6667f; - pts[0*2+0] = 0; pts[0*2+1] = 0; - pts[1*2+0] = x1; pts[1*2+1] = 0; - pts[2*2+0] = x2; pts[2*2+1] = 0; - - pts[3*2+0] = w; pts[3*2+1] = 0; - pts[4*2+0] = w; pts[4*2+1] = y1; - pts[5*2+0] = w; pts[5*2+1] = y2; - - pts[6*2+0] = w; pts[6*2+1] = h; - pts[7*2+0] = x2; pts[7*2+1] = h; - pts[8*2+0] = x1; pts[8*2+1] = h; - - pts[9*2+0] = 0; pts[9*2+1] = h; - pts[10*2+0] = 0; pts[10*2+1] = y2; - pts[11*2+0] = 0; pts[11*2+1] = y1; - - System.arraycopy(pts, 0, mOrig, 0, 24); - - // recall our stretcher - setStretch(mStretchX, mStretchY); - } - - public void setBitmap(Bitmap bm) { - mPatch.setTexture(bm); - setSize(bm.getWidth(), bm.getHeight()); - } - - // first experimental behavior - private void doit1(float dx, float dy) { - final float scale = 0.75f; // temper how far we actually move - dx *= scale; - dy *= scale; - - int index; - if (dx < 0) { - index = 10; - } else { - index = 4; - } - mCubics[index*2 + 0] = mOrig[index*2 + 0] + dx; - mCubics[index*2 + 2] = mOrig[index*2 + 2] + dx; - - if (dy < 0) { - index = 1; - } else { - index = 7; - } - mCubics[index*2 + 1] = mOrig[index*2 + 1] + dy; - mCubics[index*2 + 3] = mOrig[index*2 + 3] + dy; - } - - private void doit2(float dx, float dy) { - final float scale = 0.35f; // temper how far we actually move - dx *= scale; - dy *= scale; - final float cornerScale = 0.25f; - - int index; - if (dx < 0) { - index = 4; - } else { - index = 10; - } - mCubics[index*2 + 0] = mOrig[index*2 + 0] + dx; - mCubics[index*2 + 2] = mOrig[index*2 + 2] + dx; - // corners - index -= 1; - mCubics[index*2 + 0] = mOrig[index*2 + 0] + dx * cornerScale; - index = (index + 3) % 12; // next corner - mCubics[index*2 + 0] = mOrig[index*2 + 0] + dx * cornerScale; - - if (dy < 0) { - index = 7; - } else { - index = 1; - } - mCubics[index*2 + 1] = mOrig[index*2 + 1] + dy; - mCubics[index*2 + 3] = mOrig[index*2 + 3] + dy; - // corners - index -= 1; - mCubics[index*2 + 1] = mOrig[index*2 + 1] + dy * cornerScale; - index = (index + 3) % 12; // next corner - mCubics[index*2 + 1] = mOrig[index*2 + 1] + dy * cornerScale; - } - - public void setStretch(float dx, float dy) { - mStretchX = dx; - mStretchY = dy; - switch (mWhich) { - case 1: - doit1(dx, dy); - break; - case 2: - doit2(dx, dy); - break; - } - rebuildPatch(); - } - - public void draw(Canvas canvas) { - mPatch.draw(canvas); - } - } - - private Mesh mMesh; - private Bitmap mBitmap; - private int mWhich; - private Paint mBGPaint; - - public MeshTracker(int which) { - mWhich = which; - } - - public void setBGPaint(Paint paint) { - mBGPaint = paint; - } - - @Override public void onStartDrag(float x, float y) { - mMesh = new Mesh(mWhich, 16, 16); - } - - @Override public void onBitmapChange(Bitmap bm) { - mBitmap = bm; - mMesh.setBitmap(bm); - } - - @Override public boolean onStretchChange(float sx, float sy) { - mMesh.setStretch(-sx, -sy); - return true; - } - - @Override public void onStopDrag() { - mMesh = null; - } - - @Override public void onDraw(Canvas canvas) { - if (mWhich == 2) { - if (mBGPaint != null) { - canvas.drawPaint(mBGPaint); - } else { - canvas.drawColor(0xFF000000); - } - } - mMesh.draw(canvas); - } -} - diff --git a/src/com/android/browser/NetworkStateHandler.java b/src/com/android/browser/NetworkStateHandler.java new file mode 100644 index 000000000..3b2007ee3 --- /dev/null +++ b/src/com/android/browser/NetworkStateHandler.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2010 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.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.webkit.WebView; + +/** + * Handle network state changes + */ +public class NetworkStateHandler { + + Activity mActivity; + Controller mController; + + // monitor platform changes + private IntentFilter mNetworkStateChangedFilter; + private BroadcastReceiver mNetworkStateIntentReceiver; + private boolean mIsNetworkUp; + + /* hold a ref so we can auto-cancel if necessary */ + private AlertDialog mAlertDialog; + + public NetworkStateHandler(Activity activity, Controller controller) { + mActivity = activity; + mController = controller; + // Find out if the network is currently up. + ConnectivityManager cm = (ConnectivityManager) mActivity + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo info = cm.getActiveNetworkInfo(); + if (info != null) { + mIsNetworkUp = info.isAvailable(); + } + + /* + * enables registration for changes in network status from http stack + */ + mNetworkStateChangedFilter = new IntentFilter(); + mNetworkStateChangedFilter.addAction( + ConnectivityManager.CONNECTIVITY_ACTION); + mNetworkStateIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals( + ConnectivityManager.CONNECTIVITY_ACTION)) { + + NetworkInfo info = intent.getParcelableExtra( + ConnectivityManager.EXTRA_NETWORK_INFO); + String typeName = info.getTypeName(); + String subtypeName = info.getSubtypeName(); + sendNetworkType(typeName.toLowerCase(), + (subtypeName != null ? subtypeName.toLowerCase() : "")); + + onNetworkToggle(info.isAvailable()); + } + } + }; + + } + + void onPause() { + // unregister network state listener + mActivity.unregisterReceiver(mNetworkStateIntentReceiver); + } + + void onResume() { + mActivity.registerReceiver(mNetworkStateIntentReceiver, + mNetworkStateChangedFilter); + } + + /** + * connectivity manager says net has come or gone... inform the user + * @param up true if net has come up, false if net has gone down + */ + void onNetworkToggle(boolean up) { + if (up == mIsNetworkUp) { + return; + } else if (up) { + mIsNetworkUp = true; + if (mAlertDialog != null) { + mAlertDialog.cancel(); + mAlertDialog = null; + } + } else { + mIsNetworkUp = false; + if (mController.isInLoad()) { + createAndShowNetworkDialog(); + } + } + WebView w = mController.getCurrentWebView(); + if (w != null) { + w.setNetworkAvailable(up); + } + } + + boolean isNetworkUp() { + return mIsNetworkUp; + } + + // This method shows the network dialog alerting the user that the net is + // down. It will only show the dialog if mAlertDialog is null. + void createAndShowNetworkDialog() { + if (mAlertDialog == null) { + mAlertDialog = new AlertDialog.Builder(mActivity) + .setTitle(R.string.loadSuspendedTitle) + .setMessage(R.string.loadSuspended) + .setPositiveButton(R.string.ok, null) + .show(); + } + } + + private void sendNetworkType(String type, String subtype) { + WebView w = mController.getCurrentWebView(); + if (w != null) { + w.setNetworkType(type, subtype); + } + } + +} diff --git a/src/com/android/browser/OpenDownloadReceiver.java b/src/com/android/browser/OpenDownloadReceiver.java index 99e5f4106..4277ff493 100644 --- a/src/com/android/browser/OpenDownloadReceiver.java +++ b/src/com/android/browser/OpenDownloadReceiver.java @@ -19,15 +19,11 @@ package com.android.browser; import android.app.DownloadManager; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.database.Cursor; import android.net.Uri; -import android.provider.Downloads; -import android.widget.Toast; - -import java.io.File; +import android.os.Handler; +import android.os.HandlerThread; /** * This {@link BroadcastReceiver} handles clicks to notifications that @@ -36,49 +32,63 @@ import java.io.File; * a complete, successful download will open the file. */ public class OpenDownloadReceiver extends BroadcastReceiver { - public void onReceive(Context context, Intent intent) { - ContentResolver cr = context.getContentResolver(); - Uri data = intent.getData(); - Cursor cursor = null; - try { - cursor = cr.query(data, - new String[] { Downloads.Impl._ID, Downloads.Impl._DATA, - Downloads.Impl.COLUMN_MIME_TYPE, Downloads.COLUMN_STATUS }, - null, null, null); - if (cursor.moveToFirst()) { - String filename = cursor.getString(1); - String mimetype = cursor.getString(2); - String action = intent.getAction(); - if (Downloads.ACTION_NOTIFICATION_CLICKED.equals(action)) { - int status = cursor.getInt(3); - if (Downloads.isStatusCompleted(status) - && Downloads.isStatusSuccess(status)) { - Intent launchIntent = new Intent(Intent.ACTION_VIEW); - Uri path = Uri.parse(filename); - // If there is no scheme, then it must be a file - if (path.getScheme() == null) { - path = Uri.fromFile(new File(filename)); - } - launchIntent.setDataAndType(path, mimetype); - launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - try { - context.startActivity(launchIntent); - } catch (ActivityNotFoundException ex) { - Toast.makeText(context, - R.string.download_no_application_title, - Toast.LENGTH_LONG).show(); - } - } else { - // Open the downloads page - Intent pageView = new Intent( - DownloadManager.ACTION_VIEW_DOWNLOADS); - pageView.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(pageView); - } - } + private static Handler sAsyncHandler; + static { + HandlerThread thr = new HandlerThread("Open browser download async"); + thr.start(); + sAsyncHandler = new Handler(thr.getLooper()); + } + @Override + public void onReceive(final Context context, Intent intent) { + String action = intent.getAction(); + if (!DownloadManager.ACTION_NOTIFICATION_CLICKED.equals(action)) { + openDownloadsPage(context); + return; + } + long ids[] = intent.getLongArrayExtra( + DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS); + if (ids == null || ids.length == 0) { + openDownloadsPage(context); + return; + } + final long id = ids[0]; + final PendingResult result = goAsync(); + Runnable worker = new Runnable() { + @Override + public void run() { + onReceiveAsync(context, id); + result.finish(); + } + }; + sAsyncHandler.post(worker); + } + + private void onReceiveAsync(Context context, long id) { + DownloadManager manager = (DownloadManager) context.getSystemService( + Context.DOWNLOAD_SERVICE); + Uri uri = manager.getUriForDownloadedFile(id); + if (uri == null) { + // Open the downloads page + openDownloadsPage(context); + } else { + Intent launchIntent = new Intent(Intent.ACTION_VIEW); + launchIntent.setDataAndType(uri, manager.getMimeTypeForDownloadedFile(id)); + launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + context.startActivity(launchIntent); + } catch (ActivityNotFoundException e) { + openDownloadsPage(context); } - } finally { - if (cursor != null) cursor.close(); } } + + /** + * Open the Activity which shows a list of all downloads. + * @param context + */ + private void openDownloadsPage(Context context) { + Intent pageView = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS); + pageView.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(pageView); + } } diff --git a/src/com/android/browser/PageDialogsHandler.java b/src/com/android/browser/PageDialogsHandler.java new file mode 100644 index 000000000..6843a10fc --- /dev/null +++ b/src/com/android/browser/PageDialogsHandler.java @@ -0,0 +1,465 @@ +/* + * Copyright (C) 2010 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.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.Configuration; +import android.net.http.SslCertificate; +import android.net.http.SslError; +import android.text.format.DateFormat; +import android.view.LayoutInflater; +import android.view.View; +import android.webkit.HttpAuthHandler; +import android.webkit.SslErrorHandler; +import android.webkit.WebView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.Date; + +/** + * Displays page info + * + */ +public class PageDialogsHandler { + + private Context mContext; + private Controller mController; + private boolean mPageInfoFromShowSSLCertificateOnError; + private Tab mPageInfoView; + private AlertDialog mPageInfoDialog; + + // as SSLCertificateOnError has different style for landscape / portrait, + // we have to re-open it when configuration changed + private AlertDialog mSSLCertificateOnErrorDialog; + private WebView mSSLCertificateOnErrorView; + private SslErrorHandler mSSLCertificateOnErrorHandler; + private SslError mSSLCertificateOnErrorError; + + // as SSLCertificate has different style for landscape / portrait, we + // have to re-open it when configuration changed + private AlertDialog mSSLCertificateDialog; + private Tab mSSLCertificateView; + private HttpAuthenticationDialog mHttpAuthenticationDialog; + + public PageDialogsHandler(Context context, Controller controller) { + mContext = context; + mController = controller; + } + + public void onConfigurationChanged(Configuration config) { + if (mPageInfoDialog != null) { + mPageInfoDialog.dismiss(); + showPageInfo(mPageInfoView, mPageInfoFromShowSSLCertificateOnError); + } + if (mSSLCertificateDialog != null) { + mSSLCertificateDialog.dismiss(); + showSSLCertificate(mSSLCertificateView); + } + if (mSSLCertificateOnErrorDialog != null) { + mSSLCertificateOnErrorDialog.dismiss(); + showSSLCertificateOnError(mSSLCertificateOnErrorView, mSSLCertificateOnErrorHandler, + mSSLCertificateOnErrorError); + } + if (mHttpAuthenticationDialog != null) { + mHttpAuthenticationDialog.reshow(); + } + } + + /** + * Displays an http-authentication dialog. + */ + void showHttpAuthentication(final Tab tab, final HttpAuthHandler handler, String host, String realm) { + mHttpAuthenticationDialog = new HttpAuthenticationDialog(mContext, host, realm); + mHttpAuthenticationDialog.setOkListener(new HttpAuthenticationDialog.OkListener() { + public void onOk(String host, String realm, String username, String password) { + setHttpAuthUsernamePassword(host, realm, username, password); + handler.proceed(username, password); + mHttpAuthenticationDialog = null; + } + }); + mHttpAuthenticationDialog.setCancelListener(new HttpAuthenticationDialog.CancelListener() { + public void onCancel() { + handler.cancel(); + mController.resetTitleAndRevertLockIcon(tab); + mHttpAuthenticationDialog = null; + } + }); + mHttpAuthenticationDialog.show(); + } + + /** + * Set HTTP authentication password. + * + * @param host The host for the password + * @param realm The realm for the password + * @param username The username for the password. If it is null, it means + * password can't be saved. + * @param password The password + */ + public void setHttpAuthUsernamePassword(String host, String realm, + String username, + String password) { + WebView w = mController.getCurrentTopWebView(); + if (w != null) { + w.setHttpAuthUsernamePassword(host, realm, username, password); + } + } + + /** + * Displays a page-info dialog. + * @param tab The tab to show info about + * @param fromShowSSLCertificateOnError The flag that indicates whether + * this dialog was opened from the SSL-certificate-on-error dialog or + * not. This is important, since we need to know whether to return to + * the parent dialog or simply dismiss. + */ + void showPageInfo(final Tab tab, + final boolean fromShowSSLCertificateOnError) { + final LayoutInflater factory = LayoutInflater.from(mContext); + + final View pageInfoView = factory.inflate(R.layout.page_info, null); + + final WebView view = tab.getWebView(); + + String url = null; + String title = null; + + if (view == null) { + url = tab.getUrl(); + title = tab.getTitle(); + } else if (view == mController.getCurrentWebView()) { + // Use the cached title and url if this is the current WebView + url = tab.getCurrentUrl(); + title = tab.getCurrentTitle(); + } else { + url = view.getUrl(); + title = view.getTitle(); + } + + if (url == null) { + url = ""; + } + if (title == null) { + title = ""; + } + + ((TextView) pageInfoView.findViewById(R.id.address)).setText(url); + ((TextView) pageInfoView.findViewById(R.id.title)).setText(title); + + mPageInfoView = tab; + mPageInfoFromShowSSLCertificateOnError = fromShowSSLCertificateOnError; + + AlertDialog.Builder alertDialogBuilder = + new AlertDialog.Builder(mContext) + .setTitle(R.string.page_info) + .setIcon(android.R.drawable.ic_dialog_info) + .setView(pageInfoView) + .setPositiveButton( + R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int whichButton) { + mPageInfoDialog = null; + mPageInfoView = null; + + // if we came here from the SSL error dialog + if (fromShowSSLCertificateOnError) { + // go back to the SSL error dialog + showSSLCertificateOnError( + mSSLCertificateOnErrorView, + mSSLCertificateOnErrorHandler, + mSSLCertificateOnErrorError); + } + } + }) + .setOnCancelListener( + new DialogInterface.OnCancelListener() { + public void onCancel(DialogInterface dialog) { + mPageInfoDialog = null; + mPageInfoView = null; + + // if we came here from the SSL error dialog + if (fromShowSSLCertificateOnError) { + // go back to the SSL error dialog + showSSLCertificateOnError( + mSSLCertificateOnErrorView, + mSSLCertificateOnErrorHandler, + mSSLCertificateOnErrorError); + } + } + }); + + // if we have a main top-level page SSL certificate set or a certificate + // error + if (fromShowSSLCertificateOnError || + (view != null && view.getCertificate() != null)) { + // add a 'View Certificate' button + alertDialogBuilder.setNeutralButton( + R.string.view_certificate, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int whichButton) { + mPageInfoDialog = null; + mPageInfoView = null; + + // if we came here from the SSL error dialog + if (fromShowSSLCertificateOnError) { + // go back to the SSL error dialog + showSSLCertificateOnError( + mSSLCertificateOnErrorView, + mSSLCertificateOnErrorHandler, + mSSLCertificateOnErrorError); + } else { + // otherwise, display the top-most certificate from + // the chain + if (view.getCertificate() != null) { + showSSLCertificate(tab); + } + } + } + }); + } + + mPageInfoDialog = alertDialogBuilder.show(); + } + + /** + * Displays the main top-level page SSL certificate dialog + * (accessible from the Page-Info dialog). + * @param tab The tab to show certificate for. + */ + private void showSSLCertificate(final Tab tab) { + final View certificateView = + inflateCertificateView(tab.getWebView().getCertificate()); + if (certificateView == null) { + return; + } + + LayoutInflater factory = LayoutInflater.from(mContext); + + final LinearLayout placeholder = + (LinearLayout)certificateView.findViewById(R.id.placeholder); + + LinearLayout ll = (LinearLayout) factory.inflate( + R.layout.ssl_success, placeholder); + ((TextView)ll.findViewById(R.id.success)) + .setText(R.string.ssl_certificate_is_valid); + + mSSLCertificateView = tab; + mSSLCertificateDialog = + new AlertDialog.Builder(mContext) + .setTitle(R.string.ssl_certificate).setIcon( + R.drawable.ic_dialog_browser_certificate_secure) + .setView(certificateView) + .setPositiveButton(R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int whichButton) { + mSSLCertificateDialog = null; + mSSLCertificateView = null; + + showPageInfo(tab, false); + } + }) + .setOnCancelListener( + new DialogInterface.OnCancelListener() { + public void onCancel(DialogInterface dialog) { + mSSLCertificateDialog = null; + mSSLCertificateView = null; + + showPageInfo(tab, false); + } + }) + .show(); + } + + /** + * Displays the SSL error certificate dialog. + * @param view The target web-view. + * @param handler The SSL error handler responsible for cancelling the + * connection that resulted in an SSL error or proceeding per user request. + * @param error The SSL error object. + */ + void showSSLCertificateOnError( + final WebView view, final SslErrorHandler handler, + final SslError error) { + + final View certificateView = + inflateCertificateView(error.getCertificate()); + if (certificateView == null) { + return; + } + + LayoutInflater factory = LayoutInflater.from(mContext); + + final LinearLayout placeholder = + (LinearLayout)certificateView.findViewById(R.id.placeholder); + + if (error.hasError(SslError.SSL_UNTRUSTED)) { + LinearLayout ll = (LinearLayout)factory + .inflate(R.layout.ssl_warning, placeholder); + ((TextView)ll.findViewById(R.id.warning)) + .setText(R.string.ssl_untrusted); + } + + if (error.hasError(SslError.SSL_IDMISMATCH)) { + LinearLayout ll = (LinearLayout)factory + .inflate(R.layout.ssl_warning, placeholder); + ((TextView)ll.findViewById(R.id.warning)) + .setText(R.string.ssl_mismatch); + } + + if (error.hasError(SslError.SSL_EXPIRED)) { + LinearLayout ll = (LinearLayout)factory + .inflate(R.layout.ssl_warning, placeholder); + ((TextView)ll.findViewById(R.id.warning)) + .setText(R.string.ssl_expired); + } + + if (error.hasError(SslError.SSL_NOTYETVALID)) { + LinearLayout ll = (LinearLayout)factory + .inflate(R.layout.ssl_warning, placeholder); + ((TextView)ll.findViewById(R.id.warning)) + .setText(R.string.ssl_not_yet_valid); + } + + mSSLCertificateOnErrorHandler = handler; + mSSLCertificateOnErrorView = view; + mSSLCertificateOnErrorError = error; + mSSLCertificateOnErrorDialog = + new AlertDialog.Builder(mContext) + .setTitle(R.string.ssl_certificate).setIcon( + R.drawable.ic_dialog_browser_certificate_partially_secure) + .setView(certificateView) + .setPositiveButton(R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int whichButton) { + mSSLCertificateOnErrorDialog = null; + mSSLCertificateOnErrorView = null; + mSSLCertificateOnErrorHandler = null; + mSSLCertificateOnErrorError = null; + + view.getWebViewClient().onReceivedSslError( + view, handler, error); + } + }) + .setNeutralButton(R.string.page_info_view, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int whichButton) { + mSSLCertificateOnErrorDialog = null; + + // do not clear the dialog state: we will + // need to show the dialog again once the + // user is done exploring the page-info details + + showPageInfo(mController.getTabControl() + .getTabFromView(view), + true); + } + }) + .setOnCancelListener( + new DialogInterface.OnCancelListener() { + public void onCancel(DialogInterface dialog) { + mSSLCertificateOnErrorDialog = null; + mSSLCertificateOnErrorView = null; + mSSLCertificateOnErrorHandler = null; + mSSLCertificateOnErrorError = null; + + view.getWebViewClient().onReceivedSslError( + view, handler, error); + } + }) + .show(); + } + + /** + * Inflates the SSL certificate view (helper method). + * @param certificate The SSL certificate. + * @return The resultant certificate view with issued-to, issued-by, + * issued-on, expires-on, and possibly other fields set. + * If the input certificate is null, returns null. + */ + private View inflateCertificateView(SslCertificate certificate) { + if (certificate == null) { + return null; + } + + LayoutInflater factory = LayoutInflater.from(mContext); + + View certificateView = factory.inflate( + R.layout.ssl_certificate, null); + + // issued to: + SslCertificate.DName issuedTo = certificate.getIssuedTo(); + if (issuedTo != null) { + ((TextView) certificateView.findViewById(R.id.to_common)) + .setText(issuedTo.getCName()); + ((TextView) certificateView.findViewById(R.id.to_org)) + .setText(issuedTo.getOName()); + ((TextView) certificateView.findViewById(R.id.to_org_unit)) + .setText(issuedTo.getUName()); + } + + // issued by: + SslCertificate.DName issuedBy = certificate.getIssuedBy(); + if (issuedBy != null) { + ((TextView) certificateView.findViewById(R.id.by_common)) + .setText(issuedBy.getCName()); + ((TextView) certificateView.findViewById(R.id.by_org)) + .setText(issuedBy.getOName()); + ((TextView) certificateView.findViewById(R.id.by_org_unit)) + .setText(issuedBy.getUName()); + } + + // issued on: + String issuedOn = formatCertificateDate( + certificate.getValidNotBeforeDate()); + ((TextView) certificateView.findViewById(R.id.issued_on)) + .setText(issuedOn); + + // expires on: + String expiresOn = formatCertificateDate( + certificate.getValidNotAfterDate()); + ((TextView) certificateView.findViewById(R.id.expires_on)) + .setText(expiresOn); + + return certificateView; + } + + /** + * Formats the certificate date to a properly localized date string. + * @return Properly localized version of the certificate date string and + * the "" if it fails to localize. + */ + private String formatCertificateDate(Date certificateDate) { + if (certificateDate == null) { + return ""; + } + String formattedDate = DateFormat.getDateFormat(mContext) + .format(certificateDate); + if (formattedDate == null) { + return ""; + } + return formattedDate; + } + +} diff --git a/src/com/android/browser/PageProgressView.java b/src/com/android/browser/PageProgressView.java new file mode 100644 index 000000000..f512cefa1 --- /dev/null +++ b/src/com/android/browser/PageProgressView.java @@ -0,0 +1,117 @@ + +/* + * Copyright (C) 2010 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.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.widget.ImageView; + +/** + * + */ +public class PageProgressView extends ImageView { + + public static final int MAX_PROGRESS = 10000; + private static final int MSG_UPDATE = 42; + private static final int STEPS = 10; + private static final int DELAY = 40; + + private int mCurrentProgress; + private int mTargetProgress; + private int mIncrement; + private Rect mBounds; + private Handler mHandler; + + /** + * @param context + * @param attrs + * @param defStyle + */ + public PageProgressView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + /** + * @param context + * @param attrs + */ + public PageProgressView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + /** + * @param context + */ + public PageProgressView(Context context) { + super(context); + init(context); + } + + private void init(Context ctx) { + mBounds = new Rect(0,0,0,0); + mCurrentProgress = 0; + mTargetProgress = 0; + mHandler = new Handler() { + + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_UPDATE) { + mCurrentProgress = Math.min(mTargetProgress, + mCurrentProgress + mIncrement); + mBounds.right = getWidth() * mCurrentProgress / MAX_PROGRESS; + invalidate(); + if (mCurrentProgress < mTargetProgress) { + sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE), DELAY); + } + } + } + + }; + } + + @Override + public void onLayout(boolean f, int l, int t, int r, int b) { + mBounds.left = 0; + mBounds.right = (r - l) * mCurrentProgress / MAX_PROGRESS; + mBounds.top = 0; + mBounds.bottom = b-t; + } + + void setProgress(int progress) { + mCurrentProgress = mTargetProgress; + mTargetProgress = progress; + mIncrement = (mTargetProgress - mCurrentProgress) / STEPS; + mHandler.removeMessages(MSG_UPDATE); + mHandler.sendEmptyMessage(MSG_UPDATE); + } + + @Override + public void onDraw(Canvas canvas) { +// super.onDraw(canvas); + Drawable d = getDrawable(); + d.setBounds(mBounds); + d.draw(canvas); + } + +} diff --git a/src/com/android/browser/Performance.java b/src/com/android/browser/Performance.java new file mode 100644 index 000000000..e9ddfa263 --- /dev/null +++ b/src/com/android/browser/Performance.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2010 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.net.WebAddress; +import android.os.Debug; +import android.os.Process; +import android.os.SystemClock; +import android.util.Log; + +/** + * Performance analysis + */ +public class Performance { + + private static final String LOGTAG = "browser"; + + private final static boolean LOGD_ENABLED = + com.android.browser.Browser.LOGD_ENABLED; + + private static boolean mInTrace; + + // Performance probe + private static final int[] SYSTEM_CPU_FORMAT = new int[] { + Process.PROC_SPACE_TERM | Process.PROC_COMBINE, + Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 1: user time + Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 2: nice time + Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 3: sys time + Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 4: idle time + Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 5: iowait time + Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 6: irq time + Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG // 7: softirq time + }; + + private static long mStart; + private static long mProcessStart; + private static long mUserStart; + private static long mSystemStart; + private static long mIdleStart; + private static long mIrqStart; + + private static long mUiStart; + + static void tracePageStart(String url) { + if (BrowserSettings.getInstance().isTracing()) { + String host; + try { + WebAddress uri = new WebAddress(url); + host = uri.getHost(); + } catch (android.net.ParseException ex) { + host = "browser"; + } + host = host.replace('.', '_'); + host += ".trace"; + mInTrace = true; + Debug.startMethodTracing(host, 20 * 1024 * 1024); + } + } + + static void tracePageFinished() { + if (mInTrace) { + mInTrace = false; + Debug.stopMethodTracing(); + } + } + + static void onPageStarted() { + mStart = SystemClock.uptimeMillis(); + mProcessStart = Process.getElapsedCpuTime(); + long[] sysCpu = new long[7]; + if (Process.readProcFile("/proc/stat", SYSTEM_CPU_FORMAT, null, sysCpu, null)) { + mUserStart = sysCpu[0] + sysCpu[1]; + mSystemStart = sysCpu[2]; + mIdleStart = sysCpu[3]; + mIrqStart = sysCpu[4] + sysCpu[5] + sysCpu[6]; + } + mUiStart = SystemClock.currentThreadTimeMillis(); + } + + static void onPageFinished(String url) { + long[] sysCpu = new long[7]; + if (Process.readProcFile("/proc/stat", SYSTEM_CPU_FORMAT, null, sysCpu, null)) { + String uiInfo = + "UI thread used " + (SystemClock.currentThreadTimeMillis() - mUiStart) + " ms"; + if (LOGD_ENABLED) { + Log.d(LOGTAG, uiInfo); + } + // The string that gets written to the log + String performanceString = + "It took total " + (SystemClock.uptimeMillis() - mStart) + + " ms clock time to load the page." + "\nbrowser process used " + + (Process.getElapsedCpuTime() - mProcessStart) + + " ms, user processes used " + (sysCpu[0] + sysCpu[1] - mUserStart) + * 10 + " ms, kernel used " + (sysCpu[2] - mSystemStart) * 10 + + " ms, idle took " + (sysCpu[3] - mIdleStart) * 10 + + " ms and irq took " + (sysCpu[4] + sysCpu[5] + sysCpu[6] - mIrqStart) + * 10 + " ms, " + uiInfo; + if (LOGD_ENABLED) { + Log.d(LOGTAG, performanceString + "\nWebpage: " + url); + } + if (url != null) { + // strip the url to maintain consistency + String newUrl = new String(url); + if (newUrl.startsWith("http://www.")) { + newUrl = newUrl.substring(11); + } else if (newUrl.startsWith("http://")) { + newUrl = newUrl.substring(7); + } else if (newUrl.startsWith("https://www.")) { + newUrl = newUrl.substring(12); + } else if (newUrl.startsWith("https://")) { + newUrl = newUrl.substring(8); + } + if (LOGD_ENABLED) { + Log.d(LOGTAG, newUrl + " loaded"); + } + } + } + } +} diff --git a/src/com/android/browser/ScrollWebView.java b/src/com/android/browser/ScrollWebView.java new file mode 100644 index 000000000..97bd2c7a1 --- /dev/null +++ b/src/com/android/browser/ScrollWebView.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2010 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.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.webkit.WebView; + +import java.util.Map; + +/** + * Manage WebView scroll events + */ +public class ScrollWebView extends WebView { + + private ScrollListener mScrollListener; + private boolean mIsCancelled; + private Runnable mScrollRunnable; + + /** + * @param context + * @param attrs + * @param defStyle + * @param javascriptInterfaces + */ + public ScrollWebView(Context context, AttributeSet attrs, int defStyle, + Map<String, Object> javascriptInterfaces, boolean privateBrowsing) { + super(context, attrs, defStyle, javascriptInterfaces, privateBrowsing); + } + + /** + * @param context + * @param attrs + * @param defStyle + */ + public ScrollWebView(Context context, AttributeSet attrs, int defStyle, + boolean privateBrowsing) { + super(context, attrs, defStyle, privateBrowsing); + } + + /** + * @param context + * @param attrs + */ + public ScrollWebView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + /** + * @param context + */ + public ScrollWebView(Context context) { + super(context); + } + + void hideEmbeddedTitleBar() { + scrollBy(0, getVisibleTitleHeight()); + } + + @Override + public void setEmbeddedTitleBar(final View title) { + super.setEmbeddedTitleBar(title); + if (title != null && mScrollListener != null) { + // allow the scroll listener to initialize its state + post(new Runnable() { + @Override + public void run() { + mScrollListener.onScroll((title.getHeight() == 0 || + getVisibleTitleHeight() > 0)); + } + }); + } + } + + @Override + public void stopScroll() { + mIsCancelled = true; + super.stopScroll(); + } + + @Override + protected void onScrollChanged(int l, final int t, int ol, int ot) { + super.onScrollChanged(l, t, ol, ot); + if (!mIsCancelled) { + post(mScrollRunnable); + } else { + mIsCancelled = false; + } + } + + void setScrollListener(ScrollListener l) { + mScrollListener = l; + if (mScrollListener != null) { + mScrollRunnable = new Runnable() { + public void run() { + if (!mIsCancelled) { + mScrollListener.onScroll(getVisibleTitleHeight() > 0); + } + } + }; + } + } + + // callback for scroll events + + interface ScrollListener { + public void onScroll(boolean titleVisible); + } + +} diff --git a/src/com/android/browser/SelectDialog.java b/src/com/android/browser/SelectDialog.java deleted file mode 100644 index 461127a5e..000000000 --- a/src/com/android/browser/SelectDialog.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (C) 2010 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.provider.Browser; -import android.view.LayoutInflater; -import android.view.View; - -/* package */ class SelectDialog extends WebDialog { - private View mCopyButton; - private View mSelectAllButton; - private View mShareButton; - private View mFindButton; - - SelectDialog(BrowserActivity context) { - super(context); - LayoutInflater factory = LayoutInflater.from(context); - factory.inflate(R.layout.browser_select, this); - addCancel(); - - mCopyButton = findViewById(R.id.copy); - mCopyButton.setOnClickListener(mCopyListener); - mSelectAllButton = findViewById(R.id.select_all); - mSelectAllButton.setOnClickListener(mSelectAllListener); - mShareButton = findViewById(R.id.share); - mShareButton.setOnClickListener(mShareListener); - mFindButton = findViewById(R.id.find); - mFindButton.setOnClickListener(mFindListener); - } - - private View.OnClickListener mCopyListener = new View.OnClickListener() { - public void onClick(View v) { - mWebView.copySelection(); - mBrowserActivity.closeDialogs(); - } - }; - - private View.OnClickListener mSelectAllListener = new View.OnClickListener() { - public void onClick(View v) { - mWebView.selectAll(); - } - }; - - private View.OnClickListener mShareListener = new View.OnClickListener() { - public void onClick(View v) { - String selection = mWebView.getSelection(); - Browser.sendString(mBrowserActivity, selection); - mBrowserActivity.closeDialogs(); - } - }; - - private View.OnClickListener mFindListener = new View.OnClickListener() { - public void onClick(View v) { - String selection = mWebView.getSelection(); - mBrowserActivity.closeDialogs(); - mBrowserActivity.showFindDialog(); - mBrowserActivity.setFindDialogText(selection); - } - }; - - /** - * Called by BrowserActivity.closeDialog. Start the animation to hide - * the dialog, and inform the WebView that the dialog is being dismissed. - */ - @Override - public void dismiss() { - super.dismiss(); - mWebView.notifySelectDialogDismissed(); - } - -} diff --git a/src/com/android/browser/ShortcutActivity.java b/src/com/android/browser/ShortcutActivity.java new file mode 100644 index 000000000..57cb4a7f2 --- /dev/null +++ b/src/com/android/browser/ShortcutActivity.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2010 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.content.Intent; +import android.database.Cursor; +import android.os.Bundle; + +public class ShortcutActivity extends Activity + implements BookmarksPageCallbacks { + + private BrowserBookmarksPage mBookmarks; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // TODO: Is this needed? + setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); + mBookmarks = BrowserBookmarksPage.newInstance(this, null, null); + mBookmarks.setEnableContextMenu(false); + mBookmarks.setShowRootFolder(true); + getFragmentManager() + .openTransaction() + .add(android.R.id.content, mBookmarks) + .commit(); + } + + // BookmarksPageCallbacks + + @Override + public boolean onBookmarkSelected(Cursor c, boolean isFolder) { + if (isFolder) { + return false; + } + Intent intent = BrowserBookmarksPage.createShortcutIntent(this, c); + setResult(RESULT_OK, intent); + finish(); + return true; + } + + @Override + public boolean onOpenInNewWindow(Cursor c) { + return false; + } + + @Override + public void onBackPressed() { + if (!mBookmarks.onBackPressed()) { + super.onBackPressed(); + } + } +} diff --git a/src/com/android/browser/SuggestionsAdapter.java b/src/com/android/browser/SuggestionsAdapter.java new file mode 100644 index 000000000..8c0635360 --- /dev/null +++ b/src/com/android/browser/SuggestionsAdapter.java @@ -0,0 +1,623 @@ +/* + * Copyright (C) 2010 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 com.android.browser.search.SearchEngine; + +import android.app.SearchManager; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Handler; +import android.provider.BrowserContract; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Filter; +import android.widget.Filterable; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +/** + * adapter to wrap multiple cursors for url/search completions + */ +public class SuggestionsAdapter extends BaseAdapter implements Filterable, OnClickListener { + + static final int TYPE_BOOKMARK = 0; + static final int TYPE_SUGGEST_URL = 1; + static final int TYPE_HISTORY = 2; + static final int TYPE_SEARCH = 3; + static final int TYPE_SUGGEST = 4; + + private static final String[] COMBINED_PROJECTION = + {BrowserContract.Combined._ID, BrowserContract.Combined.TITLE, + BrowserContract.Combined.URL, BrowserContract.Combined.IS_BOOKMARK}; + + private static final String[] SEARCHES_PROJECTION = {BrowserContract.Searches.SEARCH}; + + private static final String COMBINED_SELECTION = + "(url LIKE ? OR url LIKE ? OR url LIKE ? OR url LIKE ? OR title LIKE ?)"; + + Context mContext; + Filter mFilter; + SuggestionResults mMixedResults; + List<SuggestItem> mSuggestResults, mFilterResults; + List<CursorSource> mSources; + boolean mLandscapeMode; + CompletionListener mListener; + int mLinesPortrait; + int mLinesLandscape; + Object mResultsLock = new Object(); + + interface CompletionListener { + + public void onSearch(String txt); + + public void onSelect(String txt, String extraData); + + public void onFilterComplete(int count); + + } + + public SuggestionsAdapter(Context ctx, CompletionListener listener) { + mContext = ctx; + mListener = listener; + mLinesPortrait = mContext.getResources(). + getInteger(R.integer.max_suggest_lines_portrait); + mLinesLandscape = mContext.getResources(). + getInteger(R.integer.max_suggest_lines_landscape); + mFilter = new SuggestFilter(); + addSource(new SearchesCursor()); + addSource(new CombinedCursor()); + } + + public void setLandscapeMode(boolean mode) { + mLandscapeMode = mode; + notifyDataSetChanged(); + } + + public int getLeftCount() { + return mMixedResults.getLeftCount(); + } + + public int getRightCount() { + return mMixedResults.getRightCount(); + } + + public void addSource(CursorSource c) { + if (mSources == null) { + mSources = new ArrayList<CursorSource>(5); + } + mSources.add(c); + } + + @Override + public void onClick(View v) { + if (R.id.icon2 == v.getId()) { + // replace input field text with suggestion text + SuggestItem item = (SuggestItem) ((View) v.getParent()).getTag(); + mListener.onSearch(item.title); + } else { + SuggestItem item = (SuggestItem) v.getTag(); + mListener.onSelect((TextUtils.isEmpty(item.url)? item.title : item.url), + item.extra); + } + } + + @Override + public Filter getFilter() { + return mFilter; + } + + @Override + public int getCount() { + return (mMixedResults == null) ? 0 : mMixedResults.getLineCount(); + } + + @Override + public SuggestItem getItem(int position) { + if (mMixedResults == null) { + return null; + } + if (mLandscapeMode) { + if (position >= mMixedResults.getLineCount()) { + // right column + position = position - mMixedResults.getLineCount(); + // index in column + if (position >= mMixedResults.getRightCount()) { + return null; + } + return mMixedResults.items.get(position + mMixedResults.getLeftCount()); + } else { + // left column + if (position >= mMixedResults.getLeftCount()) { + return null; + } + return mMixedResults.items.get(position); + } + } else { + return mMixedResults.items.get(position); + } + } + + @Override + public long getItemId(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final LayoutInflater inflater = LayoutInflater.from(mContext); + View view = convertView; + if (view == null) { + view = inflater.inflate(R.layout.suggestion_two_column, parent, false); + } + View s1 = view.findViewById(R.id.suggest1); + View s2 = view.findViewById(R.id.suggest2); + View div = view.findViewById(R.id.suggestion_divider); + if (mLandscapeMode) { + SuggestItem item = getItem(position); + div.setVisibility(View.VISIBLE); + if (item != null) { + s1.setVisibility(View.VISIBLE); + bindView(s1, item); + } else { + s1.setVisibility(View.INVISIBLE); + } + item = getItem(position + mMixedResults.getLineCount()); + if (item != null) { + s2.setVisibility(View.VISIBLE); + bindView(s2, item); + } else { + s2.setVisibility(View.INVISIBLE); + } + return view; + } else { + s1.setVisibility(View.VISIBLE); + div.setVisibility(View.GONE); + s2.setVisibility(View.GONE); + bindView(s1, getItem(position)); + return view; + } + } + + private void bindView(View view, SuggestItem item) { + // store item for click handling + view.setTag(item); + TextView tv1 = (TextView) view.findViewById(android.R.id.text1); + TextView tv2 = (TextView) view.findViewById(android.R.id.text2); + ImageView ic1 = (ImageView) view.findViewById(R.id.icon1); + View spacer = view.findViewById(R.id.spacer); + View ic2 = view.findViewById(R.id.icon2); + View div = view.findViewById(R.id.divider); + tv1.setText(item.title); + tv2.setText(item.url); + int id = -1; + switch (item.type) { + case TYPE_SUGGEST: + case TYPE_SEARCH: + id = R.drawable.ic_search_category_suggest; + break; + case TYPE_BOOKMARK: + id = R.drawable.ic_search_category_bookmark; + break; + case TYPE_HISTORY: + id = R.drawable.ic_search_category_history; + break; + case TYPE_SUGGEST_URL: + id = R.drawable.ic_search_category_browser; + break; + default: + id = -1; + } + if (id != -1) { + ic1.setImageDrawable(mContext.getResources().getDrawable(id)); + } + ic2.setVisibility(((TYPE_SUGGEST == item.type) || (TYPE_SEARCH == item.type)) + ? View.VISIBLE : View.GONE); + div.setVisibility(ic2.getVisibility()); + spacer.setVisibility(((TYPE_SUGGEST == item.type) || (TYPE_SEARCH == item.type)) + ? View.GONE : View.INVISIBLE); + view.setOnClickListener(this); + ic2.setOnClickListener(this); + } + + class SlowFilterTask extends AsyncTask<CharSequence, Void, List<SuggestItem>> { + + @Override + protected List<SuggestItem> doInBackground(CharSequence... params) { + SuggestCursor cursor = new SuggestCursor(); + cursor.runQuery(params[0]); + List<SuggestItem> results = new ArrayList<SuggestItem>(); + int count = cursor.getCount(); + for (int i = 0; i < count; i++) { + results.add(cursor.getItem()); + cursor.moveToNext(); + } + cursor.close(); + return results; + } + + @Override + protected void onPostExecute(List<SuggestItem> items) { + mSuggestResults = items; + mMixedResults = buildSuggestionResults(); + notifyDataSetChanged(); + mListener.onFilterComplete(mMixedResults.getLineCount()); + } + } + + SuggestionResults buildSuggestionResults() { + SuggestionResults mixed = new SuggestionResults(); + List<SuggestItem> filter, suggest; + synchronized (mResultsLock) { + filter = mFilterResults; + suggest = mSuggestResults; + } + if (filter != null) { + for (SuggestItem item : filter) { + mixed.addResult(item); + } + } + if (suggest != null) { + for (SuggestItem item : suggest) { + mixed.addResult(item); + } + } + return mixed; + } + + class SuggestFilter extends Filter { + + @Override + public CharSequence convertResultToString(Object item) { + if (item == null) { + return ""; + } + SuggestItem sitem = (SuggestItem) item; + if (sitem.title != null) { + return sitem.title; + } else { + return sitem.url; + } + } + + void startSuggestionsAsync(final CharSequence constraint) { + new SlowFilterTask().execute(constraint); + } + + @Override + protected FilterResults performFiltering(CharSequence constraint) { + FilterResults res = new FilterResults(); + if (TextUtils.isEmpty(constraint)) { + res.count = 0; + res.values = null; + return res; + } + startSuggestionsAsync(constraint); + List<SuggestItem> filterResults = new ArrayList<SuggestItem>(); + if (constraint != null) { + for (CursorSource sc : mSources) { + sc.runQuery(constraint); + } + mixResults(filterResults); + } + synchronized (mResultsLock) { + mFilterResults = filterResults; + } + SuggestionResults mixed = buildSuggestionResults(); + res.count = mixed.getLineCount(); + res.values = mixed; + return res; + } + + void mixResults(List<SuggestItem> results) { + int maxLines = mLandscapeMode ? mLinesLandscape : (mLinesPortrait / 2); + for (int i = 0; i < mSources.size(); i++) { + CursorSource s = mSources.get(i); + int n = Math.min(s.getCount(), maxLines); + maxLines -= n; + boolean more = false; + for (int j = 0; j < n; j++) { + results.add(s.getItem()); + more = s.moveToNext(); + } + } + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults fresults) { + mMixedResults = (SuggestionResults) fresults.values; + mListener.onFilterComplete(fresults.count); + notifyDataSetChanged(); + } + + } + + /** + * sorted list of results of a suggestion query + * + */ + class SuggestionResults { + + ArrayList<SuggestItem> items; + // count per type + int[] counts; + + SuggestionResults() { + items = new ArrayList<SuggestItem>(24); + // n of types: + counts = new int[5]; + } + + int getTypeCount(int type) { + return counts[type]; + } + + void addResult(SuggestItem item) { + int ix = 0; + while ((ix < items.size()) && (item.type >= items.get(ix).type)) + ix++; + items.add(ix, item); + counts[item.type]++; + } + + int getLineCount() { + if (mLandscapeMode) { + return Math.min(mLinesLandscape, + Math.max(getLeftCount(), getRightCount())); + } else { + return Math.min(mLinesPortrait, getLeftCount() + getRightCount()); + } + } + + int getLeftCount() { + return counts[TYPE_BOOKMARK] + counts[TYPE_HISTORY] + counts[TYPE_SUGGEST_URL]; + } + + int getRightCount() { + return counts[TYPE_SEARCH] + counts[TYPE_SUGGEST]; + } + + @Override + public String toString() { + if (items == null) return null; + if (items.size() == 0) return "[]"; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < items.size(); i++) { + SuggestItem item = items.get(i); + sb.append(item.type + ": " + item.title); + if (i < items.size() - 1) { + sb.append(", "); + } + } + return sb.toString(); + } + } + + /** + * data object to hold suggestion values + */ + class SuggestItem { + String title; + String url; + int type; + String extra; + + public SuggestItem(String text, String u, int t) { + title = text; + url = u; + type = t; + } + } + + abstract class CursorSource { + + Cursor mCursor; + + boolean moveToNext() { + return mCursor.moveToNext(); + } + + public abstract void runQuery(CharSequence constraint); + + public abstract SuggestItem getItem(); + + public int getCount() { + return (mCursor != null) ? mCursor.getCount() : 0; + } + + public void close() { + if (mCursor != null) { + mCursor.close(); + } + } + } + + /** + * combined bookmark & history source + */ + class CombinedCursor extends CursorSource { + + @Override + public SuggestItem getItem() { + if ((mCursor != null) && (!mCursor.isAfterLast())) { + String title = mCursor.getString(1); + String url = mCursor.getString(2); + boolean isBookmark = (mCursor.getInt(3) == 1); + return new SuggestItem(getTitle(title, url), getUrl(title, url), + isBookmark ? TYPE_BOOKMARK : TYPE_HISTORY); + } + return null; + } + + @Override + public void runQuery(CharSequence constraint) { + // constraint != null + if (mCursor != null) { + mCursor.close(); + } + String like = constraint + "%"; + String[] args = null; + String selection = null; + if (like.startsWith("http") || like.startsWith("file")) { + args = new String[1]; + args[0] = like; + selection = "url LIKE ?"; + } else { + args = new String[5]; + args[0] = "http://" + like; + args[1] = "http://www." + like; + args[2] = "https://" + like; + args[3] = "https://www." + like; + // To match against titles. + args[4] = like; + selection = COMBINED_SELECTION; + } + Uri.Builder ub = BrowserContract.Combined.CONTENT_URI.buildUpon(); + ub.appendQueryParameter(BrowserContract.PARAM_LIMIT, + Integer.toString(mLinesPortrait)); + mCursor = + mContext.getContentResolver().query(ub.build(), COMBINED_PROJECTION, + selection, + (constraint != null) ? args : null, + BrowserContract.Combined.VISITS + " DESC, " + + BrowserContract.Combined.DATE_LAST_VISITED + " DESC"); + if (mCursor != null) { + mCursor.moveToFirst(); + } + } + + /** + * Provides the title (text line 1) for a browser suggestion, which should be the + * webpage title. If the webpage title is empty, returns the stripped url instead. + * + * @return the title string to use + */ + private String getTitle(String title, String url) { + if (TextUtils.isEmpty(title) || TextUtils.getTrimmedLength(title) == 0) { + title = UrlUtils.stripUrl(url); + } + return title; + } + + /** + * Provides the subtitle (text line 2) for a browser suggestion, which should be the + * webpage url. If the webpage title is empty, then the url should go in the title + * instead, and the subtitle should be empty, so this would return null. + * + * @return the subtitle string to use, or null if none + */ + private String getUrl(String title, String url) { + if (TextUtils.isEmpty(title) + || TextUtils.getTrimmedLength(title) == 0 + || title.equals(url)) { + return null; + } else { + return UrlUtils.stripUrl(url); + } + } + } + + class SearchesCursor extends CursorSource { + + @Override + public SuggestItem getItem() { + if ((mCursor != null) && (!mCursor.isAfterLast())) { + return new SuggestItem(mCursor.getString(0), null, TYPE_SEARCH); + } + return null; + } + + @Override + public void runQuery(CharSequence constraint) { + // constraint != null + if (mCursor != null) { + mCursor.close(); + } + String like = constraint + "%"; + String[] args = new String[] {like}; + String selection = BrowserContract.Searches.SEARCH + " LIKE ?"; + Uri.Builder ub = BrowserContract.Searches.CONTENT_URI.buildUpon(); + ub.appendQueryParameter(BrowserContract.PARAM_LIMIT, + Integer.toString(mLinesPortrait)); + mCursor = + mContext.getContentResolver().query(ub.build(), SEARCHES_PROJECTION, + selection, + args, BrowserContract.Searches.DATE + " DESC"); + if (mCursor != null) { + mCursor.moveToFirst(); + } + } + + } + + class SuggestCursor extends CursorSource { + + @Override + public SuggestItem getItem() { + if (mCursor != null) { + String title = mCursor.getString( + mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1)); + String text2 = mCursor.getString( + mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2)); + String url = mCursor.getString( + mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL)); + String uri = mCursor.getString( + mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA)); + int type = (TextUtils.isEmpty(url)) ? TYPE_SUGGEST : TYPE_SUGGEST_URL; + SuggestItem item = new SuggestItem(title, url, type); + item.extra = mCursor.getString( + mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA)); + return item; + } + return null; + } + + @Override + public void runQuery(CharSequence constraint) { + if (mCursor != null) { + mCursor.close(); + } + if (!TextUtils.isEmpty(constraint)) { + SearchEngine searchEngine = BrowserSettings.getInstance().getSearchEngine(); + if (searchEngine != null && searchEngine.supportsSuggestions()) { + mCursor = searchEngine.getSuggestions(mContext, constraint.toString()); + if (mCursor != null) { + mCursor.moveToFirst(); + } + } + } else { + mCursor = null; + } + } + + } + + public void clearCache() { + mFilterResults = null; + mSuggestResults = null; + } + +} diff --git a/src/com/android/browser/Tab.java b/src/com/android/browser/Tab.java index 7019c8a01..a048c2da9 100644 --- a/src/com/android/browser/Tab.java +++ b/src/com/android/browser/Tab.java @@ -16,42 +16,28 @@ package com.android.browser; -import java.io.File; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.Map; -import java.util.Vector; +import com.android.common.speech.LoggingEvents; +import android.app.Activity; import android.app.AlertDialog; import android.app.SearchManager; import android.content.ContentResolver; -import android.content.ContentValues; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.Intent; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteException; import android.graphics.Bitmap; import android.net.Uri; import android.net.http.SslError; -import android.os.AsyncTask; import android.os.Bundle; import android.os.Message; import android.os.SystemClock; -import android.provider.Browser; import android.speech.RecognizerResultsIntent; import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; import android.view.ViewStub; -import android.view.View.OnClickListener; import android.webkit.ConsoleMessage; -import android.webkit.CookieSyncManager; import android.webkit.DownloadListener; import android.webkit.GeolocationPermissions; import android.webkit.HttpAuthHandler; @@ -62,21 +48,26 @@ import android.webkit.WebBackForwardList; import android.webkit.WebBackForwardListClient; import android.webkit.WebChromeClient; import android.webkit.WebHistoryItem; -import android.webkit.WebIconDatabase; import android.webkit.WebStorage; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.FrameLayout; -import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.TextView; +import android.widget.Toast; -import com.android.common.speech.LoggingEvents; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import java.util.Vector; /** * Class for maintaining Tabs with a main WebView and a subwindow. */ class Tab { + // Log Tag private static final String LOGTAG = "Tab"; // Special case the logtag for messages for the Console to make it easier to @@ -84,6 +75,13 @@ class Tab { // of the browser. private static final String CONSOLE_LOGTAG = "browser"; + final static int LOCK_ICON_UNSECURE = 0; + final static int LOCK_ICON_SECURE = 1; + final static int LOCK_ICON_MIXED = 2; + + Activity mActivity; + private WebViewController mWebViewController; + // The Geolocation permissions prompt private GeolocationPermissionsPrompt mGeolocationPermissionsPrompt; // Main WebView wrapper @@ -110,8 +108,9 @@ class Tab { private boolean mCloseOnExit; // If true, the tab is in the foreground of the current activity. private boolean mInForeground; - // If true, the tab is in loading state. - private boolean mInLoad; + // If true, the tab is in page loading state (after onPageStarted, + // before onPageFinsihed) + private boolean mInPageLoad; // The time the load started, used to find load page time private long mLoadStartTime; // Application identifier used to find tabs that another application wants @@ -120,6 +119,10 @@ class Tab { // Keep the original url around to avoid killing the old WebView if the url // has not changed. private String mOriginalUrl; + // Hold on to the currently loaded url + private String mCurrentUrl; + //The currently loaded title + private String mCurrentTitle; // Error console for the tab private ErrorConsoleView mErrorConsole; // the lock icon type and previous lock icon type for the tab @@ -127,8 +130,6 @@ class Tab { private int mPrevLockIconType; // Inflation service for making subwindows. private final LayoutInflater mInflateService; - // The BrowserActivity which owners the Tab - private final BrowserActivity mActivity; // The listener that gets invoked when a download is started from the // mMainView private final DownloadListener mDownloadListener; @@ -155,6 +156,7 @@ class Tab { static final String PARENTTAB = "parentTab"; static final String APPID = "appid"; static final String ORIGINALURL = "originalUrl"; + static final String INCOGNITO = "privateBrowsingEnabled"; // ------------------------------------------------------------------------- @@ -170,10 +172,11 @@ class Tab { if (mVoiceSearchData != null) { mVoiceSearchData = null; if (mInForeground) { - mActivity.revertVoiceTitleBar(); + mWebViewController.revertVoiceSearchMode(this); } } } + /** * Return whether the tab is in voice search mode. */ @@ -271,7 +274,7 @@ class Tab { mVoiceSearchData.mLastVoiceSearchTitle = mVoiceSearchData.mVoiceSearchResults.get(index); if (mInForeground) { - mActivity.showVoiceTitleBar(mVoiceSearchData.mLastVoiceSearchTitle); + mWebViewController.activateVoiceSearchMode(mVoiceSearchData.mLastVoiceSearchTitle); } if (mVoiceSearchData.mVoiceSearchHtmls != null) { // When index was found it was already ensured that it was valid @@ -298,7 +301,7 @@ class Tab { mVoiceSearchData.mLastVoiceSearchUrl = mVoiceSearchData.mVoiceSearchUrls.get(index); if (null == mVoiceSearchData.mLastVoiceSearchUrl) { - mVoiceSearchData.mLastVoiceSearchUrl = mActivity.smartUrlFilter( + mVoiceSearchData.mLastVoiceSearchUrl = UrlUtils.smartUrlFilter( mVoiceSearchData.mLastVoiceSearchTitle); } Map<String, String> headers = null; @@ -391,7 +394,7 @@ class Tab { mDescription = desc; mError = error; } - }; + } private void processNextError() { if (mQueuedErrors == null) { @@ -458,7 +461,7 @@ class Tab { private Message mResend; @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { - mInLoad = true; + mInPageLoad = true; mLoadStartTime = SystemClock.uptimeMillis(); if (mVoiceSearchData != null && !url.equals(mVoiceSearchData.mLastVoiceSearchUrl)) { @@ -470,12 +473,6 @@ class Tab { revertVoiceSearchMode(); } - // We've started to load a new page. If there was a pending message - // to save a screenshot then we will now take the new page and save - // an incorrect screenshot. Therefore, remove any pending thumbnail - // messages from the queue. - mActivity.removeMessages(BrowserActivity.UPDATE_BOOKMARK_THUMBNAIL, - view); // If we start a touch icon load and then load a new page, we don't // want to cancel the current touch icon loader. But, we do want to @@ -488,49 +485,23 @@ class Tab { // reset the error console if (mErrorConsole != null) { mErrorConsole.clearErrorMessages(); - if (mActivity.shouldShowErrorConsole()) { + if (mWebViewController.shouldShowErrorConsole()) { mErrorConsole.showConsole(ErrorConsoleView.SHOW_NONE); } } - // update the bookmark database for favicon - if (favicon != null) { - BrowserBookmarksAdapter.updateBookmarkFavicon(mActivity - .getContentResolver(), null, url, favicon); - } - - // reset sync timer to avoid sync starts during loading a page - CookieSyncManager.getInstance().resetSync(); - - if (!mActivity.isNetworkUp()) { - view.setNetworkAvailable(false); - } // finally update the UI in the activity if it is in the foreground - if (mInForeground) { - mActivity.onPageStarted(view, url, favicon); - } + mWebViewController.onPageStarted(Tab.this, view, url, favicon); } @Override public void onPageFinished(WebView view, String url) { LogTag.logPageFinishedLoading( url, SystemClock.uptimeMillis() - mLoadStartTime); - mInLoad = false; - - if (mInForeground && !mActivity.didUserStopLoading() - || !mInForeground) { - // Only update the bookmark screenshot if the user did not - // cancel the load early. - mActivity.postMessage( - BrowserActivity.UPDATE_BOOKMARK_THUMBNAIL, 0, 0, view, - 500); - } + mInPageLoad = false; - // finally update the UI in the activity if it is in the foreground - if (mInForeground) { - mActivity.onPageFinished(view, url); - } + mWebViewController.onPageFinished(Tab.this, url); } // return true if want to hijack the url to let another app to handle it @@ -549,7 +520,7 @@ class Tab { mActivity.sendBroadcast(logIntent); } if (mInForeground) { - return mActivity.shouldOverrideUrlLoading(view, url); + return mWebViewController.shouldOverrideUrlLoading(view, url); } else { return false; } @@ -566,11 +537,11 @@ class Tab { if (url != null && url.length() > 0) { // It is only if the page claims to be secure that we may have // to update the lock: - if (mLockIconType == BrowserActivity.LOCK_ICON_SECURE) { + if (mLockIconType == LOCK_ICON_SECURE) { // If NOT a 'safe' url, change the lock to mixed content! if (!(URLUtil.isHttpsUrl(url) || URLUtil.isDataUrl(url) || URLUtil.isAboutUrl(url))) { - mLockIconType = BrowserActivity.LOCK_ICON_MIXED; + mLockIconType = LOCK_ICON_MIXED; } } } @@ -590,12 +561,16 @@ class Tab { errorCode != WebViewClient.ERROR_FILE) { queueError(errorCode, description); } - Log.e(LOGTAG, "onReceivedError " + errorCode + " " + failingUrl - + " " + description); + + // Don't log URLs when in private browsing mode + if (!isPrivateBrowsingEnabled()) { + Log.e(LOGTAG, "onReceivedError " + errorCode + " " + failingUrl + + " " + description); + } // We need to reset the title after an error if it is in foreground. if (mInForeground) { - mActivity.resetTitleAndRevertLockIcon(); + mWebViewController.resetTitleAndRevertLockIcon(Tab.this); } } @@ -661,31 +636,7 @@ class Tab { @Override public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) { - if (url.regionMatches(true, 0, "about:", 0, 6)) { - return; - } - // remove "client" before updating it to the history so that it wont - // show up in the auto-complete list. - int index = url.indexOf("client=ms-"); - if (index > 0 && url.contains(".google.")) { - int end = url.indexOf('&', index); - if (end > 0) { - url = url.substring(0, index) - .concat(url.substring(end + 1)); - } else { - // the url.charAt(index-1) should be either '?' or '&' - url = url.substring(0, index-1); - } - } - final ContentResolver cr = mActivity.getContentResolver(); - final String newUrl = url; - new AsyncTask<Void, Void, Void>() { - protected Void doInBackground(Void... unused) { - Browser.updateVisitedHistory(cr, newUrl, true); - return null; - } - }.execute(); - WebIconDatabase.getInstance().retainIconForPageUrl(url); + mWebViewController.doUpdateVisitedHistory(Tab.this, url, isReload); } /** @@ -751,21 +702,21 @@ class Tab { new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { - mActivity.showSSLCertificateOnError(view, + mWebViewController.showSslCertificateOnError(view, handler, error); } - }).setNegativeButton(R.string.cancel, + }).setNegativeButton(R.string.ssl_go_back, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { handler.cancel(); - mActivity.resetTitleAndRevertLockIcon(); + mWebViewController.resetTitleAndRevertLockIcon(Tab.this); } }).setOnCancelListener( new DialogInterface.OnCancelListener() { public void onCancel(DialogInterface dialog) { handler.cancel(); - mActivity.resetTitleAndRevertLockIcon(); + mWebViewController.resetTitleAndRevertLockIcon(Tab.this); } }).show(); } else { @@ -784,31 +735,7 @@ class Tab { public void onReceivedHttpAuthRequest(WebView view, final HttpAuthHandler handler, final String host, final String realm) { - String username = null; - String password = null; - - boolean reuseHttpAuthUsernamePassword = handler - .useHttpAuthUsernamePassword(); - - if (reuseHttpAuthUsernamePassword && view != null) { - String[] credentials = view.getHttpAuthUsernamePassword( - host, realm); - if (credentials != null && credentials.length == 2) { - username = credentials[0]; - password = credentials[1]; - } - } - - if (username != null && password != null) { - handler.proceed(username, password); - } else { - if (mInForeground) { - mActivity.showHttpAuthentication(handler, host, realm, - null, null, null, 0); - } else { - handler.cancel(); - } - } + mWebViewController.onReceivedHttpAuthRequest(Tab.this, view, handler, host, realm); } @Override @@ -816,25 +743,15 @@ class Tab { if (!mInForeground) { return false; } - if (mActivity.isMenuDown()) { - // only check shortcut key when MENU is held - return mActivity.getWindow().isShortcutKey(event.getKeyCode(), - event); - } else { - return false; - } + return mWebViewController.shouldOverrideKeyEvent(event); } @Override public void onUnhandledKeyEvent(WebView view, KeyEvent event) { - if (!mInForeground || mActivity.mActivityInPause) { + if (!mInForeground) { return; } - if (event.isDown()) { - mActivity.onKeyDown(event.getKeyCode(), event); - } else { - mActivity.onKeyUp(event.getKeyCode(), event); - } + mWebViewController.onUnhandledKeyEvent(event); } }; @@ -849,11 +766,11 @@ class Tab { (WebView.WebViewTransport) msg.obj; if (dialog) { createSubWindow(); - mActivity.attachSubWindow(Tab.this); + mWebViewController.attachSubWindow(Tab.this); transport.setWebView(mSubView); } else { - final Tab newTab = mActivity.openTabAndShow( - BrowserActivity.EMPTY_URL_DATA, false, null); + final Tab newTab = mWebViewController.openTabAndShow( + IntentHandler.EMPTY_URL_DATA, false, null); if (newTab != Tab.this) { Tab.this.addChildTab(newTab); } @@ -878,7 +795,7 @@ class Tab { .setPositiveButton(R.string.ok, null) .show(); return false; - } else if (!mActivity.getTabControl().canCreateNewTab()) { + } else if (!mWebViewController.getTabControl().canCreateNewTab()) { new AlertDialog.Builder(mActivity) .setTitle(R.string.too_many_windows_dialog_title) .setIcon(android.R.drawable.ic_dialog_alert) @@ -930,7 +847,7 @@ class Tab { @Override public void onRequestFocus(WebView view) { if (!mInForeground) { - mActivity.switchToTab(mActivity.getTabControl().getTabIndex( + mWebViewController.switchToTab(mWebViewController.getTabControl().getTabIndex( Tab.this)); } } @@ -940,94 +857,26 @@ class Tab { if (mParentTab != null) { // JavaScript can only close popup window. if (mInForeground) { - mActivity.switchToTab(mActivity.getTabControl() + mWebViewController.switchToTab(mWebViewController.getTabControl() .getTabIndex(mParentTab)); } - mActivity.closeTab(Tab.this); + mWebViewController.closeTab(Tab.this); } } @Override public void onProgressChanged(WebView view, int newProgress) { - if (newProgress == 100) { - // sync cookies and cache promptly here. - CookieSyncManager.getInstance().sync(); - } - if (mInForeground) { - mActivity.onProgressChanged(view, newProgress); - } + mWebViewController.onProgressChanged(Tab.this, newProgress); } @Override public void onReceivedTitle(WebView view, final String title) { - final String pageUrl = view.getUrl(); - if (mInForeground) { - // here, if url is null, we want to reset the title - mActivity.setUrlTitle(pageUrl, title); - } - if (pageUrl == null || pageUrl.length() - >= SQLiteDatabase.SQLITE_MAX_LIKE_PATTERN_LENGTH) { - return; - } - new AsyncTask<Void, Void, Void>() { - protected Void doInBackground(Void... unused) { - // See if we can find the current url in our history - // database and add the new title to it. - String url = pageUrl; - if (url.startsWith("http://www.")) { - url = url.substring(11); - } else if (url.startsWith("http://")) { - url = url.substring(4); - } - // Escape wildcards for LIKE operator. - url = url.replace("\\", "\\\\").replace("%", "\\%") - .replace("_", "\\_"); - Cursor c = null; - try { - final ContentResolver cr - = mActivity.getContentResolver(); - url = "%" + url; - String [] selArgs = new String[] { url }; - String where = Browser.BookmarkColumns.URL - + " LIKE ? ESCAPE '\\' AND " - + Browser.BookmarkColumns.BOOKMARK + " = 0"; - c = cr.query(Browser.BOOKMARKS_URI, new String[] - { Browser.BookmarkColumns._ID }, where, selArgs, - null); - if (c.moveToFirst()) { - // Current implementation of database only has one - // entry per url. - ContentValues map = new ContentValues(); - map.put(Browser.BookmarkColumns.TITLE, title); - String[] projection = new String[] - { Integer.valueOf(c.getInt(0)).toString() }; - cr.update(Browser.BOOKMARKS_URI, map, "_id = ?", - projection); - } - } catch (IllegalStateException e) { - Log.e(LOGTAG, "Tab onReceived title", e); - } catch (SQLiteException ex) { - Log.e(LOGTAG, - "onReceivedTitle() caught SQLiteException: ", - ex); - } finally { - if (c != null) c.close(); - } - return null; - } - }.execute(); + mWebViewController.onReceivedTitle(Tab.this, title); } @Override public void onReceivedIcon(WebView view, Bitmap icon) { - if (icon != null) { - BrowserBookmarksAdapter.updateBookmarkFavicon(mActivity - .getContentResolver(), view.getOriginalUrl(), view - .getUrl(), icon); - } - if (mInForeground) { - mActivity.setFavicon(icon); - } + mWebViewController.onFavicon(Tab.this, view, icon); } @Override @@ -1042,30 +891,22 @@ class Tab { } // Have only one async task at a time. if (mTouchIconLoader == null) { - mTouchIconLoader = new DownloadTouchIcon(Tab.this, mActivity, cr, view); + mTouchIconLoader = new DownloadTouchIcon(Tab.this, + mActivity, cr, view); mTouchIconLoader.execute(url); } } @Override - public void onSelectionDone(WebView view) { - if (mInForeground) mActivity.closeDialogs(); - } - - @Override - public void onSelectionStart(WebView view) { - if (false && mInForeground) mActivity.showSelectDialog(); - } - - @Override public void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) { - if (mInForeground) mActivity.onShowCustomView(view, callback); + if (mInForeground) mWebViewController.showCustomView(Tab.this, view, + callback); } @Override public void onHideCustomView() { - if (mInForeground) mActivity.onHideCustomView(); + if (mInForeground) mWebViewController.hideCustomView(); } /** @@ -1144,12 +985,16 @@ class Tab { // call getErrorConsole(true) so it will create one if needed ErrorConsoleView errorConsole = getErrorConsole(true); errorConsole.addErrorMessage(consoleMessage); - if (mActivity.shouldShowErrorConsole() - && errorConsole.getShowState() != ErrorConsoleView.SHOW_MAXIMIZED) { + if (mWebViewController.shouldShowErrorConsole() + && errorConsole.getShowState() != + ErrorConsoleView.SHOW_MAXIMIZED) { errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED); } } + // Don't log console messages in private browsing mode + if (isPrivateBrowsingEnabled()) return true; + String message = "Console: " + consoleMessage.message() + " " + consoleMessage.sourceId() + ":" + consoleMessage.lineNumber(); @@ -1183,7 +1028,7 @@ class Tab { @Override public Bitmap getDefaultVideoPoster() { if (mInForeground) { - return mActivity.getDefaultVideoPoster(); + return mWebViewController.getDefaultVideoPoster(); } return null; } @@ -1196,15 +1041,15 @@ class Tab { @Override public View getVideoLoadingProgressView() { if (mInForeground) { - return mActivity.getVideoLoadingProgressView(); + return mWebViewController.getVideoLoadingProgressView(); } return null; } @Override - public void openFileChooser(ValueCallback<Uri> uploadMsg) { + public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) { if (mInForeground) { - mActivity.openFileChooser(uploadMsg); + mWebViewController.openFileChooser(uploadMsg, acceptType); } else { uploadMsg.onReceiveValue(null); } @@ -1215,17 +1060,37 @@ class Tab { */ @Override public void getVisitedHistory(final ValueCallback<String[]> callback) { - AsyncTask<Void, Void, String[]> task = new AsyncTask<Void, Void, String[]>() { - public String[] doInBackground(Void... unused) { - return Browser.getVisitedHistory(mActivity - .getContentResolver()); - } - public void onPostExecute(String[] result) { - callback.onReceiveValue(result); - }; - }; - task.execute(); - }; + mWebViewController.getVisitedHistory(callback); + } + + @Override + public void setupAutoFill(Message message) { + // Prompt the user to set up their profile. + final Message msg = message; + AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); + builder.setMessage(R.string.autofill_setup_dialog_message) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + // Take user to the AutoFill profile editor. When they return, + // we will send the message that we pass here which will trigger + // the form to get filled out with their new profile. + mWebViewController.setupAutoFill(msg); + } + }) + .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + // Disable autofill and show a toast with how to turn it on again. + BrowserSettings s = BrowserSettings.getInstance(); + s.addObserver(mMainView.getSettings()); + s.disableAutoFill(mActivity); + s.update(); + Toast.makeText(mActivity, R.string.autofill_setup_dialog_negative_toast, + Toast.LENGTH_LONG).show(); + } + }).show(); + } }; // ------------------------------------------------------------------------- @@ -1237,18 +1102,18 @@ class Tab { private static class SubWindowClient extends WebViewClient { // The main WebViewClient. private final WebViewClient mClient; - private final BrowserActivity mBrowserActivity; + private final WebViewController mController; - SubWindowClient(WebViewClient client, BrowserActivity activity) { + SubWindowClient(WebViewClient client, WebViewController controller) { mClient = client; - mBrowserActivity = activity; + mController = controller; } @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { // Unlike the others, do not call mClient's version, which would // change the progress bar. However, we do want to remove the // find or select dialog. - mBrowserActivity.closeDialogs(); + mController.endActionMode(); } @Override public void doUpdateVisitedHistory(WebView view, String url, @@ -1316,25 +1181,29 @@ class Tab { if (window != mSubView) { Log.e(LOGTAG, "Can't close the window"); } - mActivity.dismissSubWindow(Tab.this); + mWebViewController.dismissSubWindow(Tab.this); } } // ------------------------------------------------------------------------- + // TODO temporarily use activity here + // remove later + // Construct a new tab - Tab(BrowserActivity activity, WebView w, boolean closeOnExit, String appId, + Tab(WebViewController wvcontroller, WebView w, boolean closeOnExit, String appId, String url) { - mActivity = activity; + mWebViewController = wvcontroller; + mActivity = mWebViewController.getActivity(); mCloseOnExit = closeOnExit; mAppId = appId; mOriginalUrl = url; - mLockIconType = BrowserActivity.LOCK_ICON_UNSECURE; - mPrevLockIconType = BrowserActivity.LOCK_ICON_UNSECURE; - mInLoad = false; + mLockIconType = LOCK_ICON_UNSECURE; + mPrevLockIconType = LOCK_ICON_UNSECURE; + mInPageLoad = false; mInForeground = false; - mInflateService = LayoutInflater.from(activity); + mInflateService = LayoutInflater.from(mActivity); // The tab consists of a container view, which contains the main // WebView, as well as any other UI elements associated with the tab. @@ -1344,20 +1213,8 @@ class Tab { public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) { - mActivity.onDownloadStart(url, userAgent, contentDisposition, + mWebViewController.onDownloadStart(Tab.this, url, userAgent, contentDisposition, mimetype, contentLength); - if (mMainView.copyBackForwardList().getSize() == 0) { - // This Tab was opened for the sole purpose of downloading a - // file. Remove it. - if (mActivity.getTabControl().getCurrentWebView() - == mMainView) { - // In this case, the Tab is still on top. - mActivity.goBackOnePageOrQuit(); - } else { - // In this case, it is not. - mActivity.closeTab(Tab.this); - } - } } }; mWebBackForwardListClient = new WebBackForwardListClient() { @@ -1449,17 +1306,9 @@ class Tab { */ boolean createSubWindow() { if (mSubView == null) { - mActivity.closeDialogs(); - mSubViewContainer = mInflateService.inflate( - R.layout.browser_subwindow, null); - mSubView = (WebView) mSubViewContainer.findViewById(R.id.webview); - mSubView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); - // use trackball directly - mSubView.setMapTrackballToArrowKeys(false); - // Enable the built-in zoom - mSubView.getSettings().setBuiltInZoomControls(true); + mWebViewController.createSubWindow(this); mSubView.setWebViewClient(new SubWindowClient(mWebViewClient, - mActivity)); + mWebViewController)); mSubView.setWebChromeClient(new SubWindowChromeClient( mWebChromeClient)); // Set a different DownloadListener for the mSubView, since it will @@ -1468,25 +1317,16 @@ class Tab { public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) { - mActivity.onDownloadStart(url, userAgent, + mWebViewController.onDownloadStart(Tab.this, url, userAgent, contentDisposition, mimetype, contentLength); if (mSubView.copyBackForwardList().getSize() == 0) { // This subwindow was opened for the sole purpose of // downloading a file. Remove it. - mActivity.dismissSubWindow(Tab.this); + mWebViewController.dismissSubWindow(Tab.this); } } }); mSubView.setOnCreateContextMenuListener(mActivity); - final BrowserSettings s = BrowserSettings.getInstance(); - s.addObserver(mSubView.getSettings()).update(s, null); - final ImageButton cancel = (ImageButton) mSubViewContainer - .findViewById(R.id.subwindow_close); - cancel.setOnClickListener(new OnClickListener() { - public void onClick(View v) { - mSubView.getWebChromeClient().onCloseWindow(mSubView); - } - }); return true; } return false; @@ -1497,7 +1337,7 @@ class Tab { */ void dismissSubWindow() { if (mSubView != null) { - mActivity.closeDialogs(); + mWebViewController.endActionMode(); BrowserSettings.getInstance().deleteObserver( mSubView.getSettings()); mSubView.destroy(); @@ -1506,84 +1346,6 @@ class Tab { } } - /** - * Attach the sub window to the content view. - */ - void attachSubWindow(ViewGroup content) { - if (mSubView != null) { - content.addView(mSubViewContainer, - BrowserActivity.COVER_SCREEN_PARAMS); - } - } - - /** - * Remove the sub window from the content view. - */ - void removeSubWindow(ViewGroup content) { - if (mSubView != null) { - content.removeView(mSubViewContainer); - mActivity.closeDialogs(); - } - } - - /** - * This method attaches both the WebView and any sub window to the - * given content view. - */ - void attachTabToContentView(ViewGroup content) { - if (mMainView == null) { - return; - } - - // Attach the WebView to the container and then attach the - // container to the content view. - FrameLayout wrapper = - (FrameLayout) mContainer.findViewById(R.id.webview_wrapper); - ViewGroup parent = (ViewGroup) mMainView.getParent(); - if (parent != wrapper) { - if (parent != null) { - Log.w(LOGTAG, "mMainView already has a parent in" - + " attachTabToContentView!"); - parent.removeView(mMainView); - } - wrapper.addView(mMainView); - } else { - Log.w(LOGTAG, "mMainView is already attached to wrapper in" - + " attachTabToContentView!"); - } - parent = (ViewGroup) mContainer.getParent(); - if (parent != content) { - if (parent != null) { - Log.w(LOGTAG, "mContainer already has a parent in" - + " attachTabToContentView!"); - parent.removeView(mContainer); - } - content.addView(mContainer, BrowserActivity.COVER_SCREEN_PARAMS); - } else { - Log.w(LOGTAG, "mContainer is already attached to content in" - + " attachTabToContentView!"); - } - attachSubWindow(content); - } - - /** - * Remove the WebView and any sub window from the given content view. - */ - void removeTabFromContentView(ViewGroup content) { - if (mMainView == null) { - return; - } - - // Remove the container from the content and then remove the - // WebView from the container. This will trigger a focus change - // needed by WebView. - FrameLayout wrapper = - (FrameLayout) mContainer.findViewById(R.id.webview_wrapper); - wrapper.removeView(mMainView); - content.removeView(mContainer); - mActivity.closeDialogs(); - removeSubWindow(content); - } /** * Set the parent tab of this tab. @@ -1598,7 +1360,7 @@ class Tab { if (parent == null) { mSavedState.remove(PARENTTAB); } else { - mSavedState.putInt(PARENTTAB, mActivity.getTabControl() + mSavedState.putInt(PARENTTAB, mWebViewController.getTabControl() .getTabIndex(parent)); } } @@ -1661,6 +1423,10 @@ class Tab { } } + boolean inForeground() { + return mInForeground; + } + /** * Return the top window of this tab; either the subwindow if it is not * null or the main window. @@ -1683,6 +1449,23 @@ class Tab { return mMainView; } + View getViewContainer() { + return mContainer; + } + + /** + * Return whether private browsing is enabled for the main window of + * this tab. + * @return True if private browsing is enabled. + */ + boolean isPrivateBrowsingEnabled() { + WebView webView = getWebView(); + if (webView == null) { + return false; + } + return webView.isPrivateBrowsingEnabled(); + } + /** * Return the subwindow of this tab or null if there is no subwindow. * @return The subwindow of this tab or null. @@ -1691,6 +1474,18 @@ class Tab { return mSubView; } + void setSubWebView(WebView subView) { + mSubView = subView; + } + + View getSubViewContainer() { + return mSubViewContainer; + } + + void setSubViewContainer(View subViewContainer) { + mSubViewContainer = subViewContainer; + } + /** * @return The geolocation permissions prompt for this tab. */ @@ -1735,6 +1530,28 @@ class Tab { } /** + * set the title for the tab + */ + void setCurrentTitle(String title) { + mCurrentTitle = title; + } + + /** + * set url for this tab + * @param url + */ + void setCurrentUrl(String url) { + mCurrentUrl = url; + } + + String getCurrentTitle() { + return mCurrentTitle; + } + + String getCurrentUrl() { + return mCurrentUrl; + } + /** * Get the url of this tab. Valid after calling populatePickerData, but * before calling wipePickerData, or if the webview has been destroyed. * @return The WebView's url or null. @@ -1771,6 +1588,7 @@ class Tab { return null; } + /** * Return the tab's error console. Creates the console if createIfNEcessary * is true and we haven't already created the console. @@ -1811,9 +1629,9 @@ class Tab { */ void resetLockIcon(String url) { mPrevLockIconType = mLockIconType; - mLockIconType = BrowserActivity.LOCK_ICON_UNSECURE; + mLockIconType = LOCK_ICON_UNSECURE; if (URLUtil.isHttpsUrl(url)) { - mLockIconType = BrowserActivity.LOCK_ICON_SECURE; + mLockIconType = LOCK_ICON_SECURE; } } @@ -1836,14 +1654,14 @@ class Tab { * @return TRUE if onPageStarted is called while onPageFinished is not * called yet. */ - boolean inLoad() { - return mInLoad; + boolean inPageLoad() { + return mInPageLoad; } // force mInLoad to be false. This should only be called before closing the // tab to ensure BrowserActivity's pauseWebViewTimers() is called correctly. - void clearInLoad() { - mInLoad = false; + void clearInPageLoad() { + mInPageLoad = false; } void populatePickerData() { @@ -1855,6 +1673,9 @@ class Tab { // FIXME: The only place we cared about subwindow was for // bookmarking (i.e. not when saving state). Was this deliberate? final WebBackForwardList list = mMainView.copyBackForwardList(); + if (list == null) { + Log.w(LOGTAG, "populatePickerData called and WebBackForwardList is null"); + } final WebHistoryItem item = list != null ? list.getCurrentItem() : null; populatePickerData(item); } @@ -1863,7 +1684,9 @@ class Tab { // WebView. private void populatePickerData(WebHistoryItem item) { mPickerData = new PickerData(); - if (item != null) { + if (item == null) { + Log.w(LOGTAG, "populatePickerData called with a null WebHistoryItem"); + } else { mPickerData.mUrl = item.getUrl(); mPickerData.mTitle = item.getTitle(); mPickerData.mFavicon = item.getFavicon(); @@ -1934,7 +1757,7 @@ class Tab { } // Remember the parent tab so the relationship can be restored. if (mParentTab != null) { - mSavedState.putInt(PARENTTAB, mActivity.getTabControl().getTabIndex( + mSavedState.putInt(PARENTTAB, mWebViewController.getTabControl().getTabIndex( mParentTab)); } return true; @@ -1962,35 +1785,4 @@ class Tab { return true; } - /* - * Opens the find and select text dialogs. Called by BrowserActivity. - */ - WebView showDialog(WebDialog dialog) { - LinearLayout container; - WebView view; - if (mSubView != null) { - view = mSubView; - container = (LinearLayout) mSubViewContainer.findViewById( - R.id.inner_container); - } else { - view = mMainView; - container = mContainer; - } - dialog.show(); - container.addView(dialog, 0, new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT)); - dialog.setWebView(view); - return view; - } - - /* - * Close the find or select dialog. Called by BrowserActivity.closeDialog. - */ - void closeDialog(WebDialog dialog) { - // The dialog may be attached to the subwindow. Ensure that the - // correct parent has it removed. - LinearLayout parent = (LinearLayout) dialog.getParent(); - if (parent != null) parent.removeView(dialog); - } } diff --git a/src/com/android/browser/TabBar.java b/src/com/android/browser/TabBar.java new file mode 100644 index 000000000..69e0bd2a1 --- /dev/null +++ b/src/com/android/browser/TabBar.java @@ -0,0 +1,472 @@ +/* + * Copyright (C) 2010 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 com.android.browser.ScrollWebView.ScrollListener; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.graphics.drawable.PaintDrawable; +import android.view.ContextMenu; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.webkit.WebView; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * tabbed title bar for xlarge screen browser + */ +public class TabBar extends LinearLayout + implements ScrollListener, OnClickListener { + + private static final int PROGRESS_MAX = 100; + + private Activity mActivity; + private UiController mUiController; + private TabControl mTabControl; + private BaseUi mUi; + + private final int mTabWidthSelected; + private final int mTabWidthUnselected; + + private TabScrollView mTabs; + + private ImageButton mNewTab; + private int mButtonWidth; + + private Map<Tab, TabViewData> mTabMap; + + private boolean mUserRequestedUrlbar; + private boolean mTitleVisible; + private boolean mShowUrlMode; + private boolean mHasReceivedTitle; + + private Drawable mGenericFavicon; + private String mLoadingText; + + public TabBar(Activity activity, UiController controller, BaseUi ui) { + super(activity); + mActivity = activity; + mUiController = controller; + mTabControl = mUiController.getTabControl(); + mUi = ui; + Resources res = activity.getResources(); + mTabWidthSelected = (int) res.getDimension(R.dimen.tab_width_selected); + mTabWidthUnselected = (int) res.getDimension(R.dimen.tab_width_unselected); + + mTabMap = new HashMap<Tab, TabViewData>(); + Resources resources = activity.getResources(); + LayoutInflater factory = LayoutInflater.from(activity); + factory.inflate(R.layout.tab_bar, this); + mTabs = (TabScrollView) findViewById(R.id.tabs); + mNewTab = (ImageButton) findViewById(R.id.newtab); + mNewTab.setOnClickListener(this); + mGenericFavicon = res.getDrawable(R.drawable.app_web_browser_sm); + mLoadingText = res.getString(R.string.title_bar_loading); + + // TODO: Change enabled states based on whether you can go + // back/forward. Probably should be done inside onPageStarted. + + updateTabs(mUiController.getTabs()); + + mUserRequestedUrlbar = false; + mTitleVisible = true; + mButtonWidth = -1; + } + + void updateTabs(List<Tab> tabs) { + mTabs.clearTabs(); + mTabMap.clear(); + for (Tab tab : tabs) { + TabViewData data = buildTab(tab); + TabView tv = buildView(data); + } + mTabs.setSelectedTab(mTabControl.getCurrentIndex()); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (mButtonWidth == -1) { + mButtonWidth = mNewTab.getMeasuredWidth(); + } + int sw = mTabs.getMeasuredWidth(); + int w = right-left; + if (w-sw < mButtonWidth) { + sw = w - mButtonWidth; + } + mTabs.layout(0, 0, sw, bottom-top ); + mNewTab.layout(sw, 0, sw+mButtonWidth, bottom-top); + } + + public void onClick(View view) { + mUi.hideComboView(); + if (mNewTab == view) { + mUiController.openTabToHomePage(); + } else if (mTabs.getSelectedTab() == view) { + if (mUi.isFakeTitleBarShowing() && !isLoading()) { + mUi.hideFakeTitleBar(); + } else { + showUrlBar(); + } + } else { + int ix = mTabs.getChildIndex(view); + if (ix >= 0) { + mTabs.setSelectedTab(ix); + mUiController.switchToTab(ix); + } + } + } + + private void showUrlBar() { + mUi.stopWebViewScrolling(); + mUi.showFakeTitleBar(); + mUserRequestedUrlbar = true; + } + + private void setShowUrlMode(boolean showUrl) { + mShowUrlMode = showUrl; + } + + // callback after fake titlebar is shown + void onShowTitleBar() { + setShowUrlMode(false); + } + + // callback after fake titlebar is hidden + void onHideTitleBar() { + setShowUrlMode(!mTitleVisible); + Tab tab = mTabControl.getCurrentTab(); + tab.getWebView().requestFocus(); + mUserRequestedUrlbar = false; + } + + // webview scroll listener + + @Override + public void onScroll(boolean titleVisible) { + // isLoading is using the current tab, which initially might not be set yet + if (mTabControl.getCurrentTab() != null) { + mTitleVisible = titleVisible; + if (!mShowUrlMode && !mTitleVisible && !isLoading()) { + if (mUserRequestedUrlbar) { + mUi.hideFakeTitleBar(); + } else { + setShowUrlMode(true); + } + } else if (mTitleVisible && !isLoading()) { + if (mShowUrlMode) { + setShowUrlMode(false); + } + } + } + } + + @Override + public void createContextMenu(ContextMenu menu) { + MenuInflater inflater = mActivity.getMenuInflater(); + inflater.inflate(R.menu.title_context, menu); + mActivity.onCreateContextMenu(menu, this, null); + } + + private TabViewData buildTab(Tab tab) { + TabViewData data = new TabViewData(tab); + mTabMap.put(tab, data); + return data; + } + + private TabView buildView(final TabViewData data) { + TabView tv = new TabView(mActivity, data); + tv.setTag(data); + tv.setOnClickListener(this); + mTabs.addTab(tv); + return tv; + } + + /** + * View used in the tab bar + */ + class TabView extends LinearLayout implements OnClickListener { + + TabViewData mTabData; + View mTabContent; + TextView mTitle; + View mIncognito; + ImageView mIconView; + ImageView mLock; + ImageView mClose; + boolean mSelected; + boolean mInLoad; + + /** + * @param context + */ + public TabView(Context context, TabViewData tab) { + super(context); + mTabData = tab; + setGravity(Gravity.CENTER_VERTICAL); + setOrientation(LinearLayout.HORIZONTAL); + setBackgroundResource(R.drawable.tab_background); + LayoutInflater inflater = LayoutInflater.from(getContext()); + mTabContent = inflater.inflate(R.layout.tab_title, this, true); + mTitle = (TextView) mTabContent.findViewById(R.id.title); + mIconView = (ImageView) mTabContent.findViewById(R.id.favicon); + mLock = (ImageView) mTabContent.findViewById(R.id.lock); + mClose = (ImageView) mTabContent.findViewById(R.id.close); + mClose.setOnClickListener(this); + mIncognito = mTabContent.findViewById(R.id.incognito); + mSelected = false; + mInLoad = false; + // update the status + updateFromData(); + } + + @Override + public void onClick(View v) { + if (v == mClose) { + closeTab(); + } + } + + private void updateFromData() { + mTabData.mTabView = this; + if (mTabData.mUrl != null) { + setDisplayTitle(mTabData.mUrl); + } + if (mTabData.mTitle != null) { + setDisplayTitle(mTabData.mTitle); + } + setProgress(mTabData.mProgress); + if (mTabData.mIcon != null) { + setFavicon(mTabData.mIcon); + } + if (mTabData.mLock != null) { + setLock(mTabData.mLock); + } + if (mTabData.mTab != null) { + mIncognito.setVisibility( + mTabData.mTab.isPrivateBrowsingEnabled() ? + View.VISIBLE : View.GONE); + } + } + + @Override + public void setActivated(boolean selected) { + mSelected = selected; + mClose.setVisibility(mSelected ? View.VISIBLE : View.GONE); + mTitle.setTextAppearance(mActivity, mSelected ? + R.style.TabTitleSelected : R.style.TabTitleUnselected); + setHorizontalFadingEdgeEnabled(!mSelected); + setFadingEdgeLength(50); + super.setActivated(selected); + setLayoutParams(new LayoutParams(selected ? + mTabWidthSelected : mTabWidthUnselected, + LayoutParams.MATCH_PARENT)); + } + + void setDisplayTitle(String title) { + mTitle.setText(title); + } + + void setFavicon(Drawable d) { + mIconView.setImageDrawable(d); + } + + void setLock(Drawable d) { + if (null == d) { + mLock.setVisibility(View.GONE); + } else { + mLock.setImageDrawable(d); + mLock.setVisibility(View.VISIBLE); + } + } + + void setProgress(int newProgress) { + if (newProgress >= PROGRESS_MAX) { + mInLoad = false; + } else { + if (!mInLoad && getWindowToken() != null) { + mInLoad = true; + } + } + } + + private void closeTab() { + if (mTabData.mTab == mTabControl.getCurrentTab()) { + mUiController.closeCurrentTab(); + } else { + mUiController.closeTab(mTabData.mTab); + } + } + + } + + /** + * Store tab state within the title bar + */ + class TabViewData { + + Tab mTab; + TabView mTabView; + int mProgress; + Drawable mIcon; + Drawable mLock; + String mTitle; + String mUrl; + + TabViewData(Tab tab) { + mTab = tab; + WebView web = tab.getWebView(); + if (web != null) { + setUrlAndTitle(web.getUrl(), web.getTitle()); + } + } + + void setUrlAndTitle(String url, String title) { + mUrl = url; + mTitle = title; + if (mTabView != null) { + if (title != null) { + mTabView.setDisplayTitle(title); + } else if (url != null) { + mTabView.setDisplayTitle(UrlUtils.stripUrl(url)); + } + } + } + + void setProgress(int newProgress) { + mProgress = newProgress; + if (mTabView != null) { + mTabView.setProgress(mProgress); + } + } + + void setFavicon(Bitmap icon) { + Drawable[] array = new Drawable[3]; + array[0] = new PaintDrawable(Color.BLACK); + array[1] = new PaintDrawable(Color.WHITE); + if (icon == null) { + array[2] = mGenericFavicon; + } else { + array[2] = new BitmapDrawable(icon); + } + LayerDrawable d = new LayerDrawable(array); + d.setLayerInset(1, 1, 1, 1, 1); + d.setLayerInset(2, 2, 2, 2, 2); + mIcon = d; + if (mTabView != null) { + mTabView.setFavicon(mIcon); + } + } + + } + + // TabChangeListener implementation + + public void onSetActiveTab(Tab tab) { + mTabs.setSelectedTab(mTabControl.getTabIndex(tab)); + TabViewData tvd = mTabMap.get(tab); + if (tvd != null) { + tvd.setProgress(tvd.mProgress); + // update the scroll state + WebView webview = tab.getWebView(); + onScroll(webview.getVisibleTitleHeight() > 0); + } + } + + public void onFavicon(Tab tab, Bitmap favicon) { + TabViewData tvd = mTabMap.get(tab); + if (tvd != null) { + tvd.setFavicon(favicon); + } + } + + public void onNewTab(Tab tab) { + TabViewData tvd = buildTab(tab); + buildView(tvd); + } + + public void onProgress(Tab tab, int progress) { + TabViewData tvd = mTabMap.get(tab); + if (tvd != null) { + tvd.setProgress(progress); + } + } + + public void onRemoveTab(Tab tab) { + TabViewData tvd = mTabMap.get(tab); + if (tvd != null) { + TabView tv = tvd.mTabView; + if (tv != null) { + mTabs.removeTab(tv); + } + } + mTabMap.remove(tab); + } + + public void onUrlAndTitle(Tab tab, String url, String title) { + mHasReceivedTitle = true; + TabViewData tvd = mTabMap.get(tab); + if (tvd != null) { + tvd.setUrlAndTitle(url, title); + } + } + + public void onPageFinished(Tab tab) { + if (!mHasReceivedTitle) { + TabViewData tvd = mTabMap.get(tab); + if (tvd != null) { + tvd.setUrlAndTitle(tvd.mUrl, null); + } + } + } + + public void onPageStarted(Tab tab, String url, Bitmap favicon) { + mHasReceivedTitle = false; + TabViewData tvd = mTabMap.get(tab); + if (tvd != null) { + tvd.setUrlAndTitle(url, null); + tvd.setFavicon(favicon); + tvd.setUrlAndTitle(url, mLoadingText); + } + } + + private boolean isLoading() { + TabViewData tvd = mTabMap.get(mTabControl.getCurrentTab()); + if ((tvd != null) && (tvd.mTabView != null)) { + return tvd.mTabView.mInLoad; + } else { + return false; + } + } + +} diff --git a/src/com/android/browser/TabControl.java b/src/com/android/browser/TabControl.java index afd4ea827..2d90d2317 100644 --- a/src/com/android/browser/TabControl.java +++ b/src/com/android/browser/TabControl.java @@ -16,57 +16,51 @@ package com.android.browser; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.BitmapShader; -import android.graphics.Paint; -import android.graphics.Shader; +import com.android.browser.IntentHandler.UrlData; + import android.os.Bundle; import android.util.Log; -import android.view.View; import android.webkit.WebBackForwardList; import android.webkit.WebView; import java.io.File; import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Vector; class TabControl { // Log Tag private static final String LOGTAG = "TabControl"; // Maximum number of tabs. - private static final int MAX_TABS = 8; + private int mMaxTabs; // Private array of WebViews that are used as tabs. - private ArrayList<Tab> mTabs = new ArrayList<Tab>(MAX_TABS); + private ArrayList<Tab> mTabs; // Queue of most recently viewed tabs. - private ArrayList<Tab> mTabQueue = new ArrayList<Tab>(MAX_TABS); + private ArrayList<Tab> mTabQueue; // Current position in mTabs. private int mCurrentTab = -1; - // A private instance of BrowserActivity to interface with when adding and - // switching between tabs. - private final BrowserActivity mActivity; - // Directory to store thumbnails for each WebView. + // the main browser controller + private final Controller mController; + private final File mThumbnailDir; /** - * Construct a new TabControl object that interfaces with the given - * BrowserActivity instance. - * @param activity A BrowserActivity instance that TabControl will interface - * with. + * Construct a new TabControl object */ - TabControl(BrowserActivity activity) { - mActivity = activity; - mThumbnailDir = activity.getDir("thumbnails", 0); + TabControl(Controller controller) { + mController = controller; + mThumbnailDir = mController.getActivity() + .getDir("thumbnails", 0); + mMaxTabs = mController.getMaxTabs(); + mTabs = new ArrayList<Tab>(mMaxTabs); + mTabQueue = new ArrayList<Tab>(mMaxTabs); } File getThumbnailDir() { return mThumbnailDir; } - BrowserActivity getBrowserActivity() { - return mActivity; - } - /** * Return the current tab's main WebView. This will always return the main * WebView for a given tab and not a subwindow. @@ -106,6 +100,13 @@ class TabControl { } /** + * return the list of tabs + */ + List<Tab> getTabs() { + return mTabs; + } + + /** * Return the tab at the specified index. * @return The Tab for the specified index or null if the tab does not * exist. @@ -132,7 +133,7 @@ class TabControl { int getCurrentIndex() { return mCurrentTab; } - + /** * Given a Tab, find it's index * @param Tab to find @@ -146,7 +147,20 @@ class TabControl { } boolean canCreateNewTab() { - return MAX_TABS != mTabs.size(); + return mMaxTabs != mTabs.size(); + } + + /** + * Returns true if there are any incognito tabs open. + * @return True when any incognito tabs are open, false otherwise. + */ + boolean hasAnyOpenIncognitoTabs() { + for (Tab tab : mTabs) { + if (tab.getWebView() != null && tab.getWebView().isPrivateBrowsingEnabled()) { + return true; + } + } + return false; } /** @@ -154,16 +168,17 @@ class TabControl { * @return The newly createTab or null if we have reached the maximum * number of open tabs. */ - Tab createNewTab(boolean closeOnExit, String appId, String url) { + Tab createNewTab(boolean closeOnExit, String appId, String url, + boolean privateBrowsing) { int size = mTabs.size(); // Return false if we have maxed out on tabs - if (MAX_TABS == size) { + if (mMaxTabs == size) { return null; } - final WebView w = createNewWebView(); + final WebView w = createNewWebView(privateBrowsing); // Create a new tab and add it to the tab list - Tab t = new Tab(mActivity, w, closeOnExit, appId, url); + Tab t = new Tab(mController, w, closeOnExit, appId, url); mTabs.add(t); // Initially put the tab in the background. t.putInBackground(); @@ -172,10 +187,10 @@ class TabControl { /** * Create a new tab with default values for closeOnExit(false), - * appId(null), and url(null). + * appId(null), url(null), and privateBrowsing(false). */ Tab createNewTab() { - return createNewTab(false, null, null); + return createNewTab(false, null, null, false); } /** @@ -274,32 +289,65 @@ class TabControl { /** * Restore the state of all the tabs. * @param inState The saved state of all the tabs. + * @param restoreIncognitoTabs Restoring private browsing tabs + * @param restoreAll All webviews get restored, not just the current tab + * (this does not override handling of incognito tabs) * @return True if there were previous tabs that were restored. False if * there was no saved state or restoring the state failed. */ - boolean restoreState(Bundle inState) { + boolean restoreState(Bundle inState, boolean restoreIncognitoTabs, + boolean restoreAll) { final int numTabs = (inState == null) ? -1 : inState.getInt(Tab.NUMTABS, -1); if (numTabs == -1) { return false; } else { - final int currentTab = inState.getInt(Tab.CURRTAB, -1); + final int oldCurrentTab = inState.getInt(Tab.CURRTAB, -1); + + // Determine whether the saved current tab can be restored, and + // if not, which tab will take its place. + int currentTab = -1; + if (restoreIncognitoTabs + || !inState.getBundle(Tab.WEBVIEW + oldCurrentTab).getBoolean(Tab.INCOGNITO)) { + currentTab = oldCurrentTab; + } else { + for (int i = 0; i < numTabs; i++) { + if (!inState.getBundle(Tab.WEBVIEW + i).getBoolean(Tab.INCOGNITO)) { + currentTab = i; + break; + } + } + } + if (currentTab < 0) { + return false; + } + + // Map saved tab indices to new indices, in case any incognito tabs + // need to not be restored. + HashMap<Integer, Integer> originalTabIndices = new HashMap<Integer, Integer>(); + originalTabIndices.put(-1, -1); for (int i = 0; i < numTabs; i++) { - if (i == currentTab) { + Bundle state = inState.getBundle(Tab.WEBVIEW + i); + + if (!restoreIncognitoTabs && state != null && state.getBoolean(Tab.INCOGNITO)) { + originalTabIndices.put(i, -1); + } else if (i == currentTab || restoreAll) { Tab t = createNewTab(); // Me must set the current tab before restoring the state // so that all the client classes are set. - setCurrentTab(t); - if (!t.restoreState(inState.getBundle(Tab.WEBVIEW + i))) { + if (i == currentTab) { + setCurrentTab(t); + } + if (!t.restoreState(state)) { Log.w(LOGTAG, "Fail in restoreState, load home page."); t.getWebView().loadUrl(BrowserSettings.getInstance() .getHomePage()); } + originalTabIndices.put(i, getTabCount() - 1); } else { // Create a new tab and don't restore the state yet, add it // to the tab list - Tab t = new Tab(mActivity, null, false, null, null); - Bundle state = inState.getBundle(Tab.WEBVIEW + i); + Tab t = new Tab(mController, null, false, null, null); if (state != null) { t.setSavedState(state); t.populatePickerDataFromSavedState(); @@ -311,15 +359,17 @@ class TabControl { mTabs.add(t); // added the tab to the front as they are not current mTabQueue.add(0, t); + originalTabIndices.put(i, getTabCount() - 1); } } + // Rebuild the tree of tabs. Do this after all tabs have been // created/restored so that the parent tab exists. for (int i = 0; i < numTabs; i++) { final Bundle b = inState.getBundle(Tab.WEBVIEW + i); final Tab t = getTab(i); if (b != null && t != null) { - final int parentIndex = b.getInt(Tab.PARENTTAB, -1); + final Integer parentIndex = originalTabIndices.get(b.getInt(Tab.PARENTTAB, -1)); if (parentIndex != -1) { final Tab parent = getTab(parentIndex); if (parent != null) { @@ -491,7 +541,7 @@ class TabControl { * requires a load, whether it was due to the fact that it was deleted, or * it is because it was a voice search. */ - boolean recreateWebView(Tab t, BrowserActivity.UrlData urlData) { + boolean recreateWebView(Tab t, UrlData urlData) { final String url = urlData.mUrl; final WebView w = t.getWebView(); if (w != null) { @@ -529,30 +579,16 @@ class TabControl { * Creates a new WebView and registers it with the global settings. */ private WebView createNewWebView() { - // Create a new WebView - WebView w = new WebView(mActivity); - w.setScrollbarFadingEnabled(true); - w.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); - w.setMapTrackballToArrowKeys(false); // use trackball directly - // Enable the built-in zoom - w.getSettings().setBuiltInZoomControls(true); - // Add this WebView to the settings observer list and update the - // settings - final BrowserSettings s = BrowserSettings.getInstance(); - s.addObserver(w.getSettings()).update(s, null); - - // pick a default - if (false) { - MeshTracker mt = new MeshTracker(2); - Paint paint = new Paint(); - Bitmap bm = BitmapFactory.decodeResource(mActivity.getResources(), - R.drawable.pattern_carbon_fiber_dark); - paint.setShader(new BitmapShader(bm, Shader.TileMode.REPEAT, - Shader.TileMode.REPEAT)); - mt.setBGPaint(paint); - w.setDragTracker(mt); - } - return w; + return createNewWebView(false); + } + + /** + * Creates a new WebView and registers it with the global settings. + * @param privateBrowsing When true, enables private browsing in the new + * WebView. + */ + private WebView createNewWebView(boolean privateBrowsing) { + return mController.getWebViewFactory().createWebView(privateBrowsing); } /** @@ -619,4 +655,5 @@ class TabControl { } return true; } + } diff --git a/src/com/android/browser/TabScrollView.java b/src/com/android/browser/TabScrollView.java new file mode 100644 index 000000000..fbb40aa9c --- /dev/null +++ b/src/com/android/browser/TabScrollView.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2010 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.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.AccelerateInterpolator; +import android.widget.HorizontalScrollView; +import android.widget.LinearLayout; + +/** + * custom view for displaying tabs in the tabbed title bar + */ +public class TabScrollView extends HorizontalScrollView { + + private Context mContext; + private LinearLayout mContentView; + private int mSelected; + private Drawable mArrowLeft; + private Drawable mArrowRight; + private int mAnimationDuration; + + /** + * @param context + * @param attrs + * @param defStyle + */ + public TabScrollView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + /** + * @param context + * @param attrs + */ + public TabScrollView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + /** + * @param context + */ + public TabScrollView(Context context) { + super(context); + init(context); + } + + private void init(Context ctx) { + mContext = ctx; + mAnimationDuration = ctx.getResources().getInteger( + R.integer.tab_animation_duration); + setHorizontalScrollBarEnabled(false); + mContentView = new LinearLayout(mContext); + mContentView.setOrientation(LinearLayout.HORIZONTAL); + mContentView.setLayoutParams( + new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); + addView(mContentView); + mSelected = -1; + mArrowLeft = ctx.getResources().getDrawable(R.drawable.ic_arrow_left); + mArrowRight = ctx.getResources().getDrawable(R.drawable.ic_arrow_right); + // prevent ProGuard from removing the property methods + setScroll(getScroll()); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + ensureChildVisible(getSelectedTab()); + } + + void setSelectedTab(int position) { + View v = getSelectedTab(); + if (v != null) { + v.setActivated(false); + } + mSelected = position; + v = getSelectedTab(); + if (v != null) { + v.setActivated(true); + } + requestLayout(); + } + + int getChildIndex(View v) { + return mContentView.indexOfChild(v); + } + + View getSelectedTab() { + if ((mSelected >= 0) && (mSelected < mContentView.getChildCount())) { + return mContentView.getChildAt(mSelected); + } else { + return null; + } + } + + void clearTabs() { + mContentView.removeAllViews(); + } + + void addTab(View tab) { + mContentView.addView(tab); + animateIn(tab); + tab.setActivated(false); + } + + void removeTab(View tab) { + int ix = mContentView.indexOfChild(tab); + if (ix == mSelected) { + mSelected = -1; + } else if (ix < mSelected) { + mSelected--; + } + animateOut(tab); + } + + private void ensureChildVisible(View child) { + if (child != null) { + int childl = child.getLeft(); + int childr = childl + child.getWidth(); + int viewl = getScrollX(); + int viewr = viewl + getWidth(); + if (childl < viewl) { + // need scrolling to left + animateScroll(childl); + } else if (childr > viewr) { + // need scrolling to right + animateScroll(childr - viewr + viewl); + } + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + int l = getScrollX(); + int r = l + getWidth(); + int dis = 8; + if (l > 0) { + int aw = mArrowLeft.getIntrinsicWidth(); + mArrowLeft.setBounds(l + dis, 0, l + dis + aw, getHeight()); + mArrowLeft.draw(canvas); + } + if (r < mContentView.getWidth()) { + int aw = mArrowRight.getIntrinsicWidth(); + mArrowRight.setBounds(r - dis - aw, 0, r - dis, getHeight()); + mArrowRight.draw(canvas); + } + } + + private void animateIn(View tab) { + ObjectAnimator animator = ObjectAnimator.ofInt(tab, "TranslationX", 500, 0); + animator.setDuration(mAnimationDuration); + animator.start(); + } + + private void animateOut(final View tab) { + ObjectAnimator animator = ObjectAnimator.ofInt( + tab, "TranslationX", 0, getScrollX() - tab.getRight()); + animator.setDuration(mAnimationDuration); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mContentView.removeView(tab); + } + }); + animator.setInterpolator(new AccelerateInterpolator()); + animator.start(); + } + + private void animateScroll(int newscroll) { + ObjectAnimator animator = ObjectAnimator.ofInt(this, "scroll", getScrollX(), newscroll); + animator.setDuration(mAnimationDuration); + animator.start(); + } + + /** + * required for animation + */ + public void setScroll(int newscroll) { + scrollTo(newscroll, getScrollY()); + } + + /** + * required for animation + */ + public int getScroll() { + return getScrollX(); + } + +} diff --git a/src/com/android/browser/TitleBar.java b/src/com/android/browser/TitleBar.java index dc4979bd3..6dabd76f2 100644 --- a/src/com/android/browser/TitleBar.java +++ b/src/com/android/browser/TitleBar.java @@ -16,19 +16,15 @@ package com.android.browser; -import android.content.Context; +import com.android.common.speech.LoggingEvents; + +import android.app.Activity; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Rect; import android.graphics.drawable.Animatable; -import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; -import android.graphics.drawable.PaintDrawable; import android.os.Handler; import android.os.Message; import android.speech.RecognizerIntent; @@ -45,50 +41,45 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; -import com.android.common.speech.LoggingEvents; - /** * This class represents a title bar for a particular "tab" or "window" in the * browser. */ -public class TitleBar extends LinearLayout { - private TextView mTitle; - private Drawable mCloseDrawable; - private ImageView mRtButton; - private Drawable mCircularProgress; - private ProgressBar mHorizontalProgress; - private ImageView mFavicon; - private ImageView mLockIcon; - private ImageView mStopButton; - private Drawable mBookmarkDrawable; - private Drawable mVoiceDrawable; - private boolean mInLoad; - private BrowserActivity mBrowserActivity; - private Drawable mGenericFavicon; - private int mIconDimension; - private View mTitleBg; - private MyHandler mHandler; - private Intent mVoiceSearchIntent; - private boolean mInVoiceMode; - private Drawable mVoiceModeBackground; - private Drawable mNormalBackground; - private Drawable mLoadingBackground; - private ImageSpan mArcsSpan; - private int mLeftMargin; - private int mRightMargin; +public class TitleBar extends TitleBarBase { + + private Activity mActivity; + private UiController mController; + private TextView mTitle; + private ImageView mRtButton; + private Drawable mCircularProgress; + private ProgressBar mHorizontalProgress; + private ImageView mStopButton; + private Drawable mBookmarkDrawable; + private Drawable mVoiceDrawable; + private boolean mInLoad; + private View mTitleBg; + private MyHandler mHandler; + private Intent mVoiceSearchIntent; + private boolean mInVoiceMode; + private Drawable mVoiceModeBackground; + private Drawable mNormalBackground; + private Drawable mLoadingBackground; + private ImageSpan mArcsSpan; + private int mLeftMargin; + private int mRightMargin; private static int LONG_PRESS = 1; - public TitleBar(BrowserActivity context) { - super(context, null); + public TitleBar(Activity activity, UiController controller) { + super(activity); mHandler = new MyHandler(); - LayoutInflater factory = LayoutInflater.from(context); + LayoutInflater factory = LayoutInflater.from(activity); factory.inflate(R.layout.title_bar, this); - mBrowserActivity = context; + mActivity = activity; + mController = controller; mTitle = (TextView) findViewById(R.id.title); mTitle.setCompoundDrawablePadding(5); @@ -99,21 +90,19 @@ public class TitleBar extends LinearLayout { mStopButton = (ImageView) findViewById(R.id.stop); mRtButton = (ImageView) findViewById(R.id.rt_btn); - Resources resources = context.getResources(); - mCircularProgress = (Drawable) resources.getDrawable( + Resources resources = activity.getResources(); + mCircularProgress = resources.getDrawable( com.android.internal.R.drawable.search_spinner); DisplayMetrics metrics = resources.getDisplayMetrics(); mLeftMargin = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 8f, metrics); mRightMargin = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 6f, metrics); - mIconDimension = (int) TypedValue.applyDimension( + int iconDimension = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 20f, metrics); - mCircularProgress.setBounds(0, 0, mIconDimension, mIconDimension); + mCircularProgress.setBounds(0, 0, iconDimension, iconDimension); mHorizontalProgress = (ProgressBar) findViewById( R.id.progress_horizontal); - mGenericFavicon = context.getResources().getDrawable( - R.drawable.app_web_browser_sm); mVoiceSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); mVoiceSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); @@ -121,8 +110,9 @@ public class TitleBar extends LinearLayout { // results intent - http://b/2546173 // // TODO: Make a constant for this extra. - mVoiceSearchIntent.putExtra("android.speech.extras.SEND_APPLICATION_ID_EXTRA", false); - PackageManager pm = context.getPackageManager(); + mVoiceSearchIntent.putExtra("android.speech.extras.SEND_APPLICATION_ID_EXTRA", + false); + PackageManager pm = activity.getPackageManager(); ResolveInfo ri = pm.resolveActivity(mVoiceSearchIntent, PackageManager.MATCH_DEFAULT_ONLY); if (ri == null) { @@ -136,11 +126,12 @@ public class TitleBar extends LinearLayout { R.drawable.title_voice); mNormalBackground = mTitleBg.getBackground(); mLoadingBackground = resources.getDrawable(R.drawable.title_loading); - mArcsSpan = new ImageSpan(context, R.drawable.arcs, + mArcsSpan = new ImageSpan(activity, R.drawable.arcs, ImageSpan.ALIGN_BASELINE); } private class MyHandler extends Handler { + @Override public void handleMessage(Message msg) { if (msg.what == LONG_PRESS) { // Prevent the normal action from happening by setting the title @@ -149,16 +140,20 @@ public class TitleBar extends LinearLayout { // Need to call a special method on BrowserActivity for when the // fake title bar is up, because its ViewGroup does not show a // context menu. - mBrowserActivity.showTitleBarContextMenu(); + // TODO: + // this test is not valid for all UIs; fix later + if (getParent() != null) { + mActivity.openContextMenu(TitleBar.this); + } } } }; @Override public void createContextMenu(ContextMenu menu) { - MenuInflater inflater = mBrowserActivity.getMenuInflater(); + MenuInflater inflater = mActivity.getMenuInflater(); inflater.inflate(R.menu.title_context, menu); - mBrowserActivity.onCreateContextMenu(menu, this, null); + mActivity.onCreateContextMenu(menu, this, null); } @Override @@ -179,7 +174,7 @@ public class TitleBar extends LinearLayout { } break; case MotionEvent.ACTION_MOVE: - int slop = ViewConfiguration.get(mBrowserActivity) + int slop = ViewConfiguration.get(mActivity) .getScaledTouchSlop(); if ((int) event.getY() > getHeight() + slop) { // We only trigger the actions in ACTION_UP if one or the @@ -207,36 +202,37 @@ public class TitleBar extends LinearLayout { case MotionEvent.ACTION_UP: if (button.isPressed()) { if (mInVoiceMode) { - if (mBrowserActivity.getTabControl().getCurrentTab() + if (mController.getTabControl().getCurrentTab() .voiceSearchSourceIsGoogle()) { Intent i = new Intent( LoggingEvents.ACTION_LOG_EVENT); i.putExtra(LoggingEvents.EXTRA_EVENT, LoggingEvents.VoiceSearch.RETRY); - mBrowserActivity.sendBroadcast(i); + mActivity.sendBroadcast(i); } - mBrowserActivity.startActivity(mVoiceSearchIntent); + mActivity.startActivity(mVoiceSearchIntent); } else if (mInLoad) { - mBrowserActivity.stopLoading(); + mController.stopLoading(); } else { - mBrowserActivity.bookmarksOrHistoryPicker(false); + mController.bookmarkCurrentPage( + AddBookmarkPage.DEFAULT_FOLDER_ID); } button.setPressed(false); } else if (mTitleBg.isPressed()) { mHandler.removeMessages(LONG_PRESS); if (mInVoiceMode) { - if (mBrowserActivity.getTabControl().getCurrentTab() + if (mController.getTabControl().getCurrentTab() .voiceSearchSourceIsGoogle()) { Intent i = new Intent( LoggingEvents.ACTION_LOG_EVENT); i.putExtra(LoggingEvents.EXTRA_EVENT, LoggingEvents.VoiceSearch.N_BEST_REVEAL); - mBrowserActivity.sendBroadcast(i); + mActivity.sendBroadcast(i); } - mBrowserActivity.showVoiceSearchResults( + mController.showVoiceSearchResults( mTitle.getText().toString().trim()); } else { - mBrowserActivity.editUrl(); + mController.editUrl(); } mTitleBg.setPressed(false); } @@ -248,29 +244,11 @@ public class TitleBar extends LinearLayout { } /** - * Set a new Bitmap for the Favicon. - */ - /* package */ void setFavicon(Bitmap icon) { - Drawable[] array = new Drawable[3]; - array[0] = new PaintDrawable(Color.BLACK); - PaintDrawable p = new PaintDrawable(Color.WHITE); - array[1] = p; - if (icon == null) { - array[2] = mGenericFavicon; - } else { - array[2] = new BitmapDrawable(icon); - } - LayerDrawable d = new LayerDrawable(array); - d.setLayerInset(1, 1, 1, 1, 1); - d.setLayerInset(2, 2, 2, 2, 2); - mFavicon.setImageDrawable(d); - } - - /** * Change the TitleBar to or from voice mode. If there is no package to * handle voice search, the TitleBar cannot be set to voice mode. */ - /* package */ void setInVoiceMode(boolean inVoiceMode) { + @Override + void setInVoiceMode(boolean inVoiceMode) { if (mInVoiceMode == inVoiceMode) return; mInVoiceMode = inVoiceMode && mVoiceSearchIntent != null; Drawable titleDrawable; @@ -302,21 +280,10 @@ public class TitleBar extends LinearLayout { } /** - * Set the Drawable for the lock icon, or null to hide it. - */ - /* package */ void setLock(Drawable d) { - if (null == d) { - mLockIcon.setVisibility(View.GONE); - } else { - mLockIcon.setImageDrawable(d); - mLockIcon.setVisibility(View.VISIBLE); - } - } - - /** * Update the progress, from 0 to 100. */ - /* package */ void setProgress(int newProgress) { + @Override + void setProgress(int newProgress) { if (newProgress >= mHorizontalProgress.getMax()) { mTitle.setCompoundDrawables(null, null, null, null); ((Animatable) mCircularProgress).stop(); @@ -356,7 +323,8 @@ public class TitleBar extends LinearLayout { * @param title String to display. If null, the loading string will be * shown. */ - /* package */ void setDisplayTitle(String title) { + @Override + void setDisplayTitle(String title) { if (title == null) { mTitle.setText(R.string.title_bar_loading); } else { @@ -374,11 +342,4 @@ public class TitleBar extends LinearLayout { } } } - - /* package */ void setToTabPicker() { - mTitle.setText(R.string.tab_picker_title); - setFavicon(null); - setLock(null); - mHorizontalProgress.setVisibility(View.GONE); - } } diff --git a/src/com/android/browser/TitleBarBase.java b/src/com/android/browser/TitleBarBase.java new file mode 100644 index 000000000..7016dc020 --- /dev/null +++ b/src/com/android/browser/TitleBarBase.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2010 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.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.graphics.drawable.PaintDrawable; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; + +/** + * Base class for a title bar used by the browser. + */ +public class TitleBarBase extends LinearLayout { + // These need to be set by the subclass. + protected ImageView mFavicon; + protected ImageView mLockIcon; + + protected Drawable mGenericFavicon; + + public TitleBarBase(Context context) { + super(context, null); + mGenericFavicon = context.getResources().getDrawable( + R.drawable.app_web_browser_sm); + } + + /* package */ void setProgress(int newProgress) {} + /* package */ void setDisplayTitle(String title) {} + + /* package */ void setLock(Drawable d) { + assert mLockIcon != null; + if (null == d) { + mLockIcon.setVisibility(View.GONE); + } else { + mLockIcon.setImageDrawable(d); + mLockIcon.setVisibility(View.VISIBLE); + } + } + + /* package */ void setFavicon(Bitmap icon) { + assert mFavicon != null; + Drawable[] array = new Drawable[3]; + array[0] = new PaintDrawable(Color.BLACK); + PaintDrawable p = new PaintDrawable(Color.WHITE); + array[1] = p; + if (icon == null) { + array[2] = mGenericFavicon; + } else { + array[2] = new BitmapDrawable(icon); + } + LayerDrawable d = new LayerDrawable(array); + d.setLayerInset(1, 1, 1, 1, 1); + d.setLayerInset(2, 2, 2, 2, 2); + mFavicon.setImageDrawable(d); + } + + /* package */ void setInVoiceMode(boolean inVoiceMode) {} + +} diff --git a/src/com/android/browser/TitleBarXLarge.java b/src/com/android/browser/TitleBarXLarge.java new file mode 100644 index 000000000..7e54710c9 --- /dev/null +++ b/src/com/android/browser/TitleBarXLarge.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2010 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 com.android.browser.UrlInputView.UrlInputListener; + +import android.app.Activity; +import android.app.SearchManager; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnFocusChangeListener; +import android.widget.ImageView; +import android.widget.TextView; + +/** + * tabbed title bar for xlarge screen browser + */ +public class TitleBarXLarge extends TitleBarBase + implements UrlInputListener, OnClickListener, OnFocusChangeListener { + + private static final int PROGRESS_MAX = 100; + + private Activity mActivity; + private UiController mUiController; + + private Drawable mStopDrawable; + private Drawable mReloadDrawable; + + private View mContainer; + private View mBackButton; + private View mForwardButton; + private View mStar; + private View mSearchButton; + private View mFocusContainer; + private View mUnfocusContainer; + private View mGoButton; + private ImageView mStopButton; + private View mAllButton; + private View mClearButton; + private PageProgressView mProgressView; + private UrlInputView mUrlFocused; + private TextView mUrlUnfocused; + private boolean mInLoad; + + public TitleBarXLarge(Activity activity, UiController controller) { + super(activity); + mActivity = activity; + mUiController = controller; + Resources resources = activity.getResources(); + mStopDrawable = resources.getDrawable(R.drawable.ic_stop_normal); + mReloadDrawable = resources.getDrawable(R.drawable.ic_refresh_normal); + rebuildLayout(activity, true); + } + + private void rebuildLayout(Context context, boolean rebuildData) { + LayoutInflater factory = LayoutInflater.from(context); + factory.inflate(R.layout.url_bar, this); + + mContainer = findViewById(R.id.taburlbar); + mUrlFocused = (UrlInputView) findViewById(R.id.url_focused); + mUrlUnfocused = (TextView) findViewById(R.id.url_unfocused); + mAllButton = findViewById(R.id.all_btn); + // TODO: Change enabled states based on whether you can go + // back/forward. Probably should be done inside onPageStarted. + mBackButton = findViewById(R.id.back); + mForwardButton = findViewById(R.id.forward); + mStar = findViewById(R.id.star); + mStopButton = (ImageView) findViewById(R.id.stop); + mSearchButton = findViewById(R.id.search); + mLockIcon = (ImageView) findViewById(R.id.lock); + mGoButton = findViewById(R.id.go); + mClearButton = findViewById(R.id.clear); + mProgressView = (PageProgressView) findViewById(R.id.progress); + mFocusContainer = findViewById(R.id.urlbar_focused); + mUnfocusContainer = findViewById(R.id.urlbar_unfocused); + + mBackButton.setOnClickListener(this); + mForwardButton.setOnClickListener(this); + mStar.setOnClickListener(this); + mAllButton.setOnClickListener(this); + mStopButton.setOnClickListener(this); + mSearchButton.setOnClickListener(this); + mGoButton.setOnClickListener(this); + mClearButton.setOnClickListener(this); + mUrlFocused.setUrlInputListener(this); + mUrlUnfocused.setOnFocusChangeListener(this); + mUrlFocused.setContainer(mFocusContainer); + mUnfocusContainer.setOnClickListener(this); + } + + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + setUrlMode(true); + mUrlFocused.selectAll(); + mUrlFocused.requestFocus(); + mUrlFocused.setDropDownWidth(mUnfocusContainer.getWidth()); + mUrlFocused.setDropDownHorizontalOffset(-mUrlFocused.getLeft()); + } + } + + @Override + public void onClick(View v) { + if (mUnfocusContainer == v) { + mUrlUnfocused.requestFocus(); + } else if (mBackButton == v) { + mUiController.getCurrentTopWebView().goBack(); + } else if (mForwardButton == v) { + mUiController.getCurrentTopWebView().goForward(); + } else if (mStar == v) { + mUiController.bookmarkCurrentPage( + AddBookmarkPage.DEFAULT_FOLDER_ID); + } else if (mAllButton == v) { + mUiController.bookmarksOrHistoryPicker(false); + } else if (mSearchButton == v) { + search(); + } else if (mStopButton == v) { + stopOrRefresh(); + } else if (mGoButton == v) { + if (!TextUtils.isEmpty(mUrlFocused.getText())) { + onAction(mUrlFocused.getText().toString(), null); + } + } else if (mClearButton == v) { + mUrlFocused.setText(""); + } + } + + int getHeightWithoutProgress() { + return mContainer.getHeight(); + } + + @Override + void setFavicon(Bitmap icon) { } + + // UrlInputListener implementation + + @Override + public void onAction(String text, String extra) { + mUiController.getCurrentTopWebView().requestFocus(); + ((BaseUi) mUiController.getUi()).hideFakeTitleBar(); + Intent i = new Intent(); + i.setAction(Intent.ACTION_SEARCH); + i.putExtra(SearchManager.QUERY, text); + if (extra != null) { + i.putExtra(SearchManager.EXTRA_DATA_KEY, extra); + } + mUiController.handleNewIntent(i); + setUrlMode(false); + setDisplayTitle(text); + } + + @Override + public void onDismiss() { + mUiController.getCurrentTopWebView().requestFocus(); + ((BaseUi) mUiController.getUi()).hideFakeTitleBar(); + setUrlMode(false); + setDisplayTitle(mUiController.getCurrentWebView().getUrl()); + } + + @Override + public void onEdit(String text) { + setDisplayTitle(text, true); + if (text != null) { + mUrlFocused.setSelection(text.length()); + } + } + + private void setUrlMode(boolean focused) { + swapUrlContainer(focused); + if (focused) { + mSearchButton.setVisibility(View.GONE); + mGoButton.setVisibility(View.VISIBLE); + } else { + mSearchButton.setVisibility(View.VISIBLE); + mGoButton.setVisibility(View.GONE); + } + } + + private void swapUrlContainer(boolean focus) { + mUnfocusContainer.setVisibility(focus ? View.GONE : View.VISIBLE); + mFocusContainer.setVisibility(focus ? View.VISIBLE : View.GONE); + } + + @Override + public void createContextMenu(ContextMenu menu) { + MenuInflater inflater = mActivity.getMenuInflater(); + inflater.inflate(R.menu.title_context, menu); + mActivity.onCreateContextMenu(menu, this, null); + } + + private void search() { + setDisplayTitle(""); + mUrlUnfocused.requestFocus(); + } + + private void stopOrRefresh() { + if (mInLoad) { + mUiController.stopLoading(); + } else { + mUiController.getCurrentTopWebView().reload(); + } + } + + /** + * Update the progress, from 0 to 100. + */ + @Override + void setProgress(int newProgress) { + if (newProgress >= PROGRESS_MAX) { + mProgressView.setProgress(PageProgressView.MAX_PROGRESS); + mProgressView.setVisibility(View.GONE); + mInLoad = false; + mStopButton.setImageDrawable(mReloadDrawable); + } else { + if (!mInLoad) { + mProgressView.setVisibility(View.VISIBLE); + mInLoad = true; + mStopButton.setImageDrawable(mStopDrawable); + } + mProgressView.setProgress(newProgress * PageProgressView.MAX_PROGRESS + / PROGRESS_MAX); + } + } + + @Override + /* package */ void setDisplayTitle(String title) { + mUrlFocused.setText(title, false); + mUrlUnfocused.setText(title); + } + + void setDisplayTitle(String title, boolean filter) { + mUrlFocused.setText(title, filter); + mUrlUnfocused.setText(title); + } + +} diff --git a/src/com/android/browser/UI.java b/src/com/android/browser/UI.java new file mode 100644 index 000000000..3a8a5cd4c --- /dev/null +++ b/src/com/android/browser/UI.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2010 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 com.android.browser.ScrollWebView.ScrollListener; + +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.view.ActionMode; +import android.view.Menu; +import android.view.View; +import android.webkit.WebChromeClient.CustomViewCallback; +import android.webkit.WebView; + +import java.util.List; + +/** + * UI interface definitions + */ +public interface UI extends ScrollListener { + + public void onPause(); + + public void onResume(); + + public void onDestroy(); + + public void onConfigurationChanged(Configuration config); + + public boolean onBackKey(); + + public boolean needsRestoreAllTabs(); + + public void addTab(Tab tab); + + public void removeTab(Tab tab); + + public void setActiveTab(Tab tab); + + public void updateTabs(List<Tab> tabs); + + public void detachTab(Tab tab); + + public void attachTab(Tab tab); + + public void createSubWindow(Tab tab, WebView subWebView); + + public void attachSubWindow(View subContainer); + + public void removeSubWindow(View subContainer); + + // TODO: consolidate + public void setUrlTitle(Tab tab, String url, String title); + + // TODO: consolidate + public void setFavicon(Tab tab, Bitmap icon); + + public void resetTitleAndRevertLockIcon(Tab tab); + + public void resetTitleAndIcon(Tab tab); + + public void onPageStarted(Tab tab, String url, Bitmap favicon); + + public void onPageFinished(Tab tab, String url); + + public void onPageStopped(Tab tab); + + public void onProgressChanged(Tab tab, int progress); + + public void showActiveTabsPage(); + + public void removeActiveTabsPage(); + + public void showComboView(boolean startWithHistory, Bundle extra); + + public void hideComboView(); + + public void showCustomView(View view, CustomViewCallback callback); + + public void onHideCustomView(); + + public boolean isCustomViewShowing(); + + public void showVoiceTitleBar(String title); + + public void revertVoiceTitleBar(Tab tab); + + // allow the ui to update state + public void onPrepareOptionsMenu(Menu menu); + + public void onOptionsMenuOpened(); + + public void onExtendedMenuOpened(); + + public void onOptionsMenuClosed(boolean inLoad); + + public void onExtendedMenuClosed(boolean inLoad); + + public void onContextMenuCreated(Menu menu); + + public void onContextMenuClosed(Menu menu, boolean inLoad); + + public void onActionModeStarted(ActionMode mode); + + public void onActionModeFinished(boolean inLoad); + + public void setShouldShowErrorConsole(Tab tab, boolean show); + + // returns if the web page is clear of any overlays (not including sub windows) + public boolean showsWeb(); + + Bitmap getDefaultVideoPoster(); + + View getVideoLoadingProgressView(); + +} diff --git a/src/com/android/browser/UiController.java b/src/com/android/browser/UiController.java new file mode 100644 index 000000000..dffebbae5 --- /dev/null +++ b/src/com/android/browser/UiController.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2010 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.content.Intent; +import android.webkit.WebView; + +import java.util.List; + + +/** + * UI aspect of the controller + */ +public interface UiController extends BookmarksHistoryCallbacks { + + UI getUi(); + + WebView getCurrentWebView(); + + WebView getCurrentTopWebView(); + + TabControl getTabControl(); + + List<Tab> getTabs(); + + Tab openTabToHomePage(); + + Tab openIncognitoTab(); + + boolean switchToTab(int tabIndex); + + void closeCurrentTab(); + + void closeTab(Tab tab); + + void stopLoading(); + + void bookmarkCurrentPage(long folderId); + + void bookmarksOrHistoryPicker(boolean openHistory); + + void showVoiceSearchResults(String title); + + void editUrl(); + + void removeActiveTabsPage(boolean attach); + + void handleNewIntent(Intent intent); + + boolean shouldShowErrorConsole(); + + void removeComboView(); + + void hideCustomView(); + + void attachSubWindow(Tab tab); + + void removeSubWindow(Tab tab); + + boolean isInCustomActionMode(); + + void endActionMode(); + +} diff --git a/src/com/android/browser/UploadHandler.java b/src/com/android/browser/UploadHandler.java new file mode 100644 index 000000000..d9b387fbf --- /dev/null +++ b/src/com/android/browser/UploadHandler.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2010 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.content.Intent; +import android.net.Uri; +import android.os.Environment; +import android.provider.MediaStore; +import android.webkit.ValueCallback; + +import java.io.File; +import java.util.Vector; + +/** + * Handle the file upload callbacks from WebView here + */ +public class UploadHandler { + + /* + * The Object used to inform the WebView of the file to upload. + */ + private ValueCallback<Uri> mUploadMessage; + private String mCameraFilePath; + + private Controller mController; + + public UploadHandler(Controller controller) { + mController = controller; + } + + String getFilePath() { + return mCameraFilePath; + } + + void onResult(int resultCode, Intent intent) { + Uri result = intent == null || resultCode != Activity.RESULT_OK ? null + : intent.getData(); + + // As we ask the camera to save the result of the user taking + // a picture, the camera application does not return anything other + // than RESULT_OK. So we need to check whether the file we expected + // was written to disk in the in the case that we + // did not get an intent returned but did get a RESULT_OK. If it was, + // we assume that this result has came back from the camera. + if (result == null && intent == null && resultCode == Activity.RESULT_OK) { + File cameraFile = new File(mCameraFilePath); + if (cameraFile.exists()) { + result = Uri.fromFile(cameraFile); + // Broadcast to the media scanner that we have a new photo + // so it will be added into the gallery for the user. + mController.getActivity().sendBroadcast( + new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, result)); + } + } + + mUploadMessage.onReceiveValue(result); + } + + void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) { + + final String imageMimeType = "image/*"; + final String videoMimeType = "video/*"; + final String audioMimeType = "audio/*"; + final String mediaSourceKey = "capture"; + final String mediaSourceValueCamera = "camera"; + final String mediaSourceValueFileSystem = "filesystem"; + final String mediaSourceValueCamcorder = "camcorder"; + final String mediaSourceValueMicrophone = "microphone"; + + // media source can be 'filesystem' or 'camera' or 'camcorder' or 'microphone'. + String mediaSource = ""; + + // We add the camera intent if there was no accept type (or '*/*' or 'image/*'). + boolean addCameraIntent = true; + // We add the camcorder intent if there was no accept type (or '*/*' or 'video/*'). + boolean addCamcorderIntent = true; + + if (mUploadMessage != null) { + // Already a file picker operation in progress. + return; + } + + mUploadMessage = uploadMsg; + + // Parse the accept type. + String params[] = acceptType.split(";"); + String mimeType = params[0]; + + for (String p : params) { + String[] keyValue = p.split("="); + if (keyValue.length == 2) { + // Process key=value parameters. + if (mediaSourceKey.equals(keyValue[0])) { + mediaSource = keyValue[1]; + } + } + } + + // This intent will display the standard OPENABLE file picker. + Intent i = new Intent(Intent.ACTION_GET_CONTENT); + i.addCategory(Intent.CATEGORY_OPENABLE); + + // Create an intent to add to the standard file picker that will + // capture an image from the camera. We'll combine this intent with + // the standard OPENABLE picker unless the web developer specifically + // requested the camera or gallery be opened by passing a parameter + // in the accept type. + Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + File externalDataDir = Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DCIM); + File cameraDataDir = new File(externalDataDir.getAbsolutePath() + + File.separator + "browser-photos"); + cameraDataDir.mkdirs(); + mCameraFilePath = cameraDataDir.getAbsolutePath() + File.separator + + System.currentTimeMillis() + ".jpg"; + cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(mCameraFilePath))); + + Intent camcorderIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); + + Intent soundRecIntent = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION); + + if (mimeType.equals(imageMimeType)) { + i.setType(imageMimeType); + addCamcorderIntent = false; + if (mediaSource.equals(mediaSourceValueCamera)) { + // Specified 'image/*' and requested the camera, so go ahead and launch the camera + // directly. + startActivity(cameraIntent); + return; + } else if (mediaSource.equals(mediaSourceValueFileSystem)) { + // Specified filesytem as the source, so don't want to consider the camera. + addCameraIntent = false; + } + } else if (mimeType.equals(videoMimeType)) { + i.setType(videoMimeType); + addCameraIntent = false; + // The camcorder saves it's own file and returns it to us in the intent, so + // we don't need to generate one here. + mCameraFilePath = null; + + if (mediaSource.equals(mediaSourceValueCamcorder)) { + // Specified 'video/*' and requested the camcorder, so go ahead and launch the + // camcorder directly. + startActivity(camcorderIntent); + return; + } else if (mediaSource.equals(mediaSourceValueFileSystem)) { + // Specified filesystem as the source, so don't want to consider the camcorder. + addCamcorderIntent = false; + } + } else if (mimeType.equals(audioMimeType)) { + i.setType(audioMimeType); + addCameraIntent = false; + addCamcorderIntent = false; + if (mediaSource.equals(mediaSourceValueMicrophone)) { + // Specified 'audio/*' and requested microphone, so go ahead and launch the sound + // recorder. + startActivity(soundRecIntent); + return; + } + // On a default system, there is no single option to open an audio "gallery". Both the + // sound recorder and music browser respond to the OPENABLE/audio/* intent unlike the + // image/* and video/* OPENABLE intents where the image / video gallery are the only + // respondants (and so the user is not prompted by default). + } else { + i.setType("*/*"); + } + + // Combine the chooser and the extra choices (like camera or camcorder) + Intent chooser = new Intent(Intent.ACTION_CHOOSER); + chooser.putExtra(Intent.EXTRA_INTENT, i); + + Vector<Intent> extraInitialIntents = new Vector<Intent>(0); + + if (addCameraIntent) { + extraInitialIntents.add(cameraIntent); + } + + if (addCamcorderIntent) { + extraInitialIntents.add(camcorderIntent); + } + + if (extraInitialIntents.size() > 0) { + Intent[] extraIntents = new Intent[extraInitialIntents.size()]; + chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, + extraInitialIntents.toArray(extraIntents)); + } + + chooser.putExtra(Intent.EXTRA_TITLE, + mController.getActivity().getResources() + .getString(R.string.choose_upload)); + startActivity(chooser); + } + + private void startActivity(Intent intent) { + mController.getActivity().startActivityForResult(intent, + Controller.FILE_SELECTED); + } + +} diff --git a/src/com/android/browser/UrlHandler.java b/src/com/android/browser/UrlHandler.java new file mode 100644 index 000000000..72704e012 --- /dev/null +++ b/src/com/android/browser/UrlHandler.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2010 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.content.ActivityNotFoundException; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; +import android.webkit.WebView; + +import java.net.URISyntaxException; + +/** + * + */ +public class UrlHandler { + + // Use in overrideUrlLoading + /* package */ final static String SCHEME_WTAI = "wtai://wp/"; + /* package */ final static String SCHEME_WTAI_MC = "wtai://wp/mc;"; + /* package */ final static String SCHEME_WTAI_SD = "wtai://wp/sd;"; + /* package */ final static String SCHEME_WTAI_AP = "wtai://wp/ap;"; + + Controller mController; + Activity mActivity; + + private Boolean mIsProviderPresent = null; + private Uri mRlzUri = null; + + public UrlHandler(Controller controller) { + mController = controller; + mActivity = mController.getActivity(); + } + + boolean shouldOverrideUrlLoading(WebView view, String url) { + if (view.isPrivateBrowsingEnabled()) { + // Don't allow urls to leave the browser app when in + // private browsing mode + mController.loadUrl(view, url); + return true; + } + + if (url.startsWith(SCHEME_WTAI)) { + // wtai://wp/mc;number + // number=string(phone-number) + if (url.startsWith(SCHEME_WTAI_MC)) { + Intent intent = new Intent(Intent.ACTION_VIEW, + Uri.parse(WebView.SCHEME_TEL + + url.substring(SCHEME_WTAI_MC.length()))); + mActivity.startActivity(intent); + // before leaving BrowserActivity, close the empty child tab. + // If a new tab is created through JavaScript open to load this + // url, we would like to close it as we will load this url in a + // different Activity. + mController.closeEmptyChildTab(); + return true; + } + // wtai://wp/sd;dtmf + // dtmf=string(dialstring) + if (url.startsWith(SCHEME_WTAI_SD)) { + // TODO: only send when there is active voice connection + return false; + } + // wtai://wp/ap;number;name + // number=string(phone-number) + // name=string + if (url.startsWith(SCHEME_WTAI_AP)) { + // TODO + return false; + } + } + + // The "about:" schemes are internal to the browser; don't want these to + // be dispatched to other apps. + if (url.startsWith("about:")) { + return false; + } + + // If this is a Google search, attempt to add an RLZ string + // (if one isn't already present). + if (rlzProviderPresent()) { + Uri siteUri = Uri.parse(url); + if (needsRlzString(siteUri)) { + String rlz = null; + Cursor cur = null; + try { + cur = mActivity.getContentResolver() + .query(getRlzUri(), null, null, null, null); + if (cur != null && cur.moveToFirst() && !cur.isNull(0)) { + url = siteUri.buildUpon() + .appendQueryParameter("rlz", cur.getString(0)) + .build().toString(); + } + } finally { + if (cur != null) { + cur.close(); + } + } + mController.loadUrl(view, url); + return true; + } + } + + Intent intent; + // perform generic parsing of the URI to turn it into an Intent. + try { + intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); + } catch (URISyntaxException ex) { + Log.w("Browser", "Bad URI " + url + ": " + ex.getMessage()); + return false; + } + + // check whether the intent can be resolved. If not, we will see + // whether we can download it from the Market. + if (mActivity.getPackageManager().resolveActivity(intent, 0) == null) { + String packagename = intent.getPackage(); + if (packagename != null) { + intent = new Intent(Intent.ACTION_VIEW, Uri + .parse("market://search?q=pname:" + packagename)); + intent.addCategory(Intent.CATEGORY_BROWSABLE); + mActivity.startActivity(intent); + // before leaving BrowserActivity, close the empty child tab. + // If a new tab is created through JavaScript open to load this + // url, we would like to close it as we will load this url in a + // different Activity. + mController.closeEmptyChildTab(); + return true; + } else { + return false; + } + } + + // sanitize the Intent, ensuring web pages can not bypass browser + // security (only access to BROWSABLE activities). + intent.addCategory(Intent.CATEGORY_BROWSABLE); + intent.setComponent(null); + try { + if (mActivity.startActivityIfNeeded(intent, -1)) { + // before leaving BrowserActivity, close the empty child tab. + // If a new tab is created through JavaScript open to load this + // url, we would like to close it as we will load this url in a + // different Activity. + mController.closeEmptyChildTab(); + return true; + } + } catch (ActivityNotFoundException ex) { + // ignore the error. If no application can handle the URL, + // eg about:blank, assume the browser can handle it. + } + + if (mController.isMenuDown()) { + mController.openTab(url, false); + mActivity.closeOptionsMenu(); + return true; + } + return false; + } + + // Determine whether the RLZ provider is present on the system. + private boolean rlzProviderPresent() { + if (mIsProviderPresent == null) { + PackageManager pm = mActivity.getPackageManager(); + mIsProviderPresent = pm.resolveContentProvider( + BrowserSettings.RLZ_PROVIDER, 0) != null; + } + return mIsProviderPresent; + } + + // Retrieve the RLZ access point string and cache the URI used to + // retrieve RLZ values. + private Uri getRlzUri() { + if (mRlzUri == null) { + String ap = mActivity.getResources() + .getString(R.string.rlz_access_point); + mRlzUri = Uri.withAppendedPath(BrowserSettings.RLZ_PROVIDER_URI, ap); + } + return mRlzUri; + } + + // Determine if this URI appears to be for a Google search + // and does not have an RLZ parameter. + // Taken largely from Chrome source, src/chrome/browser/google_url_tracker.cc + private static boolean needsRlzString(Uri uri) { + String scheme = uri.getScheme(); + if (("http".equals(scheme) || "https".equals(scheme)) && + (uri.getQueryParameter("q") != null) && + (uri.getQueryParameter("rlz") == null)) { + String host = uri.getHost(); + if (host == null) { + return false; + } + String[] hostComponents = host.split("\\."); + + if (hostComponents.length < 2) { + return false; + } + int googleComponent = hostComponents.length - 2; + String component = hostComponents[googleComponent]; + if (!"google".equals(component)) { + if (hostComponents.length < 3 || + (!"co".equals(component) && !"com".equals(component))) { + return false; + } + googleComponent = hostComponents.length - 3; + if (!"google".equals(hostComponents[googleComponent])) { + return false; + } + } + + // Google corp network handling. + if (googleComponent > 0 && "corp".equals( + hostComponents[googleComponent - 1])) { + return false; + } + + return true; + } + return false; + } + +} diff --git a/src/com/android/browser/UrlInputView.java b/src/com/android/browser/UrlInputView.java new file mode 100644 index 000000000..2e29f261c --- /dev/null +++ b/src/com/android/browser/UrlInputView.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2010 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 com.android.browser.SuggestionsAdapter.CompletionListener; + +import android.content.Context; +import android.content.res.Configuration; +import android.util.AttributeSet; +import android.view.ActionMode; +import android.view.KeyEvent; +import android.view.View; +import android.view.View.OnFocusChangeListener; +import android.view.inputmethod.InputMethodManager; +import android.widget.AutoCompleteTextView; +import android.widget.TextView; +import android.widget.TextView.OnEditorActionListener; + +/** + * url/search input view + * handling suggestions + */ +public class UrlInputView extends AutoCompleteTextView + implements OnFocusChangeListener, OnEditorActionListener, CompletionListener { + + private UrlInputListener mListener; + private InputMethodManager mInputManager; + private SuggestionsAdapter mAdapter; + private OnFocusChangeListener mWrappedFocusListener; + private View mContainer; + private boolean mLandscape; + + public UrlInputView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + public UrlInputView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public UrlInputView(Context context) { + super(context); + init(context); + } + + private void init(Context ctx) { + mInputManager = (InputMethodManager) ctx.getSystemService(Context.INPUT_METHOD_SERVICE); + setOnEditorActionListener(this); + super.setOnFocusChangeListener(this); + mAdapter = new SuggestionsAdapter(ctx, this); + setAdapter(mAdapter); + setSelectAllOnFocus(false); + onConfigurationChanged(ctx.getResources().getConfiguration()); + setThreshold(1); + } + + void setContainer(View container) { + mContainer = container; + } + + @Override + protected void onConfigurationChanged(Configuration config) { + super.onConfigurationChanged(config); + mLandscape = (config.orientation & + Configuration.ORIENTATION_LANDSCAPE) > 0; + mAdapter.setLandscapeMode(mLandscape); + if (isPopupShowing() && (getVisibility() == View.VISIBLE)) { + setupDropDown(); + } + } + + @Override + public void showDropDown() { + setupDropDown(); + super.showDropDown(); + } + + @Override + public void dismissDropDown() { + super.dismissDropDown(); + mAdapter.clearCache(); + } + + private void setupDropDown() { + int width = mContainer.getWidth(); + if (width != getDropDownWidth()) { + setDropDownWidth(width); + } + if (getLeft() != -getDropDownHorizontalOffset()) { + setDropDownHorizontalOffset(-getLeft()); + } + } + + @Override + public ActionMode startActionMode(ActionMode.Callback callback) { + // suppress selection action mode + return null; + } + + @Override + public void setOnFocusChangeListener(OnFocusChangeListener focusListener) { + mWrappedFocusListener = focusListener; + } + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + finishInput(getText().toString(), null); + return true; + } + + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + forceIme(); + } else { + finishInput(null, null); + } + if (mWrappedFocusListener != null) { + mWrappedFocusListener.onFocusChange(v, hasFocus); + } + } + + public void setUrlInputListener(UrlInputListener listener) { + mListener = listener; + } + + public void forceIme() { + mInputManager.showSoftInput(this, 0); + } + + private void finishInput(String url, String extra) { + this.dismissDropDown(); + this.setSelection(0,0); + mInputManager.hideSoftInputFromWindow(getWindowToken(), 0); + if (url == null) { + mListener.onDismiss(); + } else { + mListener.onAction(url, extra); + } + } + + // Completion Listener + + @Override + public void onSearch(String search) { + mListener.onEdit(search); + } + + @Override + public void onSelect(String url, String extra) { + finishInput(url, extra); + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent evt) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + // catch back key in order to do slightly more cleanup than usual + finishInput(null, null); + return true; + } + return super.onKeyPreIme(keyCode, evt); + } + + interface UrlInputListener { + + public void onDismiss(); + + public void onAction(String text, String extra); + + public void onEdit(String text); + + } + +} diff --git a/src/com/android/browser/UrlUtils.java b/src/com/android/browser/UrlUtils.java new file mode 100644 index 000000000..2df0a616b --- /dev/null +++ b/src/com/android/browser/UrlUtils.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2010 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.net.Uri; +import android.util.Patterns; +import android.webkit.URLUtil; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility methods for Url manipulation + */ +public class UrlUtils { + + static final Pattern ACCEPTED_URI_SCHEMA = Pattern.compile( + "(?i)" + // switch on case insensitive matching + "(" + // begin group for schema + "(?:http|https|file):\\/\\/" + + "|(?:inline|data|about|content|javascript):" + + ")" + + "(.*)" ); + + // Google search + private final static String QUICKSEARCH_G = "http://www.google.com/m?q=%s"; + private final static String QUERY_PLACE_HOLDER = "%s"; + + // Regular expression which matches http://, followed by some stuff, followed by + // optionally a trailing slash, all matched as separate groups. + private static final Pattern STRIP_URL_PATTERN = Pattern.compile("^(http://)(.*?)(/$)?"); + + private UrlUtils() { /* cannot be instantiated */ } + + /** + * Strips the provided url of preceding "http://" and any trailing "/". Does not + * strip "https://". If the provided string cannot be stripped, the original string + * is returned. + * + * TODO: Put this in TextUtils to be used by other packages doing something similar. + * + * @param url a url to strip, like "http://www.google.com/" + * @return a stripped url like "www.google.com", or the original string if it could + * not be stripped + */ + /* package */ static String stripUrl(String url) { + if (url == null) return null; + Matcher m = STRIP_URL_PATTERN.matcher(url); + if (m.matches() && m.groupCount() == 3) { + return m.group(2); + } else { + return url; + } + } + + protected static String smartUrlFilter(Uri inUri) { + if (inUri != null) { + return smartUrlFilter(inUri.toString()); + } + return null; + } + + /** + * Attempts to determine whether user input is a URL or search + * terms. Anything with a space is passed to search. + * + * Converts to lowercase any mistakenly uppercased schema (i.e., + * "Http://" converts to "http://" + * + * @return Original or modified URL + * + */ + protected static String smartUrlFilter(String url) { + + String inUrl = url.trim(); + boolean hasSpace = inUrl.indexOf(' ') != -1; + + Matcher matcher = ACCEPTED_URI_SCHEMA.matcher(inUrl); + if (matcher.matches()) { + // force scheme to lowercase + String scheme = matcher.group(1); + String lcScheme = scheme.toLowerCase(); + if (!lcScheme.equals(scheme)) { + inUrl = lcScheme + matcher.group(2); + } + if (hasSpace) { + inUrl = inUrl.replace(" ", "%20"); + } + return inUrl; + } + if (!hasSpace) { + if (Patterns.WEB_URL.matcher(inUrl).matches()) { + return URLUtil.guessUrl(inUrl); + } + } + + // FIXME: Is this the correct place to add to searches? + // what if someone else calls this function? + +// Browser.addSearchUrl(mBrowser.getContentResolver(), inUrl); + return URLUtil.composeSearchUrl(inUrl, QUICKSEARCH_G, QUERY_PLACE_HOLDER); + } + + /* package */ static String fixUrl(String inUrl) { + // FIXME: Converting the url to lower case + // duplicates functionality in smartUrlFilter(). + // However, changing all current callers of fixUrl to + // call smartUrlFilter in addition may have unwanted + // consequences, and is deferred for now. + int colon = inUrl.indexOf(':'); + boolean allLower = true; + for (int index = 0; index < colon; index++) { + char ch = inUrl.charAt(index); + if (!Character.isLetter(ch)) { + break; + } + allLower &= Character.isLowerCase(ch); + if (index == colon - 1 && !allLower) { + inUrl = inUrl.substring(0, colon).toLowerCase() + + inUrl.substring(colon); + } + } + if (inUrl.startsWith("http://") || inUrl.startsWith("https://")) + return inUrl; + if (inUrl.startsWith("http:") || + inUrl.startsWith("https:")) { + if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) { + inUrl = inUrl.replaceFirst("/", "//"); + } else inUrl = inUrl.replaceFirst(":", "://"); + } + return inUrl; + } + +} diff --git a/src/com/android/browser/WallpaperHandler.java b/src/com/android/browser/WallpaperHandler.java new file mode 100644 index 000000000..0c88a50fd --- /dev/null +++ b/src/com/android/browser/WallpaperHandler.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2010 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.ProgressDialog; +import android.app.WallpaperManager; +import android.content.Context; +import android.content.DialogInterface; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.util.Log; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; + +/** + * Handle setWallpaper requests + * + */ +public class WallpaperHandler extends Thread + implements OnMenuItemClickListener, DialogInterface.OnCancelListener { + + + private static final String LOGTAG = "WallpaperHandler"; + + private Context mContext; + private URL mUrl; + private ProgressDialog mWallpaperProgress; + private boolean mCanceled = false; + + public WallpaperHandler(Context context, String url) { + mContext = context; + try { + mUrl = new URL(url); + } catch (MalformedURLException e) { + mUrl = null; + } + } + + @Override + public void onCancel(DialogInterface dialog) { + mCanceled = true; + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (mUrl != null) { + // The user may have tried to set a image with a large file size as + // their background so it may take a few moments to perform the + // operation. + // Display a progress spinner while it is working. + mWallpaperProgress = new ProgressDialog(mContext); + mWallpaperProgress.setIndeterminate(true); + mWallpaperProgress.setMessage(mContext.getResources() + .getText(R.string.progress_dialog_setting_wallpaper)); + mWallpaperProgress.setCancelable(true); + mWallpaperProgress.setOnCancelListener(this); + mWallpaperProgress.show(); + start(); + } + return true; + } + + @Override + public void run() { + Drawable oldWallpaper = + WallpaperManager.getInstance(mContext).getDrawable(); + try { + // TODO: This will cause the resource to be downloaded again, when + // we should in most cases be able to grab it from the cache. To fix + // this we should query WebCore to see if we can access a cached + // version and instead open an input stream on that. This pattern + // could also be used in the download manager where the same problem + // exists. + InputStream inputstream = mUrl.openStream(); + if (inputstream != null) { + WallpaperManager.getInstance(mContext).setStream(inputstream); + } + } catch (IOException e) { + Log.e(LOGTAG, "Unable to set new wallpaper"); + // Act as though the user canceled the operation so we try to + // restore the old wallpaper. + mCanceled = true; + } + + if (mCanceled) { + // Restore the old wallpaper if the user cancelled whilst we were + // setting + // the new wallpaper. + int width = oldWallpaper.getIntrinsicWidth(); + int height = oldWallpaper.getIntrinsicHeight(); + Bitmap bm = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565); + Canvas canvas = new Canvas(bm); + oldWallpaper.setBounds(0, 0, width, height); + oldWallpaper.draw(canvas); + try { + WallpaperManager.getInstance(mContext).setBitmap(bm); + } catch (IOException e) { + Log.e(LOGTAG, "Unable to restore old wallpaper."); + } + mCanceled = false; + } + + if (mWallpaperProgress.isShowing()) { + mWallpaperProgress.dismiss(); + } + } +} diff --git a/src/com/android/browser/WebDialog.java b/src/com/android/browser/WebDialog.java deleted file mode 100644 index 9995e8f31..000000000 --- a/src/com/android/browser/WebDialog.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2010 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.content.Context; -import android.view.View; -import android.view.animation.AnimationUtils; -import android.view.inputmethod.InputMethodManager; -import android.webkit.WebView; -import android.widget.LinearLayout; - -/* package */ class WebDialog extends LinearLayout { - protected WebView mWebView; - protected BrowserActivity mBrowserActivity; - private boolean mIsVisible; - - /* package */ WebDialog(BrowserActivity context) { - super(context); - mBrowserActivity = context; - } - - /* dialogs that have cancel buttons can optionally share code by including a - * view with an id of 'done'. - */ - protected void addCancel() { - View button = findViewById(R.id.done); - if (button != null) button.setOnClickListener(mCancelListener); - } - - private View.OnClickListener mCancelListener = new View.OnClickListener() { - public void onClick(View v) { - mBrowserActivity.closeDialogs(); - } - }; - - protected void dismiss() { - startAnimation(AnimationUtils.loadAnimation(mBrowserActivity, - R.anim.dialog_exit)); - mIsVisible = false; - } - - /* - * Remove the soft keyboard from the screen. - */ - protected void hideSoftInput() { - InputMethodManager imm = (InputMethodManager) - mBrowserActivity.getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(mWebView.getWindowToken(), 0); - } - - protected boolean isVisible() { - return mIsVisible; - } - - /* package */ void setWebView(WebView webview) { - mWebView = webview; - } - - protected void show() { - startAnimation(AnimationUtils.loadAnimation(mBrowserActivity, - R.anim.dialog_enter)); - mIsVisible = true; - } - -} diff --git a/src/com/android/browser/WebViewController.java b/src/com/android/browser/WebViewController.java new file mode 100644 index 000000000..eeeee1850 --- /dev/null +++ b/src/com/android/browser/WebViewController.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2010 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 com.android.browser.IntentHandler.UrlData; + +import android.app.Activity; +import android.graphics.Bitmap; +import android.net.Uri; +import android.net.http.SslError; +import android.os.Message; +import android.view.KeyEvent; +import android.view.View; +import android.webkit.HttpAuthHandler; +import android.webkit.SslErrorHandler; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; +import android.webkit.WebView; + +/** + * WebView aspect of the controller + */ +public interface WebViewController { + + Activity getActivity(); + + TabControl getTabControl(); + + WebViewFactory getWebViewFactory(); + + void createSubWindow(Tab tab); + + void onPageStarted(Tab tab, WebView view, String url, Bitmap favicon); + + void onPageFinished(Tab tab, String url); + + void onProgressChanged(Tab tab, int newProgress); + + void onReceivedTitle(Tab tab, final String title); + + void onFavicon(Tab tab, WebView view, Bitmap icon); + + boolean shouldOverrideUrlLoading(WebView view, String url); + + boolean shouldOverrideKeyEvent(KeyEvent event); + + void onUnhandledKeyEvent(KeyEvent event); + + void doUpdateVisitedHistory(Tab tab, String url, boolean isReload); + + void getVisitedHistory(final ValueCallback<String[]> callback); + + void onReceivedHttpAuthRequest(Tab tab, WebView view, final HttpAuthHandler handler, + final String host, final String realm); + + void onDownloadStart(Tab tab, String url, String useragent, String contentDisposition, + String mimeType, long contentLength); + + void showCustomView(Tab tab, View view, WebChromeClient.CustomViewCallback callback); + + void hideCustomView(); + + Bitmap getDefaultVideoPoster(); + + View getVideoLoadingProgressView(); + + void showSslCertificateOnError(WebView view, SslErrorHandler handler, + SslError error); + + void activateVoiceSearchMode(String title); + + void revertVoiceSearchMode(Tab tab); + + boolean shouldShowErrorConsole(); + + void resetTitleAndRevertLockIcon(Tab tab); + + void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType); + + void endActionMode(); + + void attachSubWindow(Tab tab); + + void dismissSubWindow(Tab tab); + + Tab openTabAndShow(UrlData urlData, boolean closeOnExit, String appId); + + boolean switchToTab(int tabindex); + + void closeTab(Tab tab); + + void setupAutoFill(Message message); + +} diff --git a/src/com/android/browser/WebViewFactory.java b/src/com/android/browser/WebViewFactory.java new file mode 100644 index 000000000..1186e65bc --- /dev/null +++ b/src/com/android/browser/WebViewFactory.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2010 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.webkit.WebView; + +/** + * Factory for WebViews + */ +public interface WebViewFactory { + + public WebView createWebView(boolean privateBrowsing); + + public WebView createSubWebView(boolean privateBrowsing); + +} diff --git a/src/com/android/browser/WebsiteSettingsActivity.java b/src/com/android/browser/WebsiteSettingsActivity.java index 1e27092b9..95f8fb02c 100644 --- a/src/com/android/browser/WebsiteSettingsActivity.java +++ b/src/com/android/browser/WebsiteSettingsActivity.java @@ -24,8 +24,9 @@ import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; +import android.os.AsyncTask; import android.os.Bundle; -import android.provider.Browser; +import android.provider.BrowserContract.Bookmarks; import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -242,67 +243,94 @@ public class WebsiteSettingsActivity extends ListActivity { public void populateIcons(Map<String, Site> sites) { // Create a map from host to origin. This is used to add metadata - // (title, icon) for this origin from the bookmarks DB. - HashMap<String, Set<Site>> hosts = new HashMap<String, Set<Site>>(); - Set<Map.Entry<String, Site>> elements = sites.entrySet(); - Iterator<Map.Entry<String, Site>> originIter = elements.iterator(); - while (originIter.hasNext()) { - Map.Entry<String, Site> entry = originIter.next(); - Site site = entry.getValue(); - String host = Uri.parse(entry.getKey()).getHost(); - Set<Site> hostSites = null; - if (hosts.containsKey(host)) { - hostSites = (Set<Site>)hosts.get(host); - } else { - hostSites = new HashSet<Site>(); - hosts.put(host, hostSites); - } - hostSites.add(site); + // (title, icon) for this origin from the bookmarks DB. We must do + // the DB access on a background thread. + new UpdateFromBookmarksDbTask(this.getContext(), sites).execute(); + } + + private class UpdateFromBookmarksDbTask extends AsyncTask<Void, Void, Void> { + + private Context mContext; + private boolean mDataSetChanged; + private Map<String, Site> mSites; + + public UpdateFromBookmarksDbTask(Context ctx, Map<String, Site> sites) { + mContext = ctx; + mSites = sites; } - // Check the bookmark DB. If we have data for a host used by any of - // our origins, use it to set their title and favicon - Cursor c = getContext().getContentResolver().query(Browser.BOOKMARKS_URI, - new String[] { Browser.BookmarkColumns.URL, Browser.BookmarkColumns.TITLE, - Browser.BookmarkColumns.FAVICON }, "bookmark = 1", null, null); - - if (c != null) { - if (c.moveToFirst()) { - int urlIndex = c.getColumnIndex(Browser.BookmarkColumns.URL); - int titleIndex = c.getColumnIndex(Browser.BookmarkColumns.TITLE); - int faviconIndex = c.getColumnIndex(Browser.BookmarkColumns.FAVICON); - do { - String url = c.getString(urlIndex); - String host = Uri.parse(url).getHost(); - if (hosts.containsKey(host)) { - String title = c.getString(titleIndex); - Bitmap bmp = null; - byte[] data = c.getBlob(faviconIndex); - if (data != null) { - bmp = BitmapFactory.decodeByteArray(data, 0, data.length); - } - Set matchingSites = (Set) hosts.get(host); - Iterator<Site> sitesIter = matchingSites.iterator(); - while (sitesIter.hasNext()) { - Site site = sitesIter.next(); - // We should only set the title if the bookmark is for the root - // (i.e. www.google.com), as website settings act on the origin - // as a whole rather than a single page under that origin. If the - // user has bookmarked a page under the root but *not* the root, - // then we risk displaying the title of that page which may or - // may not have any relevance to the origin. - if (url.equals(site.getOrigin()) || - (new String(site.getOrigin()+"/")).equals(url)) { - site.setTitle(title); + protected Void doInBackground(Void... unused) { + HashMap<String, Set<Site>> hosts = new HashMap<String, Set<Site>>(); + Set<Map.Entry<String, Site>> elements = mSites.entrySet(); + Iterator<Map.Entry<String, Site>> originIter = elements.iterator(); + while (originIter.hasNext()) { + Map.Entry<String, Site> entry = originIter.next(); + Site site = entry.getValue(); + String host = Uri.parse(entry.getKey()).getHost(); + Set<Site> hostSites = null; + if (hosts.containsKey(host)) { + hostSites = (Set<Site>)hosts.get(host); + } else { + hostSites = new HashSet<Site>(); + hosts.put(host, hostSites); + } + hostSites.add(site); + } + + // Check the bookmark DB. If we have data for a host used by any of + // our origins, use it to set their title and favicon + Cursor c = mContext.getContentResolver().query(Bookmarks.CONTENT_URI, + new String[] { Bookmarks.URL, Bookmarks.TITLE, Bookmarks.FAVICON }, + Bookmarks.IS_FOLDER + " == 0", null, null); + + if (c != null) { + if (c.moveToFirst()) { + int urlIndex = c.getColumnIndex(Bookmarks.URL); + int titleIndex = c.getColumnIndex(Bookmarks.TITLE); + int faviconIndex = c.getColumnIndex(Bookmarks.FAVICON); + do { + String url = c.getString(urlIndex); + String host = Uri.parse(url).getHost(); + if (hosts.containsKey(host)) { + String title = c.getString(titleIndex); + Bitmap bmp = null; + byte[] data = c.getBlob(faviconIndex); + if (data != null) { + bmp = BitmapFactory.decodeByteArray(data, 0, data.length); } - if (bmp != null) { - site.setIcon(bmp); + Set matchingSites = (Set) hosts.get(host); + Iterator<Site> sitesIter = matchingSites.iterator(); + while (sitesIter.hasNext()) { + Site site = sitesIter.next(); + // We should only set the title if the bookmark is for the root + // (i.e. www.google.com), as website settings act on the origin + // as a whole rather than a single page under that origin. If the + // user has bookmarked a page under the root but *not* the root, + // then we risk displaying the title of that page which may or + // may not have any relevance to the origin. + if (url.equals(site.getOrigin()) || + (new String(site.getOrigin()+"/")).equals(url)) { + mDataSetChanged = true; + site.setTitle(title); + } + + if (bmp != null) { + mDataSetChanged = true; + site.setIcon(bmp); + } } } - } - } while (c.moveToNext()); + } while (c.moveToNext()); + } + c.close(); + } + return null; + } + + protected void onPostExecute(Void unused) { + if (mDataSetChanged) { + notifyDataSetChanged(); } - c.close(); } } diff --git a/src/com/android/browser/preferences/AdvancedPreferencesFragment.java b/src/com/android/browser/preferences/AdvancedPreferencesFragment.java new file mode 100644 index 000000000..59b6ce1f5 --- /dev/null +++ b/src/com/android/browser/preferences/AdvancedPreferencesFragment.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2010 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.preferences; + +import com.android.browser.BrowserSettings; +import com.android.browser.R; +import com.android.browser.WebsiteSettingsActivity; + +import android.content.Intent; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.preference.PreferenceScreen; +import android.webkit.GeolocationPermissions; +import android.webkit.ValueCallback; +import android.webkit.WebStorage; + +import java.util.Map; +import java.util.Set; + +public class AdvancedPreferencesFragment extends PreferenceFragment + implements Preference.OnPreferenceChangeListener { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the XML preferences file + addPreferencesFromResource(R.xml.advanced_preferences); + + PreferenceScreen websiteSettings = (PreferenceScreen) findPreference( + BrowserSettings.PREF_WEBSITE_SETTINGS); + Intent intent = new Intent(getActivity(), WebsiteSettingsActivity.class); + websiteSettings.setIntent(intent); + } + + /* + * We need to set the PreferenceScreen state in onResume(), as the number of + * origins with active features (WebStorage, Geolocation etc) could have + * changed after calling the WebsiteSettingsActivity. + */ + @Override + public void onResume() { + super.onResume(); + final PreferenceScreen websiteSettings = (PreferenceScreen) findPreference( + BrowserSettings.PREF_WEBSITE_SETTINGS); + websiteSettings.setEnabled(false); + WebStorage.getInstance().getOrigins(new ValueCallback<Map>() { + @Override + public void onReceiveValue(Map webStorageOrigins) { + if ((webStorageOrigins != null) && !webStorageOrigins.isEmpty()) { + websiteSettings.setEnabled(true); + } + } + }); + GeolocationPermissions.getInstance().getOrigins(new ValueCallback<Set<String> >() { + @Override + public void onReceiveValue(Set<String> geolocationOrigins) { + if ((geolocationOrigins != null) && !geolocationOrigins.isEmpty()) { + websiteSettings.setEnabled(true); + } + } + }); + } + + @Override + public boolean onPreferenceChange(Preference pref, Object objValue) { + if (pref.getKey().equals(BrowserSettings.PREF_EXTRAS_RESET_DEFAULTS)) { + Boolean value = (Boolean) objValue; + if (value.booleanValue() == true) { + getActivity().finish(); + return true; + } + } + return false; + } +}
\ No newline at end of file diff --git a/src/com/android/browser/preferences/DebugPreferencesFragment.java b/src/com/android/browser/preferences/DebugPreferencesFragment.java new file mode 100644 index 000000000..d643a9718 --- /dev/null +++ b/src/com/android/browser/preferences/DebugPreferencesFragment.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2010 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.preferences; + +import com.android.browser.R; + +import android.os.Bundle; +import android.preference.PreferenceFragment; + +public class DebugPreferencesFragment extends PreferenceFragment { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the XML preferences file + addPreferencesFromResource(R.xml.debug_preferences); + } +} diff --git a/src/com/android/browser/preferences/PageContentPreferencesFragment.java b/src/com/android/browser/preferences/PageContentPreferencesFragment.java new file mode 100644 index 000000000..1b5d0feee --- /dev/null +++ b/src/com/android/browser/preferences/PageContentPreferencesFragment.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2010 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.preferences; + +import com.android.browser.BrowserHomepagePreference; +import com.android.browser.BrowserPreferencesPage; +import com.android.browser.BrowserSettings; +import com.android.browser.R; + +import android.content.res.Resources; +import android.net.Uri; +import android.os.Bundle; +import android.preference.EditTextPreference; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.util.Log; + +public class PageContentPreferencesFragment extends PreferenceFragment + implements Preference.OnPreferenceChangeListener { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.page_content_preferences); + + Preference e = findPreference(BrowserSettings.PREF_HOMEPAGE); + e.setOnPreferenceChangeListener(this); + e.setSummary(getPreferenceScreen().getSharedPreferences() + .getString(BrowserSettings.PREF_HOMEPAGE, null)); + ((BrowserHomepagePreference) e).setCurrentPage( + getActivity().getIntent().getStringExtra(BrowserPreferencesPage.CURRENT_PAGE)); + + e = findPreference(BrowserSettings.PREF_TEXT_SIZE); + e.setOnPreferenceChangeListener(this); + e.setSummary(getVisualTextSizeName( + getPreferenceScreen().getSharedPreferences() + .getString(BrowserSettings.PREF_TEXT_SIZE, null)) ); + + e = findPreference(BrowserSettings.PREF_DEFAULT_ZOOM); + e.setOnPreferenceChangeListener(this); + e.setSummary(getVisualDefaultZoomName( + getPreferenceScreen().getSharedPreferences() + .getString(BrowserSettings.PREF_DEFAULT_ZOOM, null)) ); + + e = findPreference(BrowserSettings.PREF_DEFAULT_TEXT_ENCODING); + e.setOnPreferenceChangeListener(this); + } + + @Override + public boolean onPreferenceChange(Preference pref, Object objValue) { + if (getActivity() == null) { + // We aren't attached, so don't accept preferences changes from the + // invisible UI. + Log.w("PageContentPreferencesFragment", "onPreferenceChange called from detached fragment!"); + return false; + } + + if (pref.getKey().equals(BrowserSettings.PREF_HOMEPAGE)) { + String value = (String) objValue; + boolean needUpdate = value.indexOf(' ') != -1; + if (needUpdate) { + value = value.trim().replace(" ", "%20"); + } + if (value.length() != 0 && Uri.parse(value).getScheme() == null) { + value = "http://" + value; + needUpdate = true; + } + // Set the summary value. + pref.setSummary(value); + if (needUpdate) { + // Update through the EditText control as it has a cached copy + // of the string and it will handle persisting the value + ((EditTextPreference) pref).setText(value); + + // as we update the value above, we need to return false + // here so that setText() is not called by EditTextPref + // with the old value. + return false; + } else { + return true; + } + } else if (pref.getKey().equals(BrowserSettings.PREF_TEXT_SIZE)) { + pref.setSummary(getVisualTextSizeName((String) objValue)); + return true; + } else if (pref.getKey().equals(BrowserSettings.PREF_DEFAULT_ZOOM)) { + pref.setSummary(getVisualDefaultZoomName((String) objValue)); + return true; + } else if (pref.getKey().equals(BrowserSettings.PREF_DEFAULT_TEXT_ENCODING)) { + pref.setSummary((String) objValue); + return true; + } + + return false; + } + + private CharSequence getVisualTextSizeName(String enumName) { + Resources res = getActivity().getResources(); + CharSequence[] visualNames = res.getTextArray(R.array.pref_text_size_choices); + CharSequence[] enumNames = res.getTextArray(R.array.pref_text_size_values); + + // Sanity check + if (visualNames.length != enumNames.length) { + return ""; + } + + int length = enumNames.length; + for (int i = 0; i < length; i++) { + if (enumNames[i].equals(enumName)) { + return visualNames[i]; + } + } + + return ""; + } + + private CharSequence getVisualDefaultZoomName(String enumName) { + Resources res = getActivity().getResources(); + CharSequence[] visualNames = res.getTextArray(R.array.pref_default_zoom_choices); + CharSequence[] enumNames = res.getTextArray(R.array.pref_default_zoom_values); + + // Sanity check + if (visualNames.length != enumNames.length) { + return ""; + } + + int length = enumNames.length; + for (int i = 0; i < length; i++) { + if (enumNames[i].equals(enumName)) { + return visualNames[i]; + } + } + + return ""; + } +}
\ No newline at end of file diff --git a/src/com/android/browser/preferences/PersonalPreferencesFragment.java b/src/com/android/browser/preferences/PersonalPreferencesFragment.java new file mode 100644 index 000000000..a0c8ea022 --- /dev/null +++ b/src/com/android/browser/preferences/PersonalPreferencesFragment.java @@ -0,0 +1,371 @@ +/* + * Copyright (C) 2010 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.preferences; + +import com.android.browser.BrowserBookmarksPage; +import com.android.browser.BrowserSettings; +import com.android.browser.R; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.Fragment; +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.OperationApplicationException; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.RemoteException; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; +import android.preference.PreferenceScreen; +import android.provider.BrowserContract; +import android.provider.BrowserContract.Bookmarks; +import android.provider.BrowserContract.ChromeSyncColumns; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.LinearLayout; + +import java.util.ArrayList; + +public class PersonalPreferencesFragment extends PreferenceFragment + implements OnPreferenceClickListener { + static final String TAG = "PersonalPreferencesFragment"; + + static final String PREF_CHROME_SYNC = "sync_with_chrome"; + + Preference mChromeSync; + boolean mEnabled; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the XML preferences file + addPreferencesFromResource(R.xml.personal_preferences); + } + + @Override + public void onResume() { + super.onResume(); + + // Setup the proper state for the sync with chrome item + Context context = getActivity(); + mChromeSync = findPreference(PREF_CHROME_SYNC); + refreshUi(context); + } + + private class GetAccountsTask extends AsyncTask<Void, Void, Void> { + private Context mContext; + + GetAccountsTask(Context ctx) { + mContext = ctx; + } + + protected Void doInBackground(Void... unused) { + AccountManager am = (AccountManager) mContext.getSystemService(Context.ACCOUNT_SERVICE); + Account[] accounts = am.getAccountsByType("com.google"); + if (accounts == null || accounts.length == 0) { + // No Google accounts setup, don't offer Chrome sync + if (mChromeSync != null) { + getPreferenceScreen().removePreference(mChromeSync); + } + } else { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); + Bundle args = mChromeSync.getExtras(); + args.putParcelableArray("accounts", accounts); + mEnabled = BrowserContract.Settings.isSyncEnabled(mContext); + if (!mEnabled) { + // Google accounts are present, but Chrome sync isn't enabled yet. + // Setup a link to the enable wizard + mChromeSync.setSummary(R.string.pref_personal_sync_with_chrome_summary); + } else { + // Chrome sync is enabled, setup a link to account switcher + String accountName = prefs.getString(BrowserBookmarksPage.PREF_ACCOUNT_NAME, + null); + mChromeSync.setSummary(accountName); + args.putString("curAccount", accountName); + } + mChromeSync.setOnPreferenceClickListener(PersonalPreferencesFragment.this); + } + + return null; + } + } + + void refreshUi(Context context) { + new GetAccountsTask(context).execute(); + + PreferenceScreen autoFillSettings = + (PreferenceScreen)findPreference(BrowserSettings.PREF_AUTOFILL_PROFILE); + autoFillSettings.setDependency(BrowserSettings.PREF_AUTOFILL_ENABLED); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + Fragment frag; + if (mEnabled) { + frag = new AccountChooserDialog(); + } else { + frag = new ImportWizardDialog(); + } + frag.setArguments(preference.getExtras()); + getFragmentManager().openTransaction() + .add(frag, null) + .commit(); + return true; + } + + final class AccountChooserDialog extends DialogFragment + implements DialogInterface.OnClickListener { + + AlertDialog mDialog; + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Bundle args = getArguments(); + Account[] accounts = (Account[]) args.getParcelableArray("accounts"); + String curAccount = args.getString("curAccount"); + int length = accounts.length; + int curAccountOffset = 0; + CharSequence[] accountNames = new CharSequence[length]; + for (int i = 0; i < length; i++) { + String name = accounts[i].name; + if (name.equals(curAccount)) { + curAccountOffset = i; + } + accountNames[i] = name; + } + + mDialog = new AlertDialog.Builder(getActivity()) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle("Choose account") // STOPSHIP localize + .setSingleChoiceItems(accountNames, curAccountOffset, this) + .create(); + return mDialog; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + String accountName = mDialog.getListView().getAdapter().getItem(which).toString(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); + prefs.edit().putString(BrowserBookmarksPage.PREF_ACCOUNT_NAME, accountName).apply(); + refreshUi(getActivity()); + dismiss(); + } + } + + final class ImportWizardDialog extends DialogFragment implements OnClickListener { + View mRemoveButton; + View mCancelButton; + String mDefaultAccount; + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Context context = getActivity(); + Dialog dialog = new Dialog(context); + dialog.setTitle(R.string.import_bookmarks_dialog_title); + dialog.setContentView(R.layout.import_bookmarks_dialog); + mRemoveButton = dialog.findViewById(R.id.remove); + mRemoveButton.setOnClickListener(this); + mCancelButton = dialog.findViewById(R.id.cancel); + mCancelButton.setOnClickListener(this); + + LayoutInflater inflater = dialog.getLayoutInflater(); + LinearLayout accountList = (LinearLayout) dialog.findViewById(R.id.accountList); + Account[] accounts = (Account[]) getArguments().getParcelableArray("accounts"); + mDefaultAccount = accounts[0].name; + int length = accounts.length; + for (int i = 0; i < length; i++) { + Button button = (Button) inflater.inflate(R.layout.import_bookmarks_dialog_button, + null); + button.setText(context.getString(R.string.import_bookmarks_dialog_import, + accounts[i].name)); + button.setTag(accounts[i].name); + button.setOnClickListener(this); + accountList.addView(button); + } + + return dialog; + } + + @Override + public void onClick(View view) { + if (view == mCancelButton) { + dismiss(); + return; + } + + ContentResolver resolver = getActivity().getContentResolver(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); + String accountName; + if (view == mRemoveButton) { + // The user chose to remove their old bookmarks, delete them now + resolver.delete(Bookmarks.CONTENT_URI, + Bookmarks.PARENT + "=1 AND " + Bookmarks.ACCOUNT_NAME + " IS NULL", null); + accountName = mDefaultAccount; + } else { + // The user chose to migrate their old bookmarks to the account they're syncing + accountName = view.getTag().toString(); + migrateBookmarks(resolver, accountName); + } + + // Record the fact that we turned on sync + BrowserContract.Settings.setSyncEnabled(getActivity(), true); + prefs.edit() + .putString(BrowserBookmarksPage.PREF_ACCOUNT_TYPE, "com.google") + .putString(BrowserBookmarksPage.PREF_ACCOUNT_NAME, accountName) + .apply(); + + // Enable bookmark sync on all accounts + Account[] accounts = (Account[]) getArguments().getParcelableArray("accounts"); + for (Account account : accounts) { + ContentResolver.setIsSyncable(account, BrowserContract.AUTHORITY, 1); + } + + refreshUi(getActivity()); + dismiss(); + } + + /** + * Migrates bookmarks to the given account + */ + void migrateBookmarks(ContentResolver resolver, String accountName) { + Cursor cursor = null; + try { + // Re-parent the bookmarks in the default root folder + cursor = resolver.query(Bookmarks.CONTENT_URI, new String[] { Bookmarks._ID }, + Bookmarks.ACCOUNT_NAME + " =? AND " + + ChromeSyncColumns.SERVER_UNIQUE + " =?", + new String[] { accountName, + ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR }, + null); + ContentValues values = new ContentValues(); + if (cursor == null || !cursor.moveToFirst()) { + // The root folders don't exist for the account, create them now + ArrayList<ContentProviderOperation> ops = + new ArrayList<ContentProviderOperation>(); + + // Chrome sync root folder + values.clear(); + values.put(ChromeSyncColumns.SERVER_UNIQUE, ChromeSyncColumns.FOLDER_NAME_ROOT); + values.put(Bookmarks.TITLE, "Google Chrome"); + values.put(Bookmarks.POSITION, 0); + values.put(Bookmarks.IS_FOLDER, true); + values.put(Bookmarks.DIRTY, true); + ops.add(ContentProviderOperation.newInsert( + Bookmarks.CONTENT_URI.buildUpon().appendQueryParameter( + BrowserContract.CALLER_IS_SYNCADAPTER, "true").build()) + .withValues(values) + .build()); + + // Bookmarks folder + values.clear(); + values.put(ChromeSyncColumns.SERVER_UNIQUE, + ChromeSyncColumns.FOLDER_NAME_BOOKMARKS); + values.put(Bookmarks.TITLE, "Bookmarks"); + values.put(Bookmarks.POSITION, 0); + values.put(Bookmarks.IS_FOLDER, true); + values.put(Bookmarks.DIRTY, true); + ops.add(ContentProviderOperation.newInsert(Bookmarks.CONTENT_URI) + .withValues(values) + .withValueBackReference(Bookmarks.PARENT, 0) + .build()); + + // Bookmarks Bar folder + values.clear(); + values.put(ChromeSyncColumns.SERVER_UNIQUE, + ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR); + values.put(Bookmarks.TITLE, "Bookmarks Bar"); + values.put(Bookmarks.POSITION, 0); + values.put(Bookmarks.IS_FOLDER, true); + values.put(Bookmarks.DIRTY, true); + ops.add(ContentProviderOperation.newInsert(Bookmarks.CONTENT_URI) + .withValues(values) + .withValueBackReference(Bookmarks.PARENT, 1) + .build()); + + // Other Bookmarks folder + values.clear(); + values.put(ChromeSyncColumns.SERVER_UNIQUE, + ChromeSyncColumns.FOLDER_NAME_OTHER_BOOKMARKS); + values.put(Bookmarks.TITLE, "Other Bookmarks"); + values.put(Bookmarks.POSITION, 1000); + values.put(Bookmarks.IS_FOLDER, true); + values.put(Bookmarks.DIRTY, true); + ops.add(ContentProviderOperation.newInsert(Bookmarks.CONTENT_URI) + .withValues(values) + .withValueBackReference(Bookmarks.PARENT, 1) + .build()); + + // Re-parent the existing bookmarks to the newly create bookmarks bar folder + ops.add(ContentProviderOperation.newUpdate(Bookmarks.CONTENT_URI) + .withValueBackReference(Bookmarks.PARENT, 2) + .withSelection(Bookmarks.PARENT + "=?", + new String[] { Integer.toString(1) }) + .build()); + + // Mark all non-root folder items as belonging to the new account + values.clear(); + values.put(Bookmarks.ACCOUNT_TYPE, "com.google"); + values.put(Bookmarks.ACCOUNT_NAME, accountName); + ops.add(ContentProviderOperation.newUpdate(Bookmarks.CONTENT_URI) + .withValues(values) + .withSelection(Bookmarks.ACCOUNT_NAME + " IS NULL AND " + + Bookmarks._ID + "<>1", null) + .build()); + + try { + resolver.applyBatch(BrowserContract.AUTHORITY, ops); + } catch (RemoteException e) { + Log.e(TAG, "failed to create root folder for account " + accountName, e); + return; + } catch (OperationApplicationException e) { + Log.e(TAG, "failed to create root folder for account " + accountName, e); + return; + } + } else { + values.put(Bookmarks.PARENT, cursor.getLong(0)); + resolver.update(Bookmarks.CONTENT_URI, values, Bookmarks.PARENT + "=?", + new String[] { Integer.toString(1) }); + + // Mark all bookmarks at all levels as part of the new account + values.clear(); + values.put(Bookmarks.ACCOUNT_TYPE, "com.google"); + values.put(Bookmarks.ACCOUNT_NAME, accountName); + resolver.update(Bookmarks.CONTENT_URI, values, + Bookmarks.ACCOUNT_NAME + " IS NULL AND " + Bookmarks._ID + "<>1", + null); + } + } finally { + if (cursor != null) cursor.close(); + } + } + } +} diff --git a/src/com/android/browser/preferences/PrivacyPreferencesFragment.java b/src/com/android/browser/preferences/PrivacyPreferencesFragment.java new file mode 100644 index 000000000..79f20842e --- /dev/null +++ b/src/com/android/browser/preferences/PrivacyPreferencesFragment.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2010 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.preferences; + +import com.android.browser.BrowserSettings; +import com.android.browser.R; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceFragment; + +public class PrivacyPreferencesFragment extends PreferenceFragment + implements Preference.OnPreferenceChangeListener { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.privacy_preferences); + + Preference e = findPreference(BrowserSettings.PREF_CLEAR_HISTORY); + e.setOnPreferenceChangeListener(this); + } + + @Override + public boolean onPreferenceChange(Preference pref, Object objValue) { + if (pref.getKey().equals(BrowserSettings.PREF_CLEAR_HISTORY) + && ((Boolean) objValue).booleanValue() == true) { + // Need to tell the browser to remove the parent/child relationship + // between tabs + getActivity().setResult(Activity.RESULT_OK, (new Intent()).putExtra(Intent.EXTRA_TEXT, + pref.getKey())); + return true; + } + + return false; + } +} diff --git a/src/com/android/browser/preferences/SecurityPreferencesFragment.java b/src/com/android/browser/preferences/SecurityPreferencesFragment.java new file mode 100644 index 000000000..d20a50c9a --- /dev/null +++ b/src/com/android/browser/preferences/SecurityPreferencesFragment.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2010 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.preferences; + +import com.android.browser.R; + +import android.os.Bundle; +import android.preference.PreferenceFragment; + +public class SecurityPreferencesFragment extends PreferenceFragment { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the XML preferences file + addPreferencesFromResource(R.xml.security_preferences); + } +} diff --git a/src/com/android/browser/provider/BrowserProvider2.java b/src/com/android/browser/provider/BrowserProvider2.java new file mode 100644 index 000000000..8d9f1fe4e --- /dev/null +++ b/src/com/android/browser/provider/BrowserProvider2.java @@ -0,0 +1,1186 @@ +/* + * Copyright (C) 2010 he 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.provider; + +import com.android.browser.R; +import com.android.common.content.SyncStateContentProviderHelper; + +import android.accounts.Account; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.UriMatcher; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.provider.BrowserContract; +import android.provider.BrowserContract.Accounts; +import android.provider.BrowserContract.Bookmarks; +import android.provider.BrowserContract.ChromeSyncColumns; +import android.provider.BrowserContract.Combined; +import android.provider.BrowserContract.History; +import android.provider.BrowserContract.Images; +import android.provider.BrowserContract.Searches; +import android.provider.BrowserContract.Settings; +import android.provider.BrowserContract.SyncState; +import android.provider.ContactsContract.RawContacts; +import android.provider.SyncStateContract; +import android.text.TextUtils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; + +public class BrowserProvider2 extends SQLiteContentProvider { + + static final String LEGACY_AUTHORITY = "browser"; + static final Uri LEGACY_AUTHORITY_URI = new Uri.Builder().authority(LEGACY_AUTHORITY).build(); + + static final String TABLE_BOOKMARKS = "bookmarks"; + static final String TABLE_HISTORY = "history"; + static final String TABLE_IMAGES = "images"; + static final String TABLE_SEARCHES = "searches"; + static final String TABLE_SYNC_STATE = "syncstate"; + static final String TABLE_SETTINGS = "settings"; + static final String VIEW_COMBINED = "combined"; + + static final String TABLE_BOOKMARKS_JOIN_IMAGES = "bookmarks LEFT OUTER JOIN images " + + "ON bookmarks.url = images." + Images.URL; + static final String TABLE_HISTORY_JOIN_IMAGES = "history LEFT OUTER JOIN images " + + "ON history.url = images." + Images.URL; + + static final String DEFAULT_SORT_HISTORY = History.DATE_LAST_VISITED + " DESC"; + + static final String DEFAULT_SORT_SEARCHES = Searches.DATE + " DESC"; + + static final int BOOKMARKS = 1000; + static final int BOOKMARKS_ID = 1001; + static final int BOOKMARKS_FOLDER = 1002; + static final int BOOKMARKS_FOLDER_ID = 1003; + + static final int HISTORY = 2000; + static final int HISTORY_ID = 2001; + + static final int SEARCHES = 3000; + static final int SEARCHES_ID = 3001; + + static final int SYNCSTATE = 4000; + static final int SYNCSTATE_ID = 4001; + + static final int IMAGES = 5000; + + static final int COMBINED = 6000; + static final int COMBINED_ID = 6001; + + static final int ACCOUNTS = 7000; + + static final int SETTINGS = 8000; + + public static final long FIXED_ID_ROOT = 1; + + // Default sort order for unsync'd bookmarks + static final String DEFAULT_BOOKMARKS_SORT_ORDER = + Bookmarks.IS_FOLDER + " DESC, position ASC, _id ASC"; + + // Default sort order for sync'd bookmarks + static final String DEFAULT_BOOKMARKS_SORT_ORDER_SYNC = "position ASC, _id ASC"; + + static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); + + static final HashMap<String, String> ACCOUNTS_PROJECTION_MAP = new HashMap<String, String>(); + static final HashMap<String, String> BOOKMARKS_PROJECTION_MAP = new HashMap<String, String>(); + static final HashMap<String, String> OTHER_BOOKMARKS_PROJECTION_MAP = + new HashMap<String, String>(); + static final HashMap<String, String> HISTORY_PROJECTION_MAP = new HashMap<String, String>(); + static final HashMap<String, String> SYNC_STATE_PROJECTION_MAP = new HashMap<String, String>(); + static final HashMap<String, String> IMAGES_PROJECTION_MAP = new HashMap<String, String>(); + static final HashMap<String, String> COMBINED_PROJECTION_MAP = new HashMap<String, String>(); + static final HashMap<String, String> SEARCHES_PROJECTION_MAP = new HashMap<String, String>(); + static final HashMap<String, String> SETTINGS_PROJECTION_MAP = new HashMap<String, String>(); + + static { + final UriMatcher matcher = URI_MATCHER; + final String authority = BrowserContract.AUTHORITY; + matcher.addURI(authority, "accounts", ACCOUNTS); + matcher.addURI(authority, "bookmarks", BOOKMARKS); + matcher.addURI(authority, "bookmarks/#", BOOKMARKS_ID); + matcher.addURI(authority, "bookmarks/folder", BOOKMARKS_FOLDER); + matcher.addURI(authority, "bookmarks/folder/#", BOOKMARKS_FOLDER_ID); + matcher.addURI(authority, "history", HISTORY); + matcher.addURI(authority, "history/#", HISTORY_ID); + matcher.addURI(authority, "searches", SEARCHES); + matcher.addURI(authority, "searches/#", SEARCHES_ID); + matcher.addURI(authority, "syncstate", SYNCSTATE); + matcher.addURI(authority, "syncstate/#", SYNCSTATE_ID); + matcher.addURI(authority, "images", IMAGES); + matcher.addURI(authority, "combined", COMBINED); + matcher.addURI(authority, "combined/#", COMBINED_ID); + matcher.addURI(authority, "settings", SETTINGS); + + // Projection maps + HashMap<String, String> map; + + // Accounts + map = ACCOUNTS_PROJECTION_MAP; + map.put(Accounts.ACCOUNT_TYPE, Accounts.ACCOUNT_TYPE); + map.put(Accounts.ACCOUNT_NAME, Accounts.ACCOUNT_NAME); + + // Bookmarks + map = BOOKMARKS_PROJECTION_MAP; + map.put(Bookmarks._ID, qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID)); + map.put(Bookmarks.TITLE, Bookmarks.TITLE); + map.put(Bookmarks.URL, Bookmarks.URL); + map.put(Bookmarks.FAVICON, Bookmarks.FAVICON); + map.put(Bookmarks.THUMBNAIL, Bookmarks.THUMBNAIL); + map.put(Bookmarks.TOUCH_ICON, Bookmarks.TOUCH_ICON); + map.put(Bookmarks.IS_FOLDER, Bookmarks.IS_FOLDER); + map.put(Bookmarks.PARENT, Bookmarks.PARENT); + map.put(Bookmarks.POSITION, Bookmarks.POSITION); + map.put(Bookmarks.INSERT_AFTER, Bookmarks.INSERT_AFTER); + map.put(Bookmarks.IS_DELETED, Bookmarks.IS_DELETED); + map.put(Bookmarks.ACCOUNT_NAME, Bookmarks.ACCOUNT_NAME); + map.put(Bookmarks.ACCOUNT_TYPE, Bookmarks.ACCOUNT_TYPE); + map.put(Bookmarks.SOURCE_ID, Bookmarks.SOURCE_ID); + map.put(Bookmarks.VERSION, Bookmarks.VERSION); + map.put(Bookmarks.DATE_CREATED, Bookmarks.DATE_CREATED); + map.put(Bookmarks.DATE_MODIFIED, Bookmarks.DATE_MODIFIED); + map.put(Bookmarks.DIRTY, Bookmarks.DIRTY); + map.put(Bookmarks.SYNC1, Bookmarks.SYNC1); + map.put(Bookmarks.SYNC2, Bookmarks.SYNC2); + map.put(Bookmarks.SYNC3, Bookmarks.SYNC3); + map.put(Bookmarks.SYNC4, Bookmarks.SYNC4); + map.put(Bookmarks.SYNC5, Bookmarks.SYNC5); + map.put(Bookmarks.PARENT_SOURCE_ID, "(SELECT " + Bookmarks.SOURCE_ID + + " FROM " + TABLE_BOOKMARKS + " A WHERE " + + "A." + Bookmarks._ID + "=" + TABLE_BOOKMARKS + "." + Bookmarks.PARENT + + ") AS " + Bookmarks.PARENT_SOURCE_ID); + map.put(Bookmarks.INSERT_AFTER_SOURCE_ID, "(SELECT " + Bookmarks.SOURCE_ID + + " FROM " + TABLE_BOOKMARKS + " A WHERE " + + "A." + Bookmarks._ID + "=" + TABLE_BOOKMARKS + "." + Bookmarks.INSERT_AFTER + + ") AS " + Bookmarks.INSERT_AFTER_SOURCE_ID); + + // Other bookmarks + OTHER_BOOKMARKS_PROJECTION_MAP.putAll(BOOKMARKS_PROJECTION_MAP); + OTHER_BOOKMARKS_PROJECTION_MAP.put(Bookmarks.POSITION, + Long.toString(Long.MAX_VALUE) + " AS " + Bookmarks.POSITION); + + // History + map = HISTORY_PROJECTION_MAP; + map.put(History._ID, qualifyColumn(TABLE_HISTORY, History._ID)); + map.put(History.TITLE, History.TITLE); + map.put(History.URL, History.URL); + map.put(History.FAVICON, History.FAVICON); + map.put(History.THUMBNAIL, History.THUMBNAIL); + map.put(History.TOUCH_ICON, History.TOUCH_ICON); + map.put(History.DATE_CREATED, History.DATE_CREATED); + map.put(History.DATE_LAST_VISITED, History.DATE_LAST_VISITED); + map.put(History.VISITS, History.VISITS); + map.put(History.USER_ENTERED, History.USER_ENTERED); + + // Sync state + map = SYNC_STATE_PROJECTION_MAP; + map.put(SyncState._ID, SyncState._ID); + map.put(SyncState.ACCOUNT_NAME, SyncState.ACCOUNT_NAME); + map.put(SyncState.ACCOUNT_TYPE, SyncState.ACCOUNT_TYPE); + map.put(SyncState.DATA, SyncState.DATA); + + // Images + map = IMAGES_PROJECTION_MAP; + map.put(Images.URL, Images.URL); + map.put(Images.FAVICON, Images.FAVICON); + map.put(Images.THUMBNAIL, Images.THUMBNAIL); + map.put(Images.TOUCH_ICON, Images.TOUCH_ICON); + + // Combined history half + map = COMBINED_PROJECTION_MAP; + map.put(Combined._ID, Combined._ID); + map.put(Combined.TITLE, Combined.TITLE); + map.put(Combined.URL, Combined.URL); + map.put(Combined.DATE_CREATED, Combined.DATE_CREATED); + map.put(Combined.DATE_LAST_VISITED, Combined.DATE_LAST_VISITED); + map.put(Combined.IS_BOOKMARK, Combined.IS_BOOKMARK); + map.put(Combined.VISITS, Combined.VISITS); + map.put(Combined.FAVICON, Combined.FAVICON); + map.put(Combined.THUMBNAIL, Combined.THUMBNAIL); + map.put(Combined.TOUCH_ICON, Combined.TOUCH_ICON); + map.put(Combined.USER_ENTERED, Combined.USER_ENTERED); + + // Searches + map = SEARCHES_PROJECTION_MAP; + map.put(Searches._ID, Searches._ID); + map.put(Searches.SEARCH, Searches.SEARCH); + map.put(Searches.DATE, Searches.DATE); + + // Settings + map = SETTINGS_PROJECTION_MAP; + map.put(Settings.KEY, Settings.KEY); + map.put(Settings.VALUE, Settings.VALUE); + } + + static final String bookmarkOrHistoryColumn(String column) { + return "CASE WHEN bookmarks." + column + " IS NOT NULL THEN " + + "bookmarks." + column + " ELSE history." + column + " END AS " + column; + } + + static final String qualifyColumn(String table, String column) { + return table + "." + column + " AS " + column; + } + + DatabaseHelper mOpenHelper; + SyncStateContentProviderHelper mSyncHelper = new SyncStateContentProviderHelper(); + + final class DatabaseHelper extends SQLiteOpenHelper { + static final String DATABASE_NAME = "browser2.db"; + static final int DATABASE_VERSION = 25; + public DatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + TABLE_BOOKMARKS + "(" + + Bookmarks._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + Bookmarks.TITLE + " TEXT," + + Bookmarks.URL + " TEXT," + + Bookmarks.IS_FOLDER + " INTEGER NOT NULL DEFAULT 0," + + Bookmarks.PARENT + " INTEGER," + + Bookmarks.POSITION + " INTEGER NOT NULL," + + Bookmarks.INSERT_AFTER + " INTEGER," + + Bookmarks.IS_DELETED + " INTEGER NOT NULL DEFAULT 0," + + Bookmarks.ACCOUNT_NAME + " TEXT," + + Bookmarks.ACCOUNT_TYPE + " TEXT," + + Bookmarks.SOURCE_ID + " TEXT," + + Bookmarks.VERSION + " INTEGER NOT NULL DEFAULT 1," + + Bookmarks.DATE_CREATED + " INTEGER," + + Bookmarks.DATE_MODIFIED + " INTEGER," + + Bookmarks.DIRTY + " INTEGER NOT NULL DEFAULT 0," + + Bookmarks.SYNC1 + " TEXT," + + Bookmarks.SYNC2 + " TEXT," + + Bookmarks.SYNC3 + " TEXT," + + Bookmarks.SYNC4 + " TEXT," + + Bookmarks.SYNC5 + " TEXT" + + ");"); + + // TODO indices + + db.execSQL("CREATE TABLE " + TABLE_HISTORY + "(" + + History._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + History.TITLE + " TEXT," + + History.URL + " TEXT NOT NULL," + + History.DATE_CREATED + " INTEGER," + + History.DATE_LAST_VISITED + " INTEGER," + + History.VISITS + " INTEGER NOT NULL DEFAULT 0," + + History.USER_ENTERED + " INTEGER" + + ");"); + + db.execSQL("CREATE TABLE " + TABLE_IMAGES + " (" + + Images.URL + " TEXT UNIQUE NOT NULL," + + Images.FAVICON + " BLOB," + + Images.THUMBNAIL + " BLOB," + + Images.TOUCH_ICON + " BLOB" + + ");"); + db.execSQL("CREATE INDEX imagesUrlIndex ON " + TABLE_IMAGES + + "(" + Images.URL + ")"); + + db.execSQL("CREATE TABLE " + TABLE_SEARCHES + " (" + + Searches._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + Searches.SEARCH + " TEXT," + + Searches.DATE + " LONG" + + ");"); + + db.execSQL("CREATE TABLE " + TABLE_SETTINGS + " (" + + Settings.KEY + " TEXT PRIMARY KEY," + + Settings.VALUE + " TEXT NOT NULL" + + ");"); + + db.execSQL("CREATE VIEW " + VIEW_COMBINED + " AS " + + "SELECT " + + bookmarkOrHistoryColumn(Combined._ID) + ", " + + bookmarkOrHistoryColumn(Combined.TITLE) + ", " + + qualifyColumn(TABLE_HISTORY, Combined.URL) + ", " + + qualifyColumn(TABLE_HISTORY, Combined.DATE_CREATED) + ", " + + Combined.DATE_LAST_VISITED + ", " + + "CASE WHEN bookmarks._id IS NOT NULL THEN 1 ELSE 0 END AS " + Combined.IS_BOOKMARK + ", " + + Combined.VISITS + ", " + + Combined.FAVICON + ", " + + Combined.THUMBNAIL + ", " + + Combined.TOUCH_ICON + ", " + + "NULL AS " + Combined.USER_ENTERED + " "+ + "FROM history LEFT OUTER JOIN bookmarks ON history.url = bookmarks.url LEFT OUTER JOIN images ON history.url = images.url_key " + + + "UNION ALL " + + + "SELECT " + + Combined._ID + ", " + + Combined.TITLE + ", " + + Combined.URL + ", " + + Combined.DATE_CREATED + ", " + + "NULL AS " + Combined.DATE_LAST_VISITED + ", "+ + "1 AS " + Combined.IS_BOOKMARK + ", " + + "0 AS " + Combined.VISITS + ", "+ + Combined.FAVICON + ", " + + Combined.THUMBNAIL + ", " + + Combined.TOUCH_ICON + ", " + + "NULL AS " + Combined.USER_ENTERED + " "+ + "FROM bookmarks LEFT OUTER JOIN images ON bookmarks.url = images.url_key WHERE url NOT IN (SELECT url FROM history)"); + + mSyncHelper.createDatabase(db); + + createDefaultBookmarks(db); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // TODO write upgrade logic + db.execSQL("DROP TABLE IF EXISTS " + TABLE_BOOKMARKS); + db.execSQL("DROP TABLE IF EXISTS " + TABLE_HISTORY); + db.execSQL("DROP TABLE IF EXISTS " + TABLE_SEARCHES); + db.execSQL("DROP TABLE IF EXISTS " + TABLE_IMAGES); + db.execSQL("DROP TABLE IF EXISTS " + TABLE_SETTINGS); + db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED); + mSyncHelper.onAccountsChanged(db, new Account[] {}); // remove all sync info + onCreate(db); + } + + @Override + public void onOpen(SQLiteDatabase db) { + mSyncHelper.onDatabaseOpened(db); + } + + private void createDefaultBookmarks(SQLiteDatabase db) { + ContentValues values = new ContentValues(); + // TODO figure out how to deal with localization for the defaults + + // Bookmarks folder + values.put(Bookmarks._ID, FIXED_ID_ROOT); + values.put(ChromeSyncColumns.SERVER_UNIQUE, ChromeSyncColumns.FOLDER_NAME_BOOKMARKS); + values.put(Bookmarks.TITLE, "Bookmarks"); + values.putNull(Bookmarks.PARENT); + values.put(Bookmarks.POSITION, 0); + values.put(Bookmarks.IS_FOLDER, true); + values.put(Bookmarks.DIRTY, true); + db.insertOrThrow(TABLE_BOOKMARKS, null, values); + + addDefaultBookmarks(db, FIXED_ID_ROOT); + } + + private void addDefaultBookmarks(SQLiteDatabase db, long parentId) { + Resources res = getContext().getResources(); + final CharSequence[] bookmarks = res.getTextArray( + R.array.bookmarks); + int size = bookmarks.length; + TypedArray preloads = res.obtainTypedArray(R.array.bookmark_preloads); + try { + String parent = Long.toString(parentId); + String now = Long.toString(System.currentTimeMillis()); + for (int i = 0; i < size; i = i + 2) { + CharSequence bookmarkDestination = replaceSystemPropertyInString(getContext(), + bookmarks[i + 1]); + db.execSQL("INSERT INTO bookmarks (" + + Bookmarks.TITLE + ", " + + Bookmarks.URL + ", " + + Bookmarks.IS_FOLDER + "," + + Bookmarks.PARENT + "," + + Bookmarks.POSITION + "," + + Bookmarks.DATE_CREATED + + ") VALUES (" + + "'" + bookmarks[i] + "', " + + "'" + bookmarkDestination + "', " + + "0," + + parent + "," + + Integer.toString(i) + "," + + now + + ");"); + + int faviconId = preloads.getResourceId(i, 0); + int thumbId = preloads.getResourceId(i + 1, 0); + byte[] thumb = null, favicon = null; + try { + thumb = readRaw(res, thumbId); + } catch (IOException e) { + } + try { + favicon = readRaw(res, faviconId); + } catch (IOException e) { + } + if (thumb != null || favicon != null) { + ContentValues imageValues = new ContentValues(); + imageValues.put(Images.URL, bookmarkDestination.toString()); + if (favicon != null) { + imageValues.put(Images.FAVICON, favicon); + } + if (thumb != null) { + imageValues.put(Images.THUMBNAIL, thumb); + } + db.insert(TABLE_IMAGES, Images.FAVICON, imageValues); + } + } + } catch (ArrayIndexOutOfBoundsException e) { + } + } + + private byte[] readRaw(Resources res, int id) throws IOException { + InputStream is = res.openRawResource(id); + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int read; + while ((read = is.read(buf)) > 0) { + bos.write(buf, 0, read); + } + bos.flush(); + return bos.toByteArray(); + } finally { + is.close(); + } + } + + // XXX: This is a major hack to remove our dependency on gsf constants and + // its content provider. http://b/issue?id=2425179 + private String getClientId(ContentResolver cr) { + String ret = "android-google"; + Cursor c = null; + try { + c = cr.query(Uri.parse("content://com.google.settings/partner"), + new String[] { "value" }, "name='client_id'", null, null); + if (c != null && c.moveToNext()) { + ret = c.getString(0); + } + } catch (RuntimeException ex) { + // fall through to return the default + } finally { + if (c != null) { + c.close(); + } + } + return ret; + } + + private CharSequence replaceSystemPropertyInString(Context context, CharSequence srcString) { + StringBuffer sb = new StringBuffer(); + int lastCharLoc = 0; + + final String client_id = getClientId(context.getContentResolver()); + + for (int i = 0; i < srcString.length(); ++i) { + char c = srcString.charAt(i); + if (c == '{') { + sb.append(srcString.subSequence(lastCharLoc, i)); + lastCharLoc = i; + inner: + for (int j = i; j < srcString.length(); ++j) { + char k = srcString.charAt(j); + if (k == '}') { + String propertyKeyValue = srcString.subSequence(i + 1, j).toString(); + if (propertyKeyValue.equals("CLIENT_ID")) { + sb.append(client_id); + } else { + sb.append("unknown"); + } + lastCharLoc = j + 1; + i = j; + break inner; + } + } + } + } + if (srcString.length() - lastCharLoc > 0) { + // Put on the tail, if there is one + sb.append(srcString.subSequence(lastCharLoc, srcString.length())); + } + return sb; + } + } + + @Override + public SQLiteOpenHelper getDatabaseHelper(Context context) { + synchronized (this) { + if (mOpenHelper == null) { + mOpenHelper = new DatabaseHelper(context); + } + return mOpenHelper; + } + } + + @Override + public boolean isCallerSyncAdapter(Uri uri) { + return uri.getBooleanQueryParameter(BrowserContract.CALLER_IS_SYNCADAPTER, false); + } + + @Override + public void notifyChange(boolean callerIsSyncAdapter) { + ContentResolver resolver = getContext().getContentResolver(); + resolver.notifyChange(BrowserContract.AUTHORITY_URI, null, !callerIsSyncAdapter); + resolver.notifyChange(LEGACY_AUTHORITY_URI, null, !callerIsSyncAdapter); + } + + @Override + public String getType(Uri uri) { + final int match = URI_MATCHER.match(uri); + switch (match) { + case BOOKMARKS: + return Bookmarks.CONTENT_TYPE; + case BOOKMARKS_ID: + return Bookmarks.CONTENT_ITEM_TYPE; + case HISTORY: + return History.CONTENT_TYPE; + case HISTORY_ID: + return History.CONTENT_ITEM_TYPE; + case SEARCHES: + return Searches.CONTENT_TYPE; + case SEARCHES_ID: + return Searches.CONTENT_ITEM_TYPE; +// case SUGGEST: +// return SearchManager.SUGGEST_MIME_TYPE; + } + return null; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + final int match = URI_MATCHER.match(uri); + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT); + switch (match) { + case ACCOUNTS: { + qb.setTables(TABLE_BOOKMARKS); + qb.setProjectionMap(ACCOUNTS_PROJECTION_MAP); + qb.setDistinct(true); + qb.appendWhere(Bookmarks.ACCOUNT_NAME + " IS NOT NULL"); + break; + } + + case BOOKMARKS_FOLDER_ID: + case BOOKMARKS_ID: + case BOOKMARKS: { + // Only show deleted bookmarks if requested to do so + if (!uri.getBooleanQueryParameter(Bookmarks.QUERY_PARAMETER_SHOW_DELETED, false)) { + selection = DatabaseUtils.concatenateWhere( + Bookmarks.IS_DELETED + "=0", selection); + } + + if (match == BOOKMARKS_ID) { + // Tack on the ID of the specific bookmark requested + selection = DatabaseUtils.concatenateWhere(selection, + TABLE_BOOKMARKS + "." + Bookmarks._ID + "=?"); + selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + } else if (match == BOOKMARKS_FOLDER_ID) { + // Tack on the ID of the specific folder requested + selection = DatabaseUtils.concatenateWhere(selection, + TABLE_BOOKMARKS + "." + Bookmarks.PARENT + "=?"); + selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + } + + // Look for account info + String accountType = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE); + String accountName = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME); + if (!TextUtils.isEmpty(accountType) && !TextUtils.isEmpty(accountName)) { + selection = DatabaseUtils.concatenateWhere(selection, + Bookmarks.ACCOUNT_TYPE + "=? AND " + Bookmarks.ACCOUNT_NAME + "=? "); + selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, + new String[] { accountType, accountName }); + } + + // Set a default sort order if one isn't specified + if (TextUtils.isEmpty(sortOrder)) { + if (!TextUtils.isEmpty(accountType) + && !TextUtils.isEmpty(accountName)) { + sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER_SYNC; + } else { + sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER; + } + } + + qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP); + qb.setTables(TABLE_BOOKMARKS_JOIN_IMAGES); + break; + } + + case BOOKMARKS_FOLDER: { + // Don't allow selections to be applied to the default folder + if (!TextUtils.isEmpty(selection) || selectionArgs != null) { + throw new UnsupportedOperationException( + "selections aren't supported on this URI"); + } + + // Look for an account + boolean useAccount = false; + String accountType = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE); + String accountName = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME); + if (!TextUtils.isEmpty(accountType) && !TextUtils.isEmpty(accountName)) { + useAccount = true; + } + + qb.setTables(TABLE_BOOKMARKS_JOIN_IMAGES); + String[] args; + String query; + // Set a default sort order if one isn't specified + if (TextUtils.isEmpty(sortOrder)) { + if (useAccount) { + sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER_SYNC; + } else { + sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER; + } + } + if (!useAccount) { + qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP); + query = qb.buildQuery(projection, + Bookmarks.PARENT + "=? AND " + Bookmarks.IS_DELETED + "=0", + null, null, null, sortOrder, null); + + args = new String[] { Long.toString(FIXED_ID_ROOT) }; + } else { + qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP); + String bookmarksBarQuery = qb.buildQuery(projection, + Bookmarks.ACCOUNT_TYPE + "=? AND " + Bookmarks.ACCOUNT_NAME + "=? " + + "AND parent = " + + "(SELECT _id FROM " + TABLE_BOOKMARKS + " WHERE " + + ChromeSyncColumns.SERVER_UNIQUE + "=" + + "'" + ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR + "' " + + "AND account_type = ? AND account_name = ?) " + + "AND " + Bookmarks.IS_DELETED + "=0", + null, null, null, null, null); + + qb.setProjectionMap(OTHER_BOOKMARKS_PROJECTION_MAP); + String otherBookmarksQuery = qb.buildQuery(projection, + Bookmarks.ACCOUNT_TYPE + "=? AND " + Bookmarks.ACCOUNT_NAME + "=?" + + " AND " + ChromeSyncColumns.SERVER_UNIQUE + "=?", + null, null, null, null, null); + + query = qb.buildUnionQuery( + new String[] { bookmarksBarQuery, otherBookmarksQuery }, + sortOrder, limit); + + args = new String[] { + accountType, accountName, accountType, accountName, + accountType, accountName, ChromeSyncColumns.FOLDER_NAME_OTHER_BOOKMARKS, + }; + } + + Cursor cursor = db.rawQuery(query, args); + if (cursor != null) { + cursor.setNotificationUri(getContext().getContentResolver(), + BrowserContract.AUTHORITY_URI); + } + return cursor; + } + + case HISTORY_ID: { + selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?"); + selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + // fall through + } + case HISTORY: { + if (sortOrder == null) { + sortOrder = DEFAULT_SORT_HISTORY; + } + qb.setProjectionMap(HISTORY_PROJECTION_MAP); + qb.setTables(TABLE_HISTORY_JOIN_IMAGES); + break; + } + + case SEARCHES_ID: { + selection = DatabaseUtils.concatenateWhere(selection, TABLE_SEARCHES + "._id=?"); + selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + // fall through + } + case SEARCHES: { + if (sortOrder == null) { + sortOrder = DEFAULT_SORT_SEARCHES; + } + qb.setTables(TABLE_SEARCHES); + qb.setProjectionMap(SEARCHES_PROJECTION_MAP); + break; + } + + case SYNCSTATE: { + return mSyncHelper.query(db, projection, selection, selectionArgs, sortOrder); + } + + case SYNCSTATE_ID: { + selection = appendAccountToSelection(uri, selection); + String selectionWithId = + (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") + + (selection == null ? "" : " AND (" + selection + ")"); + return mSyncHelper.query(db, projection, selectionWithId, selectionArgs, sortOrder); + } + + case IMAGES: { + qb.setTables(TABLE_IMAGES); + qb.setProjectionMap(IMAGES_PROJECTION_MAP); + break; + } + + case COMBINED_ID: { + selection = DatabaseUtils.concatenateWhere(selection, VIEW_COMBINED + "._id=?"); + selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + // fall through + } + case COMBINED: { + qb.setTables(VIEW_COMBINED); + qb.setProjectionMap(COMBINED_PROJECTION_MAP); + break; + } + + case SETTINGS: { + qb.setTables(TABLE_SETTINGS); + qb.setProjectionMap(SETTINGS_PROJECTION_MAP); + break; + } + + default: { + throw new UnsupportedOperationException("Unknown URL " + uri.toString()); + } + } + + Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder, + limit); + cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.AUTHORITY_URI); + return cursor; + } + + @Override + public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs, + boolean callerIsSyncAdapter) { + final int match = URI_MATCHER.match(uri); + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + switch (match) { + case BOOKMARKS_ID: + case BOOKMARKS: { + //TODO cascade deletes down from folders + if (!callerIsSyncAdapter) { + // If the caller isn't a sync adapter just go through and update all the + // bookmarks to have the deleted flag set. + ContentValues values = new ContentValues(); + values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis()); + values.put(Bookmarks.IS_DELETED, 1); + return updateInTransaction(uri, values, selection, selectionArgs, + callerIsSyncAdapter); + } else { + // Sync adapters are allowed to actually delete things + if (match == BOOKMARKS_ID) { + selection = DatabaseUtils.concatenateWhere(selection, + TABLE_BOOKMARKS + "._id=?"); + selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + } + return db.delete(TABLE_BOOKMARKS, selection, selectionArgs); + } + } + + case HISTORY_ID: { + selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?"); + selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + // fall through + } + case HISTORY: { + return db.delete(TABLE_HISTORY, selection, selectionArgs); + } + + case SEARCHES_ID: { + selection = DatabaseUtils.concatenateWhere(selection, TABLE_SEARCHES + "._id=?"); + selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + // fall through + } + case SEARCHES: { + return db.delete(TABLE_SEARCHES, selection, selectionArgs); + } + + case SYNCSTATE: { + return mSyncHelper.delete(db, selection, selectionArgs); + } + case SYNCSTATE_ID: { + String selectionWithId = + (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") + + (selection == null ? "" : " AND (" + selection + ")"); + return mSyncHelper.delete(db, selectionWithId, selectionArgs); + } + } + throw new UnsupportedOperationException("Unknown update URI " + uri); + } + + @Override + public Uri insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { + final int match = URI_MATCHER.match(uri); + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + long id = -1; + switch (match) { + case BOOKMARKS: { + // Mark rows dirty if they're not coming from a sync adapter + if (!callerIsSyncAdapter) { + long now = System.currentTimeMillis(); + values.put(Bookmarks.DATE_CREATED, now); + values.put(Bookmarks.DATE_MODIFIED, now); + values.put(Bookmarks.DIRTY, 1); + + // If no parent is set default to the "Bookmarks Bar" folder + // TODO set the parent based on the account info + if (!values.containsKey(Bookmarks.PARENT)) { + values.put(Bookmarks.PARENT, FIXED_ID_ROOT); + } + } + + // If no position is requested put the bookmark at the beginning of the list + if (!values.containsKey(Bookmarks.POSITION)) { + values.put(Bookmarks.POSITION, Long.toString(Long.MIN_VALUE)); + } + + // Extract out the image values so they can be inserted into the images table + String url = values.getAsString(Bookmarks.URL); + ContentValues imageValues = extractImageValues(values, url); + Boolean isFolder = values.getAsBoolean(Bookmarks.IS_FOLDER); + if ((isFolder == null || !isFolder) + && imageValues != null && !TextUtils.isEmpty(url)) { + int count = db.update(TABLE_IMAGES, imageValues, Images.URL + "=?", + new String[] { url }); + if (count == 0) { + db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, imageValues); + } + } + + id = db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.DIRTY, values); + break; + } + + case HISTORY: { + // If no created time is specified set it to now + if (!values.containsKey(History.DATE_CREATED)) { + values.put(History.DATE_CREATED, System.currentTimeMillis()); + } + + // Extract out the image values so they can be inserted into the images table + ContentValues imageValues = extractImageValues(values, + values.getAsString(History.URL)); + if (imageValues != null) { + db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, imageValues); + } + + id = db.insertOrThrow(TABLE_HISTORY, History.VISITS, values); + break; + } + + case SEARCHES: { + id = insertSearchesInTransaction(db, values); + break; + } + + case SYNCSTATE: { + id = mSyncHelper.insert(db, values); + break; + } + + case SETTINGS: { + id = 0; + insertSettingsInTransaction(db, values); + break; + } + + default: { + throw new UnsupportedOperationException("Unknown insert URI " + uri); + } + } + + if (id >= 0) { + return ContentUris.withAppendedId(uri, id); + } else { + return null; + } + } + + /** + * Searches are unique, so perform an UPSERT manually since SQLite doesn't support them. + */ + private long insertSearchesInTransaction(SQLiteDatabase db, ContentValues values) { + String search = values.getAsString(Searches.SEARCH); + if (TextUtils.isEmpty(search)) { + throw new IllegalArgumentException("Must include the SEARCH field"); + } + Cursor cursor = null; + try { + cursor = db.query(TABLE_SEARCHES, new String[] { Searches._ID }, + Searches.SEARCH + "=?", new String[] { search }, null, null, null); + if (cursor.moveToNext()) { + long id = cursor.getLong(0); + db.update(TABLE_SEARCHES, values, Searches._ID + "=?", + new String[] { Long.toString(id) }); + return id; + } else { + return db.insertOrThrow(TABLE_SEARCHES, Searches.SEARCH, values); + } + } finally { + if (cursor != null) cursor.close(); + } + } + + /** + * Settings are unique, so perform an UPSERT manually since SQLite doesn't support them. + */ + private long insertSettingsInTransaction(SQLiteDatabase db, ContentValues values) { + String key = values.getAsString(Settings.KEY); + if (TextUtils.isEmpty(key)) { + throw new IllegalArgumentException("Must include the KEY field"); + } + String[] keyArray = new String[] { key }; + Cursor cursor = null; + try { + cursor = db.query(TABLE_SETTINGS, new String[] { Settings.KEY }, + Settings.KEY + "=?", keyArray, null, null, null); + if (cursor.moveToNext()) { + long id = cursor.getLong(0); + db.update(TABLE_SETTINGS, values, Settings.KEY + "=?", keyArray); + return id; + } else { + return db.insertOrThrow(TABLE_SETTINGS, Settings.VALUE, values); + } + } finally { + if (cursor != null) cursor.close(); + } + } + + @Override + public int updateInTransaction(Uri uri, ContentValues values, String selection, + String[] selectionArgs, boolean callerIsSyncAdapter) { + final int match = URI_MATCHER.match(uri); + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + switch (match) { + case BOOKMARKS_ID: { + selection = DatabaseUtils.concatenateWhere(selection, + TABLE_BOOKMARKS + "._id=?"); + selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + // fall through + } + case BOOKMARKS: { + return updateBookmarksInTransaction(values, selection, selectionArgs, + callerIsSyncAdapter); + } + + case HISTORY_ID: { + selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?"); + selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + // fall through + } + case HISTORY: { + return updateHistoryInTransaction(values, selection, selectionArgs); + } + + case SYNCSTATE: { + return mSyncHelper.update(mDb, values, + appendAccountToSelection(uri, selection), selectionArgs); + } + + case SYNCSTATE_ID: { + selection = appendAccountToSelection(uri, selection); + String selectionWithId = + (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") + + (selection == null ? "" : " AND (" + selection + ")"); + return mSyncHelper.update(mDb, values, + selectionWithId, selectionArgs); + } + + case IMAGES: { + String url = values.getAsString(Images.URL); + if (TextUtils.isEmpty(url)) { + throw new IllegalArgumentException("Images.URL is required"); + } + int count = db.update(TABLE_IMAGES, values, Images.URL + "=?", + new String[] { url }); + if (count == 0) { + db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, values); + count = 1; + } + return count; + } + } + throw new UnsupportedOperationException("Unknown update URI " + uri); + } + + /** + * Does a query to find the matching bookmarks and updates each one with the provided values. + */ + int updateBookmarksInTransaction(ContentValues values, String selection, + String[] selectionArgs, boolean callerIsSyncAdapter) { + int count = 0; + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + Cursor cursor = query(Bookmarks.CONTENT_URI, + new String[] { Bookmarks._ID, Bookmarks.VERSION, Bookmarks.URL }, + selection, selectionArgs, null); + try { + String[] args = new String[1]; + // Mark the bookmark dirty if the caller isn't a sync adapter + if (!callerIsSyncAdapter) { + values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis()); + values.put(Bookmarks.DIRTY, 1); + } + + boolean updatingUrl = values.containsKey(Bookmarks.URL); + String url = null; + if (updatingUrl) { + url = values.getAsString(Bookmarks.URL); + } + ContentValues imageValues = extractImageValues(values, url); + + while (cursor.moveToNext()) { + args[0] = cursor.getString(0); + if (!callerIsSyncAdapter) { + // increase the local version for non-sync changes + values.put(Bookmarks.VERSION, cursor.getLong(1) + 1); + } + count += db.update(TABLE_BOOKMARKS, values, "_id=?", args); + + // Update the images over in their table + if (imageValues != null) { + if (!updatingUrl) { + url = cursor.getString(2); + imageValues.put(Images.URL, url); + } + + if (!TextUtils.isEmpty(url)) { + args[0] = url; + if (db.update(TABLE_IMAGES, imageValues, Images.URL + "=?", args) == 0) { + db.insert(TABLE_IMAGES, Images.FAVICON, imageValues); + } + } + } + } + } finally { + if (cursor != null) cursor.close(); + } + return count; + } + + /** + * Does a query to find the matching bookmarks and updates each one with the provided values. + */ + int updateHistoryInTransaction(ContentValues values, String selection, String[] selectionArgs) { + int count = 0; + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + Cursor cursor = query(History.CONTENT_URI, + new String[] { History._ID, History.URL }, + selection, selectionArgs, null); + try { + String[] args = new String[1]; + + boolean updatingUrl = values.containsKey(History.URL); + String url = null; + if (updatingUrl) { + url = values.getAsString(History.URL); + } + ContentValues imageValues = extractImageValues(values, url); + + while (cursor.moveToNext()) { + args[0] = cursor.getString(0); + count += db.update(TABLE_HISTORY, values, "_id=?", args); + + // Update the images over in their table + if (imageValues != null) { + if (!updatingUrl) { + url = cursor.getString(1); + imageValues.put(Images.URL, url); + } + args[0] = url; + if (db.update(TABLE_IMAGES, imageValues, Images.URL + "=?", args) == 0) { + db.insert(TABLE_IMAGES, Images.FAVICON, imageValues); + } + } + } + } finally { + if (cursor != null) cursor.close(); + } + return count; + } + + String appendAccountToSelection(Uri uri, String selection) { + final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); + final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE); + + final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); + if (partialUri) { + // Throw when either account is incomplete + throw new IllegalArgumentException( + "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE for " + uri); + } + + // Accounts are valid by only checking one parameter, since we've + // already ruled out partial accounts. + final boolean validAccount = !TextUtils.isEmpty(accountName); + if (validAccount) { + StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "=" + + DatabaseUtils.sqlEscapeString(accountName) + " AND " + + RawContacts.ACCOUNT_TYPE + "=" + + DatabaseUtils.sqlEscapeString(accountType)); + if (!TextUtils.isEmpty(selection)) { + selectionSb.append(" AND ("); + selectionSb.append(selection); + selectionSb.append(')'); + } + return selectionSb.toString(); + } else { + return selection; + } + } + + ContentValues extractImageValues(ContentValues values, String url) { + ContentValues imageValues = null; + // favicon + if (values.containsKey(Bookmarks.FAVICON)) { + imageValues = new ContentValues(); + imageValues.put(Images.FAVICON, values.getAsByteArray(Bookmarks.FAVICON)); + values.remove(Bookmarks.FAVICON); + } + + // thumbnail + if (values.containsKey(Bookmarks.THUMBNAIL)) { + if (imageValues == null) { + imageValues = new ContentValues(); + } + imageValues.put(Images.THUMBNAIL, values.getAsByteArray(Bookmarks.THUMBNAIL)); + values.remove(Bookmarks.THUMBNAIL); + } + + // touch icon + if (values.containsKey(Bookmarks.TOUCH_ICON)) { + if (imageValues == null) { + imageValues = new ContentValues(); + } + imageValues.put(Images.TOUCH_ICON, values.getAsByteArray(Bookmarks.TOUCH_ICON)); + values.remove(Bookmarks.TOUCH_ICON); + } + + if (imageValues != null) { + imageValues.put(Images.URL, url); + } + return imageValues; + } +} diff --git a/src/com/android/browser/provider/SQLiteContentProvider.java b/src/com/android/browser/provider/SQLiteContentProvider.java new file mode 100644 index 000000000..a50894a41 --- /dev/null +++ b/src/com/android/browser/provider/SQLiteContentProvider.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2009 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.provider; + +import android.content.ContentProvider; +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteTransactionListener; +import android.net.Uri; + +import java.util.ArrayList; + +/** + * General purpose {@link ContentProvider} base class that uses SQLiteDatabase for storage. + */ +public abstract class SQLiteContentProvider extends ContentProvider + implements SQLiteTransactionListener { + + private static final String TAG = "SQLiteContentProvider"; + + private SQLiteOpenHelper mOpenHelper; + private volatile boolean mNotifyChange; + protected SQLiteDatabase mDb; + + private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>(); + private static final int SLEEP_AFTER_YIELD_DELAY = 4000; + + /** + * Maximum number of operations allowed in a batch between yield points. + */ + private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500; + + @Override + public boolean onCreate() { + Context context = getContext(); + mOpenHelper = getDatabaseHelper(context); + return true; + } + + /** + * Returns a {@link SQLiteOpenHelper} that can open the database. + */ + public abstract SQLiteOpenHelper getDatabaseHelper(Context context); + + /** + * The equivalent of the {@link #insert} method, but invoked within a transaction. + */ + public abstract Uri insertInTransaction(Uri uri, ContentValues values, + boolean callerIsSyncAdapter); + + /** + * The equivalent of the {@link #update} method, but invoked within a transaction. + */ + public abstract int updateInTransaction(Uri uri, ContentValues values, String selection, + String[] selectionArgs, boolean callerIsSyncAdapter); + + /** + * The equivalent of the {@link #delete} method, but invoked within a transaction. + */ + public abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs, + boolean callerIsSyncAdapter); + + /** + * Called when the provider needs to notify the system of a change. + * @param callerIsSyncAdapter true if the caller that caused the change was a sync adapter. + */ + public abstract void notifyChange(boolean callerIsSyncAdapter); + + public boolean isCallerSyncAdapter(Uri uri) { + return false; + } + + public SQLiteOpenHelper getDatabaseHelper() { + return mOpenHelper; + } + + private boolean applyingBatch() { + return mApplyingBatch.get() != null && mApplyingBatch.get(); + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + Uri result = null; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + boolean applyingBatch = applyingBatch(); + if (!applyingBatch) { + mDb = mOpenHelper.getWritableDatabase(); + mDb.beginTransactionWithListener(this); + try { + result = insertInTransaction(uri, values, callerIsSyncAdapter); + if (result != null) { + mNotifyChange = true; + } + mDb.setTransactionSuccessful(); + } finally { + mDb.endTransaction(); + } + + onEndTransaction(callerIsSyncAdapter); + } else { + result = insertInTransaction(uri, values, callerIsSyncAdapter); + if (result != null) { + mNotifyChange = true; + } + } + return result; + } + + @Override + public int bulkInsert(Uri uri, ContentValues[] values) { + int numValues = values.length; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + mDb = mOpenHelper.getWritableDatabase(); + mDb.beginTransactionWithListener(this); + try { + for (int i = 0; i < numValues; i++) { + Uri result = insertInTransaction(uri, values[i], callerIsSyncAdapter); + if (result != null) { + mNotifyChange = true; + } + mDb.yieldIfContendedSafely(); + } + mDb.setTransactionSuccessful(); + } finally { + mDb.endTransaction(); + } + + onEndTransaction(callerIsSyncAdapter); + return numValues; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + int count = 0; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + boolean applyingBatch = applyingBatch(); + if (!applyingBatch) { + mDb = mOpenHelper.getWritableDatabase(); + mDb.beginTransactionWithListener(this); + try { + count = updateInTransaction(uri, values, selection, selectionArgs, + callerIsSyncAdapter); + if (count > 0) { + mNotifyChange = true; + } + mDb.setTransactionSuccessful(); + } finally { + mDb.endTransaction(); + } + + onEndTransaction(callerIsSyncAdapter); + } else { + count = updateInTransaction(uri, values, selection, selectionArgs, callerIsSyncAdapter); + if (count > 0) { + mNotifyChange = true; + } + } + + return count; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + int count = 0; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + boolean applyingBatch = applyingBatch(); + if (!applyingBatch) { + mDb = mOpenHelper.getWritableDatabase(); + mDb.beginTransactionWithListener(this); + try { + count = deleteInTransaction(uri, selection, selectionArgs, callerIsSyncAdapter); + if (count > 0) { + mNotifyChange = true; + } + mDb.setTransactionSuccessful(); + } finally { + mDb.endTransaction(); + } + + onEndTransaction(callerIsSyncAdapter); + } else { + count = deleteInTransaction(uri, selection, selectionArgs, callerIsSyncAdapter); + if (count > 0) { + mNotifyChange = true; + } + } + return count; + } + + @Override + public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) + throws OperationApplicationException { + int ypCount = 0; + int opCount = 0; + boolean callerIsSyncAdapter = false; + mDb = mOpenHelper.getWritableDatabase(); + mDb.beginTransactionWithListener(this); + try { + mApplyingBatch.set(true); + final int numOperations = operations.size(); + final ContentProviderResult[] results = new ContentProviderResult[numOperations]; + for (int i = 0; i < numOperations; i++) { + if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) { + throw new OperationApplicationException( + "Too many content provider operations between yield points. " + + "The maximum number of operations per yield point is " + + MAX_OPERATIONS_PER_YIELD_POINT, ypCount); + } + final ContentProviderOperation operation = operations.get(i); + if (!callerIsSyncAdapter && isCallerSyncAdapter(operation.getUri())) { + callerIsSyncAdapter = true; + } + if (i > 0 && operation.isYieldAllowed()) { + opCount = 0; + if (mDb.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY)) { + ypCount++; + } + } + results[i] = operation.apply(this, results, i); + } + mDb.setTransactionSuccessful(); + return results; + } finally { + mApplyingBatch.set(false); + mDb.endTransaction(); + onEndTransaction(callerIsSyncAdapter); + } + } + + @Override + public void onBegin() { + onBeginTransaction(); + } + + @Override + public void onCommit() { + beforeTransactionCommit(); + } + + @Override + public void onRollback() { + // not used + } + + protected void onBeginTransaction() { + } + + protected void beforeTransactionCommit() { + } + + protected void onEndTransaction(boolean callerIsSyncAdapter) { + if (mNotifyChange) { + mNotifyChange = false; + notifyChange(callerIsSyncAdapter); + } + } +} diff --git a/src/com/android/browser/search/.DefaultSearchEngine.java.swp b/src/com/android/browser/search/.DefaultSearchEngine.java.swp Binary files differnew file mode 100644 index 000000000..441153c4a --- /dev/null +++ b/src/com/android/browser/search/.DefaultSearchEngine.java.swp diff --git a/src/com/android/browser/widget/BookmarkListWidgetProvider.java b/src/com/android/browser/widget/BookmarkListWidgetProvider.java new file mode 100644 index 000000000..04f7b07ee --- /dev/null +++ b/src/com/android/browser/widget/BookmarkListWidgetProvider.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2010 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.widget; + +import com.android.browser.BrowserActivity; +import com.android.browser.R; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; +import android.widget.RemoteViews; + +/** + * Widget that shows a preview of the user's bookmarks. + */ +public class BookmarkListWidgetProvider extends AppWidgetProvider { + static final String ACTION_BOOKMARK_APPWIDGET_UPDATE = + "com.android.browser.BOOKMARK_APPWIDGET_UPDATE"; + + /** + * {@inheritDoc} + */ + @Override + public void onReceive(Context context, Intent intent) { + // Handle bookmark-specific updates ourselves because they might be + // coming in without extras, which AppWidgetProvider then blocks. + final String action = intent.getAction(); + if (ACTION_BOOKMARK_APPWIDGET_UPDATE.equals(action)) { + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); + performUpdate(context, appWidgetManager, + appWidgetManager.getAppWidgetIds(getComponentName(context))); + } else { + super.onReceive(context, intent); + } + } + + @Override + public void onUpdate(Context context, AppWidgetManager mngr, int[] ids) { + performUpdate(context, mngr, ids); + } + + @Override + public void onEnabled(Context context) { + // Start the backing service + context.startService(new Intent(context, BookmarkListWidgetService.class)); + } + + @Override + public void onDisabled(Context context) { + // Stop the backing service + context.stopService(new Intent(context, BookmarkListWidgetService.class)); + } + + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + super.onDeleted(context, appWidgetIds); + context.startService(new Intent(BookmarkListWidgetService.ACTION_REMOVE_FACTORIES, + null, context, BookmarkListWidgetService.class) + .putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds)); + } + + private void performUpdate(Context context, + AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // Update the widgets + for (int appWidgetId : appWidgetIds) { + Intent updateIntent = new Intent(context, BookmarkListWidgetService.class); + updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + updateIntent.setData(Uri.parse(updateIntent.toUri(Intent.URI_INTENT_SCHEME))); + RemoteViews views = new RemoteViews(context.getPackageName(), + R.layout.bookmarklistwidget); + views.setRemoteAdapter(R.id.bookmarks_list, updateIntent); + appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.bookmarks_list); + Intent ic = new Intent(context, BookmarkListWidgetService.class); + views.setPendingIntentTemplate(R.id.bookmarks_list, + PendingIntent.getService(context, 0, ic, + PendingIntent.FLAG_UPDATE_CURRENT)); + Intent launch = new Intent(context, BrowserActivity.class); + views.setOnClickPendingIntent(R.id.header, PendingIntent + .getActivity(context, 0, launch, PendingIntent.FLAG_CANCEL_CURRENT)); + appWidgetManager.updateAppWidget(appWidgetId, views); + } + } + + /** + * Build {@link ComponentName} describing this specific + * {@link AppWidgetProvider} + */ + static ComponentName getComponentName(Context context) { + return new ComponentName(context, BookmarkListWidgetProvider.class); + } +} diff --git a/src/com/android/browser/widget/BookmarkListWidgetService.java b/src/com/android/browser/widget/BookmarkListWidgetService.java new file mode 100644 index 000000000..14b52b758 --- /dev/null +++ b/src/com/android/browser/widget/BookmarkListWidgetService.java @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2010 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.widget; + +import com.android.browser.BrowserBookmarksPage; +import com.android.browser.R; + +import android.appwidget.AppWidgetManager; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.ContentObserver; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.graphics.BitmapFactory.Options; +import android.net.Uri; +import android.os.Binder; +import android.os.Handler; +import android.os.HandlerThread; +import android.preference.PreferenceManager; +import android.provider.BrowserContract; +import android.provider.BrowserContract.Bookmarks; +import android.text.TextUtils; +import android.util.Log; +import android.widget.RemoteViews; +import android.widget.RemoteViewsService; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Stack; + +public class BookmarkListWidgetService extends RemoteViewsService { + + static final String TAG = "BookmarkListWidgetService"; + static final boolean USE_FOLDERS = true; + + static final String ACTION_REMOVE_FACTORIES + = "com.android.browser.widget.REMOVE_FACTORIES"; + static final String ACTION_CHANGE_FOLDER + = "com.android.browser.widget.CHANGE_FOLDER"; + + private static final String[] PROJECTION = new String[] { + BrowserContract.Bookmarks._ID, + BrowserContract.Bookmarks.TITLE, + BrowserContract.Bookmarks.URL, + BrowserContract.Bookmarks.FAVICON, + BrowserContract.Bookmarks.IS_FOLDER, + BrowserContract.Bookmarks.PARENT, + BrowserContract.Bookmarks.POSITION}; + + private Map<Integer, BookmarkFactory> mFactories; + private Handler mUiHandler; + private BookmarksObserver mBookmarksObserver; + + @Override + public void onCreate() { + super.onCreate(); + mFactories = new HashMap<Integer, BookmarkFactory>(); + mUiHandler = new Handler(); + mBookmarksObserver = new BookmarksObserver(mUiHandler); + getContentResolver().registerContentObserver( + BrowserContract.AUTHORITY_URI, true, mBookmarksObserver); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + String action = intent.getAction(); + if (Intent.ACTION_VIEW.equals(action)) { + Intent view = new Intent(intent); + view.setComponent(null); + startActivity(view); + } else if (ACTION_REMOVE_FACTORIES.equals(action)) { + int[] ids = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS); + if (ids != null) { + for (int id : ids) { + mFactories.remove(id); + } + } + } else if (ACTION_CHANGE_FOLDER.equals(action)) { + int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); + long folderId = intent.getLongExtra(Bookmarks._ID, -1); + BookmarkFactory fac = mFactories.get(widgetId); + if (fac != null && folderId >= 0) { + fac.changeFolder(folderId); + AppWidgetManager.getInstance(this).notifyAppWidgetViewDataChanged(widgetId, R.id.bookmarks_list); + } + } + return START_STICKY; + } + + @Override + public void onDestroy() { + super.onDestroy(); + getContentResolver().unregisterContentObserver(mBookmarksObserver); + } + + private class BookmarksObserver extends ContentObserver { + public BookmarksObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + + // Update all the bookmark widgets + sendBroadcast(new Intent( + BookmarkListWidgetProvider.ACTION_BOOKMARK_APPWIDGET_UPDATE, + null, BookmarkListWidgetService.this, + BookmarkListWidgetProvider.class)); + } + } + + @Override + public RemoteViewsFactory onGetViewFactory(Intent intent) { + int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); + if (widgetId < 0) { + Log.w(TAG, "Missing EXTRA_APPWIDGET_ID!"); + return null; + } else { + BookmarkFactory fac = new BookmarkFactory(this, widgetId); + mFactories.put(widgetId, fac); + return fac; + } + } + + private static class Breadcrumb { + long mId; + String mTitle; + public Breadcrumb(long id, String title) { + mId = id; + mTitle = title; + } + } + + static class BookmarkFactory implements RemoteViewsService.RemoteViewsFactory { + private List<RenderResult> mBookmarks; + private Context mContext; + private int mWidgetId; + private String mAccountType; + private String mAccountName; + private Stack<Breadcrumb> mBreadcrumbs; + + public BookmarkFactory(Context context, int widgetId) { + mBreadcrumbs = new Stack<Breadcrumb>(); + mContext = context; + mWidgetId = widgetId; + } + + void changeFolder(long folderId) { + if (mBookmarks == null) return; + + if (!mBreadcrumbs.empty() && mBreadcrumbs.peek().mId == folderId) { + mBreadcrumbs.pop(); + return; + } + + for (RenderResult res : mBookmarks) { + if (res.mId == folderId) { + mBreadcrumbs.push(new Breadcrumb(res.mId, res.mTitle)); + break; + } + } + } + + @Override + public int getCount() { + if (mBookmarks == null) + return 0; + return mBookmarks.size(); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public RemoteViews getLoadingView() { + return null; + } + + @Override + public RemoteViews getViewAt(int position) { + if (position < 0 || position >= getCount()) { + return null; + } + + RenderResult res = mBookmarks.get(position); + Breadcrumb folder = mBreadcrumbs.empty() ? null : mBreadcrumbs.peek(); + + RemoteViews views = new RemoteViews( + mContext.getPackageName(), R.layout.bookmarklistwidget_item); + Intent fillin; + if (res.mIsFolder) { + long nfi = res.mId; + fillin = new Intent(ACTION_CHANGE_FOLDER, null, + mContext, BookmarkListWidgetService.class) + .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId) + .putExtra(Bookmarks._ID, nfi); + } else { + fillin = new Intent(Intent.ACTION_VIEW) + .setData(Uri.parse(res.mUrl)) + .addCategory(Intent.CATEGORY_BROWSABLE); + } + views.setOnClickFillInIntent(R.id.list_item, fillin); + // Set the title of the bookmark. Use the url as a backup. + String displayTitle = res.mTitle; + if (TextUtils.isEmpty(displayTitle)) { + // The browser always requires a title for bookmarks, but jic... + displayTitle = res.mUrl; + } + views.setTextViewText(R.id.label, displayTitle); + views.setDrawableParameters(R.id.list_item, true, 0, -1, null, -1); + if (res.mIsFolder) { + if (folder != null && res.mId == folder.mId) { + views.setDrawableParameters(R.id.list_item, true, 255, -1, null, -1); + views.setImageViewResource(R.id.thumb, R.drawable.ic_back_normal); + } else { + views.setImageViewResource(R.id.thumb, R.drawable.ic_folder); + } + } else { + if (res.mBitmap != null) { + views.setImageViewBitmap(R.id.thumb, res.mBitmap); + } else { + views.setImageViewResource(R.id.thumb, + R.drawable.browser_thumbnail); + } + } + return views; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public boolean hasStableIds() { + return false; + } + + @Override + public void onCreate() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); + mAccountType = prefs.getString(BrowserBookmarksPage.PREF_ACCOUNT_TYPE, null); + mAccountName = prefs.getString(BrowserBookmarksPage.PREF_ACCOUNT_NAME, null); + loadData(); + } + + @Override + public void onDestroy() { + recycleBitmaps(); + } + + @Override + public void onDataSetChanged() { + loadData(); + } + + void loadData() { + // Reset identity since this could be an IPC call + long token = Binder.clearCallingIdentity(); + update(); + Binder.restoreCallingIdentity(token); + } + + void update() { + recycleBitmaps(); + String where = null; + Breadcrumb folder = mBreadcrumbs.empty() ? null : mBreadcrumbs.peek(); + Uri uri; + if (USE_FOLDERS) { + uri = BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER; + if (folder != null) { + uri = ContentUris.withAppendedId(uri, folder.mId); + } + } else { + uri = BrowserContract.Bookmarks.CONTENT_URI; + where = Bookmarks.IS_FOLDER + " == 0"; + } + if (!TextUtils.isEmpty(mAccountType) && !TextUtils.isEmpty(mAccountName)) { + uri = uri.buildUpon() + .appendQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE, mAccountType) + .appendQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME, mAccountName).build(); + } + Cursor c = null; + try { + c = mContext.getContentResolver().query(uri, PROJECTION, + where, null, null); + if (c != null) { + mBookmarks = new ArrayList<RenderResult>(c.getCount() + 1); + if (folder != null) { + RenderResult res = new RenderResult(folder.mId, folder.mTitle, null); + res.mIsFolder = true; + mBookmarks.add(res); + } + while (c.moveToNext()) { + long id = c.getLong(0); + String title = c.getString(1); + String url = c.getString(2); + RenderResult res = new RenderResult(id, title, url); + byte[] blob = c.getBlob(3); + if (blob != null) { + // RemoteViews require a valid bitmap config + Options options = new Options(); + options.inPreferredConfig = Config.ARGB_8888; + res.mBitmap = BitmapFactory.decodeByteArray( + blob, 0, blob.length, options); + } + res.mIsFolder = c.getInt(4) != 0; + mBookmarks.add(res); + } + } + } catch (IllegalStateException e) { + Log.e(TAG, "update bookmark widget", e); + } finally { + if (c != null) { + c.close(); + } + } + } + + private void recycleBitmaps() { + // Do a bit of house cleaning for the system + if (mBookmarks != null) { + for (RenderResult res : mBookmarks) { + if (res.mBitmap != null) { + res.mBitmap.recycle(); + res.mBitmap = null; + } + } + } + } + } + + // Class containing the rendering information for a specific bookmark. + private static class RenderResult { + final String mTitle; + final String mUrl; + Bitmap mBitmap; + boolean mIsFolder; + long mId; + + RenderResult(long id, String title, String url) { + mId = id; + mTitle = title; + mUrl = url; + } + + } + +} diff --git a/src/com/android/browser/widget/BookmarkWidgetProvider.java b/src/com/android/browser/widget/BookmarkWidgetProvider.java deleted file mode 100644 index 62b48c068..000000000 --- a/src/com/android/browser/widget/BookmarkWidgetProvider.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2010 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.widget; - -import android.app.PendingIntent; -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProvider; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -/** - * Widget that shows a preview of the user's bookmarks. - */ -public class BookmarkWidgetProvider extends AppWidgetProvider { - - static final String TAG = "BookmarkWidgetProvider"; - - @Override - public void onUpdate(Context context, AppWidgetManager mngr, int[] ids) { - context.startService(new Intent(BookmarkWidgetService.UPDATE, null, - context, BookmarkWidgetService.class)); - } - - @Override - public void onEnabled(Context context) { - context.startService(new Intent(context, BookmarkWidgetService.class)); - } - - @Override - public void onDisabled(Context context) { - context.stopService(new Intent(context, BookmarkWidgetService.class)); - } -} - diff --git a/src/com/android/browser/widget/BookmarkWidgetService.java b/src/com/android/browser/widget/BookmarkWidgetService.java deleted file mode 100644 index 1fd916377..000000000 --- a/src/com/android/browser/widget/BookmarkWidgetService.java +++ /dev/null @@ -1,385 +0,0 @@ -/* - * Copyright (C) 2010 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.widget; - -import android.app.PendingIntent; -import android.app.Service; -import android.appwidget.AppWidgetManager; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.os.Handler; -import android.os.IBinder; -import android.os.Message; -import android.os.ParcelFileDescriptor; -import android.provider.Browser; -import android.provider.Browser.BookmarkColumns; -import android.service.urlrenderer.UrlRenderer; -import android.service.urlrenderer.UrlRendererService; -import android.util.Log; -import android.view.View; -import android.widget.RemoteViews; - -import com.android.browser.R; - -import java.io.InputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; - -public class BookmarkWidgetService extends Service - implements UrlRenderer.Callback { - - private static final String TAG = "BookmarkWidgetService"; - - /** Force the bookmarks to be re-renderer. */ - public static final String UPDATE = "com.android.browser.widget.UPDATE"; - - /** Change the widget to the next bookmark. */ - private static final String NEXT = "com.android.browser.widget.NEXT"; - - /** Change the widget to the previous bookmark. */ - private static final String PREV = "com.android.browser.widget.PREV"; - - /** Id of the current item displayed in the widget. */ - private static final String EXTRA_ID = - "com.android.browser.widget.extra.ID"; - - // XXX: Remove these magic numbers once the dimensions of the widget can be - // queried. - private static final int WIDTH = 306; - private static final int HEIGHT = 386; - - // Limit the number of connection attempts. - private static final int MAX_SERVICE_RETRY_COUNT = 5; - - // No id specified. - private static final int NO_ID = -1; - - private static final int MSG_UPDATE = 0; - private final Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_UPDATE: - if (mRenderer != null) { - queryCursorAndRender(); - } else { - if (++mServiceRetryCount <= MAX_SERVICE_RETRY_COUNT) { - // Service is not connected, try again in a second. - mHandler.sendEmptyMessageDelayed(MSG_UPDATE, 1000); - } - } - break; - default: - break; - } - } - }; - - private final ServiceConnection mConnection = new ServiceConnection() { - public void onServiceConnected(ComponentName className, - IBinder service) { - mRenderer = new UrlRenderer(service); - } - - public void onServiceDisconnected(ComponentName className) { - mRenderer = null; - } - }; - - // Id -> information map storing db ids and their result. - private final HashMap<Integer, RenderResult> mIdsToResults = - new HashMap<Integer, RenderResult>(); - - // List of ids in order - private final ArrayList<Integer> mIdList = new ArrayList<Integer>(); - - // Map of urls to ids for when a url is complete. - private final HashMap<String, Integer> mUrlsToIds = - new HashMap<String, Integer>(); - - // The current id used by the widget during an update. - private int mCurrentId = NO_ID; - // Class that contacts the service on the phone to render bookmarks. - private UrlRenderer mRenderer; - // Number of service retries. Stop trying to connect after - // MAX_SERVICE_RETRY_COUNT - private int mServiceRetryCount; - - @Override - public void onCreate() { - bindService(new Intent(UrlRendererService.SERVICE_INTERFACE), - mConnection, Context.BIND_AUTO_CREATE); - } - - @Override - public void onDestroy() { - unbindService(mConnection); - } - - @Override - public android.os.IBinder onBind(Intent intent) { - return null; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - final String action = intent.getAction(); - if (UPDATE.equals(action)) { - mHandler.sendEmptyMessage(MSG_UPDATE); - } else if (PREV.equals(action) && mIdList.size() > 1) { - int prev = getPreviousId(intent); - if (prev == NO_ID) { - Log.d(TAG, "Could not determine previous id"); - return START_NOT_STICKY; - } - RenderResult res = mIdsToResults.get(prev); - if (res != null) { - updateWidget(res); - } - } else if (NEXT.equals(action) && mIdList.size() > 1) { - int next = getNextId(intent); - if (next == NO_ID) { - Log.d(TAG, "Could not determine next id"); - return START_NOT_STICKY; - } - RenderResult res = mIdsToResults.get(next); - if (res != null) { - updateWidget(res); - } - } - return START_STICKY; - } - - private int getPreviousId(Intent intent) { - int listSize = mIdList.size(); - // If the list contains 1 or fewer entries, return NO_ID so that the - // widget does not update. - if (listSize <= 1) { - return NO_ID; - } - - int curr = intent.getIntExtra(EXTRA_ID, NO_ID); - if (curr == NO_ID) { - return NO_ID; - } - - // Check if the current id is the beginning of the list so we can skip - // iterating through. - if (mIdList.get(0) == curr) { - return mIdList.get(listSize - 1); - } - - // Search for the current id and remember the previous id. - int prev = NO_ID; - for (int id : mIdList) { - if (id == curr) { - break; - } - prev = id; - } - return prev; - } - - private int getNextId(Intent intent) { - int listSize = mIdList.size(); - // If the list contains 1 or fewer entries, return NO_ID so that the - // widget does not update. - if (listSize <= 1) { - return NO_ID; - } - - int curr = intent.getIntExtra(EXTRA_ID, NO_ID); - if (curr == NO_ID) { - return NO_ID; - } - - // Check if the current id is at the end of the list so we can skip - // iterating through. - if (mIdList.get(listSize - 1) == curr) { - return mIdList.get(0); - } - - // Iterate through the ids. i is set to the current index + 1. - int i = 1; - for (int id : mIdList) { - if (id == curr) { - break; - } - i++; - } - return mIdList.get(i); - } - - private void updateWidget(RenderResult res) { - RemoteViews views = new RemoteViews(getPackageName(), - R.layout.bookmarkwidget); - - Intent prev = new Intent(PREV, null, this, BookmarkWidgetService.class); - prev.putExtra(EXTRA_ID, res.mId); - views.setOnClickPendingIntent(R.id.previous, - PendingIntent.getService(this, 0, prev, - PendingIntent.FLAG_CANCEL_CURRENT)); - - Intent next = new Intent(NEXT, null, this, BookmarkWidgetService.class); - next.putExtra(EXTRA_ID, res.mId); - views.setOnClickPendingIntent(R.id.next, - PendingIntent.getService(this, 0, next, - PendingIntent.FLAG_CANCEL_CURRENT)); - - // Set the title of the bookmark. Use the url as a backup. - String displayTitle = res.mTitle; - if (displayTitle == null) { - displayTitle = res.mUrl; - } - views.setTextViewText(R.id.title, displayTitle); - - // Set the image or revert to the progress indicator. - if (res.mBitmap != null) { - views.setImageViewBitmap(R.id.image, res.mBitmap); - views.setViewVisibility(R.id.image, View.VISIBLE); - views.setViewVisibility(R.id.progress, View.GONE); - } else { - views.setViewVisibility(R.id.progress, View.VISIBLE); - views.setViewVisibility(R.id.image, View.GONE); - } - - // Update the current id. - mCurrentId = res.mId; - - AppWidgetManager.getInstance(this).updateAppWidget( - new ComponentName(this, BookmarkWidgetProvider.class), - views); - } - - // Default WHERE clause is all bookmarks. - private static final String QUERY_WHERE = - BookmarkColumns.BOOKMARK + " == 1"; - private static final String[] PROJECTION = new String[] { - BookmarkColumns._ID, BookmarkColumns.TITLE, BookmarkColumns.URL }; - - // Class containing the rendering information for a specific bookmark. - private static class RenderResult { - final int mId; - final String mTitle; - final String mUrl; - Bitmap mBitmap; - - RenderResult(int id, String title, String url) { - mId = id; - mTitle = title; - mUrl = url; - } - } - - private void queryCursorAndRender() { - // Clear the ordered list of ids and the map of ids to bitmaps. - mIdList.clear(); - mIdsToResults.clear(); - - // Look up all the bookmarks - Cursor c = getContentResolver().query(Browser.BOOKMARKS_URI, PROJECTION, - QUERY_WHERE, null, null); - if (c != null) { - if (c.moveToFirst()) { - ArrayList<String> urls = new ArrayList<String>(c.getCount()); - boolean sawCurrentId = false; - do { - int id = c.getInt(0); - String title = c.getString(1); - String url = c.getString(2); - - // Linear list of ids to obtain the previous and next. - mIdList.add(id); - - // Map the url to its db id for lookup when complete. - mUrlsToIds.put(url, id); - - // Is this the current id? - if (mCurrentId == id) { - sawCurrentId = true; - } - - // Store the current information to at least display the - // title. - RenderResult res = new RenderResult(id, title, url); - mIdsToResults.put(id, res); - - // Add the url to our list to render. - urls.add(url); - } while (c.moveToNext()); - - // Request a rendering of the urls. XXX: Hard-coded dimensions - // until the view's orientation and size can be determined. Or - // in the future the image will be a picture that can be - // scaled/zoomed arbitrarily. - mRenderer.render(urls, WIDTH, HEIGHT, this); - - // Set the current id to the very first id if we did not see - // the current id in the list (the bookmark could have been - // deleted or this is the first update). - if (!sawCurrentId) { - mCurrentId = mIdList.get(0); - } - } - c.close(); - } - } - - // UrlRenderer.Callback implementation - public void complete(String url, ParcelFileDescriptor result) { - int id = mUrlsToIds.get(url); - if (id == NO_ID) { - Log.d(TAG, "No matching id found during completion of " - + url); - return; - } - - RenderResult res = mIdsToResults.get(id); - if (res == null) { - Log.d(TAG, "No result found during completion of " - + url); - return; - } - - // Set the result. - if (result != null) { - InputStream input = - new ParcelFileDescriptor.AutoCloseInputStream(result); - Bitmap orig = BitmapFactory.decodeStream(input, null, null); - // XXX: Hard-coded scaled bitmap until I can query the image - // dimensions. - res.mBitmap = Bitmap.createScaledBitmap(orig, WIDTH, HEIGHT, true); - try { - input.close(); - } catch (IOException e) { - // oh well... - } - } - - // If we are currently looking at the bookmark that just finished, - // update the widget. - if (mCurrentId == id) { - updateWidget(res); - } - } -} |
