Change SWE app properties back to stock Android
- Changed project package name from com.android.swe.browser
back to com.android.browser along with code references to
old package name.
- Changes to AndroidManifest making it conform closer to stock
browser manifest.
- Changed app and apk name back to Browser.
Change-Id: I778ee1d1197bd50bd4a4850eef6d1d7f4ef0ad0b
diff --git a/src/com/android/browser/AccountsChangedReceiver.java b/src/com/android/browser/AccountsChangedReceiver.java
new file mode 100644
index 0000000..a4d10d7
--- /dev/null
+++ b/src/com/android/browser/AccountsChangedReceiver.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2011 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.accounts.Account;
+import android.accounts.AccountManager;
+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 com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.BrowserContract.Accounts;
+import com.android.browser.platformsupport.BrowserContract.Bookmarks;
+
+import android.text.TextUtils;
+
+public class AccountsChangedReceiver extends BroadcastReceiver {
+
+ private static final String[] PROJECTION = new String[] {
+ Accounts.ACCOUNT_NAME,
+ Accounts.ACCOUNT_TYPE,
+ };
+ private static final String SELECTION = Accounts.ACCOUNT_NAME + " IS NOT NULL";
+ private static final String DELETE_SELECTION = Accounts.ACCOUNT_NAME + "=? AND "
+ + Accounts.ACCOUNT_TYPE + "=?";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ new DeleteRemovedAccounts(context).start();
+ }
+
+ static class DeleteRemovedAccounts extends Thread {
+ Context mContext;
+ public DeleteRemovedAccounts(Context context) {
+ mContext = context.getApplicationContext();
+ }
+
+ @Override
+ public void run() {
+ Account[] accounts = AccountManager.get(mContext).getAccounts();
+ ContentResolver cr = mContext.getContentResolver();
+ Cursor c = cr.query(Accounts.CONTENT_URI, PROJECTION,
+ SELECTION, null, null);
+ while (c.moveToNext()) {
+ String name = c.getString(0);
+ String type = c.getString(1);
+ if (!contains(accounts, name, type)) {
+ delete(cr, name, type);
+ }
+ }
+ cr.update(Accounts.CONTENT_URI, null, null, null);
+ c.close();
+ }
+
+ void delete(ContentResolver cr, String name, String type) {
+ // Pretend to be a sync adapter to delete the data and not mark
+ // it for deletion. Without this, the bookmarks will be marked to
+ // be deleted, which will propagate to the server if the account
+ // is added back.
+ Uri uri = Bookmarks.CONTENT_URI.buildUpon()
+ .appendQueryParameter(BrowserContract.CALLER_IS_SYNCADAPTER, "true")
+ .build();
+ cr.delete(uri, DELETE_SELECTION, new String[] { name, type });
+ }
+
+ boolean contains(Account[] accounts, String name, String type) {
+ for (Account a : accounts) {
+ if (TextUtils.equals(a.name, name)
+ && TextUtils.equals(a.type, type)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/src/com/android/browser/ActivityController.java b/src/com/android/browser/ActivityController.java
new file mode 100644
index 0000000..ac248b8
--- /dev/null
+++ b/src/com/android/browser/ActivityController.java
@@ -0,0 +1,74 @@
+package com.android.browser;
+
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.view.ActionMode;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+
+
+public interface ActivityController {
+
+ void start(Intent intent);
+
+ void onSaveInstanceState(Bundle outState);
+
+ void handleNewIntent(Intent intent);
+
+ void onResume();
+
+ boolean onMenuOpened(int featureId, Menu menu);
+
+ void onOptionsMenuClosed(Menu menu);
+
+ void onContextMenuClosed(Menu menu);
+
+ void onPause();
+
+ void onDestroy();
+
+ void onConfgurationChanged(Configuration newConfig);
+
+ void onLowMemory();
+
+ boolean onCreateOptionsMenu(Menu menu);
+
+ boolean onPrepareOptionsMenu(Menu menu);
+
+ boolean onOptionsItemSelected(MenuItem item);
+
+ void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo);
+
+ boolean onContextItemSelected(MenuItem item);
+
+ boolean onKeyDown(int keyCode, KeyEvent event);
+
+ boolean onKeyLongPress(int keyCode, KeyEvent event);
+
+ boolean onKeyUp(int keyCode, KeyEvent event);
+
+ void onActionModeStarted(ActionMode mode);
+
+ void onActionModeFinished(ActionMode mode);
+
+ void onActivityResult(int requestCode, int resultCode, Intent intent);
+
+ boolean onSearchRequested();
+
+ boolean dispatchKeyEvent(KeyEvent event);
+
+ boolean dispatchKeyShortcutEvent(KeyEvent event);
+
+ boolean dispatchTouchEvent(MotionEvent ev);
+
+ boolean dispatchTrackballEvent(MotionEvent ev);
+
+ boolean dispatchGenericMotionEvent(MotionEvent ev);
+
+}
diff --git a/src/com/android/browser/AddBookmarkFolder.java b/src/com/android/browser/AddBookmarkFolder.java
new file mode 100644
index 0000000..4a9c13c
--- /dev/null
+++ b/src/com/android/browser/AddBookmarkFolder.java
@@ -0,0 +1,951 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.Activity;
+import android.app.LoaderManager;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.AsyncTaskLoader;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+
+import android.text.TextUtils;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.CursorAdapter;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.browser.R;
+import com.android.browser.addbookmark.FolderSpinner;
+import com.android.browser.addbookmark.FolderSpinnerAdapter;
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.BrowserContract.Accounts;
+import com.android.browser.provider.BrowserProvider2;
+import com.android.browser.reflect.ReflectHelper;
+
+public class AddBookmarkFolder extends Activity implements View.OnClickListener,
+ TextView.OnEditorActionListener, AdapterView.OnItemClickListener,
+ LoaderManager.LoaderCallbacks<Cursor>, BreadCrumbView.Controller,
+ FolderSpinner.OnSetSelectionListener, OnItemSelectedListener {
+
+ public static final long DEFAULT_FOLDER_ID = -1;
+
+ // Place on an edited bookmark to remove the saved thumbnail
+ public static final String CHECK_FOR_DUPE = "check_for_dupe";
+
+ public static final String BOOKMARK_CURRENT_ID = "bookmark_current_id";
+
+ /* package */static final String EXTRA_EDIT_BOOKMARK = "bookmark";
+
+ /* package */static final String EXTRA_IS_FOLDER = "is_folder";
+
+ private static final int MAX_CRUMBS_SHOWN = 1;
+
+ private long mOriginalFolder = -1;
+
+ private boolean mIsFolderChanged = false;
+
+ private boolean mIsOtherFolderSelected = false;
+
+ private boolean mIsRecentFolder = false;
+
+ // IDs for the CursorLoaders that are used.
+ private static final int LOADER_ID_ACCOUNTS = 0;
+
+ private static final int LOADER_ID_FOLDER_CONTENTS = 1;
+
+ private static final int LOADER_ID_EDIT_INFO = 2;
+
+ private EditText mTitle;
+
+ private EditText mAddress;
+
+ private TextView mButton;
+
+ private View mCancelButton;
+
+ private Bundle mMap;
+
+ private FolderSpinner mFolder;
+
+ private View mDefaultView;
+
+ private View mFolderSelector;
+
+ private EditText mFolderNamer;
+
+ private View mFolderCancel;
+
+ private boolean mIsFolderNamerShowing;
+
+ private View mFolderNamerHolder;
+
+ private View mAddNewFolder;
+
+ private View mAddSeparator;
+
+ private long mCurrentFolder;
+
+ private FolderAdapter mAdapter;
+
+ private BreadCrumbView mCrumbs;
+
+ private TextView mFakeTitle;
+
+ private View mCrumbHolder;
+
+ private AddBookmarkPage.CustomListView mListView;
+
+ private long mRootFolder;
+
+ private TextView mTopLevelLabel;
+
+ private Drawable mHeaderIcon;
+
+ private View mRemoveLink;
+
+ private View mFakeTitleHolder;
+
+ private FolderSpinnerAdapter mFolderAdapter;
+
+ private Spinner mAccountSpinner;
+
+ private ArrayAdapter<BookmarkAccount> mAccountAdapter;
+
+
+ private static class Folder {
+ String mName;
+
+ long mId;
+
+ Folder(String name, long id) {
+ mName = name;
+ mId = id;
+ }
+ }
+
+ private InputMethodManager getInputMethodManager() {
+ return (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
+ }
+
+ private Uri getUriForFolder(long folder) {
+ BookmarkAccount account = (BookmarkAccount) mAccountSpinner.getSelectedItem();
+ if (folder == mRootFolder && account != null) {
+ return BookmarksLoader.addAccount(BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER,
+ account.mAccountType, account.mAccountName);
+ }
+ return BrowserContract.Bookmarks.buildFolderUri(folder);
+ }
+
+ public static long getIdFromData(Object data) {
+ if (data == null) {
+ return BrowserProvider2.FIXED_ID_ROOT;
+ } else {
+ Folder folder = (Folder) data;
+ return folder.mId;
+ }
+ }
+
+ @Override
+ public void onTop(BreadCrumbView view, int level, Object data) {
+ if (null == data) {
+ return;
+ }
+ Folder folderData = (Folder) data;
+ long folder = folderData.mId;
+ LoaderManager manager = getLoaderManager();
+ CursorLoader loader = (CursorLoader) ((Loader<?>) manager
+ .getLoader(LOADER_ID_FOLDER_CONTENTS));
+ loader.setUri(getUriForFolder(folder));
+ loader.forceLoad();
+ if (mIsFolderNamerShowing) {
+ completeOrCancelFolderNaming(true);
+ }
+ setShowBookmarkIcon(level == 1);
+ }
+
+ /**
+ * Show or hide the icon for bookmarks next to "Bookmarks" in the crumb
+ * view.
+ *
+ * @param show True if the icon should visible, false otherwise.
+ */
+ private void setShowBookmarkIcon(boolean show) {
+ Drawable drawable = show ? mHeaderIcon : null;
+ mTopLevelLabel.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null);
+ }
+
+ @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);
+ mFakeTitleHolder.setVisibility(View.VISIBLE);
+ if (changedFolder) {
+ Object data = mCrumbs.getTopData();
+ if (data != null) {
+ Folder folder = (Folder) data;
+ mCurrentFolder = folder.mId;
+ if (mCurrentFolder == mRootFolder) {
+ // The Spinner changed to show "Other folder ..." Change
+ // it back to "Bookmarks", which is position 0 if we are
+ // editing a folder, 1 otherwise.
+ mFolder.setSelectionIgnoringSelectionChange(0);
+ } else {
+ mFolderAdapter.setOtherFolderDisplayText(folder.mName);
+ }
+ }
+ } else {
+ if (mCurrentFolder == mRootFolder) {
+ mFolder.setSelectionIgnoringSelectionChange(0);
+ } else {
+ Object data = mCrumbs.getTopData();
+ if (data != null && ((Folder) data).mId == mCurrentFolder) {
+ // We are showing the correct folder hierarchy. The
+ // folder selector will say "Other folder..." Change it
+ // to say the name of the folder once again.
+ mFolderAdapter.setOtherFolderDisplayText(((Folder) data).mName);
+ } else {
+ // We are not showing the correct folder hierarchy.
+ // Clear the Crumbs and find the proper folder
+ setupTopCrumb();
+ LoaderManager manager = getLoaderManager();
+ manager.restartLoader(LOADER_ID_FOLDER_CONTENTS, null, this);
+
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == mButton) {
+ if (mFolderSelector.getVisibility() == View.VISIBLE) {
+ // We are showing the folder selector.
+ if (mIsFolderNamerShowing) {
+ completeOrCancelFolderNaming(false);
+ } else {
+ switchToDefaultView(true);
+ }
+ } else {
+ if (save()) {
+ finish();
+ }
+ }
+ } else if (v == mCancelButton) {
+ if (mIsFolderNamerShowing) {
+ completeOrCancelFolderNaming(true);
+ } else if (mFolderSelector.getVisibility() == View.VISIBLE) {
+ switchToDefaultView(false);
+ } else {
+ finish();
+ }
+ } else if (v == mFolderCancel) {
+ completeOrCancelFolderNaming(true);
+ }
+ }
+
+ private void displayToastForExistingFolder() {
+ Toast.makeText(getApplicationContext(), R.string.duplicated_folder_warning,
+ Toast.LENGTH_LONG).show();
+ }
+
+ @Override
+ public void onSetSelection(long id) {
+ int intId = (int) id;
+ mIsFolderChanged = true;
+ mIsOtherFolderSelected = false;
+ mIsRecentFolder = false;
+ switch (intId) {
+ case FolderSpinnerAdapter.ROOT_FOLDER:
+ mCurrentFolder = mRootFolder;
+ mOriginalFolder = mCurrentFolder;
+ break;
+ case FolderSpinnerAdapter.HOME_SCREEN:
+
+ break;
+ case FolderSpinnerAdapter.OTHER_FOLDER:
+ mIsOtherFolderSelected = true;
+ switchToFolderSelector();
+ break;
+ case FolderSpinnerAdapter.RECENT_FOLDER:
+ mCurrentFolder = mFolderAdapter.recentFolderId();
+ mOriginalFolder = mCurrentFolder;
+ mIsRecentFolder = true;
+ // In case the user decides to select OTHER_FOLDER
+ // and choose a different one, so that we will start from
+ // the correct place.
+ LoaderManager manager = getLoaderManager();
+ manager.restartLoader(LOADER_ID_FOLDER_CONTENTS, null, this);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Finish naming a folder, and close the IME
+ *
+ * @param cancel If true, the new folder is not created. If false, the new
+ * folder is created and the user is taken inside it.
+ */
+ 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);
+ }
+ setShowFolderNamer(false);
+ getInputMethodManager().hideSoftInputFromWindow(mListView.getWindowToken(), 0);
+ }
+
+ 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);
+ long currentFolder;
+ Object data = null;
+ if (null != mCrumbs) {
+ data = mCrumbs.getTopData();
+ }
+ if (data != null) {
+ currentFolder = ((Folder) data).mId;
+ } else {
+ currentFolder = mRootFolder;
+ }
+ currentFolder = mCurrentFolder;
+ if (mIsRecentFolder) {
+ values.put(BrowserContract.Bookmarks.PARENT, mCurrentFolder);
+ } else if (!(mIsFolderChanged && mIsOtherFolderSelected) && mOriginalFolder != -1) {
+ values.put(BrowserContract.Bookmarks.PARENT, mOriginalFolder);
+ } else {
+ 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() {
+ // Set the list to the top in case it is scrolled.
+ mListView.setSelection(0);
+ mFakeTitleHolder.setVisibility(View.GONE);
+ // mFakeTitle.setVisibility(View.GONE);
+ mDefaultView.setVisibility(View.GONE);
+ mFolderSelector.setVisibility(View.VISIBLE);
+ mCrumbHolder.setVisibility(View.VISIBLE);
+ getInputMethodManager().hideSoftInputFromWindow(mListView.getWindowToken(), 0);
+ }
+
+ private void descendInto(String foldername, long id) {
+ if (id != DEFAULT_FOLDER_ID) {
+ mCrumbs.pushView(foldername, new Folder(foldername, id));
+ mCrumbs.notifyController();
+ } else {
+ Toast.makeText(getApplicationContext(), R.string.duplicated_folder_warning,
+ Toast.LENGTH_LONG).show();
+ }
+ }
+
+ private LoaderCallbacks<EditBookmarkInfo> mEditInfoLoaderCallbacks = new LoaderCallbacks<EditBookmarkInfo>() {
+
+ @Override
+ public void onLoaderReset(Loader<EditBookmarkInfo> loader) {
+ // Don't care
+ }
+
+ @Override
+ public void onLoadFinished(Loader<EditBookmarkInfo> loader, EditBookmarkInfo info) {
+ boolean setAccount = false;
+ // TODO: Detect if lastUsedId is a subfolder of info.id in the
+ // editing folder case. For now, just don't show the last used
+ // folder at all to prevent any chance of the user adding a parent
+ // folder to a child folder
+ if (info.mLastUsedId != -1 && info.mLastUsedId != info.mId) {
+ if (setAccount && info.mLastUsedId != mRootFolder
+ && TextUtils.equals(info.mLastUsedAccountName, info.mAccountName)
+ && TextUtils.equals(info.mLastUsedAccountType, info.mAccountType)) {
+ mFolderAdapter.addRecentFolder(info.mLastUsedId, info.mLastUsedTitle);
+ } else if (!setAccount) {
+ setAccount = true;
+ setAccount(info.mLastUsedAccountName, info.mLastUsedAccountType);
+ if (info.mLastUsedId != mRootFolder) {
+ mFolderAdapter.addRecentFolder(info.mLastUsedId, info.mLastUsedTitle);
+ }
+ }
+ }
+ if (!setAccount) {
+ mAccountSpinner.setSelection(0);
+ }
+ }
+
+ @Override
+ public Loader<EditBookmarkInfo> onCreateLoader(int id, Bundle args) {
+ return new EditBookmarkInfoLoader(AddBookmarkFolder.this, mMap);
+ }
+ };
+
+ void setAccount(String accountName, String accountType) {
+ for (int i = 0; i < mAccountAdapter.getCount(); i++) {
+ BookmarkAccount account = mAccountAdapter.getItem(i);
+ if (TextUtils.equals(account.mAccountName, accountName)
+ && TextUtils.equals(account.mAccountType, accountType)) {
+ mAccountSpinner.setSelection(i);
+ onRootFolderFound(account.rootFolderId);
+ return;
+ }
+ }
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ String[] projection;
+ switch (id) {
+ case LOADER_ID_ACCOUNTS:
+ return new AccountsLoader(this);
+ case LOADER_ID_FOLDER_CONTENTS:
+ projection = new String[] {
+ BrowserContract.Bookmarks._ID, BrowserContract.Bookmarks.TITLE,
+ BrowserContract.Bookmarks.IS_FOLDER
+ };
+ String where = BrowserContract.Bookmarks.IS_FOLDER + " != 0" + " AND "
+ + BrowserContract.Bookmarks._ID + " != ?";
+ String whereArgs[] = new String[] {
+ Long.toString(mMap.getLong(BrowserContract.Bookmarks._ID))
+ };
+ long currentFolder;
+ Object data = mCrumbs.getTopData();
+ if (data != null) {
+ currentFolder = ((Folder) data).mId;
+ } else {
+ currentFolder = mRootFolder;
+ }
+ return new CursorLoader(this, getUriForFolder(currentFolder), projection, where,
+ whereArgs, BrowserContract.Bookmarks._ID + " ASC");
+ default:
+ throw new AssertionError("Asking for nonexistant loader!");
+ }
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+ switch (loader.getId()) {
+ case LOADER_ID_ACCOUNTS:
+ mAccountAdapter.clear();
+ while (cursor.moveToNext()) {
+ mAccountAdapter.add(new BookmarkAccount(this, cursor));
+ }
+ getLoaderManager().destroyLoader(LOADER_ID_ACCOUNTS);
+ getLoaderManager().restartLoader(LOADER_ID_EDIT_INFO, null,
+ mEditInfoLoaderCallbacks);
+ break;
+ case LOADER_ID_FOLDER_CONTENTS:
+ mAdapter.changeCursor(cursor);
+ break;
+ default:
+ break;
+ }
+ }
+
+ public void onLoaderReset(Loader<Cursor> loader) {
+ switch (loader.getId()) {
+ case LOADER_ID_FOLDER_CONTENTS:
+ mAdapter.changeCursor(null);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Move cursor to the position that has folderToFind as its "_id".
+ *
+ * @param cursor Cursor containing folders in the bookmarks database
+ * @param folderToFind "_id" of the folder to move to.
+ * @param idIndex Index in cursor of "_id"
+ * @throws AssertionError if cursor is empty or there is no row with
+ * folderToFind as its "_id".
+ */
+ void moveCursorToFolder(Cursor cursor, long folderToFind, int idIndex) throws AssertionError {
+ if (!cursor.moveToFirst()) {
+ throw new AssertionError("No folders in the database!");
+ }
+ long folder;
+ do {
+ folder = cursor.getLong(idIndex);
+ } while (folder != folderToFind && cursor.moveToNext());
+ if (cursor.isAfterLast()) {
+ throw new AssertionError("Folder(id=" + folderToFind
+ + ") holding this bookmark does not exist!");
+ }
+ }
+
+ @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);
+ }
+
+ private void setShowFolderNamer(boolean show) {
+ if (show != mIsFolderNamerShowing) {
+ mIsFolderNamerShowing = show;
+ if (show) {
+ // Set the selection to the folder namer so it will be in
+ // view.
+ mListView.addFooterView(mFolderNamerHolder);
+ } else {
+ mListView.removeFooterView(mFolderNamerHolder);
+ }
+ // Refresh the list.
+ mListView.setAdapter(mAdapter);
+ if (show) {
+ mListView.setSelection(mListView.getCount() - 1);
+ }
+ }
+ }
+
+ /**
+ * 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() && !mIsFolderNamerShowing;
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+
+ mMap = getIntent().getExtras();
+
+ setContentView(R.layout.browser_add_bookmark);
+
+ Window window = getWindow();
+
+ String title = this.getString(R.string.new_folder);
+ mFakeTitle = (TextView) findViewById(R.id.fake_title);
+ mFakeTitleHolder = findViewById(R.id.title_holder);
+ mFakeTitle.setText(this.getString(R.string.new_folder));
+
+ mTitle = (EditText) findViewById(R.id.title);
+ // add for cmcc test about waring limit of edit text
+ BrowserUtils.maxLengthFilter(AddBookmarkFolder.this, mTitle, BrowserUtils.FILENAME_MAX_LENGTH);
+
+ mTitle.setText(title);
+ mAddress = (EditText) findViewById(R.id.address);
+ mAddress.setVisibility(View.GONE);
+ findViewById(R.id.row_address).setVisibility(View.GONE);
+
+ mButton = (TextView) findViewById(R.id.OK);
+ mButton.setOnClickListener(this);
+
+ mCancelButton = findViewById(R.id.cancel);
+ mCancelButton.setOnClickListener(this);
+
+ mFolder = (FolderSpinner) findViewById(R.id.folder);
+ mFolderAdapter = new FolderSpinnerAdapter(this, false);
+ mFolder.setAdapter(mFolderAdapter);
+ mFolder.setOnSetSelectionListener(this);
+
+ mDefaultView = findViewById(R.id.default_view);
+ mFolderSelector = findViewById(R.id.folder_selector);
+
+ mFolderNamerHolder = getLayoutInflater().inflate(R.layout.new_folder_layout, null);
+ mFolderNamer = (EditText) mFolderNamerHolder.findViewById(R.id.folder_namer);
+ mFolderNamer.setOnEditorActionListener(this);
+ mFolderCancel = mFolderNamerHolder.findViewById(R.id.close);
+ mFolderCancel.setOnClickListener(this);
+
+ mAddNewFolder = findViewById(R.id.add_new_folder);
+ mAddNewFolder.setVisibility(View.GONE);
+ mAddSeparator = findViewById(R.id.add_divider);
+ mAddSeparator.setVisibility(View.GONE);
+
+ mCrumbs = (BreadCrumbView) findViewById(R.id.crumbs);
+ mCrumbs.setUseBackButton(true);
+ mCrumbs.setController(this);
+ mHeaderIcon = getResources().getDrawable(R.drawable.ic_folder_holo_dark);
+ mCrumbHolder = findViewById(R.id.crumb_holder);
+ mCrumbs.setMaxVisible(MAX_CRUMBS_SHOWN);
+
+ mAdapter = new FolderAdapter(this);
+ mListView = (AddBookmarkPage.CustomListView) findViewById(R.id.list);
+ View empty = findViewById(R.id.empty);
+ mListView.setEmptyView(empty);
+ mListView.setAdapter(mAdapter);
+ mListView.setOnItemClickListener(this);
+ mListView.addEditText(mFolderNamer);
+
+ mAccountAdapter = new ArrayAdapter<BookmarkAccount>(this,
+ android.R.layout.simple_spinner_item);
+ mAccountAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mAccountSpinner = (Spinner) findViewById(R.id.accounts);
+ mAccountSpinner.setAdapter(mAccountAdapter);
+ mAccountSpinner.setOnItemSelectedListener(this);
+
+ if (!window.getDecorView().isInTouchMode()) {
+ mButton.requestFocus();
+ }
+ // getLoaderManager().restartLoader(LOADER_ID_ACCOUNTS, null, this);
+
+ setShowFolderNamer(false);
+ mFolderNamer.setText(R.string.new_folder);
+ mFolderNamer.requestFocus();
+ InputMethodManager imm = getInputMethodManager();
+ Object[] params = {mListView};
+ Class[] type = new Class[] {View.class};
+ ReflectHelper.invokeMethod(imm, "focusIn", type, params);
+ imm.showSoftInput(mFolderNamer, InputMethodManager.SHOW_IMPLICIT);
+
+ mCurrentFolder = getIntent().getLongExtra(
+ BrowserContract.Bookmarks.PARENT, DEFAULT_FOLDER_ID);
+ mOriginalFolder = mCurrentFolder;
+ if (!(mCurrentFolder == -1 || mCurrentFolder == 1)) {
+ mFolder.setSelectionIgnoringSelectionChange(1);
+ mFolderAdapter.setOtherFolderDisplayText(getNameFromId(mOriginalFolder));
+ }
+
+ getLoaderManager().restartLoader(LOADER_ID_ACCOUNTS, null, this);
+ }
+
+ // get folder title from folder id
+ private String getNameFromId(long mCurrentFolder2) {
+ String title = "";
+ Cursor cursor = null;
+ try {
+ cursor = getApplicationContext().getContentResolver().query(
+ BrowserContract.Bookmarks.CONTENT_URI,
+ new String[] {
+ BrowserContract.Bookmarks.TITLE
+ },
+ BrowserContract.Bookmarks._ID + " = ? AND "
+ + BrowserContract.Bookmarks.IS_DELETED + " = ? AND "
+ + BrowserContract.Bookmarks.IS_FOLDER + " = ? ", new String[] {
+ String.valueOf(mCurrentFolder2), 0 + "", 1 + ""
+ }, null);
+ if (cursor != null && cursor.getCount() != 0) {
+ while (cursor.moveToNext()) {
+ title = cursor.getString(0);
+ }
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ return title;
+ }
+
+ private void showRemoveButton() {
+ findViewById(R.id.remove_divider).setVisibility(View.VISIBLE);
+ mRemoveLink = findViewById(R.id.remove);
+ mRemoveLink.setVisibility(View.VISIBLE);
+ mRemoveLink.setOnClickListener(this);
+ }
+
+ // Called once we have determined which folder is the root folder
+ private void onRootFolderFound(long root) {
+ mRootFolder = root;
+ mCurrentFolder = mRootFolder;
+ setupTopCrumb();
+ onCurrentFolderFound();
+ }
+
+ private void setupTopCrumb() {
+ mCrumbs.clear();
+ String name = getString(R.string.bookmarks);
+ mTopLevelLabel = (TextView) mCrumbs.pushView(name, false, new Folder(name, mRootFolder));
+ // To better match the other folders.
+ mTopLevelLabel.setCompoundDrawablePadding(6);
+ }
+
+ private void onCurrentFolderFound() {
+ LoaderManager manager = getLoaderManager();
+ if (mCurrentFolder != mRootFolder) {
+ // Since we're not in the root folder, change the selection to other
+ // folder now. The text will get changed once we select the correct
+ // folder.
+ mFolder.setSelectionIgnoringSelectionChange(1);
+ } else {
+ setShowBookmarkIcon(true);
+ }
+ // Find the contents of the current folder
+ manager.restartLoader(LOADER_ID_FOLDER_CONTENTS, null, this);
+ }
+
+ /**
+ * Parse the data entered in the dialog and post a message to update the
+ * bookmarks database.
+ */
+ private boolean save() {
+ String title = mTitle.getText().toString().trim();
+
+ boolean emptyTitle = title.length() == 0;
+ Resources r = getResources();
+ if (emptyTitle) {
+ mTitle.setError(r.getText(R.string.bookmark_needs_title));
+ return false;
+ }
+
+ long id = addFolderToCurrent(title);
+ if (id == -1) {
+ displayToastForExistingFolder();
+ return false;
+ }
+
+ setResult(RESULT_OK);
+ return true;
+ }
+
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ if (mAccountSpinner == parent) {
+ long root = mAccountAdapter.getItem(position).rootFolderId;
+ if (root != mRootFolder) {
+ onRootFolderFound(root);
+ mFolderAdapter.clearRecentFolder();
+ }
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ // Don't care
+ }
+
+ static class AccountsLoader extends CursorLoader {
+
+ static final String[] PROJECTION = new String[] {
+ Accounts.ACCOUNT_NAME, Accounts.ACCOUNT_TYPE, Accounts.ROOT_ID,
+ };
+
+ static final int COLUMN_INDEX_ACCOUNT_NAME = 0;
+
+ static final int COLUMN_INDEX_ACCOUNT_TYPE = 1;
+
+ static final int COLUMN_INDEX_ROOT_ID = 2;
+
+ public AccountsLoader(Context context) {
+ super(context, Accounts.CONTENT_URI, PROJECTION, null, null, null);
+ }
+
+ }
+
+ public static class BookmarkAccount {
+
+ private String mLabel;
+
+ String mAccountName;
+ String mAccountType;
+
+ public long rootFolderId;
+
+ public BookmarkAccount(Context context, Cursor cursor) {
+ mAccountName = cursor.getString(AccountsLoader.COLUMN_INDEX_ACCOUNT_NAME);
+ mAccountType = cursor.getString(AccountsLoader.COLUMN_INDEX_ACCOUNT_TYPE);
+ rootFolderId = cursor.getLong(AccountsLoader.COLUMN_INDEX_ROOT_ID);
+ mLabel = mAccountName;
+ if (TextUtils.isEmpty(mLabel)) {
+ mLabel = context.getString(R.string.local_bookmarks);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return mLabel;
+ }
+ }
+
+ static class EditBookmarkInfo {
+ long mId = -1;
+
+ long mParentId = -1;
+
+ String mParentTitle;
+
+ String mEBITitle;
+
+ String mAccountName;
+
+ String mAccountType;
+
+ long mLastUsedId = -1;
+
+ String mLastUsedTitle;
+
+ String mLastUsedAccountName;
+
+ String mLastUsedAccountType;
+ }
+
+ static class EditBookmarkInfoLoader extends AsyncTaskLoader<EditBookmarkInfo> {
+
+ private Context mContext;
+
+ private Bundle mMap;
+
+ public EditBookmarkInfoLoader(Context context, Bundle bundle) {
+ super(context);
+ mContext = context.getApplicationContext();
+ mMap = bundle;
+ }
+
+ @Override
+ public EditBookmarkInfo loadInBackground() {
+ final ContentResolver cr = mContext.getContentResolver();
+ EditBookmarkInfo info = new EditBookmarkInfo();
+ Cursor c = null;
+ try {
+ // First, let's lookup the bookmark (check for dupes, get needed
+ // info)
+ String url = mMap.getString(BrowserContract.Bookmarks.URL);
+ info.mId = mMap.getLong(BrowserContract.Bookmarks._ID, -1);
+ boolean checkForDupe = mMap.getBoolean(CHECK_FOR_DUPE);
+ if (checkForDupe && info.mId == -1 && !TextUtils.isEmpty(url)) {
+ c = cr.query(BrowserContract.Bookmarks.CONTENT_URI, new String[] {
+ BrowserContract.Bookmarks._ID
+ }, BrowserContract.Bookmarks.URL + "=?", new String[] {
+ url
+ }, null);
+ if (c.getCount() == 1 && c.moveToFirst()) {
+ info.mId = c.getLong(0);
+ }
+ c.close();
+ }
+ if (info.mId != -1) {
+ c = cr.query(ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI,
+ info.mId), new String[] {
+ BrowserContract.Bookmarks.PARENT,
+ BrowserContract.Bookmarks.ACCOUNT_NAME,
+ BrowserContract.Bookmarks.ACCOUNT_TYPE, BrowserContract.Bookmarks.TITLE
+ }, null, null, null);
+ if (c.moveToFirst()) {
+ info.mParentId = c.getLong(0);
+ info.mAccountName = c.getString(1);
+ info.mAccountType = c.getString(2);
+ info.mEBITitle = c.getString(3);
+ }
+ c.close();
+ c = cr.query(ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI,
+ info.mParentId), new String[] {
+ BrowserContract.Bookmarks.TITLE,
+ }, null, null, null);
+ if (c.moveToFirst()) {
+ info.mParentTitle = c.getString(0);
+ }
+ c.close();
+ }
+
+ // Figure out the last used folder/account
+ c = cr.query(BrowserContract.Bookmarks.CONTENT_URI, new String[] {
+ BrowserContract.Bookmarks.PARENT,
+ }, null, null, BrowserContract.Bookmarks.DATE_MODIFIED + " DESC LIMIT 1");
+ if (c.moveToFirst()) {
+ long parent = c.getLong(0);
+ c.close();
+ c = cr.query(BrowserContract.Bookmarks.CONTENT_URI, new String[] {
+ BrowserContract.Bookmarks.TITLE,
+ BrowserContract.Bookmarks.ACCOUNT_NAME,
+ BrowserContract.Bookmarks.ACCOUNT_TYPE
+ }, BrowserContract.Bookmarks._ID + "=?", new String[] {
+ Long.toString(parent)
+ }, null);
+ if (c.moveToFirst()) {
+ info.mLastUsedId = parent;
+ info.mLastUsedTitle = c.getString(0);
+ info.mLastUsedAccountName = c.getString(1);
+ info.mLastUsedAccountType = c.getString(2);
+ }
+ c.close();
+ }
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ return info;
+ }
+
+ @Override
+ protected void onStartLoading() {
+ forceLoad();
+ }
+ }
+}
diff --git a/src/com/android/browser/AddBookmarkPage.java b/src/com/android/browser/AddBookmarkPage.java
new file mode 100644
index 0000000..73a1ebf
--- /dev/null
+++ b/src/com/android/browser/AddBookmarkPage.java
@@ -0,0 +1,1257 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.LoaderManager;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.AsyncTaskLoader;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.DialogInterface;
+import android.content.Loader;
+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.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.Browser;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+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.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.CursorAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.browser.BrowserUtils;
+import com.android.browser.R;
+import com.android.browser.addbookmark.FolderSpinner;
+import com.android.browser.addbookmark.FolderSpinnerAdapter;
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.WebAddress;
+import com.android.browser.platformsupport.BrowserContract.Accounts;
+import com.android.browser.reflect.ReflectHelper;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+public class AddBookmarkPage extends Activity
+ implements View.OnClickListener, TextView.OnEditorActionListener,
+ AdapterView.OnItemClickListener, LoaderManager.LoaderCallbacks<Cursor>,
+ BreadCrumbView.Controller, FolderSpinner.OnSetSelectionListener,
+ OnItemSelectedListener {
+
+ 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 static final String CHECK_FOR_DUPE = "check_for_dupe";
+
+ /* package */ static final String EXTRA_EDIT_BOOKMARK = "bookmark";
+ /* package */ static final String EXTRA_IS_FOLDER = "is_folder";
+
+ private static final int MAX_CRUMBS_SHOWN = 1;
+
+ private final String LOGTAG = "Bookmarks";
+
+ // IDs for the CursorLoaders that are used.
+ private final int LOADER_ID_ACCOUNTS = 0;
+ private final int LOADER_ID_FOLDER_CONTENTS = 1;
+ private final int LOADER_ID_EDIT_INFO = 2;
+
+ 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 String mOriginalUrl;
+ private FolderSpinner mFolder;
+ private View mDefaultView;
+ private View mFolderSelector;
+ private EditText mFolderNamer;
+ private View mFolderCancel;
+ private boolean mIsFolderNamerShowing;
+ private View mFolderNamerHolder;
+ private View mAddNewFolder;
+ private View mAddSeparator;
+ private long mCurrentFolder;
+ private FolderAdapter mAdapter;
+ private BreadCrumbView mCrumbs;
+ private TextView mFakeTitle;
+ private View mCrumbHolder;
+ private CustomListView mListView;
+ private boolean mSaveToHomeScreen;
+ private long mRootFolder;
+ private TextView mTopLevelLabel;
+ private Drawable mHeaderIcon;
+ private View mRemoveLink;
+ private View mFakeTitleHolder;
+ private FolderSpinnerAdapter mFolderAdapter;
+ private Spinner mAccountSpinner;
+ private ArrayAdapter<BookmarkAccount> mAccountAdapter;
+ // add for carrier which requires same title or address can not exist.
+ private long mDuplicateId;
+ private Context mDuplicateContext;
+
+ 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 static final int BOOKMARK_DELETED = 102;
+
+ private Handler mHandler;
+
+ private InputMethodManager getInputMethodManager() {
+ return (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
+ }
+
+ private Uri getUriForFolder(long folder) {
+ BookmarkAccount account =
+ (BookmarkAccount) mAccountSpinner.getSelectedItem();
+ if (folder == mRootFolder && account != null) {
+ return BookmarksLoader.addAccount(
+ BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER,
+ account.accountType, account.accountName);
+ }
+ return BrowserContract.Bookmarks.buildFolderUri(folder);
+ }
+
+ @Override
+ public void onTop(BreadCrumbView view, int level, Object data) {
+ if (null == data) return;
+ Folder folderData = (Folder) data;
+ long folder = folderData.Id;
+ LoaderManager manager = getLoaderManager();
+ CursorLoader loader = (CursorLoader) ((Loader<?>) manager.getLoader(
+ LOADER_ID_FOLDER_CONTENTS));
+ loader.setUri(getUriForFolder(folder));
+ loader.forceLoad();
+ if (mIsFolderNamerShowing) {
+ completeOrCancelFolderNaming(true);
+ }
+ setShowBookmarkIcon(level == 1);
+ }
+
+ /**
+ * Show or hide the icon for bookmarks next to "Bookmarks" in the crumb view.
+ * @param show True if the icon should visible, false otherwise.
+ */
+ private void setShowBookmarkIcon(boolean show) {
+ Drawable drawable = show ? mHeaderIcon: null;
+ mTopLevelLabel.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null);
+ }
+
+ @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);
+ mFakeTitleHolder.setVisibility(View.VISIBLE);
+ if (changedFolder) {
+ Object data = mCrumbs.getTopData();
+ if (data != null) {
+ Folder folder = (Folder) data;
+ mCurrentFolder = folder.Id;
+ if (mCurrentFolder == mRootFolder) {
+ // The Spinner changed to show "Other folder ..." Change
+ // it back to "Bookmarks", which is position 0 if we are
+ // editing a folder, 1 otherwise.
+ mFolder.setSelectionIgnoringSelectionChange(mEditingFolder ? 0 : 1);
+ } else {
+ mFolderAdapter.setOtherFolderDisplayText(folder.Name);
+ }
+ }
+ } else {
+ // The user canceled selecting a folder. Revert back to the earlier
+ // selection.
+ if (mSaveToHomeScreen) {
+ mFolder.setSelectionIgnoringSelectionChange(0);
+ } else {
+ if (mCurrentFolder == mRootFolder) {
+ mFolder.setSelectionIgnoringSelectionChange(mEditingFolder ? 0 : 1);
+ } else {
+ Object data = mCrumbs.getTopData();
+ if (data != null && ((Folder) data).Id == mCurrentFolder) {
+ // We are showing the correct folder hierarchy. The
+ // folder selector will say "Other folder..." Change it
+ // to say the name of the folder once again.
+ mFolderAdapter.setOtherFolderDisplayText(((Folder) data).Name);
+ } else {
+ // We are not showing the correct folder hierarchy.
+ // Clear the Crumbs and find the proper folder
+ setupTopCrumb();
+ LoaderManager manager = getLoaderManager();
+ manager.restartLoader(LOADER_ID_FOLDER_CONTENTS, null, this);
+
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == mButton) {
+ if (mFolderSelector.getVisibility() == View.VISIBLE) {
+ // We are showing the folder selector.
+ if (mIsFolderNamerShowing) {
+ completeOrCancelFolderNaming(false);
+ } else {
+ // User has selected a folder. Go back to the opening page
+ mSaveToHomeScreen = false;
+ switchToDefaultView(true);
+ }
+ } else {
+ // add for carrier which requires same title or address can not
+ // exist.
+ if (mSaveToHomeScreen) {
+ if (save()) {
+ return;
+ }
+ } else {
+ onSaveWithConfirm();
+ }
+ }
+ } else if (v == mCancelButton) {
+ if (mIsFolderNamerShowing) {
+ completeOrCancelFolderNaming(true);
+ } else if (mFolderSelector.getVisibility() == View.VISIBLE) {
+ switchToDefaultView(false);
+ } else {
+ finish();
+ }
+ } else if (v == mFolderCancel) {
+ completeOrCancelFolderNaming(true);
+ } else if (v == mAddNewFolder) {
+ setShowFolderNamer(true);
+ mFolderNamer.setText(R.string.new_folder);
+ mFolderNamer.requestFocus();
+ mAddNewFolder.setVisibility(View.GONE);
+ mAddSeparator.setVisibility(View.GONE);
+ InputMethodManager imm = getInputMethodManager();
+ // Set the InputMethodManager to focus on the ListView so that it
+ // can transfer the focus to mFolderNamer.
+ //imm.focusIn(mListView);
+ Object[] params = {mListView};
+ Class[] type = new Class[] {View.class};
+ ReflectHelper.invokeMethod(imm, "focusIn", type, params);
+ imm.showSoftInput(mFolderNamer, InputMethodManager.SHOW_IMPLICIT);
+ } else if (v == mRemoveLink) {
+ if (!mEditingExisting) {
+ throw new AssertionError("Remove button should not be shown for"
+ + " new bookmarks");
+ }
+ long id = mMap.getLong(BrowserContract.Bookmarks._ID);
+ createHandler();
+ Message msg = Message.obtain(mHandler, BOOKMARK_DELETED);
+ BookmarkUtils.displayRemoveBookmarkDialog(id,
+ mTitle.getText().toString(), this, msg);
+ }
+ }
+
+ // FolderSpinner.OnSetSelectionListener
+
+ @Override
+ public void onSetSelection(long id) {
+ int intId = (int) id;
+ switch (intId) {
+ case FolderSpinnerAdapter.ROOT_FOLDER:
+ mCurrentFolder = mRootFolder;
+ mSaveToHomeScreen = false;
+ break;
+ case FolderSpinnerAdapter.HOME_SCREEN:
+ // Create a short cut to the home screen
+ mSaveToHomeScreen = true;
+ break;
+ case FolderSpinnerAdapter.OTHER_FOLDER:
+ switchToFolderSelector();
+ break;
+ case FolderSpinnerAdapter.RECENT_FOLDER:
+ mCurrentFolder = mFolderAdapter.recentFolderId();
+ mSaveToHomeScreen = false;
+ // In case the user decides to select OTHER_FOLDER
+ // and choose a different one, so that we will start from
+ // the correct place.
+ LoaderManager manager = getLoaderManager();
+ manager.restartLoader(LOADER_ID_FOLDER_CONTENTS, null, this);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Finish naming a folder, and close the IME
+ * @param cancel If true, the new folder is not created. If false, the new
+ * folder is created and the user is taken inside it.
+ */
+ 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);
+ }
+ setShowFolderNamer(false);
+ mAddNewFolder.setVisibility(View.VISIBLE);
+ mAddSeparator.setVisibility(View.VISIBLE);
+ getInputMethodManager().hideSoftInputFromWindow(
+ mListView.getWindowToken(), 0);
+ }
+
+ 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);
+ 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() {
+ // Set the list to the top in case it is scrolled.
+ mListView.setSelection(0);
+ mDefaultView.setVisibility(View.GONE);
+ mFolderSelector.setVisibility(View.VISIBLE);
+ mCrumbHolder.setVisibility(View.VISIBLE);
+ mFakeTitleHolder.setVisibility(View.GONE);
+ mAddNewFolder.setVisibility(View.VISIBLE);
+ mAddSeparator.setVisibility(View.VISIBLE);
+ getInputMethodManager().hideSoftInputFromWindow(
+ mListView.getWindowToken(), 0);
+ }
+
+ private void descendInto(String foldername, long id) {
+ if (id != DEFAULT_FOLDER_ID) {
+ mCrumbs.pushView(foldername, new Folder(foldername, id));
+ mCrumbs.notifyController();
+ }
+ }
+
+ private LoaderCallbacks<EditBookmarkInfo> mEditInfoLoaderCallbacks =
+ new LoaderCallbacks<EditBookmarkInfo>() {
+
+ @Override
+ public void onLoaderReset(Loader<EditBookmarkInfo> loader) {
+ // Don't care
+ }
+
+ @Override
+ public void onLoadFinished(Loader<EditBookmarkInfo> loader,
+ EditBookmarkInfo info) {
+ boolean setAccount = false;
+ if (info.id != -1) {
+ mEditingExisting = true;
+ showRemoveButton();
+ mFakeTitle.setText(R.string.edit_bookmark);
+ mTitle.setText(info.title);
+ mFolderAdapter.setOtherFolderDisplayText(info.parentTitle);
+ mMap.putLong(BrowserContract.Bookmarks._ID, info.id);
+ setAccount = true;
+ setAccount(info.accountName, info.accountType);
+ mCurrentFolder = info.parentId;
+ onCurrentFolderFound();
+ }
+ // TODO: Detect if lastUsedId is a subfolder of info.id in the
+ // editing folder case. For now, just don't show the last used
+ // folder at all to prevent any chance of the user adding a parent
+ // folder to a child folder
+ if (info.lastUsedId != -1 && info.lastUsedId != info.id
+ && !mEditingFolder) {
+ if (setAccount && info.lastUsedId != mRootFolder
+ && TextUtils.equals(info.lastUsedAccountName, info.accountName)
+ && TextUtils.equals(info.lastUsedAccountType, info.accountType)) {
+ mFolderAdapter.addRecentFolder(info.lastUsedId, info.lastUsedTitle);
+ } else if (!setAccount) {
+ setAccount = true;
+ setAccount(info.lastUsedAccountName, info.lastUsedAccountType);
+ if (info.lastUsedId != mRootFolder) {
+ mFolderAdapter.addRecentFolder(info.lastUsedId,
+ info.lastUsedTitle);
+ }
+ }
+ }
+ if (!setAccount) {
+ mAccountSpinner.setSelection(0);
+ }
+ }
+
+ @Override
+ public Loader<EditBookmarkInfo> onCreateLoader(int id, Bundle args) {
+ return new EditBookmarkInfoLoader(AddBookmarkPage.this, mMap);
+ }
+ };
+
+ void setAccount(String accountName, String accountType) {
+ for (int i = 0; i < mAccountAdapter.getCount(); i++) {
+ BookmarkAccount account = mAccountAdapter.getItem(i);
+ if (TextUtils.equals(account.accountName, accountName)
+ && TextUtils.equals(account.accountType, accountType)) {
+ mAccountSpinner.setSelection(i);
+ onRootFolderFound(account.rootFolderId);
+ return;
+ }
+ }
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ String[] projection;
+ switch (id) {
+ case LOADER_ID_ACCOUNTS:
+ return new AccountsLoader(this);
+ case LOADER_ID_FOLDER_CONTENTS:
+ projection = new String[] {
+ BrowserContract.Bookmarks._ID,
+ BrowserContract.Bookmarks.TITLE,
+ BrowserContract.Bookmarks.IS_FOLDER
+ };
+ String where = BrowserContract.Bookmarks.IS_FOLDER + " != 0";
+ String whereArgs[] = null;
+ if (mEditingFolder) {
+ where += " AND " + BrowserContract.Bookmarks._ID + " != ?";
+ whereArgs = new String[] { Long.toString(mMap.getLong(
+ BrowserContract.Bookmarks._ID)) };
+ }
+ long currentFolder;
+ Object data = mCrumbs.getTopData();
+ if (data != null) {
+ currentFolder = ((Folder) data).Id;
+ } else {
+ currentFolder = mRootFolder;
+ }
+ return new CursorLoader(this,
+ getUriForFolder(currentFolder),
+ projection,
+ where,
+ whereArgs,
+ BrowserContract.Bookmarks._ID + " ASC");
+ default:
+ throw new AssertionError("Asking for nonexistant loader!");
+ }
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+ switch (loader.getId()) {
+ case LOADER_ID_ACCOUNTS:
+ mAccountAdapter.clear();
+ while (cursor.moveToNext()) {
+ mAccountAdapter.add(new BookmarkAccount(this, cursor));
+ }
+ getLoaderManager().destroyLoader(LOADER_ID_ACCOUNTS);
+ getLoaderManager().restartLoader(LOADER_ID_EDIT_INFO, null,
+ mEditInfoLoaderCallbacks);
+ break;
+ case LOADER_ID_FOLDER_CONTENTS:
+ mAdapter.changeCursor(cursor);
+ break;
+ }
+ }
+
+ public void onLoaderReset(Loader<Cursor> loader) {
+ switch (loader.getId()) {
+ case LOADER_ID_FOLDER_CONTENTS:
+ mAdapter.changeCursor(null);
+ break;
+ }
+ }
+
+ /**
+ * Move cursor to the position that has folderToFind as its "_id".
+ * @param cursor Cursor containing folders in the bookmarks database
+ * @param folderToFind "_id" of the folder to move to.
+ * @param idIndex Index in cursor of "_id"
+ * @throws AssertionError if cursor is empty or there is no row with folderToFind
+ * as its "_id".
+ */
+ void moveCursorToFolder(Cursor cursor, long folderToFind, int idIndex)
+ throws AssertionError {
+ if (!cursor.moveToFirst()) {
+ throw new AssertionError("No folders in the database!");
+ }
+ long folder;
+ do {
+ folder = cursor.getLong(idIndex);
+ } while (folder != folderToFind && cursor.moveToNext());
+ if (cursor.isAfterLast()) {
+ throw new AssertionError("Folder(id=" + folderToFind
+ + ") holding this bookmark does not exist!");
+ }
+ }
+
+ @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);
+ }
+
+ private void setShowFolderNamer(boolean show) {
+ if (show != mIsFolderNamerShowing) {
+ mIsFolderNamerShowing = show;
+ if (show) {
+ // Set the selection to the folder namer so it will be in
+ // view.
+ mListView.addFooterView(mFolderNamerHolder);
+ } else {
+ mListView.removeFooterView(mFolderNamerHolder);
+ }
+ // Refresh the list.
+ mListView.setAdapter(mAdapter);
+ if (show) {
+ mListView.setSelection(mListView.getCount() - 1);
+ }
+ }
+ }
+
+ /**
+ * 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() && !mIsFolderNamerShowing;
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+
+ mMap = getIntent().getExtras();
+
+ setContentView(R.layout.browser_add_bookmark);
+
+ Window window = getWindow();
+
+ String title = null;
+ String url = null;
+
+ mFakeTitle = (TextView) findViewById(R.id.fake_title);
+
+ if (mMap != null) {
+ Bundle b = mMap.getBundle(EXTRA_EDIT_BOOKMARK);
+ if (b != null) {
+ mEditingFolder = mMap.getBoolean(EXTRA_IS_FOLDER, false);
+ mMap = b;
+ mEditingExisting = true;
+ mFakeTitle.setText(R.string.edit_bookmark);
+ if (mEditingFolder) {
+ findViewById(R.id.row_address).setVisibility(View.GONE);
+ } else {
+ showRemoveButton();
+ }
+ } else {
+ int gravity = mMap.getInt("gravity", -1);
+ if (gravity != -1) {
+ WindowManager.LayoutParams l = window.getAttributes();
+ l.gravity = gravity;
+ window.setAttributes(l);
+ }
+ }
+ 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);
+ }
+
+ mTitle = (EditText) findViewById(R.id.title);
+ mTitle.setText(title);
+ BrowserUtils.maxLengthFilter(AddBookmarkPage.this, mTitle, BrowserUtils.FILENAME_MAX_LENGTH);
+
+ mAddress = (EditText) findViewById(R.id.address);
+ mAddress.setText(url);
+ BrowserUtils.maxLengthFilter(AddBookmarkPage.this, mAddress, BrowserUtils.ADDRESS_MAX_LENGTH);
+
+ mButton = (TextView) findViewById(R.id.OK);
+ mButton.setOnClickListener(this);
+
+ mCancelButton = findViewById(R.id.cancel);
+ mCancelButton.setOnClickListener(this);
+
+ mFolder = (FolderSpinner) findViewById(R.id.folder);
+ mFolderAdapter = new FolderSpinnerAdapter(this, !mEditingFolder);
+ mFolder.setAdapter(mFolderAdapter);
+ mFolder.setOnSetSelectionListener(this);
+
+ mDefaultView = findViewById(R.id.default_view);
+ mFolderSelector = findViewById(R.id.folder_selector);
+
+ mFolderNamerHolder = getLayoutInflater().inflate(R.layout.new_folder_layout, null);
+ mFolderNamer = (EditText) mFolderNamerHolder.findViewById(R.id.folder_namer);
+ mFolderNamer.setOnEditorActionListener(this);
+
+ // add for carrier test about warning limit of edit text
+ BrowserUtils.maxLengthFilter(AddBookmarkPage.this, mFolderNamer,
+ BrowserUtils.FILENAME_MAX_LENGTH);
+
+ mFolderCancel = mFolderNamerHolder.findViewById(R.id.close);
+ mFolderCancel.setOnClickListener(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);
+ mHeaderIcon = getResources().getDrawable(R.drawable.ic_folder_holo_dark);
+ mCrumbHolder = findViewById(R.id.crumb_holder);
+ mCrumbs.setMaxVisible(MAX_CRUMBS_SHOWN);
+
+ mAdapter = new FolderAdapter(this);
+ mListView = (CustomListView) findViewById(R.id.list);
+ View empty = findViewById(R.id.empty);
+ mListView.setEmptyView(empty);
+ mListView.setAdapter(mAdapter);
+ mListView.setOnItemClickListener(this);
+ mListView.addEditText(mFolderNamer);
+
+ mAccountAdapter = new ArrayAdapter<BookmarkAccount>(this,
+ android.R.layout.simple_spinner_item);
+ mAccountAdapter.setDropDownViewResource(
+ android.R.layout.simple_spinner_dropdown_item);
+ mAccountSpinner = (Spinner) findViewById(R.id.accounts);
+ mAccountSpinner.setAdapter(mAccountAdapter);
+ mAccountSpinner.setOnItemSelectedListener(this);
+
+
+ mFakeTitleHolder = findViewById(R.id.title_holder);
+
+ if (!window.getDecorView().isInTouchMode()) {
+ mButton.requestFocus();
+ }
+
+ getLoaderManager().restartLoader(LOADER_ID_ACCOUNTS, null, this);
+ }
+
+ private void showRemoveButton() {
+ findViewById(R.id.remove_divider).setVisibility(View.VISIBLE);
+ mRemoveLink = findViewById(R.id.remove);
+ mRemoveLink.setVisibility(View.VISIBLE);
+ mRemoveLink.setOnClickListener(this);
+ }
+
+ // Called once we have determined which folder is the root folder
+ private void onRootFolderFound(long root) {
+ mRootFolder = root;
+ mCurrentFolder = mRootFolder;
+ setupTopCrumb();
+ onCurrentFolderFound();
+ }
+
+ private void setupTopCrumb() {
+ mCrumbs.clear();
+ String name = getString(R.string.bookmarks);
+ mTopLevelLabel = (TextView) mCrumbs.pushView(name, false,
+ new Folder(name, mRootFolder));
+ // To better match the other folders.
+ mTopLevelLabel.setCompoundDrawablePadding(6);
+ }
+
+ private void onCurrentFolderFound() {
+ LoaderManager manager = getLoaderManager();
+ if (mCurrentFolder != mRootFolder) {
+ // Since we're not in the root folder, change the selection to other
+ // folder now. The text will get changed once we select the correct
+ // folder.
+ mFolder.setSelectionIgnoringSelectionChange(mEditingFolder ? 1 : 2);
+ } else {
+ setShowBookmarkIcon(true);
+ if (!mEditingFolder) {
+ // Initially the "Bookmarks" folder should be showing, rather than
+ // the home screen. In the editing folder case, home screen is not
+ // an option, so "Bookmarks" folder is already at the top.
+ mFolder.setSelectionIgnoringSelectionChange(FolderSpinnerAdapter.ROOT_FOLDER);
+ }
+ }
+ // Find the contents of the current folder
+ manager.restartLoader(LOADER_ID_FOLDER_CONTENTS, null, this);
+ }
+
+ /**
+ * 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) {
+ mContext = ctx.getApplicationContext();
+ mMessage = msg;
+ }
+ public void run() {
+ // Unbundle bookmark data.
+ Bundle bundle = mMessage.getData();
+ 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(BrowserContract.Bookmarks.THUMBNAIL);
+ String touchIconUrl = bundle.getString(TOUCH_ICON_URL);
+
+ // Save to the bookmarks DB.
+ try {
+ final ContentResolver cr = getContentResolver();
+ Bookmarks.addBookmark(AddBookmarkPage.this, false, url,
+ title, thumbnail, mCurrentFolder);
+ if (touchIconUrl != null) {
+ new DownloadTouchIcon(mContext, cr, url).execute(mTouchIconUrl);
+ }
+ mMessage.arg1 = 1;
+ } catch (IllegalStateException e) {
+ mMessage.arg1 = 0;
+ }
+ mMessage.sendToTarget();
+ }
+ }
+
+ private static class UpdateBookmarkTask extends AsyncTask<ContentValues, Void, Void> {
+ Context mContext;
+ Long mId;
+
+ public UpdateBookmarkTask(Context context, long id) {
+ mContext = context.getApplicationContext();
+ 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() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case SAVE_BOOKMARK:
+ if (1 == msg.arg1) {
+ Toast.makeText(AddBookmarkPage.this, R.string.bookmark_saved,
+ Toast.LENGTH_LONG).show();
+ } else {
+ Toast.makeText(AddBookmarkPage.this, R.string.bookmark_not_saved,
+ 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;
+ case BOOKMARK_DELETED:
+ finish();
+ break;
+ }
+ }
+ };
+ }
+ }
+
+ static void deleteDuplicateBookmark(final Context context, final long id) {
+ Uri uri = ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, id);
+ context.getContentResolver().delete(uri, null, null);
+ }
+
+ private void onSaveWithConfirm() {
+ String title = mTitle.getText().toString().trim();
+ String unfilteredUrl = UrlUtils.fixUrl(mAddress.getText().toString());
+ String url = unfilteredUrl.trim();
+ Long id = mMap.getLong(BrowserContract.Bookmarks._ID);
+ int duplicateCount;
+ final ContentResolver cr = getContentResolver();
+
+ Cursor cursor = cr.query(BrowserContract.Bookmarks.CONTENT_URI,
+ BookmarksLoader.PROJECTION,
+ "( title = ? OR url = ? ) AND parent = ?",
+ new String[] {
+ title, url, Long.toString(mCurrentFolder)
+ },
+ null);
+
+ if (cursor == null) {
+ save();
+ return;
+ }
+
+ duplicateCount = cursor.getCount();
+ if (duplicateCount <= 0) {
+ cursor.close();
+ save();
+ return;
+ } else {
+ try {
+ while (cursor.moveToNext()) {
+ mDuplicateId = cursor.getLong(BookmarksLoader.COLUMN_INDEX_ID);
+ mDuplicateContext = AddBookmarkPage.this;
+ }
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+ }
+
+ if (mEditingExisting && duplicateCount == 1 && mDuplicateId == id) {
+ save();
+ return;
+ }
+
+ new AlertDialog.Builder(this)
+ .setTitle(getString(R.string.save_to_bookmarks_title))
+ .setMessage(getString(R.string.overwrite_bookmark_msg))
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ if (mDuplicateContext == null) {
+ return;
+ }
+ deleteDuplicateBookmark(mDuplicateContext, mDuplicateId);
+ save();
+ }
+ })
+ .show();
+ }
+
+ /**
+ * Parse the data entered in the dialog and post a message to update the bookmarks database.
+ */
+ boolean save() {
+ createHandler();
+
+ String title = mTitle.getText().toString().trim();
+ String unfilteredUrl = UrlUtils.fixUrl(mAddress.getText().toString());
+
+ boolean emptyTitle = title.length() == 0;
+ boolean emptyUrl = unfilteredUrl.trim().length() == 0;
+ Resources r = getResources();
+ if (emptyTitle || (emptyUrl && !mEditingFolder)) {
+ if (emptyTitle) {
+ mTitle.setError(r.getText(R.string.bookmark_needs_title));
+ }
+ if (emptyUrl) {
+ mAddress.setError(r.getText(R.string.bookmark_needs_url));
+ }
+ return false;
+ }
+ String url = unfilteredUrl.trim();
+ 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();
+ }
+ }
+ } 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) {
+ 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 {
+ 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(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");
+ }
+ finish();
+ return true;
+ }
+
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position,
+ long id) {
+ if (mAccountSpinner == parent) {
+ long root = mAccountAdapter.getItem(position).rootFolderId;
+ if (root != mRootFolder) {
+ onRootFolderFound(root);
+ mFolderAdapter.clearRecentFolder();
+ }
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ // Don't care
+ }
+
+ /*
+ * Class used as a proxy for the InputMethodManager to get to mFolderNamer
+ */
+ public static class CustomListView extends ListView {
+ private EditText mEditText;
+
+ public void addEditText(EditText editText) {
+ mEditText = editText;
+ }
+
+ public CustomListView(Context context) {
+ super(context);
+ }
+
+ public CustomListView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CustomListView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public boolean checkInputConnectionProxy(View view) {
+ return view == mEditText;
+ }
+ }
+
+ static class AccountsLoader extends CursorLoader {
+
+ static final String[] PROJECTION = new String[] {
+ Accounts.ACCOUNT_NAME,
+ Accounts.ACCOUNT_TYPE,
+ Accounts.ROOT_ID,
+ };
+
+ static final int COLUMN_INDEX_ACCOUNT_NAME = 0;
+ static final int COLUMN_INDEX_ACCOUNT_TYPE = 1;
+ static final int COLUMN_INDEX_ROOT_ID = 2;
+
+ public AccountsLoader(Context context) {
+ super(context, Accounts.CONTENT_URI, PROJECTION, null, null, null);
+ }
+
+ }
+
+ public static class BookmarkAccount {
+
+ private String mLabel;
+ String accountName, accountType;
+ public long rootFolderId;
+
+ public BookmarkAccount(Context context, Cursor cursor) {
+ accountName = cursor.getString(
+ AccountsLoader.COLUMN_INDEX_ACCOUNT_NAME);
+ accountType = cursor.getString(
+ AccountsLoader.COLUMN_INDEX_ACCOUNT_TYPE);
+ rootFolderId = cursor.getLong(
+ AccountsLoader.COLUMN_INDEX_ROOT_ID);
+ mLabel = accountName;
+ if (TextUtils.isEmpty(mLabel)) {
+ mLabel = context.getString(R.string.local_bookmarks);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return mLabel;
+ }
+ }
+
+ static class EditBookmarkInfo {
+ long id = -1;
+ long parentId = -1;
+ String parentTitle;
+ String title;
+ String accountName;
+ String accountType;
+
+ long lastUsedId = -1;
+ String lastUsedTitle;
+ String lastUsedAccountName;
+ String lastUsedAccountType;
+ }
+
+ static class EditBookmarkInfoLoader extends AsyncTaskLoader<EditBookmarkInfo> {
+
+ private Context mContext;
+ private Bundle mMap;
+
+ public EditBookmarkInfoLoader(Context context, Bundle bundle) {
+ super(context);
+ mContext = context.getApplicationContext();
+ mMap = bundle;
+ }
+
+ @Override
+ public EditBookmarkInfo loadInBackground() {
+ final ContentResolver cr = mContext.getContentResolver();
+ EditBookmarkInfo info = new EditBookmarkInfo();
+ Cursor c = null;
+
+ try {
+ // First, let's lookup the bookmark (check for dupes, get needed info)
+ String url = mMap.getString(BrowserContract.Bookmarks.URL);
+ info.id = mMap.getLong(BrowserContract.Bookmarks._ID, -1);
+ boolean checkForDupe = mMap.getBoolean(CHECK_FOR_DUPE);
+ if (checkForDupe && info.id == -1 && !TextUtils.isEmpty(url)) {
+ c = cr.query(BrowserContract.Bookmarks.CONTENT_URI,
+ new String[] { BrowserContract.Bookmarks._ID},
+ BrowserContract.Bookmarks.URL + "=?",
+ new String[] { url }, null);
+ if (c.getCount() == 1 && c.moveToFirst()) {
+ info.id = c.getLong(0);
+ }
+ c.close();
+ }
+ if (info.id != -1) {
+ c = cr.query(ContentUris.withAppendedId(
+ BrowserContract.Bookmarks.CONTENT_URI, info.id),
+ new String[] {
+ BrowserContract.Bookmarks.PARENT,
+ BrowserContract.Bookmarks.ACCOUNT_NAME,
+ BrowserContract.Bookmarks.ACCOUNT_TYPE,
+ BrowserContract.Bookmarks.TITLE},
+ null, null, null);
+ if (c.moveToFirst()) {
+ info.parentId = c.getLong(0);
+ info.accountName = c.getString(1);
+ info.accountType = c.getString(2);
+ info.title = c.getString(3);
+ }
+ c.close();
+ c = cr.query(ContentUris.withAppendedId(
+ BrowserContract.Bookmarks.CONTENT_URI, info.parentId),
+ new String[] {
+ BrowserContract.Bookmarks.TITLE,},
+ null, null, null);
+ if (c.moveToFirst()) {
+ info.parentTitle = c.getString(0);
+ }
+ c.close();
+ }
+
+ // Figure out the last used folder/account
+ c = cr.query(BrowserContract.Bookmarks.CONTENT_URI,
+ new String[] {
+ BrowserContract.Bookmarks.PARENT,
+ }, null, null,
+ BrowserContract.Bookmarks.DATE_MODIFIED + " DESC LIMIT 1");
+ if (c.moveToFirst()) {
+ long parent = c.getLong(0);
+ c.close();
+ c = cr.query(BrowserContract.Bookmarks.CONTENT_URI,
+ new String[] {
+ BrowserContract.Bookmarks.TITLE,
+ BrowserContract.Bookmarks.ACCOUNT_NAME,
+ BrowserContract.Bookmarks.ACCOUNT_TYPE},
+ BrowserContract.Bookmarks._ID + "=?", new String[] {
+ Long.toString(parent)}, null);
+ if (c.moveToFirst()) {
+ info.lastUsedId = parent;
+ info.lastUsedTitle = c.getString(0);
+ info.lastUsedAccountName = c.getString(1);
+ info.lastUsedAccountType = c.getString(2);
+ }
+ c.close();
+ }
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+
+ return info;
+ }
+
+ @Override
+ protected void onStartLoading() {
+ forceLoad();
+ }
+
+ }
+
+}
diff --git a/src/com/android/browser/AddNewBookmark.java b/src/com/android/browser/AddNewBookmark.java
new file mode 100644
index 0000000..5decb65
--- /dev/null
+++ b/src/com/android/browser/AddNewBookmark.java
@@ -0,0 +1,55 @@
+/*
+ * 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.view.LayoutInflater;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.browser.R;
+
+/**
+ * Custom layout for an item representing a bookmark in the browser.
+ */
+ // FIXME: Remove BrowserBookmarkItem
+class AddNewBookmark extends LinearLayout {
+
+ private TextView mUrlText;
+
+ /**
+ * Instantiate a bookmark item, including a default favicon.
+ *
+ * @param context The application context for the item.
+ */
+ AddNewBookmark(Context context) {
+ super(context);
+
+ setWillNotDraw(false);
+ LayoutInflater factory = LayoutInflater.from(context);
+ factory.inflate(R.layout.add_new_bookmark, this);
+ mUrlText = (TextView) findViewById(R.id.url);
+ }
+
+ /**
+ * Set the new url for the bookmark item.
+ * @param url The new url for the bookmark item.
+ */
+ /* package */ void setUrl(String url) {
+ mUrlText.setText(url);
+ }
+}
diff --git a/src/com/android/browser/AutoFillSettingsFragment.java b/src/com/android/browser/AutoFillSettingsFragment.java
new file mode 100644
index 0000000..e87cb89
--- /dev/null
+++ b/src/com/android/browser/AutoFillSettingsFragment.java
@@ -0,0 +1,293 @@
+/*
+ * 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 org.codeaurora.swe.AutoFillProfile;
+
+import com.android.browser.R;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.inputmethod.InputMethodManager;
+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;
+
+ private MenuItem mSaveMenuItem;
+
+ private boolean mInitialised;
+
+ // Used to display toast after DB interactions complete.
+ private Handler mHandler;
+ private BrowserSettings mSettings;
+
+ 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 class PhoneNumberValidator implements TextWatcher {
+ // Keep in sync with kPhoneNumberLength in chrome/browser/autofill/phone_number.cc
+ private static final int PHONE_NUMBER_LENGTH = 7;
+ private static final String PHONE_NUMBER_SEPARATORS_REGEX = "[\\s\\.\\(\\)-]";
+
+ public void afterTextChanged(Editable s) {
+ String phoneNumber = s.toString();
+ int phoneNumberLength = phoneNumber.length();
+
+ // Strip out any phone number separators.
+ phoneNumber = phoneNumber.replaceAll(PHONE_NUMBER_SEPARATORS_REGEX, "");
+
+ int strippedPhoneNumberLength = phoneNumber.length();
+
+ if (phoneNumberLength > 0 && strippedPhoneNumberLength < PHONE_NUMBER_LENGTH) {
+ mPhoneEdit.setError(getResources().getText(
+ R.string.autofill_profile_editor_phone_number_invalid));
+ } else {
+ mPhoneEdit.setError(null);
+ }
+
+ updateSaveMenuItemState();
+ }
+
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+ }
+
+ private class FieldChangedListener implements TextWatcher {
+ public void afterTextChanged(Editable s) {
+ updateSaveMenuItemState();
+ }
+
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ }
+
+ private TextWatcher mFieldChangedListener = new FieldChangedListener();
+
+ public AutoFillSettingsFragment() {
+ mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ Context c = getActivity();
+ switch (msg.what) {
+ case PROFILE_SAVED_MSG:
+ if (c != null) {
+ Toast.makeText(c, R.string.autofill_profile_successful_save,
+ Toast.LENGTH_SHORT).show();
+ closeEditor();
+ }
+ break;
+
+ case PROFILE_DELETED_MSG:
+ if (c != null) {
+ Toast.makeText(c, R.string.autofill_profile_successful_delete,
+ Toast.LENGTH_SHORT).show();
+ }
+ break;
+ }
+ }
+ };
+ }
+
+ @Override
+ public void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+ setHasOptionsMenu(true);
+ mSettings = BrowserSettings.getInstance();
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.autofill_profile_editor, menu);
+ mSaveMenuItem = menu.findItem(R.id.autofill_profile_editor_save_profile_menu_id);
+ updateSaveMenuItemState();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.autofill_profile_editor_delete_profile_menu_id:
+ // 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.
+ mSettings.updateAutoFillProfile(null);
+
+ updateSaveMenuItemState();
+ return true;
+
+ case R.id.autofill_profile_editor_save_profile_menu_id:
+ AutoFillProfile newProfile = new AutoFillProfile(
+ mSettings.getAutoFillProfileId(),
+ 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());
+
+ mSettings.updateAutoFillProfile(newProfile);
+
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ @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);
+
+ mFullNameEdit.addTextChangedListener(mFieldChangedListener);
+ mEmailEdit.addTextChangedListener(mFieldChangedListener);
+ mCompanyEdit.addTextChangedListener(mFieldChangedListener);
+ mAddressLine1Edit.addTextChangedListener(mFieldChangedListener);
+ mAddressLine2Edit.addTextChangedListener(mFieldChangedListener);
+ mCityEdit.addTextChangedListener(mFieldChangedListener);
+ mStateEdit.addTextChangedListener(mFieldChangedListener);
+ mZipEdit.addTextChangedListener(mFieldChangedListener);
+ mCountryEdit.addTextChangedListener(mFieldChangedListener);
+ mPhoneEdit.addTextChangedListener(new PhoneNumberValidator());
+
+ // Populate the text boxes with any pre existing AutoFill data.
+ AutoFillProfile activeProfile = mSettings.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());
+ }
+
+ mInitialised = true;
+
+ updateSaveMenuItemState();
+
+ return v;
+ }
+
+ private void updateSaveMenuItemState() {
+ if (mSaveMenuItem == null) {
+ return;
+ }
+
+ if (!mInitialised) {
+ mSaveMenuItem.setEnabled(false);
+ return;
+ }
+
+ boolean currentState = mSaveMenuItem.isEnabled();
+ boolean newState = (mFullNameEdit.getText().toString().length() > 0 ||
+ mEmailEdit.getText().toString().length() > 0 ||
+ mCompanyEdit.getText().toString().length() > 0 ||
+ mAddressLine1Edit.getText().toString().length() > 0 ||
+ mAddressLine2Edit.getText().toString().length() > 0 ||
+ mCityEdit.getText().toString().length() > 0 ||
+ mStateEdit.getText().toString().length() > 0 ||
+ mZipEdit.getText().toString().length() > 0 ||
+ mCountryEdit.getText().toString().length() > 0) &&
+ mPhoneEdit.getError() == null;
+
+ if (currentState != newState) {
+ mSaveMenuItem.setEnabled(newState);
+ }
+ }
+
+ private void closeEditor() {
+ // Hide the IME if the user wants to close while an EditText has focus
+ InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(
+ Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(getView().getWindowToken(), 0);
+ if (getFragmentManager().getBackStackEntryCount() > 0) {
+ getFragmentManager().popBackStack();
+ } else {
+ getActivity().finish();
+ }
+ }
+}
diff --git a/src/com/android/browser/AutofillHandler.java b/src/com/android/browser/AutofillHandler.java
new file mode 100644
index 0000000..bb392e8
--- /dev/null
+++ b/src/com/android/browser/AutofillHandler.java
@@ -0,0 +1,84 @@
+
+/*
+ * Copyright (C) 2011 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.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Message;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+
+import java.util.concurrent.CountDownLatch;
+
+import org.codeaurora.swe.AutoFillProfile;
+
+
+public class AutofillHandler {
+
+ protected AutoFillProfile mAutoFillProfile = null;
+ // 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.
+ protected String mAutoFillActiveProfileId = "";
+ private static final int NO_AUTOFILL_PROFILE_SET = 0;
+ private Context mContext;
+
+ private static final String LOGTAG = "AutofillHandler";
+
+ public AutofillHandler(Context context) {
+ mContext = context.getApplicationContext();
+ SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(mContext);
+ mAutoFillActiveProfileId = p.getString(
+ PreferenceKeys.PREF_AUTOFILL_ACTIVE_PROFILE_ID,
+ mAutoFillActiveProfileId);
+ }
+
+ public synchronized void setAutoFillProfile(AutoFillProfile profile) {
+ mAutoFillProfile = profile;
+ if (profile == null)
+ setActiveAutoFillProfileId("");
+ else
+ setActiveAutoFillProfileId(profile.getUniqueId());
+ }
+
+ public synchronized AutoFillProfile getAutoFillProfile() {
+ return mAutoFillProfile;
+ }
+
+ public synchronized String getAutoFillProfileId() {
+ return mAutoFillActiveProfileId;
+ }
+
+ private synchronized void setActiveAutoFillProfileId(String activeProfileId) {
+ if (mAutoFillActiveProfileId.equals(activeProfileId)) {
+ return;
+ }
+ mAutoFillActiveProfileId = activeProfileId;
+ Editor ed = PreferenceManager.
+ getDefaultSharedPreferences(mContext).edit();
+ ed.putString(PreferenceKeys.PREF_AUTOFILL_ACTIVE_PROFILE_ID, activeProfileId);
+ ed.apply();
+ }
+}
diff --git a/src/com/android/browser/AutologinBar.java b/src/com/android/browser/AutologinBar.java
new file mode 100644
index 0000000..3bbfcd9
--- /dev/null
+++ b/src/com/android/browser/AutologinBar.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2011 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.ContextThemeWrapper;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import com.android.browser.R;
+import com.android.browser.DeviceAccountLogin.AutoLoginCallback;
+
+public class AutologinBar extends LinearLayout implements OnClickListener,
+ AutoLoginCallback {
+
+ protected Spinner mAutoLoginAccount;
+ protected Button mAutoLoginLogin;
+ protected ProgressBar mAutoLoginProgress;
+ protected TextView mAutoLoginError;
+ protected View mAutoLoginCancel;
+ protected DeviceAccountLogin mAutoLoginHandler;
+ protected ArrayAdapter<String> mAccountsAdapter;
+ protected TitleBar mTitleBar;
+
+ public AutologinBar(Context context) {
+ super(context);
+ }
+
+ public AutologinBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public AutologinBar(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mAutoLoginAccount = (Spinner) findViewById(R.id.autologin_account);
+ mAutoLoginLogin = (Button) findViewById(R.id.autologin_login);
+ mAutoLoginLogin.setOnClickListener(this);
+ mAutoLoginProgress = (ProgressBar) findViewById(R.id.autologin_progress);
+ mAutoLoginError = (TextView) findViewById(R.id.autologin_error);
+ mAutoLoginCancel = findViewById(R.id.autologin_close);
+ mAutoLoginCancel.setOnClickListener(this);
+ }
+
+ public void setTitleBar(TitleBar titleBar) {
+ mTitleBar = titleBar;
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mAutoLoginCancel == v) {
+ if (mAutoLoginHandler != null) {
+ mAutoLoginHandler.cancel();
+ mAutoLoginHandler = null;
+ }
+ hideAutoLogin(true);
+ } else if (mAutoLoginLogin == v) {
+ if (mAutoLoginHandler != null) {
+ mAutoLoginAccount.setEnabled(false);
+ mAutoLoginLogin.setEnabled(false);
+ mAutoLoginProgress.setVisibility(View.VISIBLE);
+ mAutoLoginError.setVisibility(View.GONE);
+ mAutoLoginHandler.login(
+ mAutoLoginAccount.getSelectedItemPosition(), this);
+ }
+ }
+ }
+
+ public void updateAutoLogin(Tab tab, boolean animate) {
+ DeviceAccountLogin login = tab.getDeviceAccountLogin();
+ if (login != null) {
+ mAutoLoginHandler = login;
+ ContextThemeWrapper wrapper = new ContextThemeWrapper(getContext(),
+ android.R.style.Theme_Holo_Light);
+ mAccountsAdapter = new ArrayAdapter<String>(wrapper,
+ android.R.layout.simple_spinner_item, login.getAccountNames());
+ mAccountsAdapter.setDropDownViewResource(
+ android.R.layout.simple_spinner_dropdown_item);
+ mAutoLoginAccount.setAdapter(mAccountsAdapter);
+ mAutoLoginAccount.setSelection(0);
+ mAutoLoginAccount.setEnabled(true);
+ mAutoLoginLogin.setEnabled(true);
+ mAutoLoginProgress.setVisibility(View.INVISIBLE);
+ mAutoLoginError.setVisibility(View.GONE);
+ switch (login.getState()) {
+ case DeviceAccountLogin.PROCESSING:
+ mAutoLoginAccount.setEnabled(false);
+ mAutoLoginLogin.setEnabled(false);
+ mAutoLoginProgress.setVisibility(View.VISIBLE);
+ break;
+ case DeviceAccountLogin.FAILED:
+ mAutoLoginProgress.setVisibility(View.INVISIBLE);
+ mAutoLoginError.setVisibility(View.VISIBLE);
+ break;
+ case DeviceAccountLogin.INITIAL:
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ showAutoLogin(animate);
+ } else {
+ hideAutoLogin(animate);
+ }
+ }
+
+ void showAutoLogin(boolean animate) {
+ mTitleBar.showAutoLogin(animate);
+ }
+
+ void hideAutoLogin(boolean animate) {
+ mTitleBar.hideAutoLogin(animate);
+ }
+
+ @Override
+ public void loginFailed() {
+ mAutoLoginAccount.setEnabled(true);
+ mAutoLoginLogin.setEnabled(true);
+ mAutoLoginProgress.setVisibility(View.INVISIBLE);
+ mAutoLoginError.setVisibility(View.VISIBLE);
+ }
+
+}
diff --git a/src/com/android/browser/BackgroundHandler.java b/src/com/android/browser/BackgroundHandler.java
new file mode 100644
index 0000000..a0d9243
--- /dev/null
+++ b/src/com/android/browser/BackgroundHandler.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2011 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.os.HandlerThread;
+import android.os.Looper;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class BackgroundHandler {
+
+ static HandlerThread sLooperThread;
+ static ExecutorService mThreadPool;
+
+ static {
+ sLooperThread = new HandlerThread("BackgroundHandler", HandlerThread.MIN_PRIORITY);
+ sLooperThread.start();
+ mThreadPool = Executors.newCachedThreadPool();
+ }
+
+ public static void execute(Runnable runnable) {
+ mThreadPool.execute(runnable);
+ }
+
+ public static Looper getLooper() {
+ return sLooperThread.getLooper();
+ }
+
+ private BackgroundHandler() {}
+}
diff --git a/src/com/android/browser/BaseUi.java b/src/com/android/browser/BaseUi.java
new file mode 100644
index 0000000..f7bd2fb
--- /dev/null
+++ b/src/com/android/browser/BaseUi.java
@@ -0,0 +1,876 @@
+/*
+ * 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.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+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.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+import android.webkit.WebChromeClient.CustomViewCallback;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.Toast;
+
+import com.android.browser.R;
+import com.android.browser.Tab.SecurityState;
+
+import org.codeaurora.swe.WebView;
+
+import java.util.List;
+
+/**
+ * UI interface definitions
+ */
+public abstract class BaseUi implements UI {
+
+ private static final String LOGTAG = "BaseUi";
+
+ protected static final FrameLayout.LayoutParams COVER_SCREEN_PARAMS =
+ new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT);
+
+ protected static final FrameLayout.LayoutParams COVER_SCREEN_GRAVITY_CENTER =
+ new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ Gravity.CENTER);
+
+ private static final int MSG_HIDE_TITLEBAR = 1;
+ public static final int HIDE_TITLEBAR_DELAY = 1500; // in ms
+
+ Activity mActivity;
+ UiController mUiController;
+ TabControl mTabControl;
+ protected Tab mActiveTab;
+ private InputMethodManager mInputManager;
+
+ private Drawable mLockIconSecure;
+ private Drawable mLockIconMixed;
+ protected Drawable mGenericFavicon;
+
+ protected FrameLayout mContentView;
+ protected FrameLayout mCustomViewContainer;
+ protected FrameLayout mFullscreenContainer;
+ private FrameLayout mFixedTitlebarContainer;
+
+ private View mCustomView;
+ private CustomViewCallback mCustomViewCallback;
+ private int mOriginalOrientation;
+
+ private LinearLayout mErrorConsoleContainer = null;
+
+ private UrlBarAutoShowManager mUrlBarAutoShowManager;
+
+ private Toast mStopToast;
+
+ // the default <video> poster
+ private Bitmap mDefaultVideoPoster;
+ // the video progress view
+ private View mVideoProgressView;
+
+ private boolean mActivityPaused;
+ protected boolean mUseQuickControls;
+ protected TitleBar mTitleBar;
+ private NavigationBarBase mNavigationBar;
+ protected PieControl mPieControl;
+ private boolean mBlockFocusAnimations;
+
+ public BaseUi(Activity browser, UiController controller) {
+ mActivity = browser;
+ mUiController = controller;
+ mTabControl = controller.getTabControl();
+ Resources res = mActivity.getResources();
+ mInputManager = (InputMethodManager)
+ browser.getSystemService(Activity.INPUT_METHOD_SERVICE);
+ mLockIconSecure = res.getDrawable(R.drawable.ic_secure_holo_dark);
+ mLockIconMixed = res.getDrawable(R.drawable.ic_secure_partial_holo_dark);
+ FrameLayout frameLayout = (FrameLayout) mActivity.getWindow()
+ .getDecorView().findViewById(android.R.id.content);
+ LayoutInflater.from(mActivity)
+ .inflate(R.layout.custom_screen, frameLayout);
+ mFixedTitlebarContainer = (FrameLayout) frameLayout.findViewById(
+ R.id.fixed_titlebar_container);
+ mContentView = (FrameLayout) frameLayout.findViewById(
+ R.id.main_content);
+ mCustomViewContainer = (FrameLayout) frameLayout.findViewById(
+ R.id.fullscreen_custom_content);
+ mErrorConsoleContainer = (LinearLayout) frameLayout
+ .findViewById(R.id.error_console);
+ setFullscreen(BrowserSettings.getInstance().useFullscreen());
+ mGenericFavicon = res.getDrawable(
+ R.drawable.app_web_browser_sm);
+ mTitleBar = new TitleBar(mActivity, mUiController, this,
+ mContentView);
+ mTitleBar.setProgress(100);
+ mNavigationBar = mTitleBar.getNavigationBar();
+ mUrlBarAutoShowManager = new UrlBarAutoShowManager(this);
+ }
+
+ private void cancelStopToast() {
+ if (mStopToast != null) {
+ mStopToast.cancel();
+ mStopToast = null;
+ }
+ }
+
+ // lifecycle
+
+ public void onPause() {
+ if (isCustomViewShowing()) {
+ onHideCustomView();
+ }
+ cancelStopToast();
+ mActivityPaused = true;
+ }
+
+ public void onResume() {
+ mActivityPaused = false;
+ // check if we exited without setting active tab
+ // b: 5188145
+ final Tab ct = mTabControl.getCurrentTab();
+ if (ct != null) {
+ setActiveTab(ct);
+ }
+ mTitleBar.onResume();
+ }
+
+ protected boolean isActivityPaused() {
+ return mActivityPaused;
+ }
+
+ public void onConfigurationChanged(Configuration config) {
+ }
+
+ public Activity getActivity() {
+ return mActivity;
+ }
+
+ // key handling
+
+ @Override
+ public boolean onBackKey() {
+ if (mCustomView != null) {
+ mUiController.hideCustomView();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onMenuKey() {
+ return false;
+ }
+
+ @Override
+ public void setUseQuickControls(boolean useQuickControls) {
+ mUseQuickControls = useQuickControls;
+ mTitleBar.setUseQuickControls(mUseQuickControls);
+ if (useQuickControls) {
+ mPieControl = new PieControl(mActivity, mUiController, this);
+ mPieControl.attachToContainer(mContentView);
+ } else {
+ if (mPieControl != null) {
+ mPieControl.removeFromContainer(mContentView);
+ }
+ }
+ updateUrlBarAutoShowManagerTarget();
+ }
+
+ // Tab callbacks
+ @Override
+ public void onTabDataChanged(Tab tab) {
+ setUrlTitle(tab);
+ setFavicon(tab);
+ updateLockIconToLatest(tab);
+ updateNavigationState(tab);
+ mTitleBar.onTabDataChanged(tab);
+ mNavigationBar.onTabDataChanged(tab);
+ onProgressChanged(tab);
+ }
+
+ @Override
+ public void onProgressChanged(Tab tab) {
+ int progress = tab.getLoadProgress();
+ if (tab.inForeground()) {
+ mTitleBar.setProgress(progress);
+ }
+ }
+
+ @Override
+ public void bookmarkedStatusHasChanged(Tab tab) {
+ if (tab.inForeground()) {
+ boolean isBookmark = tab.isBookmarkedSite();
+ mNavigationBar.setCurrentUrlIsBookmark(isBookmark);
+ }
+ }
+
+ @Override
+ public void onPageStopped(Tab tab) {
+ cancelStopToast();
+ if (tab.inForeground()) {
+ mStopToast = Toast
+ .makeText(mActivity, R.string.stopping, Toast.LENGTH_SHORT);
+ mStopToast.show();
+ }
+ }
+
+ @Override
+ public boolean needsRestoreAllTabs() {
+ return true;
+ }
+
+ @Override
+ public void addTab(Tab tab) {
+ }
+
+ @Override
+ public void setActiveTab(final Tab tab) {
+ if (tab == null) return;
+ // block unnecessary focus change animations during tab switch
+ mBlockFocusAnimations = true;
+ mHandler.removeMessages(MSG_HIDE_TITLEBAR);
+ if ((tab != mActiveTab) && (mActiveTab != null)) {
+ removeTabFromContentView(mActiveTab);
+ WebView web = mActiveTab.getWebView();
+ if (web != null) {
+ web.setOnTouchListener(null);
+ }
+ }
+ mActiveTab = tab;
+ BrowserWebView web = (BrowserWebView) mActiveTab.getWebView();
+ updateUrlBarAutoShowManagerTarget();
+ attachTabToContentView(tab);
+ if (web != null) {
+ // Request focus on the top window.
+ if (mUseQuickControls) {
+ mPieControl.forceToTop(mContentView);
+ web.setTitleBar(null);
+ mTitleBar.hide();
+ } else {
+ web.setTitleBar(mTitleBar);
+ mTitleBar.onScrollChanged();
+ }
+ }
+ mTitleBar.bringToFront();
+ tab.getTopWindow().requestFocus();
+ setShouldShowErrorConsole(tab, mUiController.shouldShowErrorConsole());
+ onTabDataChanged(tab);
+ onProgressChanged(tab);
+ mNavigationBar.setIncognitoMode(tab.isPrivateBrowsingEnabled());
+ updateAutoLogin(tab, false);
+ mBlockFocusAnimations = false;
+ }
+
+ protected void updateUrlBarAutoShowManagerTarget() {
+ WebView web = mActiveTab != null ? mActiveTab.getWebView() : null;
+ if (!mUseQuickControls && web instanceof BrowserWebView) {
+ mUrlBarAutoShowManager.setTarget((BrowserWebView) web);
+ } else {
+ mUrlBarAutoShowManager.setTarget(null);
+ }
+ }
+
+ Tab getActiveTab() {
+ return mActiveTab;
+ }
+
+ @Override
+ public void updateTabs(List<Tab> tabs) {
+ }
+
+ @Override
+ public void removeTab(Tab tab) {
+ if (mActiveTab == tab) {
+ removeTabFromContentView(tab);
+ mActiveTab = null;
+ }
+ }
+
+ @Override
+ public void detachTab(Tab tab) {
+ removeTabFromContentView(tab);
+ }
+
+ @Override
+ public void attachTab(Tab tab) {
+ attachTabToContentView(tab);
+ }
+
+ protected void attachTabToContentView(Tab tab) {
+ if ((tab == null) || (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) {
+ parent.removeView(mainView);
+ }
+ wrapper.addView(mainView);
+ }
+ parent = (ViewGroup) container.getParent();
+ if (parent != mContentView) {
+ if (parent != null) {
+ parent.removeView(container);
+ }
+ mContentView.addView(container, COVER_SCREEN_PARAMS);
+ }
+ mUiController.attachSubWindow(tab);
+ }
+
+ private void removeTabFromContentView(Tab tab) {
+ hideTitleBar();
+ // 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);
+ }
+ }
+
+ @Override
+ public void onSetWebView(Tab tab, WebView webView) {
+ View container = tab.getViewContainer();
+ if (container == null) {
+ // The tab consists of a container view, which contains the main
+ // WebView, as well as any other UI elements associated with the tab.
+ container = mActivity.getLayoutInflater().inflate(R.layout.tab,
+ mContentView, false);
+ tab.setViewContainer(container);
+ }
+ if (tab.getWebView() != webView) {
+ // Just remove the old one.
+ FrameLayout wrapper =
+ (FrameLayout) container.findViewById(R.id.webview_wrapper);
+ wrapper.removeView(tab.getWebView());
+ }
+ }
+
+ /**
+ * 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() {
+ @Override
+ public void onClick(View v) {
+ ((BrowserWebView) 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);
+ }
+
+ protected void refreshWebView() {
+ WebView web = getWebView();
+ if (web != null) {
+ web.invalidate();
+ }
+ }
+
+ public void editUrl(boolean clearInput, boolean forceIME) {
+ if (mUiController.isInCustomActionMode()) {
+ mUiController.endActionMode();
+ }
+ showTitleBar();
+ if ((getActiveTab() != null) && !getActiveTab().isSnapshot()) {
+ mNavigationBar.startEditingUrl(clearInput, forceIME);
+ }
+ }
+
+ boolean canShowTitleBar() {
+ return !isTitleBarShowing()
+ && !isActivityPaused()
+ && (getActiveTab() != null)
+ && (getWebView() != null)
+ && !mUiController.isInCustomActionMode();
+ }
+
+ protected void showTitleBar() {
+ mHandler.removeMessages(MSG_HIDE_TITLEBAR);
+ if (canShowTitleBar()) {
+ mTitleBar.show();
+ }
+ }
+
+ protected void hideTitleBar() {
+ if (mTitleBar.isShowing()) {
+ mTitleBar.hide();
+ }
+ }
+
+ protected boolean isTitleBarShowing() {
+ return mTitleBar.isShowing();
+ }
+
+ public boolean isEditingUrl() {
+ return mTitleBar.isEditingUrl();
+ }
+
+ public void stopEditingUrl() {
+ mTitleBar.getNavigationBar().stopEditingUrl();
+ }
+
+ public TitleBar getTitleBar() {
+ return mTitleBar;
+ }
+
+ @Override
+ public void showComboView(ComboViews startingView, Bundle extras) {
+ Intent intent = new Intent(mActivity, ComboViewActivity.class);
+ intent.putExtra(ComboViewActivity.EXTRA_INITIAL_VIEW, startingView.name());
+ intent.putExtra(ComboViewActivity.EXTRA_COMBO_ARGS, extras);
+ Tab t = getActiveTab();
+ if (t != null) {
+ intent.putExtra(ComboViewActivity.EXTRA_CURRENT_URL, t.getUrl());
+ }
+ mActivity.startActivityForResult(intent, Controller.COMBO_VIEW);
+ }
+
+ @Override
+ public void showCustomView(View view, int requestedOrientation,
+ CustomViewCallback callback) {
+ // if a view already exists then immediately terminate the new one
+ if (mCustomView != null) {
+ callback.onCustomViewHidden();
+ return;
+ }
+
+ mOriginalOrientation = mActivity.getRequestedOrientation();
+ FrameLayout decor = (FrameLayout) mActivity.getWindow().getDecorView();
+ mFullscreenContainer = new FullscreenHolder(mActivity);
+ mFullscreenContainer.addView(view, COVER_SCREEN_PARAMS);
+ decor.addView(mFullscreenContainer, COVER_SCREEN_PARAMS);
+ mCustomView = view;
+ setFullscreen(true);
+ ((BrowserWebView) getWebView()).setVisibility(View.INVISIBLE);
+ mCustomViewCallback = callback;
+ mActivity.setRequestedOrientation(requestedOrientation);
+ }
+
+ @Override
+ public void onHideCustomView() {
+ ((BrowserWebView) getWebView()).setVisibility(View.VISIBLE);
+ if (mCustomView == null)
+ return;
+ setFullscreen(false);
+ FrameLayout decor = (FrameLayout) mActivity.getWindow().getDecorView();
+ decor.removeView(mFullscreenContainer);
+ mFullscreenContainer = null;
+ mCustomView = null;
+ mCustomViewCallback.onCustomViewHidden();
+ // Show the content view.
+ mActivity.setRequestedOrientation(mOriginalOrientation);
+ }
+
+ @Override
+ public boolean isCustomViewShowing() {
+ return mCustomView != null;
+ }
+
+ protected void dismissIME() {
+ if (mInputManager.isActive()) {
+ mInputManager.hideSoftInputFromWindow(mContentView.getWindowToken(),
+ 0);
+ }
+ }
+
+ @Override
+ public boolean isWebShowing() {
+ return mCustomView == null;
+ }
+
+ @Override
+ public void showAutoLogin(Tab tab) {
+ updateAutoLogin(tab, true);
+ }
+
+ @Override
+ public void hideAutoLogin(Tab tab) {
+ updateAutoLogin(tab, true);
+ }
+
+ // -------------------------------------------------------------------------
+
+ protected void updateNavigationState(Tab tab) {
+ }
+
+ protected void updateAutoLogin(Tab tab, boolean animate) {
+ mTitleBar.updateAutoLogin(tab, animate);
+ }
+
+ /**
+ * Update the lock icon to correspond to our latest state.
+ */
+ protected void updateLockIconToLatest(Tab t) {
+ if (t != null && t.inForeground()) {
+ updateLockIconImage(t.getSecurityState());
+ }
+ }
+
+ /**
+ * Updates the lock-icon image in the title-bar.
+ */
+ private void updateLockIconImage(SecurityState securityState) {
+ Drawable d = null;
+ if (securityState == SecurityState.SECURITY_STATE_SECURE) {
+ d = mLockIconSecure;
+ } else if (securityState == SecurityState.SECURITY_STATE_MIXED
+ || securityState == SecurityState.SECURITY_STATE_BAD_CERTIFICATE) {
+ // TODO: It would be good to have different icons for insecure vs mixed content.
+ // See http://b/5403800
+ d = mLockIconMixed;
+ }
+ mNavigationBar.setLock(d);
+ }
+
+ protected void setUrlTitle(Tab tab) {
+ String url = tab.getUrl();
+ String title = tab.getTitle();
+ if (TextUtils.isEmpty(title)) {
+ title = url;
+ }
+ if (tab.inForeground()) {
+ mNavigationBar.setDisplayTitle(url);
+ }
+ }
+
+ // Set the favicon in the title bar.
+ protected void setFavicon(Tab tab) {
+ if (tab.inForeground()) {
+ Bitmap icon = tab.getFavicon();
+ mNavigationBar.setFavicon(icon);
+ }
+ }
+
+ @Override
+ public void onActionModeFinished(boolean inLoad) {
+ }
+
+ // active tabs page
+
+ public void showActiveTabsPage() {
+ }
+
+ /**
+ * Remove the active tabs page.
+ */
+ public void removeActiveTabsPage() {
+ }
+
+ // menu handling callbacks
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ return true;
+ }
+
+ @Override
+ public void updateMenuState(Tab tab, Menu menu) {
+ }
+
+ @Override
+ public void onOptionsMenuOpened() {
+ }
+
+ @Override
+ public void onExtendedMenuOpened() {
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ return false;
+ }
+
+ @Override
+ public void onOptionsMenuClosed(boolean inLoad) {
+ }
+
+ @Override
+ public void onExtendedMenuClosed(boolean inLoad) {
+ }
+
+ @Override
+ public void onContextMenuCreated(Menu menu) {
+ }
+
+ @Override
+ public void onContextMenuClosed(Menu menu, boolean inLoad) {
+ }
+
+ // error console
+
+ @Override
+ public void setShouldShowErrorConsole(Tab tab, boolean flag) {
+ if (tab == null) return;
+ 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);
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // 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;
+ }
+
+ @Override
+ public void showMaxTabsWarning() {
+ Toast warning = Toast.makeText(mActivity,
+ mActivity.getString(R.string.max_tabs_warning),
+ Toast.LENGTH_SHORT);
+ warning.show();
+ }
+
+ protected WebView getWebView() {
+ if (mActiveTab != null) {
+ return mActiveTab.getWebView();
+ } else {
+ return null;
+ }
+ }
+
+ public void setFullscreen(boolean enabled) {
+ Window win = mActivity.getWindow();
+ WindowManager.LayoutParams winParams = win.getAttributes();
+ final int bits = WindowManager.LayoutParams.FLAG_FULLSCREEN;
+ if (enabled) {
+ winParams.flags |= bits;
+ } else {
+ winParams.flags &= ~bits;
+ if (mCustomView != null) {
+ mCustomView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
+ } else {
+ mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
+ }
+ }
+ win.setAttributes(winParams);
+ }
+
+ public Drawable getFaviconDrawable(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);
+ return d;
+ }
+
+ public boolean isLoading() {
+ return mActiveTab != null ? mActiveTab.inPageLoad() : false;
+ }
+
+ /**
+ * Suggest to the UI that the title bar can be hidden. The UI will then
+ * decide whether or not to hide based off a number of factors, such
+ * as if the user is editing the URL bar or if the page is loading
+ */
+ public void suggestHideTitleBar() {
+ if (!isLoading() && !isEditingUrl() && !mTitleBar.wantsToBeVisible()
+ && !mNavigationBar.isMenuShowing()) {
+ hideTitleBar();
+ }
+ }
+
+ protected final void showTitleBarForDuration() {
+ showTitleBarForDuration(HIDE_TITLEBAR_DELAY);
+ }
+
+ protected final void showTitleBarForDuration(long duration) {
+ showTitleBar();
+ Message msg = Message.obtain(mHandler, MSG_HIDE_TITLEBAR);
+ mHandler.sendMessageDelayed(msg, duration);
+ }
+
+ protected Handler mHandler = new Handler() {
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_HIDE_TITLEBAR) {
+ suggestHideTitleBar();
+ }
+ BaseUi.this.handleMessage(msg);
+ }
+ };
+
+ protected void handleMessage(Message msg) {}
+
+ @Override
+ public void showWeb(boolean animate) {
+ mUiController.hideCustomView();
+ }
+
+ static class FullscreenHolder extends FrameLayout {
+
+ public FullscreenHolder(Context ctx) {
+ super(ctx);
+ setBackgroundColor(ctx.getResources().getColor(R.color.black));
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent evt) {
+ return true;
+ }
+
+ }
+
+ public void addFixedTitleBar(View view) {
+ mFixedTitlebarContainer.addView(view);
+ }
+
+ public void setContentViewMarginTop(int margin) {
+ LinearLayout.LayoutParams params =
+ (LinearLayout.LayoutParams) mContentView.getLayoutParams();
+ if (params.topMargin != margin) {
+ params.topMargin = margin;
+ mContentView.setLayoutParams(params);
+ }
+ }
+
+ @Override
+ public boolean blockFocusAnimations() {
+ return mBlockFocusAnimations;
+ }
+
+ @Override
+ public void onVoiceResult(String result) {
+ mNavigationBar.onVoiceResult(result);
+ }
+
+ protected UiController getUiController() {
+ return mUiController;
+ }
+}
diff --git a/src/com/android/browser/BookmarkItem.java b/src/com/android/browser/BookmarkItem.java
new file mode 100644
index 0000000..b41ee00
--- /dev/null
+++ b/src/com/android/browser/BookmarkItem.java
@@ -0,0 +1,197 @@
+/*
+ * 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 com.android.browser.R;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.HorizontalScrollView;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+/**
+ * Custom layout for an item representing a bookmark in the browser.
+ */
+class BookmarkItem extends HorizontalScrollView {
+
+ final static int MAX_TEXTVIEW_LEN = 80;
+
+ protected TextView mTextView;
+ protected TextView mUrlText;
+ protected ImageView mImageView;
+ protected String mUrl;
+ protected String mTitle;
+ protected boolean mEnableScrolling = false;
+
+ /**
+ * Instantiate a bookmark item, including a default favicon.
+ *
+ * @param context The application context for the item.
+ */
+ BookmarkItem(Context context) {
+ super(context);
+
+ setClickable(false);
+ setEnableScrolling(false);
+ LayoutInflater factory = LayoutInflater.from(context);
+ factory.inflate(R.layout.history_item, this);
+ mTextView = (TextView) findViewById(R.id.title);
+ mUrlText = (TextView) findViewById(R.id.url);
+ mImageView = (ImageView) findViewById(R.id.favicon);
+ View star = findViewById(R.id.star);
+ star.setVisibility(View.GONE);
+ }
+
+ /**
+ * Copy this BookmarkItem to item.
+ * @param item BookmarkItem to receive the info from this BookmarkItem.
+ */
+ /* package */ void copyTo(BookmarkItem item) {
+ item.mTextView.setText(mTextView.getText());
+ item.mUrlText.setText(mUrlText.getText());
+ item.mImageView.setImageDrawable(mImageView.getDrawable());
+ }
+
+ /**
+ * Return the name assigned to this bookmark item.
+ */
+ /* package */ String getName() {
+ return mTitle;
+ }
+
+ /* package */ String getUrl() {
+ return mUrl;
+ }
+
+ /**
+ * Set the favicon for this item.
+ *
+ * @param b The new bitmap for this item.
+ * If it is null, will use the default.
+ */
+ /* package */ void setFavicon(Bitmap b) {
+ if (b != null) {
+ mImageView.setImageBitmap(b);
+ } else {
+ mImageView.setImageResource(R.drawable.app_web_browser_sm);
+ }
+ }
+
+ void setFaviconBackground(Drawable d) {
+ mImageView.setBackgroundDrawable(d);
+ }
+
+ /**
+ * Set the new name for the bookmark item.
+ *
+ * @param name The new name for the bookmark item.
+ */
+ /* package */ void setName(String name) {
+ if (name == null) {
+ return;
+ }
+
+ mTitle = name;
+
+ if (name.length() > MAX_TEXTVIEW_LEN) {
+ name = name.substring(0, MAX_TEXTVIEW_LEN);
+ }
+
+ mTextView.setText(name);
+ }
+
+ /**
+ * Set the new url for the bookmark item.
+ * @param url The new url for the bookmark item.
+ */
+ /* package */ void setUrl(String url) {
+ if (url == null) {
+ return;
+ }
+
+ mUrl = url;
+
+ url = UrlUtils.stripUrl(url);
+ if (url.length() > MAX_TEXTVIEW_LEN) {
+ url = url.substring(0, MAX_TEXTVIEW_LEN);
+ }
+
+ mUrlText.setText(url);
+ }
+
+ void setEnableScrolling(boolean enable) {
+ mEnableScrolling = enable;
+ setFocusable(mEnableScrolling);
+ setFocusableInTouchMode(mEnableScrolling);
+ requestDisallowInterceptTouchEvent(!mEnableScrolling);
+ requestLayout();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (mEnableScrolling) {
+ return super.onTouchEvent(ev);
+ }
+ return false;
+ }
+
+ @Override
+ protected void measureChild(View child, int parentWidthMeasureSpec,
+ int parentHeightMeasureSpec) {
+ if (mEnableScrolling) {
+ super.measureChild(child, parentWidthMeasureSpec, parentHeightMeasureSpec);
+ return;
+ }
+
+ final ViewGroup.LayoutParams lp = child.getLayoutParams();
+
+ final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
+ getPaddingLeft() + getPaddingRight(), lp.width);
+ final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
+ getPaddingTop() + getPaddingBottom(), lp.height);
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+ @Override
+ protected void measureChildWithMargins(View child,
+ int parentWidthMeasureSpec, int widthUsed,
+ int parentHeightMeasureSpec, int heightUsed) {
+ if (mEnableScrolling) {
+ super.measureChildWithMargins(child, parentWidthMeasureSpec,
+ widthUsed, parentHeightMeasureSpec, heightUsed);
+ return;
+ }
+
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+ final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
+ getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
+ + widthUsed, lp.width);
+ final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
+ getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin
+ + heightUsed, lp.height);
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+}
diff --git a/src/com/android/browser/BookmarkSearch.java b/src/com/android/browser/BookmarkSearch.java
new file mode 100644
index 0000000..4d3ca0f
--- /dev/null
+++ b/src/com/android/browser/BookmarkSearch.java
@@ -0,0 +1,44 @@
+/*
+ * 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.content.Intent;
+import android.os.Bundle;
+
+/**
+ * This activity is never started from the browser. Its purpose is to provide bookmark suggestions
+ * to global search (through its searchable meta-data), and to handle the intents produced
+ * by clicking such suggestions.
+ */
+public class BookmarkSearch extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Intent intent = getIntent();
+ if (intent != null) {
+ String action = intent.getAction();
+ if (Intent.ACTION_VIEW.equals(action)) {
+ intent.setClass(this, BrowserActivity.class);
+ startActivity(intent);
+ }
+ }
+ finish();
+ }
+
+}
diff --git a/src/com/android/browser/BookmarkUtils.java b/src/com/android/browser/BookmarkUtils.java
new file mode 100644
index 0000000..b754a62
--- /dev/null
+++ b/src/com/android/browser/BookmarkUtils.java
@@ -0,0 +1,294 @@
+/*
+ * 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.ActivityManager;
+import android.app.AlertDialog;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+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.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.PaintDrawable;
+import android.net.Uri;
+import android.os.Message;
+import android.provider.Browser;
+
+import com.android.browser.R;
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.BrowserContract.Bookmarks;
+
+public 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).
+ ICON_WIDGET,
+ }
+
+ /**
+ * 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) {
+ final ActivityManager am = (ActivityManager) context
+ .getSystemService(Context.ACTIVITY_SERVICE);
+ final int iconDimension = am.getLauncherLargeIconSize();
+ final int iconDensity = am.getLauncherLargeIconDensity();
+ return createIcon(context, touchIcon, favicon, type, iconDimension, iconDensity);
+ }
+
+ static Drawable createListFaviconBackground(Context context) {
+ PaintDrawable faviconBackground = new PaintDrawable();
+ Resources res = context.getResources();
+ int padding = res.getDimensionPixelSize(R.dimen.list_favicon_padding);
+ faviconBackground.setPadding(padding, padding, padding, padding);
+ faviconBackground.getPaint().setColor(context.getResources()
+ .getColor(R.color.bookmarkListFaviconBackground));
+ faviconBackground.setCornerRadius(
+ res.getDimension(R.dimen.list_favicon_corner_radius));
+ return faviconBackground;
+ }
+
+ private static Bitmap createIcon(Context context, Bitmap touchIcon,
+ Bitmap favicon, BookmarkIconType type, int iconDimension, int iconDensity) {
+ 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, iconDensity);
+
+ if (icon != null) {
+ // Now draw the correct icon background into our new bitmap.
+ Paint p = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
+ canvas.drawBitmap(icon, null, iconBounds, p);
+ }
+
+ // If we have a favicon, overlay it in a nice rounded white box on top of the
+ // background.
+ if (favicon != null) {
+ drawFaviconToCanvas(context, favicon, canvas, iconBounds, type);
+ }
+ }
+ canvas.setBitmap(null);
+ 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 = createShortcutIntent(url);
+ 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;
+ }
+
+ static Intent createShortcutIntent(String url) {
+ 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));
+ return shortcutIntent;
+ }
+
+ private static Bitmap getIconBackground(Context context, BookmarkIconType type, int density) {
+ if (type == BookmarkIconType.ICON_HOME_SHORTCUT) {
+ // Want to create a shortcut icon on the homescreen, so the icon
+ // background is the red bookmark.
+ Drawable drawable = context.getResources().getDrawableForDensity(
+ R.mipmap.ic_launcher_shortcut_browser_bookmark, density);
+ if (drawable instanceof BitmapDrawable) {
+ BitmapDrawable bd = (BitmapDrawable) drawable;
+ return bd.getBitmap();
+ }
+ } else if (type == BookmarkIconType.ICON_INSTALLABLE_WEB_APP) {
+ // Use the web browser icon as the background for the icon for an installable
+ // web app.
+ Drawable drawable = context.getResources().getDrawableForDensity(
+ R.mipmap.ic_launcher_browser, density);
+ if (drawable instanceof BitmapDrawable) {
+ BitmapDrawable bd = (BitmapDrawable) drawable;
+ return bd.getBitmap();
+ }
+ }
+ 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(Context context, Bitmap favicon,
+ Canvas canvas, Rect iconBounds, BookmarkIconType type) {
+ // 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);
+ if (type == BookmarkIconType.ICON_WIDGET) {
+ p.setColor(context.getResources()
+ .getColor(R.color.bookmarkWidgetFaviconBackground));
+ } else {
+ p.setColor(Color.WHITE);
+ }
+
+ // Create a rectangle that is slightly wider than the favicon
+ int faviconDimension = context.getResources().getDimensionPixelSize(R.dimen.favicon_size);
+ int faviconPaddedRectDimension;
+ if (type == BookmarkIconType.ICON_WIDGET) {
+ faviconPaddedRectDimension = canvas.getWidth();
+ } else {
+ faviconPaddedRectDimension = context.getResources().getDimensionPixelSize(
+ R.dimen.favicon_padded_size);
+ }
+ float padding = (faviconPaddedRectDimension - faviconDimension) / 2;
+ final float x = iconBounds.exactCenterX() - (faviconPaddedRectDimension / 2);
+ float y = iconBounds.exactCenterY() - (faviconPaddedRectDimension / 2);
+ if (type != BookmarkIconType.ICON_WIDGET) {
+ // Note: Subtract from the y position since the box is
+ // slightly higher than center. Use padding since it is already
+ // device independent.
+ y -= padding;
+ }
+ RectF r = new RectF(x, y, x + faviconPaddedRectDimension, y + faviconPaddedRectDimension);
+ // Draw a white rounded rectangle behind the favicon
+ canvas.drawRoundRect(r, 3, 3, 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, null);
+ }
+
+ /* package */ static Uri getBookmarksUri(Context context) {
+ return BrowserContract.Bookmarks.CONTENT_URI;
+ }
+
+ /**
+ * Show a confirmation dialog to remove a bookmark.
+ * @param id Id of the bookmark to remove
+ * @param title Title of the bookmark, to be displayed in the confirmation method.
+ * @param context Package Context for strings, dialog, ContentResolver
+ * @param msg Message to send if the bookmark is deleted.
+ */
+ static void displayRemoveBookmarkDialog( final long id, final String title,
+ final Context context, final Message msg) {
+
+ new AlertDialog.Builder(context)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setMessage(context.getString(R.string.delete_bookmark_warning,
+ title))
+ .setPositiveButton(R.string.ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ if (msg != null) {
+ msg.sendToTarget();
+ }
+ Runnable runnable = new Runnable(){
+ @Override
+ public void run() {
+ removeBookmarkOrFolder(context, id);
+ }
+ };
+ new Thread(runnable).start();
+ }
+ })
+ .setNegativeButton(R.string.cancel, null)
+ .show();
+ }
+
+ /**
+ * Remove the bookmark or folder.Remove all sub folders and bookmarks under current folder.
+ * @param context Package Context for strings, dialog, ContentResolver.
+ * @param id Id of the bookmark to remove.
+ */
+ private static void removeBookmarkOrFolder(Context context, long id) {
+ Cursor cursor = context.getContentResolver().query(Bookmarks.CONTENT_URI,
+ new String[] {Bookmarks._ID},
+ Bookmarks.PARENT + "=?",
+ new String[] {Long.toString(id)},
+ null);
+
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ do {
+ removeBookmarkOrFolder(context,
+ cursor.getLong(cursor.getColumnIndex(Bookmarks._ID)));
+ } while (cursor.moveToNext());
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ cursor.close();
+ }
+ }
+
+ context.getContentResolver().delete(
+ ContentUris.withAppendedId(Bookmarks.CONTENT_URI, id), null, null);
+ }
+}
diff --git a/src/com/android/browser/Bookmarks.java b/src/com/android/browser/Bookmarks.java
new file mode 100644
index 0000000..4a49520
--- /dev/null
+++ b/src/com/android/browser/Bookmarks.java
@@ -0,0 +1,229 @@
+/*
+ * 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.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.os.AsyncTask;
+import android.preference.PreferenceManager;
+
+import com.android.browser.R;
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.BrowserContract.Combined;
+import com.android.browser.platformsupport.BrowserContract.Images;
+
+import android.text.TextUtils;
+import android.util.Log;
+import android.webkit.WebIconDatabase;
+import android.widget.Toast;
+
+import java.io.ByteArrayOutputStream;
+
+/**
+ * This class is purely to have a common place for adding/deleting bookmarks.
+ */
+public class Bookmarks {
+ // We only want the user to be able to bookmark content that
+ // the browser can handle directly.
+ private static final String acceptableBookmarkSchemes[] = {
+ "http:",
+ "https:",
+ "about:",
+ "data:",
+ "javascript:",
+ "file:",
+ "content:"
+ };
+
+ private final static String LOGTAG = "Bookmarks";
+ /**
+ * Add a bookmark to the database.
+ * @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 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, boolean showToast, String url,
+ String name, Bitmap thumbnail, long parent) {
+ // Want to append to the beginning of the list
+ ContentValues values = new ContentValues();
+ try {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ 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);
+ }
+ if (showToast) {
+ Toast.makeText(context, R.string.added_to_bookmarks,
+ Toast.LENGTH_LONG).show();
+ }
+ }
+
+ /**
+ * Remove a bookmark from the database. If the url is a visited site, it
+ * will remain in the database, but only as a history item, and not as a
+ * bookmarked site.
+ * @param context Context of the calling Activity. This is used to make
+ * Toast confirming that the bookmark has been removed and to
+ * lookup the correct content uri. It must not be null.
+ * @param cr The ContentResolver being used to remove the bookmark.
+ * @param url URL of the website to be removed.
+ */
+ /* package */ static void removeFromBookmarks(Context context,
+ ContentResolver cr, String url, String title) {
+ Cursor cursor = null;
+ try {
+ Uri uri = BookmarkUtils.getBookmarksUri(context);
+ cursor = cr.query(uri,
+ new String[] { BrowserContract.Bookmarks._ID },
+ BrowserContract.Bookmarks.URL + " = ? AND " +
+ BrowserContract.Bookmarks.TITLE + " = ?",
+ new String[] { url, title },
+ null);
+
+ if (!cursor.moveToFirst()) {
+ return;
+ }
+
+ // Remove from bookmarks
+ WebIconDatabase.getInstance().releaseIconForPageUrl(url);
+ 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();
+ }
+ } catch (IllegalStateException e) {
+ Log.e(LOGTAG, "removeFromBookmarks", e);
+ } finally {
+ if (cursor != null) cursor.close();
+ }
+ }
+
+ private static byte[] bitmapToBytes(Bitmap bm) {
+ if (bm == null) {
+ return null;
+ }
+
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ bm.compress(Bitmap.CompressFormat.PNG, 100, os);
+ return os.toByteArray();
+ }
+
+ /* package */ static boolean urlHasAcceptableScheme(String url) {
+ if (url == null) {
+ return false;
+ }
+
+ for (int i = 0; i < acceptableBookmarkSchemes.length; i++) {
+ if (url.startsWith(acceptableBookmarkSchemes[i])) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static final String QUERY_BOOKMARKS_WHERE =
+ Combined.URL + " == ? OR " +
+ Combined.URL + " == ?";
+
+ public 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.
+
+ final String[] selArgs = new String[] { 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) {
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ favicon.compress(Bitmap.CompressFormat.PNG, 100, os);
+
+ // The Images update will insert if it doesn't exist
+ ContentValues values = new ContentValues();
+ values.put(Images.FAVICON, os.toByteArray());
+ updateImages(cr, originalUrl, values);
+ updateImages(cr, url, values);
+ return null;
+ }
+
+ private void updateImages(final ContentResolver cr,
+ final String url, ContentValues values) {
+ String iurl = removeQuery(url);
+ if (!TextUtils.isEmpty(iurl)) {
+ values.put(Images.URL, iurl);
+ cr.update(BrowserContract.Images.CONTENT_URI, values, null, null);
+ }
+ }
+ }.execute();
+ }
+}
diff --git a/src/com/android/browser/BookmarksLoader.java b/src/com/android/browser/BookmarksLoader.java
new file mode 100644
index 0000000..9d551e3
--- /dev/null
+++ b/src/com/android/browser/BookmarksLoader.java
@@ -0,0 +1,71 @@
+/*
+ * 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 com.android.browser.platformsupport.BrowserContract.Bookmarks;
+
+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 int COLUMN_INDEX_TYPE = 9;
+
+ 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
+ Bookmarks.TYPE, // 9
+ };
+
+ String mAccountType;
+ 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));
+ }
+
+ static Uri addAccount(Uri uri, String accountType, String accountName) {
+ return uri.buildUpon().appendQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE, accountType).
+ appendQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME, accountName).build();
+ }
+}
diff --git a/src/com/android/browser/BreadCrumbView.java b/src/com/android/browser/BreadCrumbView.java
new file mode 100644
index 0000000..f6bee4a
--- /dev/null
+++ b/src/com/android/browser/BreadCrumbView.java
@@ -0,0 +1,413 @@
+/*
+ * 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.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+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.RelativeLayout;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.android.browser.R;
+
+/**
+ * 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 RelativeLayout implements OnClickListener {
+ private static final int DIVIDER_PADDING = 12; // dips
+ private static final int CRUMB_PADDING = 8; // dips
+
+ public interface Controller {
+ public void onTop(BreadCrumbView view, int level, Object data);
+ }
+
+ private ImageButton mBackButton;
+ private LinearLayout mCrumbLayout;
+ private LinearLayout mBackLayout;
+ private Controller mController;
+ private List<Crumb> mCrumbs;
+ private boolean mUseBackButton;
+ private Drawable mSeparatorDrawable;
+ private float mDividerPadding;
+ private int mMaxVisible = -1;
+ private Context mContext;
+ private int mCrumbPadding;
+
+ /**
+ * @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) {
+ mContext = ctx;
+ setFocusable(true);
+ setGravity(Gravity.CENTER_VERTICAL);
+ mUseBackButton = false;
+ mCrumbs = new ArrayList<Crumb>();
+ mSeparatorDrawable = ctx.getResources().getDrawable(
+ android.R.drawable.divider_horizontal_dark);
+ float density = mContext.getResources().getDisplayMetrics().density;
+ mDividerPadding = DIVIDER_PADDING * density;
+ mCrumbPadding = (int) (CRUMB_PADDING * density);
+ addCrumbLayout();
+ addBackLayout();
+ }
+
+ public void setUseBackButton(boolean useflag) {
+ mUseBackButton = useflag;
+ updateVisible();
+ }
+
+ public void setController(Controller ctl) {
+ mController = ctl;
+ }
+
+ public int getMaxVisible() {
+ return mMaxVisible;
+ }
+
+ public void setMaxVisible(int max) {
+ mMaxVisible = max;
+ updateVisible();
+ }
+
+ public int getTopLevel() {
+ return mCrumbs.size();
+ }
+
+ 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(this, mCrumbs.size(), getTopCrumb().data);
+ } else {
+ mController.onTop(this, 0, null);
+ }
+ }
+ }
+
+ public View pushView(String name, Object data) {
+ return pushView(name, true, data);
+ }
+
+ public View pushView(String name, boolean canGoBack, Object data) {
+ Crumb crumb = new Crumb(name, canGoBack, data);
+ pushCrumb(crumb);
+ return crumb.crumbView;
+ }
+
+ 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.icon_up);
+ TypedValue outValue = new TypedValue();
+ getContext().getTheme().resolveAttribute(
+ android.R.attr.selectableItemBackground, outValue, true);
+ int resid = outValue.resourceId;
+ mBackButton.setBackgroundResource(resid);
+ mBackButton.setPadding(mCrumbPadding, 0, mCrumbPadding, 0);
+ mBackButton.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.MATCH_PARENT));
+ mBackButton.setOnClickListener(this);
+ mBackButton.setContentDescription(mContext.getText(
+ R.string.accessibility_button_bookmarks_folder_up));
+ mBackLayout.addView(mBackButton);
+ }
+
+ private void addParentLabel() {
+ TextView tv = new TextView(mContext);
+ tv.setTextAppearance(mContext, android.R.style.TextAppearance_Medium);
+ tv.setPadding(mCrumbPadding, 0, 0, 0);
+ tv.setGravity(Gravity.CENTER_VERTICAL);
+ tv.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT));
+ tv.setText("/ .../");
+ tv.setSingleLine();
+ tv.setVisibility(View.GONE);
+ mCrumbLayout.addView(tv);
+ }
+
+ private void addCrumbLayout() {
+ mCrumbLayout = new LinearLayout(mContext);
+ LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT);
+ params.addRule(ALIGN_PARENT_LEFT, TRUE);
+ params.setMargins(0, 0, 4 * mCrumbPadding, 0);
+ mCrumbLayout.setLayoutParams(params);
+ mCrumbLayout.setVisibility(View.VISIBLE);
+ addParentLabel();
+ addView(mCrumbLayout);
+ }
+
+ private void addBackLayout() {
+ mBackLayout= new LinearLayout(mContext);
+ LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT);
+ params.addRule(ALIGN_PARENT_RIGHT, TRUE);
+ mBackLayout.setLayoutParams(params);
+ mBackLayout.setVisibility(View.GONE);
+ addSeparator();
+ addBackButton();
+ addView(mBackLayout);
+ }
+
+ private void pushCrumb(Crumb crumb) {
+ mCrumbs.add(crumb);
+ mCrumbLayout.addView(crumb.crumbView);
+ updateVisible();
+ crumb.crumbView.setOnClickListener(this);
+ }
+
+ private void addSeparator() {
+ View sep = makeDividerView();
+ sep.setLayoutParams(makeDividerLayoutParams());
+ mBackLayout.addView(sep);
+ }
+
+ private ImageView makeDividerView() {
+ ImageView result = new ImageView(mContext);
+ result.setImageDrawable(mSeparatorDrawable);
+ result.setScaleType(ImageView.ScaleType.FIT_XY);
+ return result;
+ }
+
+ private LinearLayout.LayoutParams makeDividerLayoutParams() {
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
+ LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
+ return params;
+ }
+
+ private void pop(boolean notify) {
+ int n = mCrumbs.size();
+ if (n > 0) {
+ removeLastView();
+ mCrumbs.remove(n - 1);
+ if (mUseBackButton) {
+ Crumb top = getTopCrumb();
+ if (top != null && top.canGoBack) {
+ mBackLayout.setVisibility(View.VISIBLE);
+ } else {
+ mBackLayout.setVisibility(View.GONE);
+ }
+ }
+ updateVisible();
+ if (notify) {
+ notifyController();
+ }
+ }
+ }
+
+ private void updateVisible() {
+ // start at index 1 (0 == parent label)
+ int childIndex = 1;
+ if (mMaxVisible >= 0) {
+ int invisibleCrumbs = size() - mMaxVisible;
+ if (invisibleCrumbs > 0) {
+ int crumbIndex = 0;
+ while (crumbIndex < invisibleCrumbs) {
+ // Set the crumb to GONE.
+ mCrumbLayout.getChildAt(childIndex).setVisibility(View.GONE);
+ childIndex++;
+ // Move to the next crumb.
+ crumbIndex++;
+ }
+ }
+ // Make sure the last is visible.
+ int childCount = mCrumbLayout.getChildCount();
+ while (childIndex < childCount) {
+ mCrumbLayout.getChildAt(childIndex).setVisibility(View.VISIBLE);
+ childIndex++;
+ }
+ } else {
+ int count = getChildCount();
+ for (int i = childIndex; i < count ; i++) {
+ getChildAt(i).setVisibility(View.VISIBLE);
+ }
+ }
+ if (mUseBackButton) {
+ boolean canGoBack = getTopCrumb() != null ? getTopCrumb().canGoBack : false;
+ mBackLayout.setVisibility(canGoBack ? View.VISIBLE : View.GONE);
+ if (canGoBack) {
+ mCrumbLayout.getChildAt(0).setVisibility(VISIBLE);
+ } else {
+ mCrumbLayout.getChildAt(0).setVisibility(GONE);
+ }
+ } else {
+ mBackLayout.setVisibility(View.GONE);
+ }
+ }
+
+ private void removeLastView() {
+ int ix = mCrumbLayout.getChildCount();
+ if (ix > 0) {
+ mCrumbLayout.removeViewAt(ix-1);
+ }
+ }
+
+ 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 (getMeasuredHeight() < 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(getMeasuredWidth(), 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(mCrumbPadding, 0, mCrumbPadding, 0);
+ tv.setGravity(Gravity.CENTER_VERTICAL);
+ tv.setText(name);
+ tv.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.MATCH_PARENT));
+ tv.setSingleLine();
+ tv.setEllipsize(TextUtils.TruncateAt.END);
+ return tv;
+ }
+
+ }
+
+}
diff --git a/src/com/android/browser/Browser.java b/src/com/android/browser/Browser.java
new file mode 100644
index 0000000..c9b8e7b
--- /dev/null
+++ b/src/com/android/browser/Browser.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.Application;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.util.Log;
+import android.os.Process;
+
+import org.codeaurora.swe.CookieSyncManager;
+
+public class Browser extends Application {
+
+ private final static String LOGTAG = "browser";
+
+ // Set to true to enable verbose logging.
+ final static boolean LOGV_ENABLED = false;
+
+ // Set to true to enable extra debug logging.
+ final static boolean LOGD_ENABLED = true;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ if (LOGV_ENABLED)
+ Log.v(LOGTAG, "Browser.onCreate: this=" + this);
+
+ // SWE: Avoid initializing databases for sandboxed processes.
+ // Must have INITIALIZE_DATABASE permission in AndroidManifest.xml only for browser process
+ final String INITIALIZE_DATABASE="com.android.browser.permission.INITIALIZE_DATABASE";
+ final Context context = getApplicationContext();
+ if (context.checkPermission(INITIALIZE_DATABASE,
+ Process.myPid(), Process.myUid()) == PackageManager.PERMISSION_GRANTED) {
+
+ // create CookieSyncManager with current Context
+ CookieSyncManager.createInstance(this);
+ BrowserSettings.initialize(getApplicationContext());
+ Preloader.initialize(getApplicationContext());
+ }
+ }
+}
+
diff --git a/src/com/android/browser/BrowserActivity.java b/src/com/android/browser/BrowserActivity.java
new file mode 100644
index 0000000..1ace9fd
--- /dev/null
+++ b/src/com/android/browser/BrowserActivity.java
@@ -0,0 +1,417 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.KeyguardManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.util.Log;
+import android.view.ActionMode;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Window;
+import android.webkit.JavascriptInterface;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.android.browser.R;
+import com.android.browser.UI.ComboViews;
+import com.android.browser.search.DefaultSearchEngine;
+import com.android.browser.search.SearchEngine;
+import com.android.browser.stub.NullController;
+
+import org.chromium.content.browser.TracingIntentHandler;
+import org.codeaurora.swe.WebSettings;
+import org.codeaurora.swe.WebView;
+
+public class BrowserActivity extends Activity {
+
+ public static final String ACTION_SHOW_BOOKMARKS = "show_bookmarks";
+ public static final String ACTION_SHOW_BROWSER = "show_browser";
+ public static final String ACTION_RESTART = "--restart--";
+ private static final String ACTION_START_TRACE =
+ "org.chromium.content_shell.action.PROFILE_START";
+ private static final String ACTION_STOP_TRACE =
+ "org.chromium.content_shell.action.PROFILE_STOP";
+ private static final String EXTRA_STATE = "state";
+ public static final String EXTRA_DISABLE_URL_OVERRIDE = "disable_url_override";
+
+ private final static String LOGTAG = "browser";
+
+ private final static boolean LOGV_ENABLED = Browser.LOGV_ENABLED;
+
+ private ActivityController mController = NullController.INSTANCE;
+
+
+ private Handler mHandler = new Handler();
+
+ private UiController mUiController;
+ private Handler mHandlerEx = new Handler();
+ private Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ if (mUiController != null) {
+ WebView current = mUiController.getCurrentWebView();
+ if (current != null) {
+ current.postInvalidate();
+ }
+ }
+ }
+ };
+
+ private BroadcastReceiver mReceiver;
+
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, this + " onStart, has state: "
+ + (icicle == null ? "false" : "true"));
+ }
+ super.onCreate(icicle);
+
+ if (shouldIgnoreIntents()) {
+ finish();
+ return;
+ }
+
+ // If this was a web search request, pass it on to the default web
+ // search provider and finish this activity.
+ /*
+ SearchEngine searchEngine = BrowserSettings.getInstance().getSearchEngine();
+ boolean result = IntentHandler.handleWebSearchIntent(this, null, getIntent());
+ if (result && (searchEngine instanceof DefaultSearchEngine)) {
+ finish();
+ return;
+ }
+ */
+ mController = createController();
+
+ Intent intent = (icicle == null) ? getIntent() : null;
+ mController.start(intent);
+ }
+
+ public static boolean isTablet(Context context) {
+ return context.getResources().getBoolean(R.bool.isTablet);
+ }
+
+ private Controller createController() {
+ Controller controller = new Controller(this);
+ boolean xlarge = isTablet(this);
+ UI ui = null;
+ if (xlarge) {
+ XLargeUi tablet = new XLargeUi(this, controller);
+ ui = tablet;
+ mUiController = tablet.getUiController();
+ } else {
+ PhoneUi phone = new PhoneUi(this, controller);
+ ui = phone;
+ mUiController = phone.getUiController();
+ }
+ controller.setUi(ui);
+ return controller;
+ }
+
+ @VisibleForTesting
+ Controller getController() {
+ return (Controller) mController;
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ if (shouldIgnoreIntents()) return;
+ if (ACTION_RESTART.equals(intent.getAction())) {
+ Bundle outState = new Bundle();
+ mController.onSaveInstanceState(outState);
+ finish();
+ getApplicationContext().startActivity(
+ new Intent(getApplicationContext(), BrowserActivity.class)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .putExtra(EXTRA_STATE, outState));
+ return;
+ }
+ mController.handleNewIntent(intent);
+ }
+
+ private KeyguardManager mKeyguardManager;
+ private PowerManager mPowerManager;
+ private boolean shouldIgnoreIntents() {
+ // Only process intents if the screen is on and the device is unlocked
+ // aka, if we will be user-visible
+ if (mKeyguardManager == null) {
+ mKeyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
+ }
+ if (mPowerManager == null) {
+ mPowerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
+ }
+ boolean ignore = !mPowerManager.isScreenOn();
+ ignore |= mKeyguardManager.inKeyguardRestrictedInputMode();
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "ignore intents: " + ignore);
+ }
+ return ignore;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "BrowserActivity.onResume: this=" + this);
+ }
+ mController.onResume();
+ IntentFilter intentFilter = new IntentFilter(ACTION_START_TRACE);
+ intentFilter.addAction(ACTION_STOP_TRACE);
+ mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ String extra = intent.getStringExtra("file");
+ if (ACTION_START_TRACE.equals(action)) {
+ if (extra.isEmpty()) {
+ Log.e(LOGTAG, "Can not start tracing without specifing saving location");
+ } else {
+ TracingIntentHandler.beginTracing(extra);
+ Log.i(LOGTAG, "start tracing");
+ }
+ } else if (ACTION_STOP_TRACE.equals(action)) {
+ Log.i(LOGTAG, "stop tracing");
+ TracingIntentHandler.endTracing();
+ }
+ }
+ };
+ registerReceiver(mReceiver, intentFilter);
+ }
+
+ @Override
+ public boolean onMenuOpened(int featureId, Menu menu) {
+ if (Window.FEATURE_OPTIONS_PANEL == featureId) {
+ mController.onMenuOpened(featureId, menu);
+ }
+ return true;
+ }
+
+ @Override
+ public void onOptionsMenuClosed(Menu menu) {
+ mController.onOptionsMenuClosed(menu);
+ }
+
+ @Override
+ public void onContextMenuClosed(Menu menu) {
+ super.onContextMenuClosed(menu);
+ mController.onContextMenuClosed(menu);
+ }
+
+ /**
+ * onSaveInstanceState(Bundle map)
+ * onSaveInstanceState is called right before onStop(). The map contains
+ * the saved state.
+ */
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "BrowserActivity.onSaveInstanceState: this=" + this);
+ }
+ mController.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onPause() {
+ mController.onPause();
+ super.onPause();
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "BrowserActivity.onDestroy: this=" + this);
+ }
+ super.onDestroy();
+ mController.onDestroy();
+ mController = NullController.INSTANCE;
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mController.onConfgurationChanged(newConfig);
+
+ //For avoiding bug CR520353 temporarily, delay 300ms to refresh WebView.
+ mHandlerEx.postDelayed(runnable, 300);
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mController.onLowMemory();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ return mController.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ return mController.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (!mController.onOptionsItemSelected(item)) {
+ if (item.getItemId() == R.id.about_menu_id) {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.about);
+ builder.setCancelable(true);
+ String ua = "";
+ final WebView current = getController().getCurrentWebView();
+ if (current != null) {
+ final WebSettings s = current.getSettings();
+ if (s != null) {
+ ua = s.getUserAgentString();
+ }
+ }
+ builder.setMessage("Agent:" + ua);
+ builder.setPositiveButton(android.R.string.ok, null);
+ builder.create().show();
+ }
+ return super.onOptionsItemSelected(item);
+ }
+ return true;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v,
+ ContextMenuInfo menuInfo) {
+ mController.onCreateContextMenu(menu, v, menuInfo);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ return mController.onContextItemSelected(item);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return mController.onKeyDown(keyCode, event) ||
+ super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ return mController.onKeyLongPress(keyCode, event) ||
+ super.onKeyLongPress(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return mController.onKeyUp(keyCode, event) ||
+ super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public void onActionModeStarted(ActionMode mode) {
+ super.onActionModeStarted(mode);
+ mController.onActionModeStarted(mode);
+ }
+
+ @Override
+ public void onActionModeFinished(ActionMode mode) {
+ super.onActionModeFinished(mode);
+ mController.onActionModeFinished(mode);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode,
+ Intent intent) {
+ mController.onActivityResult(requestCode, resultCode, intent);
+ }
+
+ @Override
+ public boolean onSearchRequested() {
+ return mController.onSearchRequested();
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ return mController.dispatchKeyEvent(event)
+ || super.dispatchKeyEvent(event);
+ }
+
+ @Override
+ public boolean dispatchKeyShortcutEvent(KeyEvent event) {
+ return mController.dispatchKeyShortcutEvent(event)
+ || super.dispatchKeyShortcutEvent(event);
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ return mController.dispatchTouchEvent(ev)
+ || super.dispatchTouchEvent(ev);
+ }
+
+ @Override
+ public boolean dispatchTrackballEvent(MotionEvent ev) {
+ return mController.dispatchTrackballEvent(ev)
+ || super.dispatchTrackballEvent(ev);
+ }
+
+ @Override
+ public boolean dispatchGenericMotionEvent(MotionEvent ev) {
+ return mController.dispatchGenericMotionEvent(ev) ||
+ super.dispatchGenericMotionEvent(ev);
+ }
+
+ // add for carrier homepage feature
+ @JavascriptInterface
+ public void loadBookmarks() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mController instanceof Controller) {
+ ((Controller)mController).bookmarksOrHistoryPicker(ComboViews.Bookmarks);
+ }
+ }
+ });
+ }
+
+ // add for carrier homepage feature
+ @JavascriptInterface
+ public void loadHistory() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mController instanceof Controller) {
+ ((Controller)mController).bookmarksOrHistoryPicker(ComboViews.History);
+ }
+ }
+ });
+ }
+}
diff --git a/src/com/android/browser/BrowserBackupAgent.java b/src/com/android/browser/BrowserBackupAgent.java
new file mode 100644
index 0000000..0f5fcd8
--- /dev/null
+++ b/src/com/android/browser/BrowserBackupAgent.java
@@ -0,0 +1,226 @@
+/*
+ * 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.backup.BackupAgent;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.os.ParcelFileDescriptor;
+
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.BrowserContract.Bookmarks;
+
+import android.util.Log;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.zip.CRC32;
+
+/**
+ * Settings backup agent for the Android browser. Currently the only thing
+ * stored is the set of bookmarks. It's okay if I/O exceptions are thrown
+ * out of the agent; the calling code handles it and the backup operation
+ * simply fails.
+ *
+ * @hide
+ */
+public class BrowserBackupAgent extends BackupAgent {
+ static final String TAG = "BrowserBackupAgent";
+ static final boolean DEBUG = false;
+
+ static final String BOOKMARK_KEY = "_bookmarks_";
+ /** this version num MUST be incremented if the flattened-file schema ever changes */
+ static final int BACKUP_AGENT_VERSION = 0;
+
+ /**
+ * This simply preserves the existing state as we now prefer Chrome Sync
+ * to handle bookmark backup.
+ */
+ @Override
+ public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState) throws IOException {
+ long savedFileSize = -1;
+ long savedCrc = -1;
+ int savedVersion = -1;
+
+ // Extract the previous bookmark file size & CRC from the saved state
+ DataInputStream in = new DataInputStream(
+ new FileInputStream(oldState.getFileDescriptor()));
+ try {
+ savedFileSize = in.readLong();
+ savedCrc = in.readLong();
+ savedVersion = in.readInt();
+ } catch (EOFException e) {
+ // It means we had no previous state; that's fine
+ return;
+ } finally {
+ if (in != null) {
+ in.close();
+ }
+ }
+ // Write the existing state
+ writeBackupState(savedFileSize, savedCrc, newState);
+ }
+
+ /**
+ * Restore from backup -- reads in the flattened bookmark file as supplied from
+ * the backup service, parses that out, and rebuilds the bookmarks table in the
+ * browser database from it.
+ */
+ @Override
+ public void onRestore(BackupDataInput data, int appVersionCode,
+ ParcelFileDescriptor newState) throws IOException {
+ long crc = -1;
+ File tmpfile = File.createTempFile("rst", null, getFilesDir());
+ try {
+ while (data.readNextHeader()) {
+ if (BOOKMARK_KEY.equals(data.getKey())) {
+ // Read the flattened bookmark data into a temp file
+ crc = copyBackupToFile(data, tmpfile, data.getDataSize());
+
+ FileInputStream infstream = new FileInputStream(tmpfile);
+ DataInputStream in = new DataInputStream(infstream);
+
+ try {
+ int count = in.readInt();
+ ArrayList<Bookmark> bookmarks = new ArrayList<Bookmark>(count);
+
+ // Read all the bookmarks, then process later -- if we can't read
+ // all the data successfully, we don't touch the bookmarks table
+ for (int i = 0; i < count; i++) {
+ Bookmark mark = new Bookmark();
+ mark.url = in.readUTF();
+ mark.visits = in.readInt();
+ mark.date = in.readLong();
+ mark.created = in.readLong();
+ mark.title = in.readUTF();
+ bookmarks.add(mark);
+ }
+
+ // Okay, we have all the bookmarks -- now see if we need to add
+ // them to the browser's database
+ int N = bookmarks.size();
+ int nUnique = 0;
+ if (DEBUG) Log.v(TAG, "Restoring " + N + " bookmarks");
+ String[] urlCol = new String[] { Bookmarks.URL };
+ for (int i = 0; i < N; i++) {
+ Bookmark mark = bookmarks.get(i);
+
+ // Does this URL exist in the bookmark table?
+ Cursor cursor = getContentResolver().query(
+ Bookmarks.CONTENT_URI, urlCol,
+ Bookmarks.URL + " == ?",
+ new String[] { mark.url }, null);
+ // if not, insert it
+ if (cursor.getCount() <= 0) {
+ if (DEBUG) Log.v(TAG, "Did not see url: " + mark.url);
+ addBookmark(mark);
+ nUnique++;
+ } else {
+ if (DEBUG) Log.v(TAG, "Skipping extant url: " + mark.url);
+ }
+ cursor.close();
+ }
+ Log.i(TAG, "Restored " + nUnique + " of " + N + " bookmarks");
+ } catch (IOException ioe) {
+ Log.w(TAG, "Bad backup data; not restoring");
+ crc = -1;
+ } finally {
+ if (in != null) {
+ in.close();
+ }
+ }
+ }
+
+ // Last, write the state we just restored from so we can discern
+ // changes whenever we get invoked for backup in the future
+ writeBackupState(tmpfile.length(), crc, newState);
+ }
+ } finally {
+ // Whatever happens, delete the temp file
+ tmpfile.delete();
+ }
+ }
+
+ void addBookmark(Bookmark mark) {
+ ContentValues values = new ContentValues();
+ values.put(Bookmarks.TITLE, mark.title);
+ values.put(Bookmarks.URL, mark.url);
+ values.put(Bookmarks.IS_FOLDER, 0);
+ values.put(Bookmarks.DATE_CREATED, mark.created);
+ values.put(Bookmarks.DATE_MODIFIED, mark.date);
+ getContentResolver().insert(Bookmarks.CONTENT_URI, values);
+ }
+
+ static class Bookmark {
+ public String url;
+ public int visits;
+ public long date;
+ public long created;
+ public String title;
+ }
+ /*
+ * Utility functions
+ */
+
+ // Read the given file from backup to a file, calculating a CRC32 along the way
+ private long copyBackupToFile(BackupDataInput data, File file, int toRead)
+ throws IOException {
+ final int CHUNK = 8192;
+ byte[] buf = new byte[CHUNK];
+ CRC32 crc = new CRC32();
+ FileOutputStream out = new FileOutputStream(file);
+
+ try {
+ while (toRead > 0) {
+ int numRead = data.readEntityData(buf, 0, CHUNK);
+ crc.update(buf, 0, numRead);
+ out.write(buf, 0, numRead);
+ toRead -= numRead;
+ }
+ } finally {
+ if (out != null) {
+ out.close();
+ }
+ }
+ return crc.getValue();
+ }
+
+ // Write the given metrics to the new state file
+ private void writeBackupState(long fileSize, long crc, ParcelFileDescriptor stateFile)
+ throws IOException {
+ DataOutputStream out = new DataOutputStream(
+ new FileOutputStream(stateFile.getFileDescriptor()));
+ try {
+ out.writeLong(fileSize);
+ out.writeLong(crc);
+ out.writeInt(BACKUP_AGENT_VERSION);
+ } finally {
+ if (out != null) {
+ out.close();
+ }
+ }
+ }
+}
diff --git a/src/com/android/browser/BrowserBookmarksAdapter.java b/src/com/android/browser/BrowserBookmarksAdapter.java
new file mode 100644
index 0000000..3b38f1e
--- /dev/null
+++ b/src/com/android/browser/BrowserBookmarksAdapter.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.TextView;
+
+import com.android.browser.R;
+import com.android.browser.platformsupport.BrowserContract.Bookmarks;
+import com.android.browser.util.ThreadedCursorAdapter;
+import com.android.browser.view.BookmarkContainer;
+
+public class BrowserBookmarksAdapter extends
+ ThreadedCursorAdapter<BrowserBookmarksAdapterItem> {
+
+ LayoutInflater mInflater;
+ Context mContext;
+
+ /**
+ * Create a new BrowserBookmarksAdapter.
+ */
+ public BrowserBookmarksAdapter(Context context) {
+ // 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);
+ mContext = context;
+ }
+
+ @Override
+ protected long getItemId(Cursor c) {
+ return c.getLong(BookmarksLoader.COLUMN_INDEX_ID);
+ }
+
+ @Override
+ public View newView(Context context, ViewGroup parent) {
+ return mInflater.inflate(R.layout.bookmark_thumbnail, parent, false);
+ }
+
+ @Override
+ public void bindView(View view, BrowserBookmarksAdapterItem object) {
+ BookmarkContainer container = (BookmarkContainer) view;
+ container.setIgnoreRequestLayout(true);
+ bindGridView(view, mContext, object);
+ container.setIgnoreRequestLayout(false);
+ }
+
+ CharSequence getTitle(Cursor cursor) {
+ int type = cursor.getInt(BookmarksLoader.COLUMN_INDEX_TYPE);
+ switch (type) {
+ case Bookmarks.BOOKMARK_TYPE_OTHER_FOLDER:
+ return mContext.getText(R.string.other_bookmarks);
+ }
+ return cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE);
+ }
+
+ void bindGridView(View view, Context context, BrowserBookmarksAdapterItem item) {
+ // We need to set this to handle rotation and other configuration change
+ // events. If the padding didn't change, this is a no op.
+ int padding = context.getResources()
+ .getDimensionPixelSize(R.dimen.combo_horizontalSpacing);
+ view.setPadding(padding, view.getPaddingTop(),
+ padding, view.getPaddingBottom());
+ ImageView thumb = (ImageView) view.findViewById(R.id.thumb);
+ TextView tv = (TextView) view.findViewById(R.id.label);
+
+ tv.setText(item.title);
+ if (item.is_folder) {
+ // folder
+ thumb.setImageResource(R.drawable.thumb_bookmark_widget_folder_holo);
+ thumb.setScaleType(ScaleType.FIT_END);
+ thumb.setBackground(null);
+ } else {
+ thumb.setScaleType(ScaleType.CENTER_CROP);
+ if (item.thumbnail == null || !item.has_thumbnail) {
+ thumb.setImageResource(R.drawable.browser_thumbnail);
+ } else {
+ thumb.setImageDrawable(item.thumbnail);
+ }
+ thumb.setBackgroundResource(R.drawable.border_thumb_bookmarks_widget_holo);
+ }
+ }
+
+ @Override
+ public BrowserBookmarksAdapterItem getRowObject(Cursor c,
+ BrowserBookmarksAdapterItem item) {
+ if (item == null) {
+ item = new BrowserBookmarksAdapterItem();
+ }
+ Bitmap thumbnail = item.thumbnail != null ? item.thumbnail.getBitmap() : null;
+ thumbnail = BrowserBookmarksPage.getBitmap(c,
+ BookmarksLoader.COLUMN_INDEX_THUMBNAIL, thumbnail);
+ item.has_thumbnail = thumbnail != null;
+ if (thumbnail != null
+ && (item.thumbnail == null || item.thumbnail.getBitmap() != thumbnail)) {
+ item.thumbnail = new BitmapDrawable(mContext.getResources(), thumbnail);
+ }
+ item.is_folder = c.getInt(BookmarksLoader.COLUMN_INDEX_IS_FOLDER) != 0;
+ item.title = getTitle(c);
+ item.url = c.getString(BookmarksLoader.COLUMN_INDEX_URL);
+ return item;
+ }
+
+ @Override
+ public BrowserBookmarksAdapterItem getLoadingObject() {
+ BrowserBookmarksAdapterItem item = new BrowserBookmarksAdapterItem();
+ return item;
+ }
+}
diff --git a/src/com/android/browser/BrowserBookmarksAdapterItem.java b/src/com/android/browser/BrowserBookmarksAdapterItem.java
new file mode 100644
index 0000000..6b99578
--- /dev/null
+++ b/src/com/android/browser/BrowserBookmarksAdapterItem.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2012 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.drawable.BitmapDrawable;
+
+public class BrowserBookmarksAdapterItem {
+ public String url;
+ public CharSequence title;
+ public BitmapDrawable thumbnail;
+ public boolean has_thumbnail;
+ public boolean is_folder;
+}
diff --git a/src/com/android/browser/BrowserBookmarksPage.java b/src/com/android/browser/BrowserBookmarksPage.java
new file mode 100644
index 0000000..a255d28
--- /dev/null
+++ b/src/com/android/browser/BrowserBookmarksPage.java
@@ -0,0 +1,698 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.LoaderManager;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Intent;
+import android.content.Loader;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapFactory.Options;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+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.widget.ExpandableListView;
+import android.widget.ExpandableListView.OnChildClickListener;
+import android.widget.Toast;
+
+import com.android.browser.R;
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.BrowserContract.Accounts;
+import com.android.browser.provider.BrowserProvider2;
+import com.android.browser.view.BookmarkExpandableView;
+import com.android.browser.view.BookmarkExpandableView.BookmarkContextMenuInfo;
+
+import java.util.HashMap;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+interface BookmarksPageCallbacks {
+ // Return true if handled
+ boolean onBookmarkSelected(Cursor c, boolean isFolder);
+ // Return true if handled
+ boolean onOpenInNewWindow(String... urls);
+}
+
+/**
+ * View showing the user's bookmarks in the browser.
+ */
+public class BrowserBookmarksPage extends Fragment implements View.OnCreateContextMenuListener,
+ LoaderManager.LoaderCallbacks<Cursor>, BreadCrumbView.Controller,
+ OnChildClickListener {
+
+ public static class ExtraDragState {
+ public int childPosition;
+ public int groupPosition;
+ }
+
+ static final String LOGTAG = "browser";
+
+ static final int LOADER_ACCOUNTS = 1;
+ static final int LOADER_BOOKMARKS = 100;
+
+ static final String EXTRA_DISABLE_WINDOW = "disable_new_window";
+ static final String PREF_GROUP_STATE = "bbp_group_state";
+
+ static final String ACCOUNT_TYPE = "account_type";
+ static final String ACCOUNT_NAME = "account_name";
+
+ static final long DEFAULT_FOLDER_ID = -1;
+
+ BookmarksPageCallbacks mCallbacks;
+ View mRoot;
+ BookmarkExpandableView mGrid;
+ boolean mDisableNewWindow;
+ boolean mEnableContextMenu = true;
+ View mEmptyView;
+ View mHeader;
+ HashMap<Integer, BrowserBookmarksAdapter> mBookmarkAdapters = new HashMap<Integer, BrowserBookmarksAdapter>();
+ JSONObject mState;
+ long mCurrentFolderId = BrowserProvider2.FIXED_ID_ROOT;
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ if (id == LOADER_ACCOUNTS) {
+ return new AccountsLoader(getActivity());
+ } else if (id >= LOADER_BOOKMARKS) {
+ String accountType = args.getString(ACCOUNT_TYPE);
+ String accountName = args.getString(ACCOUNT_NAME);
+ BookmarksLoader bl = new BookmarksLoader(getActivity(),
+ accountType, accountName);
+ return bl;
+ } else {
+ throw new UnsupportedOperationException("Unknown loader id " + id);
+ }
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+ if (loader.getId() == LOADER_ACCOUNTS) {
+ LoaderManager lm = getLoaderManager();
+ int id = LOADER_BOOKMARKS;
+ while (cursor.moveToNext()) {
+ String accountName = cursor.getString(0);
+ String accountType = cursor.getString(1);
+ Bundle args = new Bundle();
+ args.putString(ACCOUNT_NAME, accountName);
+ args.putString(ACCOUNT_TYPE, accountType);
+ BrowserBookmarksAdapter adapter = new BrowserBookmarksAdapter(
+ getActivity());
+ mBookmarkAdapters.put(id, adapter);
+ boolean expand = true;
+ try {
+ expand = mState.getBoolean(accountName != null ? accountName
+ : BookmarkExpandableView.LOCAL_ACCOUNT_NAME);
+ } catch (JSONException e) {} // no state for accountName
+ mGrid.addAccount(accountName, adapter, expand);
+ lm.restartLoader(id, args, this);
+ id++;
+ }
+ // TODO: Figure out what a reload of these means
+ // Currently, a reload is triggered whenever bookmarks change
+ // This is less than ideal
+ // It also causes UI flickering as a new adapter is created
+ // instead of re-using an existing one when the account_name is the
+ // same.
+ // For now, this is a one-shot load
+ getLoaderManager().destroyLoader(LOADER_ACCOUNTS);
+ } else if (loader.getId() >= LOADER_BOOKMARKS) {
+ BrowserBookmarksAdapter adapter = mBookmarkAdapters.get(loader.getId());
+ adapter.changeCursor(cursor);
+ if (adapter.getCount() != 0) {
+ mCurrentFolderId = adapter.getItem(0).getLong(BookmarksLoader.COLUMN_INDEX_PARENT);
+ }
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ if (loader.getId() >= LOADER_BOOKMARKS) {
+ BrowserBookmarksAdapter adapter = mBookmarkAdapters.get(loader.getId());
+ adapter.changeCursor(null);
+ }
+ }
+
+ //add for carrier feature which adds new bookmark/folder function.
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ inflater.inflate(R.menu.bookmark, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final Activity activity = getActivity();
+ if (item.getItemId() == R.id.add_bookmark_menu_id) {
+ Intent intent = new Intent(activity, AddBookmarkPage.class);
+ intent.putExtra(BrowserContract.Bookmarks.URL, "http://");
+ intent.putExtra(BrowserContract.Bookmarks.TITLE, "");
+ intent.putExtra(BrowserContract.Bookmarks.PARENT, mCurrentFolderId);
+ activity.startActivity(intent);
+ }
+ if (item.getItemId() == R.id.new_bmfolder_menu_id) {
+ Intent intent = new Intent(activity, AddBookmarkFolder.class);
+ intent.putExtra(BrowserContract.Bookmarks.PARENT, mCurrentFolderId);
+ activity.startActivity(intent);
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ if (!(item.getMenuInfo() instanceof BookmarkContextMenuInfo)) {
+ return false;
+ }
+ BookmarkContextMenuInfo i = (BookmarkContextMenuInfo) item.getMenuInfo();
+ // If we have no menu info, we can't tell which item was selected.
+ if (i == null) {
+ return false;
+ }
+
+ if (handleContextItem(item.getItemId(), i.groupPosition, i.childPosition)) {
+ return true;
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ public boolean handleContextItem(int itemId, int groupPosition,
+ int childPosition) {
+ final Activity activity = getActivity();
+ BrowserBookmarksAdapter adapter = getChildAdapter(groupPosition);
+
+ switch (itemId) {
+ case R.id.open_context_menu_id:
+ loadUrl(adapter, childPosition);
+ break;
+ case R.id.edit_context_menu_id:
+ editBookmark(adapter, childPosition);
+ break;
+ case R.id.shortcut_context_menu_id:
+ Cursor c = adapter.getItem(childPosition);
+ activity.sendBroadcast(createShortcutIntent(getActivity(), c));
+ break;
+ case R.id.delete_context_menu_id:
+ displayRemoveBookmarkDialog(adapter, childPosition);
+ break;
+ case R.id.new_window_context_menu_id:
+ openInNewWindow(adapter, childPosition);
+ break;
+ case R.id.share_link_context_menu_id: {
+ Cursor cursor = adapter.getItem(childPosition);
+ 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(adapter, childPosition));
+ break;
+ case R.id.homepage_context_menu_id: {
+ BrowserSettings.getInstance().setHomePage(getUrl(adapter, childPosition));
+ 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: {
+ Cursor cursor = adapter.getItem(childPosition);
+ 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.
+ Bookmarks.removeFromBookmarks(activity, activity.getContentResolver(), url, name);
+ break;
+ }
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ static Bitmap getBitmap(Cursor cursor, int columnIndex) {
+ return getBitmap(cursor, columnIndex, null);
+ }
+
+ static ThreadLocal<Options> sOptions = new ThreadLocal<Options>() {
+ @Override
+ protected Options initialValue() {
+ return new Options();
+ };
+ };
+ static Bitmap getBitmap(Cursor cursor, int columnIndex, Bitmap inBitmap) {
+ byte[] data = cursor.getBlob(columnIndex);
+ if (data == null) {
+ return null;
+ }
+ Options opts = sOptions.get();
+ opts.inBitmap = inBitmap;
+ opts.inSampleSize = 1;
+ opts.inScaled = false;
+ try {
+ return BitmapFactory.decodeByteArray(data, 0, data.length, opts);
+ } catch (IllegalArgumentException ex) {
+ // Failed to re-use bitmap, create a new one
+ return BitmapFactory.decodeByteArray(data, 0, data.length);
+ }
+ }
+
+ 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) {
+ BookmarkContextMenuInfo info = (BookmarkContextMenuInfo) menuInfo;
+ BrowserBookmarksAdapter adapter = getChildAdapter(info.groupPosition);
+ Cursor cursor = adapter.getItem(info.childPosition);
+ if (!canEdit(cursor)) {
+ return;
+ }
+ 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);
+ }
+ }
+ BookmarkItem header = new BookmarkItem(activity);
+ header.setEnableScrolling(true);
+ populateBookmarkItem(cursor, header, isFolder);
+ menu.setHeaderView(header);
+
+ int count = menu.size();
+ for (int i = 0; i < count; i++) {
+ menu.getItem(i).setOnMenuItemClickListener(mContextItemClickListener);
+ }
+ }
+
+ boolean canEdit(Cursor c) {
+ int type = c.getInt(BookmarksLoader.COLUMN_INDEX_TYPE);
+ return type == BrowserContract.Bookmarks.BOOKMARK_TYPE_BOOKMARK
+ || type == BrowserContract.Bookmarks.BOOKMARK_TYPE_FOLDER;
+ }
+
+ 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_holo_dark);
+ item.setFavicon(bitmap);
+ new LookupBookmarkCount(getActivity(), item)
+ .execute(cursor.getLong(BookmarksLoader.COLUMN_INDEX_ID));
+ } else {
+ String url = cursor.getString(BookmarksLoader.COLUMN_INDEX_URL);
+ item.setUrl(url);
+ Bitmap bitmap = getBitmap(cursor, BookmarksLoader.COLUMN_INDEX_FAVICON);
+ item.setFavicon(bitmap);
+ }
+ }
+
+ /**
+ * Create a new BrowserBookmarksPage.
+ */
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ SharedPreferences prefs = BrowserSettings.getInstance().getPreferences();
+ try {
+ mState = new JSONObject(prefs.getString(PREF_GROUP_STATE, "{}"));
+ } catch (JSONException e) {
+ // Parse failed, clear preference and start with empty state
+ prefs.edit().remove(PREF_GROUP_STATE).apply();
+ mState = new JSONObject();
+ }
+ Bundle args = getArguments();
+ mDisableNewWindow = args == null ? false : args.getBoolean(EXTRA_DISABLE_WINDOW, false);
+ setHasOptionsMenu(true);
+ if (mCallbacks == null && getActivity() instanceof CombinedBookmarksCallbacks) {
+ mCallbacks = new CombinedBookmarksCallbackWrapper(
+ (CombinedBookmarksCallbacks) getActivity());
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ try {
+ mState = mGrid.saveGroupState();
+ // Save state
+ SharedPreferences prefs = BrowserSettings.getInstance().getPreferences();
+ prefs.edit()
+ .putString(PREF_GROUP_STATE, mState.toString())
+ .apply();
+ } catch (JSONException e) {
+ // Not critical, ignore
+ }
+ }
+
+ private static class CombinedBookmarksCallbackWrapper
+ implements BookmarksPageCallbacks {
+
+ private CombinedBookmarksCallbacks mCombinedCallback;
+
+ private CombinedBookmarksCallbackWrapper(CombinedBookmarksCallbacks cb) {
+ mCombinedCallback = cb;
+ }
+
+ @Override
+ public boolean onOpenInNewWindow(String... urls) {
+ mCombinedCallback.openInNewTab(urls);
+ return true;
+ }
+
+ @Override
+ public boolean onBookmarkSelected(Cursor c, boolean isFolder) {
+ if (isFolder) {
+ return false;
+ }
+ mCombinedCallback.openUrl(BrowserBookmarksPage.getUrl(c));
+ return true;
+ }
+ };
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mRoot = inflater.inflate(R.layout.bookmarks, container, false);
+ mEmptyView = mRoot.findViewById(android.R.id.empty);
+
+ mGrid = (BookmarkExpandableView) mRoot.findViewById(R.id.grid);
+ mGrid.setOnChildClickListener(this);
+ mGrid.setColumnWidthFromLayout(R.layout.bookmark_thumbnail);
+ mGrid.setBreadcrumbController(this);
+ setEnableContextMenu(mEnableContextMenu);
+
+ // Start the loaders
+ LoaderManager lm = getLoaderManager();
+ lm.restartLoader(LOADER_ACCOUNTS, null, this);
+
+ return mRoot;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ mGrid.setBreadcrumbController(null);
+ mGrid.clearAccounts();
+ LoaderManager lm = getLoaderManager();
+ lm.destroyLoader(LOADER_ACCOUNTS);
+ for (int id : mBookmarkAdapters.keySet()) {
+ lm.destroyLoader(id);
+ }
+ mBookmarkAdapters.clear();
+ }
+
+ private BrowserBookmarksAdapter getChildAdapter(int groupPosition) {
+ return mGrid.getChildAdapter(groupPosition);
+ }
+
+ private BreadCrumbView getBreadCrumbs(int groupPosition) {
+ return mGrid.getBreadCrumbs(groupPosition);
+ }
+
+ @Override
+ public boolean onChildClick(ExpandableListView parent, View v,
+ int groupPosition, int childPosition, long id) {
+ BrowserBookmarksAdapter adapter = getChildAdapter(groupPosition);
+ Cursor cursor = adapter.getItem(childPosition);
+ boolean isFolder = cursor.getInt(BookmarksLoader.COLUMN_INDEX_IS_FOLDER) != 0;
+ if (mCallbacks != null &&
+ mCallbacks.onBookmarkSelected(cursor, isFolder)) {
+ return true;
+ }
+
+ if (isFolder) {
+ String title = cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE);
+ Uri uri = ContentUris.withAppendedId(
+ BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER, id);
+ BreadCrumbView crumbs = getBreadCrumbs(groupPosition);
+ if (crumbs != null) {
+ // update crumbs
+ crumbs.pushView(title, uri);
+ crumbs.setVisibility(View.VISIBLE);
+ Object data = crumbs.getTopData();
+ mCurrentFolderId = (data != null ? ContentUris.parseId((Uri) data)
+ : DEFAULT_FOLDER_ID);
+ }
+ loadFolder(groupPosition, uri);
+ }
+ return true;
+ }
+
+ /* 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);
+ }
+
+ private void loadUrl(BrowserBookmarksAdapter adapter, int position) {
+ if (mCallbacks != null && adapter != null) {
+ mCallbacks.onBookmarkSelected(adapter.getItem(position), false);
+ }
+ }
+
+ private void openInNewWindow(BrowserBookmarksAdapter adapter, int position) {
+ if (mCallbacks != null) {
+ Cursor c = adapter.getItem(position);
+ boolean isFolder = c.getInt(BookmarksLoader.COLUMN_INDEX_IS_FOLDER) == 1;
+ if (isFolder) {
+ long id = c.getLong(BookmarksLoader.COLUMN_INDEX_ID);
+ new OpenAllInTabsTask(id).execute();
+ } else {
+ mCallbacks.onOpenInNewWindow(BrowserBookmarksPage.getUrl(c));
+ }
+ }
+ }
+
+ class OpenAllInTabsTask extends AsyncTask<Void, Void, Cursor> {
+ long mFolderId;
+ public OpenAllInTabsTask(long id) {
+ mFolderId = id;
+ }
+
+ @Override
+ protected Cursor doInBackground(Void... params) {
+ Context c = getActivity();
+ if (c == null) return null;
+ return c.getContentResolver().query(BookmarkUtils.getBookmarksUri(c),
+ BookmarksLoader.PROJECTION, BrowserContract.Bookmarks.PARENT + "=?",
+ new String[] { Long.toString(mFolderId) }, null);
+ }
+
+ @Override
+ protected void onPostExecute(Cursor result) {
+ if (mCallbacks != null && result.getCount() > 0) {
+ String[] urls = new String[result.getCount()];
+ int i = 0;
+ while (result.moveToNext()) {
+ urls[i++] = BrowserBookmarksPage.getUrl(result);
+ }
+ mCallbacks.onOpenInNewWindow(urls);
+ }
+ }
+
+ }
+
+ private void editBookmark(BrowserBookmarksAdapter adapter, int position) {
+ Intent intent = new Intent(getActivity(), AddBookmarkPage.class);
+ Cursor cursor = adapter.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(BrowserBookmarksAdapter adapter,
+ int position) {
+ // Put up a dialog asking if the user really wants to
+ // delete the bookmark
+ Cursor cursor = adapter.getItem(position);
+ long id = cursor.getLong(BookmarksLoader.COLUMN_INDEX_ID);
+ String title = cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE);
+ Context context = getActivity();
+ BookmarkUtils.displayRemoveBookmarkDialog(id, title, context, null);
+ }
+
+ private String getUrl(BrowserBookmarksAdapter adapter, int position) {
+ return getUrl(adapter.getItem(position));
+ }
+
+ /* package */ static String getUrl(Cursor c) {
+ return c.getString(BookmarksLoader.COLUMN_INDEX_URL);
+ }
+
+ private void copy(CharSequence text) {
+ ClipboardManager cm = (ClipboardManager) getActivity().getSystemService(
+ Context.CLIPBOARD_SERVICE);
+ cm.setPrimaryClip(ClipData.newRawUri(null, Uri.parse(text.toString())));
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ Resources res = getActivity().getResources();
+ mGrid.setColumnWidthFromLayout(R.layout.bookmark_thumbnail);
+ int paddingTop = (int) res.getDimension(R.dimen.combo_paddingTop);
+ mRoot.setPadding(0, paddingTop, 0, 0);
+ getActivity().invalidateOptionsMenu();
+ }
+
+ /**
+ * BreadCrumb controller callback
+ */
+ @Override
+ public void onTop(BreadCrumbView view, int level, Object data) {
+ int groupPosition = (Integer) view.getTag(R.id.group_position);
+ Uri uri = (Uri) data;
+ if (uri == null) {
+ // top level
+ uri = BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER;
+ }
+ loadFolder(groupPosition, uri);
+ if (level <= 1) {
+ view.setVisibility(View.GONE);
+ } else {
+ view.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * @param uri
+ */
+ private void loadFolder(int groupPosition, Uri uri) {
+ LoaderManager manager = getLoaderManager();
+ // This assumes groups are ordered the same as loaders
+ BookmarksLoader loader = (BookmarksLoader) ((Loader<?>)
+ manager.getLoader(LOADER_BOOKMARKS + groupPosition));
+ loader.setUri(uri);
+ loader.forceLoad();
+ }
+
+ public void setCallbackListener(BookmarksPageCallbacks callbackListener) {
+ mCallbacks = callbackListener;
+ }
+
+ public void setEnableContextMenu(boolean enable) {
+ mEnableContextMenu = enable;
+ if (mGrid != null) {
+ if (mEnableContextMenu) {
+ registerForContextMenu(mGrid);
+ } else {
+ unregisterForContextMenu(mGrid);
+ mGrid.setLongClickable(false);
+ }
+ }
+ }
+
+ private static class LookupBookmarkCount extends AsyncTask<Long, Void, Integer> {
+ Context mContext;
+ BookmarkItem mHeader;
+
+ public LookupBookmarkCount(Context context, BookmarkItem header) {
+ mContext = context.getApplicationContext();
+ 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));
+ }
+ }
+ }
+
+ static class AccountsLoader extends CursorLoader {
+
+ static String[] ACCOUNTS_PROJECTION = new String[] {
+ Accounts.ACCOUNT_NAME,
+ Accounts.ACCOUNT_TYPE
+ };
+
+ public AccountsLoader(Context context) {
+ super(context, Accounts.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(BrowserProvider2.PARAM_ALLOW_EMPTY_ACCOUNTS, "false")
+ .build(),
+ ACCOUNTS_PROJECTION, null, null, null);
+ }
+
+ }
+}
diff --git a/src/com/android/browser/BrowserHistoryPage.java b/src/com/android/browser/BrowserHistoryPage.java
new file mode 100644
index 0000000..14b9e40
--- /dev/null
+++ b/src/com/android/browser/BrowserHistoryPage.java
@@ -0,0 +1,675 @@
+/*
+ * 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.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.Fragment;
+import android.app.FragmentBreadCrumbs;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.ClipboardManager;
+import android.content.ContentResolver;
+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.database.DataSetObserver;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Browser;
+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.ViewStub;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.BaseAdapter;
+import android.widget.ExpandableListView;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+import android.widget.ExpandableListView.OnChildClickListener;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.browser.R;
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.BrowserContract.Combined;
+import com.android.browser.reflect.ReflectHelper;
+
+/**
+ * Activity for displaying the browser's history, divided into
+ * days of viewing.
+ */
+public class BrowserHistoryPage extends Fragment
+ implements LoaderCallbacks<Cursor>, OnChildClickListener {
+
+ static final int LOADER_HISTORY = 1;
+ static final int LOADER_MOST_VISITED = 2;
+
+ CombinedBookmarksCallbacks mCallback;
+ HistoryAdapter mAdapter;
+ HistoryChildWrapper mChildWrapper;
+ boolean mDisableNewWindow;
+ HistoryItem mContextHeader;
+ String mMostVisitsLimit;
+ ListView mGroupList, mChildList;
+ private ViewGroup mPrefsContainer;
+ private FragmentBreadCrumbs mFragmentBreadCrumbs;
+ private ExpandableListView mHistoryList;
+
+ private View mRoot;
+
+ static interface HistoryQuery {
+ static final String[] PROJECTION = new String[] {
+ Combined._ID, // 0
+ Combined.DATE_LAST_VISITED, // 1
+ Combined.TITLE, // 2
+ Combined.URL, // 3
+ Combined.FAVICON, // 4
+ Combined.VISITS, // 5
+ Combined.IS_BOOKMARK, // 6
+ };
+
+ 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;
+ static final int INDEX_IS_BOOKMARK = 6;
+ }
+
+ private void copy(CharSequence text) {
+ ClipboardManager cm = (ClipboardManager) getActivity().getSystemService(
+ Context.CLIPBOARD_SERVICE);
+ cm.setText(text);
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ Uri.Builder combinedBuilder = Combined.CONTENT_URI.buildUpon();
+
+ switch (id) {
+ case LOADER_HISTORY: {
+ String sort = Combined.DATE_LAST_VISITED + " DESC";
+ String where = Combined.VISITS + " > 0";
+ CursorLoader loader = new CursorLoader(getActivity(), combinedBuilder.build(),
+ HistoryQuery.PROJECTION, where, null, sort);
+ return loader;
+ }
+
+ case LOADER_MOST_VISITED: {
+ Uri uri = combinedBuilder
+ .appendQueryParameter(BrowserContract.PARAM_LIMIT, mMostVisitsLimit)
+ .build();
+ String where = Combined.VISITS + " > 0";
+ CursorLoader loader = new CursorLoader(getActivity(), uri,
+ HistoryQuery.PROJECTION, where, null, Combined.VISITS + " DESC");
+ return loader;
+ }
+
+ default: {
+ throw new IllegalArgumentException();
+ }
+ }
+ }
+
+ void selectGroup(int position) {
+ mGroupItemClickListener.onItemClick(null,
+ mAdapter.getGroupView(position, false, null, null),
+ position, position);
+ }
+
+ void checkIfEmpty() {
+ if (mAdapter.mMostVisited != null && mAdapter.mHistoryCursor != null) {
+ // Both cursors have loaded - check to see if we have data
+ if (mAdapter.isEmpty()) {
+ mRoot.findViewById(R.id.history).setVisibility(View.GONE);
+ mRoot.findViewById(android.R.id.empty).setVisibility(View.VISIBLE);
+ } else {
+ mRoot.findViewById(R.id.history).setVisibility(View.VISIBLE);
+ mRoot.findViewById(android.R.id.empty).setVisibility(View.GONE);
+ }
+ }
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ switch (loader.getId()) {
+ case LOADER_HISTORY: {
+ mAdapter.changeCursor(data);
+ if (!mAdapter.isEmpty() && mGroupList != null
+ && mGroupList.getCheckedItemPosition() == ListView.INVALID_POSITION) {
+ selectGroup(0);
+ }
+
+ checkIfEmpty();
+ break;
+ }
+
+ case LOADER_MOST_VISITED: {
+ mAdapter.changeMostVisitedCursor(data);
+
+ checkIfEmpty();
+ break;
+ }
+
+ default: {
+ throw new IllegalArgumentException();
+ }
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ }
+
+ @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);
+ mCallback = (CombinedBookmarksCallbacks) getActivity();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mRoot = inflater.inflate(R.layout.history, container, false);
+ mAdapter = new HistoryAdapter(getActivity());
+ ViewStub stub = (ViewStub) mRoot.findViewById(R.id.pref_stub);
+ if (stub != null) {
+ inflateTwoPane(stub);
+ } else {
+ inflateSinglePane();
+ }
+
+ // Start the loaders
+ getLoaderManager().restartLoader(LOADER_HISTORY, null, this);
+ getLoaderManager().restartLoader(LOADER_MOST_VISITED, null, this);
+
+ return mRoot;
+ }
+
+ private void inflateSinglePane() {
+ mHistoryList = (ExpandableListView) mRoot.findViewById(R.id.history);
+ mHistoryList.setAdapter(mAdapter);
+ mHistoryList.setOnChildClickListener(this);
+ registerForContextMenu(mHistoryList);
+ }
+
+ private void inflateTwoPane(ViewStub stub) {
+ stub.setLayoutResource(R.layout.preference_list_content);
+ stub.inflate();
+ mGroupList = (ListView) mRoot.findViewById(android.R.id.list);
+ mPrefsContainer = (ViewGroup) mRoot.findViewById(R.id.prefs_frame);
+ mFragmentBreadCrumbs = (FragmentBreadCrumbs) mRoot.findViewById(android.R.id.title);
+ mFragmentBreadCrumbs.setMaxVisible(1);
+ mFragmentBreadCrumbs.setActivity(getActivity());
+ mPrefsContainer.setVisibility(View.VISIBLE);
+ mGroupList.setAdapter(new HistoryGroupWrapper(mAdapter));
+ mGroupList.setOnItemClickListener(mGroupItemClickListener);
+ mGroupList.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
+ mChildWrapper = new HistoryChildWrapper(mAdapter);
+ mChildList = new ListView(getActivity());
+ mChildList.setAdapter(mChildWrapper);
+ mChildList.setOnItemClickListener(mChildItemClickListener);
+ registerForContextMenu(mChildList);
+ ViewGroup prefs = (ViewGroup) mRoot.findViewById(R.id.prefs);
+ prefs.addView(mChildList);
+ }
+
+ private OnItemClickListener mGroupItemClickListener = new OnItemClickListener() {
+ @Override
+ public void onItemClick(
+ AdapterView<?> parent, View view, int position, long id) {
+ CharSequence title = ((TextView) view).getText();
+ mFragmentBreadCrumbs.setTitle(title, title);
+ mChildWrapper.setSelectedGroup(position);
+ mGroupList.setItemChecked(position, true);
+ }
+ };
+
+ private OnItemClickListener mChildItemClickListener = new OnItemClickListener() {
+ @Override
+ public void onItemClick(
+ AdapterView<?> parent, View view, int position, long id) {
+ mCallback.openUrl(((HistoryItem) view).getUrl());
+ }
+ };
+
+ @Override
+ public boolean onChildClick(ExpandableListView parent, View view,
+ int groupPosition, int childPosition, long id) {
+ mCallback.openUrl(((HistoryItem) view).getUrl());
+ return true;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ getLoaderManager().destroyLoader(LOADER_HISTORY);
+ getLoaderManager().destroyLoader(LOADER_MOST_VISITED);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ inflater.inflate(R.menu.history, menu);
+ }
+
+ void promptToClearHistory() {
+ final ContentResolver resolver = getActivity().getContentResolver();
+ final ClearHistoryTask clear = new ClearHistoryTask(resolver);
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
+ .setMessage(R.string.pref_privacy_clear_history_dlg)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .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) {
+ clear.start();
+ }
+ }
+ });
+ final Dialog dialog = builder.create();
+ dialog.show();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.clear_history_menu_id) {
+ promptToClearHistory();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ static class ClearHistoryTask extends Thread {
+ ContentResolver mResolver;
+
+ public ClearHistoryTask(ContentResolver resolver) {
+ mResolver = resolver;
+ }
+
+ @Override
+ public void run() {
+ Browser.clearHistory(mResolver);
+ }
+ }
+
+ View getTargetView(ContextMenuInfo menuInfo) {
+ if (menuInfo instanceof AdapterContextMenuInfo) {
+ return ((AdapterContextMenuInfo) menuInfo).targetView;
+ }
+ if (menuInfo instanceof ExpandableListContextMenuInfo) {
+ return ((ExpandableListContextMenuInfo) menuInfo).targetView;
+ }
+ return null;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+
+ View targetView = getTargetView(menuInfo);
+ if (!(targetView instanceof HistoryItem)) {
+ return;
+ }
+ HistoryItem historyItem = (HistoryItem) targetView;
+
+ // Inflate the menu
+ Activity parent = getActivity();
+ MenuInflater inflater = parent.getMenuInflater();
+ inflater.inflate(R.menu.historycontext, menu);
+
+ // Setup the header
+ if (mContextHeader == null) {
+ mContextHeader = new HistoryItem(parent, false);
+ mContextHeader.setEnableScrolling(true);
+ } else if (mContextHeader.getParent() != null) {
+ ((ViewGroup) mContextHeader.getParent()).removeView(mContextHeader);
+ }
+ historyItem.copyTo(mContextHeader);
+ menu.setHeaderView(mContextHeader);
+
+ // Only show open in new tab if it was not explicitly disabled
+ if (mDisableNewWindow) {
+ menu.findItem(R.id.new_window_context_menu_id).setVisible(false);
+ }
+ // For a bookmark, provide the option to remove it from bookmarks
+ if (historyItem.isBookmark()) {
+ MenuItem item = menu.findItem(R.id.save_to_bookmarks_menu_id);
+ item.setTitle(R.string.remove_from_bookmarks);
+ }
+ // decide whether to show the share link option
+ 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) {
+ ContextMenuInfo menuInfo = item.getMenuInfo();
+ if (menuInfo == null) {
+ return false;
+ }
+ View targetView = getTargetView(menuInfo);
+ if (!(targetView instanceof HistoryItem)) {
+ return false;
+ }
+ HistoryItem historyItem = (HistoryItem) targetView;
+ String url = historyItem.getUrl();
+ String title = historyItem.getName();
+ Activity activity = getActivity();
+ switch (item.getItemId()) {
+ case R.id.open_context_menu_id:
+ mCallback.openUrl(url);
+ return true;
+ case R.id.new_window_context_menu_id:
+ mCallback.openInNewTab(url);
+ return true;
+ case R.id.save_to_bookmarks_menu_id:
+ if (historyItem.isBookmark()) {
+ Bookmarks.removeFromBookmarks(activity, activity.getContentResolver(),
+ url, title);
+ } else {
+ Browser.saveBookmark(activity, title, url);
+ }
+ return true;
+ case R.id.share_link_context_menu_id:
+ Object[] params = {activity,
+ url,
+ activity.getText(R.string.choosertitle_sharevia).toString()};
+ Class[] type = new Class[] { android.content.Context.class,
+ String.class,
+ String.class};
+ ReflectHelper.invokeStaticMethod("android.provider.Browser","sendString",
+ type, params);
+ return true;
+ case R.id.copy_url_context_menu_id:
+ copy(url);
+ return true;
+ case R.id.delete_context_menu_id:
+ Browser.deleteFromHistory(activity.getContentResolver(), url);
+ return true;
+ case R.id.homepage_context_menu_id:
+ BrowserSettings.getInstance().setHomePage(url);
+ Toast.makeText(activity, R.string.homepage_set, Toast.LENGTH_LONG).show();
+ return true;
+ default:
+ break;
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ private static abstract class HistoryWrapper extends BaseAdapter {
+
+ protected HistoryAdapter mAdapter;
+ private DataSetObserver mObserver = new DataSetObserver() {
+ @Override
+ public void onChanged() {
+ super.onChanged();
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ super.onInvalidated();
+ notifyDataSetInvalidated();
+ }
+ };
+
+ public HistoryWrapper(HistoryAdapter adapter) {
+ mAdapter = adapter;
+ mAdapter.registerDataSetObserver(mObserver);
+ }
+
+ }
+ private static class HistoryGroupWrapper extends HistoryWrapper {
+
+ public HistoryGroupWrapper(HistoryAdapter adapter) {
+ super(adapter);
+ }
+
+ @Override
+ public int getCount() {
+ return mAdapter.getGroupCount();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return null;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ return mAdapter.getGroupView(position, false, convertView, parent);
+ }
+
+ }
+
+ private static class HistoryChildWrapper extends HistoryWrapper {
+
+ private int mSelectedGroup;
+
+ public HistoryChildWrapper(HistoryAdapter adapter) {
+ super(adapter);
+ }
+
+ void setSelectedGroup(int groupPosition) {
+ mSelectedGroup = groupPosition;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ return mAdapter.getChildrenCount(mSelectedGroup);
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return null;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ return mAdapter.getChildView(mSelectedGroup, position,
+ false, convertView, parent);
+ }
+
+ }
+
+ private class HistoryAdapter extends DateSortedExpandableListAdapter {
+
+ private Cursor mMostVisited, mHistoryCursor;
+ Drawable mFaviconBackground;
+
+ HistoryAdapter(Context context) {
+ super(context, HistoryQuery.INDEX_DATE_LAST_VISITED);
+ mFaviconBackground = BookmarkUtils.createListFaviconBackground(context);
+ }
+
+ @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;
+ if (mMostVisited != null) {
+ mMostVisited.registerDataSetObserver(mDataSetObserver);
+ }
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public long getChildId(int groupPosition, int childPosition) {
+ if (moveCursorToChildPosition(groupPosition, childPosition)) {
+ Cursor cursor = getCursor(groupPosition);
+ return cursor.getLong(HistoryQuery.INDEX_ID);
+ }
+ return 0;
+ }
+
+ @Override
+ public int getGroupCount() {
+ return super.getGroupCount() + (!isMostVisitedEmpty() ? 1 : 0);
+ }
+
+ @Override
+ public int getChildrenCount(int groupPosition) {
+ if (groupPosition >= super.getGroupCount()) {
+ if (isMostVisitedEmpty()) {
+ return 0;
+ }
+ return mMostVisited.getCount();
+ }
+ return super.getChildrenCount(groupPosition);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ if (!super.isEmpty()) {
+ return false;
+ }
+ return isMostVisitedEmpty();
+ }
+
+ private boolean isMostVisitedEmpty() {
+ 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 (mMostVisited == null || mMostVisited.isClosed()) {
+ 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 (mMostVisited != null && !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(getContext());
+ // Add padding on the left so it will be indented from the
+ // arrows on the group views.
+ item.setPadding(item.getPaddingLeft() + 10,
+ item.getPaddingTop(),
+ item.getPaddingRight(),
+ item.getPaddingBottom());
+ item.setFaviconBackground(mFaviconBackground);
+ } else {
+ item = (HistoryItem) convertView;
+ }
+
+ // Bail early if the Cursor is closed.
+ if (!moveCursorToChildPosition(groupPosition, childPosition)) {
+ return item;
+ }
+
+ Cursor cursor = getCursor(groupPosition);
+ item.setName(cursor.getString(HistoryQuery.INDEX_TITE));
+ String url = cursor.getString(HistoryQuery.INDEX_URL);
+ item.setUrl(url);
+ byte[] data = cursor.getBlob(HistoryQuery.INDEX_FAVICON);
+ if (data != null) {
+ item.setFavicon(BitmapFactory.decodeByteArray(data, 0,
+ data.length));
+ }
+ item.setIsBookmark(cursor.getInt(HistoryQuery.INDEX_IS_BOOKMARK) == 1);
+ return item;
+ }
+ }
+}
diff --git a/src/com/android/browser/BrowserPreferencesPage.java b/src/com/android/browser/BrowserPreferencesPage.java
new file mode 100644
index 0000000..ebc08a4
--- /dev/null
+++ b/src/com/android/browser/BrowserPreferencesPage.java
@@ -0,0 +1,102 @@
+/*
+ * 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.app.ActionBar;
+import android.content.Intent;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+import android.view.MenuItem;
+
+import com.android.browser.R;
+import com.android.browser.preferences.BandwidthPreferencesFragment;
+import com.android.browser.preferences.DebugPreferencesFragment;
+
+import java.util.List;
+
+public class BrowserPreferencesPage extends PreferenceActivity {
+
+ public static final String CURRENT_PAGE = "currentPage";
+ private List<Header> mHeaders;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayOptions(
+ ActionBar.DISPLAY_HOME_AS_UP, ActionBar.DISPLAY_HOME_AS_UP);
+ }
+ }
+
+ /**
+ * Populate the activity with the top-level headers.
+ */
+ @Override
+ public void onBuildHeaders(List<Header> target) {
+ loadHeadersFromResource(R.xml.preference_headers, target);
+
+ if (BrowserSettings.getInstance().isDebugEnabled()) {
+ Header debug = new Header();
+ debug.title = getText(R.string.pref_development_title);
+ debug.fragment = DebugPreferencesFragment.class.getName();
+ target.add(debug);
+ }
+ mHeaders = target;
+ }
+
+ @Override
+ public Header onGetInitialHeader() {
+ String action = getIntent().getAction();
+ if (Intent.ACTION_MANAGE_NETWORK_USAGE.equals(action)) {
+ String fragName = BandwidthPreferencesFragment.class.getName();
+ for (Header h : mHeaders) {
+ if (fragName.equals(h.fragment)) {
+ return h;
+ }
+ }
+ }
+ return super.onGetInitialHeader();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ if (getFragmentManager().getBackStackEntryCount() > 0) {
+ getFragmentManager().popBackStack();
+ } else {
+ finish();
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public Intent onBuildStartFragmentIntent(String fragmentName, Bundle args,
+ int titleRes, int shortTitleRes) {
+ Intent intent = super.onBuildStartFragmentIntent(fragmentName, args,
+ titleRes, shortTitleRes);
+ String url = getIntent().getStringExtra(CURRENT_PAGE);
+ intent.putExtra(CURRENT_PAGE, url);
+ return intent;
+ }
+
+}
diff --git a/src/com/android/browser/BrowserSettings.java b/src/com/android/browser/BrowserSettings.java
new file mode 100644
index 0000000..90dcc4f
--- /dev/null
+++ b/src/com/android/browser/BrowserSettings.java
@@ -0,0 +1,1140 @@
+/*
+ * Copyright (C) 2011 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.ContentResolver;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.content.res.AssetManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Build;
+import android.preference.PreferenceManager;
+import android.provider.Browser;
+import android.provider.Settings;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.webkit.WebIconDatabase;
+import android.webkit.WebStorage;
+import android.webkit.WebViewDatabase;
+
+import com.android.browser.R;
+import com.android.browser.homepages.HomeProvider;
+import com.android.browser.provider.BrowserProvider;
+import com.android.browser.reflect.ReflectHelper;
+import com.android.browser.search.SearchEngine;
+import com.android.browser.search.SearchEngines;
+
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Method;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Locale;
+import java.util.WeakHashMap;
+
+import org.codeaurora.swe.AutoFillProfile;
+import org.codeaurora.swe.CookieManager;
+import org.codeaurora.swe.GeolocationPermissions;
+import org.codeaurora.swe.WebSettings.LayoutAlgorithm;
+import org.codeaurora.swe.WebSettings.PluginState;
+import org.codeaurora.swe.WebSettings.TextSize;
+import org.codeaurora.swe.WebSettings.ZoomDensity;
+import org.codeaurora.swe.WebSettings;
+import org.codeaurora.swe.WebView;
+
+/**
+ * Class for managing settings
+ */
+public class BrowserSettings implements OnSharedPreferenceChangeListener,
+ PreferenceKeys {
+
+ // TODO: Do something with this UserAgent stuff
+ private static final String DESKTOP_USERAGENT = "Mozilla/5.0 (X11; " +
+ "Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) " +
+ "Chrome/11.0.696.34 Safari/534.24";
+
+ private static final String IPHONE_USERAGENT = "Mozilla/5.0 (iPhone; U; " +
+ "CPU iPhone OS 4_0 like Mac OS X; en-us) AppleWebKit/532.9 " +
+ "(KHTML, like Gecko) Version/4.0.5 Mobile/8A293 Safari/6531.22.7";
+
+ private static final String IPAD_USERAGENT = "Mozilla/5.0 (iPad; U; " +
+ "CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 " +
+ "(KHTML, like Gecko) Version/4.0.4 Mobile/7B367 Safari/531.21.10";
+
+ private static final String FROYO_USERAGENT = "Mozilla/5.0 (Linux; U; " +
+ "Android 2.2; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 " +
+ "(KHTML, like Gecko) Version/4.0 Mobile Safari/533.1";
+
+ private static final String HONEYCOMB_USERAGENT = "Mozilla/5.0 (Linux; U; " +
+ "Android 3.1; en-us; Xoom Build/HMJ25) AppleWebKit/534.13 " +
+ "(KHTML, like Gecko) Version/4.0 Safari/534.13";
+
+ private static final String USER_AGENTS[] = { null,
+ DESKTOP_USERAGENT,
+ IPHONE_USERAGENT,
+ IPAD_USERAGENT,
+ FROYO_USERAGENT,
+ HONEYCOMB_USERAGENT,
+ };
+
+ private static final String TAG = "BrowserSettings";
+ // The minimum min font size
+ // Aka, the lower bounds for the min font size range
+ // which is 1:5..24
+ private static final int MIN_FONT_SIZE_OFFSET = 5;
+ // The initial value in the text zoom range
+ // This is what represents 100% in the SeekBarPreference range
+ private static final int TEXT_ZOOM_START_VAL = 10;
+ // The size of a single step in the text zoom range, in percent
+ private static final int TEXT_ZOOM_STEP = 5;
+ // The initial value in the double tap zoom range
+ // This is what represents 100% in the SeekBarPreference range
+ private static final int DOUBLE_TAP_ZOOM_START_VAL = 5;
+ // The size of a single step in the double tap zoom range, in percent
+ private static final int DOUBLE_TAP_ZOOM_STEP = 5;
+
+ private static BrowserSettings sInstance;
+
+ private Context mContext;
+ private SharedPreferences mPrefs;
+ private LinkedList<WeakReference<WebSettings>> mManagedSettings;
+ private Controller mController;
+ private WebStorageSizeManager mWebStorageSizeManager;
+ private AutofillHandler mAutofillHandler;
+ private WeakHashMap<WebSettings, String> mCustomUserAgents;
+ private static boolean sInitialized = false;
+ private boolean mNeedsSharedSync = true;
+ private float mFontSizeMult = 1.0f;
+
+ // Current state of network-dependent settings
+ private boolean mLinkPrefetchAllowed = true;
+
+ // Cached values
+ private int mPageCacheCapacity = 1;
+ private String mAppCachePath;
+
+ // Cached settings
+ private SearchEngine mSearchEngine;
+
+ private static String sFactoryResetUrl;
+
+ // add for carrier feature
+ private static Context sResPackageCtx;
+ private android.os.CountDownTimer mCountDownTimer;
+
+ //Determine if WebView is Initialized or not
+ private boolean mWebViewInitialized;
+
+ public static void initialize(final Context context) {
+ sInstance = new BrowserSettings(context);
+ }
+
+ public static BrowserSettings getInstance() {
+ return sInstance;
+ }
+
+ private BrowserSettings(Context context) {
+ mContext = context.getApplicationContext();
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext);
+ mAutofillHandler = new AutofillHandler(mContext);
+ mManagedSettings = new LinkedList<WeakReference<WebSettings>>();
+ mCustomUserAgents = new WeakHashMap<WebSettings, String>();
+
+ // add for carrier feature
+ try {
+ sResPackageCtx = context.createPackageContext(
+ "com.android.browser.res",
+ Context.CONTEXT_IGNORE_SECURITY);
+ } catch (Exception e) {
+ Log.e("Res_Update", "Create Res Apk Failed");
+ }
+ BackgroundHandler.execute(mSetup);
+ mWebViewInitialized = false;
+ }
+
+ public void setController(Controller controller) {
+ mController = controller;
+ if (sInitialized) {
+ syncSharedSettings();
+ }
+ }
+
+ public void startManagingSettings(final WebSettings settings) {
+
+ if (mNeedsSharedSync) {
+ syncSharedSettings();
+ }
+
+ synchronized (mManagedSettings) {
+ syncStaticSettings(settings);
+ syncSetting(settings);
+ mManagedSettings.add(new WeakReference<WebSettings>(settings));
+ }
+ }
+
+ public void stopManagingSettings(WebSettings settings) {
+ Iterator<WeakReference<WebSettings>> iter = mManagedSettings.iterator();
+ while (iter.hasNext()) {
+ WeakReference<WebSettings> ref = iter.next();
+ if (ref.get() == settings) {
+ iter.remove();
+ return;
+ }
+ }
+ }
+
+ public void initializeCookieSettings() {
+ CookieManager.getInstance().setAcceptCookie(acceptCookies());
+ mWebViewInitialized = true;
+ }
+ private Runnable mSetup = new Runnable() {
+
+ @Override
+ public void run() {
+ DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
+ mFontSizeMult = metrics.scaledDensity / metrics.density;
+ // 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.
+
+ // SWE_TODO : assume a high-memory device
+ //if (ActivityManager.staticGetMemoryClass() > 16) {
+ mPageCacheCapacity = 5;
+ //}
+ mWebStorageSizeManager = new WebStorageSizeManager(mContext,
+ new WebStorageSizeManager.StatFsDiskInfo(getAppCachePath()),
+ new WebStorageSizeManager.WebKitAppCacheInfo(getAppCachePath()));
+ // Workaround b/5254577
+ mPrefs.registerOnSharedPreferenceChangeListener(BrowserSettings.this);
+ if (Build.VERSION.CODENAME.equals("REL")) {
+ // This is a release build, always startup with debug disabled
+ setDebugEnabled(false);
+ }
+ if (mPrefs.contains(PREF_TEXT_SIZE)) {
+ /*
+ * Update from TextSize enum to zoom percent
+ * SMALLEST is 50%
+ * SMALLER is 75%
+ * NORMAL is 100%
+ * LARGER is 150%
+ * LARGEST is 200%
+ */
+ switch (getTextSize()) {
+ case SMALLEST:
+ setTextZoom(50);
+ break;
+ case SMALLER:
+ setTextZoom(75);
+ break;
+ case LARGER:
+ setTextZoom(150);
+ break;
+ case LARGEST:
+ setTextZoom(200);
+ break;
+ }
+ mPrefs.edit().remove(PREF_TEXT_SIZE).apply();
+ }
+
+ // add for carrier homepage feature
+ Object[] params = { new String("persist.env.c.browser.resource"),
+ new String("default")};
+ Class[] type = new Class[] {String.class, String.class};
+ String browserRes = (String)ReflectHelper.invokeStaticMethod(
+ "android.os.SystemProperties","get",type, params);
+ if ("cu".equals(browserRes) || "cmcc".equals(browserRes)) {
+ int resID = sResPackageCtx.getResources().getIdentifier(
+ "homepage_base", "string", "com.android.browser.res");
+ sFactoryResetUrl = sResPackageCtx.getResources().getString(resID);
+ } else if ("ct".equals(browserRes)) {
+ int resID = sResPackageCtx.getResources().getIdentifier(
+ "homepage_base", "string", "com.android.browser.res");
+ sFactoryResetUrl = sResPackageCtx.getResources().getString(resID);
+
+ int pathID = sResPackageCtx.getResources().getIdentifier(
+ "homepage_path", "string", "com.android.browser.res");
+ String path = sResPackageCtx.getResources().getString(pathID);
+ Locale locale = Locale.getDefault();
+ path = path.replace("%y", locale.getLanguage().toLowerCase());
+ path = path.replace("%z", '_'+locale.getCountry().toLowerCase());
+ boolean useCountry = true;
+ boolean useLanguage = true;
+ InputStream is = null;
+ AssetManager am = mContext.getAssets();
+ try {
+ is = am.open(path);
+ } catch (Exception ignored) {
+ useCountry = false;
+ path = sResPackageCtx.getResources().getString(pathID);
+ path = path.replace("%y", locale.getLanguage().toLowerCase());
+ path = path.replace("%z", "");
+ try {
+ is = am.open(path);
+ } catch (Exception ignoredlanguage) {
+ useLanguage = false;
+ }
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (Exception ignored) {}
+ }
+ }
+
+ if (!useCountry && !useLanguage) {
+ sFactoryResetUrl = sFactoryResetUrl.replace("%y%z", "en");
+ } else {
+ sFactoryResetUrl = sFactoryResetUrl.replace("%y",
+ locale.getLanguage().toLowerCase());
+ sFactoryResetUrl = sFactoryResetUrl.replace("%z", useCountry ?
+ '_' + locale.getCountry().toLowerCase() : "");
+ }
+ } else {
+ sFactoryResetUrl = mContext.getResources().getString(R.string.homepage_base);
+ }
+
+ if (!mPrefs.contains(PREF_DEFAULT_TEXT_ENCODING)) {
+ if (!"default".equals(browserRes)) {
+ mPrefs.edit().putString(PREF_DEFAULT_TEXT_ENCODING,
+ "GBK").apply();
+ }
+ }
+ if (sFactoryResetUrl.indexOf("{CID}") != -1) {
+ sFactoryResetUrl = sFactoryResetUrl.replace("{CID}",
+ BrowserProvider.getClientId(mContext.getContentResolver()));
+ }
+
+ synchronized (BrowserSettings.class) {
+ sInitialized = true;
+ BrowserSettings.class.notifyAll();
+ }
+ }
+ };
+
+ private static void requireInitialization() {
+ synchronized (BrowserSettings.class) {
+ while (!sInitialized) {
+ try {
+ BrowserSettings.class.wait();
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+ }
+
+ /**
+ * Syncs all the settings that have a Preference UI
+ */
+ private void syncSetting(WebSettings settings) {
+ settings.setGeolocationEnabled(enableGeolocation());
+ settings.setJavaScriptEnabled(enableJavascript());
+ settings.setLightTouchEnabled(enableLightTouch());
+ settings.setNavDump(enableNavDump());
+ settings.setDefaultTextEncodingName(getDefaultTextEncoding());
+ settings.setDefaultZoom(getDefaultZoom());
+ settings.setMinimumFontSize(getMinimumFontSize());
+ settings.setMinimumLogicalFontSize(getMinimumFontSize());
+ settings.setPluginState(getPluginState());
+ settings.setTextZoom(getTextZoom());
+ settings.setLayoutAlgorithm(getLayoutAlgorithm());
+ settings.setJavaScriptCanOpenWindowsAutomatically(!blockPopupWindows());
+ settings.setLoadsImagesAutomatically(loadImages());
+ settings.setLoadWithOverviewMode(loadPageInOverviewMode());
+ settings.setSavePassword(rememberPasswords());
+ settings.setSaveFormData(saveFormdata());
+ settings.setUseWideViewPort(isWideViewport());
+
+ // add for carrier useragent feature
+ String ua = null;
+ try {
+ Class c = Class.forName("com.qrd.useragent.UserAgentHandler");
+ Object cObj = c.newInstance();
+ Method m = c.getDeclaredMethod("getUAString", Context.class);
+ ua = (String)m.invoke(cObj, mContext);
+ } catch (Exception e) {
+ Log.e(TAG, "plug in Load failed, err " + e);
+ ua = mCustomUserAgents.get(settings);
+ }
+ if (ua != null) {
+ settings.setUserAgentString(ua);
+ } else {
+ settings.setUserAgentString(USER_AGENTS[getUserAgent()]);
+ }
+
+ WebSettings settingsClassic = (WebSettings) settings;
+ settingsClassic.setHardwareAccelSkiaEnabled(isSkiaHardwareAccelerated());
+ settingsClassic.setShowVisualIndicator(enableVisualIndicator());
+ settingsClassic.setForceUserScalable(forceEnableUserScalable());
+ settingsClassic.setDoubleTapZoom(getDoubleTapZoom());
+ settingsClassic.setAutoFillEnabled(isAutofillEnabled());
+
+ boolean useInverted = useInvertedRendering();
+ settingsClassic.setProperty(WebViewProperties.gfxInvertedScreen,
+ useInverted ? "true" : "false");
+ if (useInverted) {
+ settingsClassic.setProperty(WebViewProperties.gfxInvertedScreenContrast,
+ Float.toString(getInvertedContrast()));
+ }
+
+ if (isDebugEnabled()) {
+ settingsClassic.setProperty(WebViewProperties.gfxEnableCpuUploadPath,
+ enableCpuUploadPath() ? "true" : "false");
+ }
+
+ settingsClassic.setLinkPrefetchEnabled(mLinkPrefetchAllowed);
+ }
+
+ /**
+ * Syncs all the settings that have no UI
+ * These cannot change, so we only need to set them once per WebSettings
+ */
+ private void syncStaticSettings(WebSettings settings) {
+ settings.setDefaultFontSize(16);
+ settings.setDefaultFixedFontSize(13);
+
+ // WebView inside Browser doesn't want initial focus to be set.
+ settings.setNeedInitialFocus(false);
+ // Browser supports multiple windows
+ settings.setSupportMultipleWindows(true);
+ // enable smooth transition for better performance during panning or
+ // zooming
+ settings.setEnableSmoothTransition(true);
+ // disable content url access
+ settings.setAllowContentAccess(false);
+
+ // HTML5 API flags
+ settings.setAppCacheEnabled(true);
+ settings.setDatabaseEnabled(true);
+ settings.setDomStorageEnabled(true);
+
+ // HTML5 configuration parametersettings.
+ settings.setAppCacheMaxSize(getWebStorageSizeManager().getAppCacheMaxSize());
+ settings.setAppCachePath(getAppCachePath());
+ settings.setDatabasePath(mContext.getDir("databases", 0).getPath());
+ settings.setGeolocationDatabasePath(mContext.getDir("geolocation", 0).getPath());
+ // origin policy for file access
+ settings.setAllowUniversalAccessFromFileURLs(false);
+ settings.setAllowFileAccessFromFileURLs(false);
+
+ //if (!(settings instanceof WebSettingsClassic)) return;
+ /*
+
+ WebSettingsClassic settingsClassic = (WebSettingsClassic) settings;
+ settingsClassic.setPageCacheCapacity(getPageCacheCapacity());
+ // WebView should be preserving the memory as much as possible.
+ // However, apps like browser wish to turn on the performance mode which
+ // would require more memory.
+ // TODO: We need to dynamically allocate/deallocate temporary memory for
+ // apps which are trying to use minimal memory. Currently, double
+ // buffering is always turned on, which is unnecessary.
+ settingsClassic.setProperty(WebViewProperties.gfxUseMinimalMemory, "false");
+ settingsClassic.setWorkersEnabled(true); // This only affects V8.
+ */
+ }
+
+ private void syncSharedSettings() {
+ mNeedsSharedSync = false;
+ if (mWebViewInitialized) {
+ CookieManager.getInstance().setAcceptCookie(acceptCookies());
+ }
+ if (mController != null) {
+ mController.setShouldShowErrorConsole(enableJavascriptConsole());
+ }
+ }
+
+ private void syncManagedSettings() {
+ syncSharedSettings();
+ synchronized (mManagedSettings) {
+ Iterator<WeakReference<WebSettings>> iter = mManagedSettings.iterator();
+ while (iter.hasNext()) {
+ WeakReference<WebSettings> ref = iter.next();
+ WebSettings settings = (WebSettings)ref.get();
+ if (settings == null) {
+ iter.remove();
+ continue;
+ }
+ syncSetting(settings);
+ }
+ }
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(
+ SharedPreferences sharedPreferences, String key) {
+ syncManagedSettings();
+ if (PREF_SEARCH_ENGINE.equals(key)) {
+ updateSearchEngine(false);
+ } else if (PREF_FULLSCREEN.equals(key)) {
+ if (mController != null && mController.getUi() != null) {
+ mController.getUi().setFullscreen(useFullscreen());
+ }
+ } else if (PREF_ENABLE_QUICK_CONTROLS.equals(key)) {
+ if (mController != null && mController.getUi() != null) {
+ mController.getUi().setUseQuickControls(sharedPreferences.getBoolean(key, false));
+ }
+ } else if (PREF_LINK_PREFETCH.equals(key)) {
+ updateConnectionType();
+ }
+ }
+
+ public static String getFactoryResetHomeUrl(Context context) {
+ requireInitialization();
+ return sFactoryResetUrl;
+ }
+
+ public LayoutAlgorithm getLayoutAlgorithm() {
+ LayoutAlgorithm layoutAlgorithm = LayoutAlgorithm.NORMAL;
+ if (autofitPages()) {
+ layoutAlgorithm = LayoutAlgorithm.NARROW_COLUMNS;
+ }
+ if (isDebugEnabled()) {
+ if (isSmallScreen()) {
+ layoutAlgorithm = LayoutAlgorithm.SINGLE_COLUMN;
+ } else {
+ if (isNormalLayout()) {
+ layoutAlgorithm = LayoutAlgorithm.NORMAL;
+ } else {
+ layoutAlgorithm = LayoutAlgorithm.NARROW_COLUMNS;
+ }
+ }
+ }
+ return layoutAlgorithm;
+ }
+
+ public int getPageCacheCapacity() {
+ requireInitialization();
+ return mPageCacheCapacity;
+ }
+
+ public WebStorageSizeManager getWebStorageSizeManager() {
+ requireInitialization();
+ return mWebStorageSizeManager;
+ }
+
+ private String getAppCachePath() {
+ if (mAppCachePath == null) {
+ mAppCachePath = mContext.getDir("appcache", 0).getPath();
+ }
+ return mAppCachePath;
+ }
+
+ private void updateSearchEngine(boolean force) {
+ String searchEngineName = getSearchEngineName();
+ if (force || mSearchEngine == null ||
+ !mSearchEngine.getName().equals(searchEngineName)) {
+ mSearchEngine = SearchEngines.get(mContext, searchEngineName);
+ }
+ }
+
+ public SearchEngine getSearchEngine() {
+ if (mSearchEngine == null) {
+ updateSearchEngine(false);
+ }
+ return mSearchEngine;
+ }
+
+ public boolean isDebugEnabled() {
+ requireInitialization();
+ return mPrefs.getBoolean(PREF_DEBUG_MENU, false);
+ }
+
+ public void setDebugEnabled(boolean value) {
+ Editor edit = mPrefs.edit();
+ edit.putBoolean(PREF_DEBUG_MENU, value);
+ if (!value) {
+ // Reset to "safe" value
+ edit.putBoolean(PREF_ENABLE_HARDWARE_ACCEL_SKIA, false);
+ }
+ edit.apply();
+ }
+
+ public void clearCache() {
+ WebIconDatabase.getInstance().removeAllIcons();
+ if (mController != null) {
+ WebView current = mController.getCurrentWebView();
+ if (current != null) {
+ current.clearCache(true);
+ }
+ }
+ }
+
+ public void clearCookies() {
+ CookieManager.getInstance().removeAllCookie();
+ }
+
+ public void clearHistory() {
+ ContentResolver resolver = mContext.getContentResolver();
+ Browser.clearHistory(resolver);
+ Browser.clearSearches(resolver);
+ }
+
+ public void clearFormData() {
+ WebViewDatabase.getInstance(mContext).clearFormData();
+ if (mController!= null) {
+ WebView currentTopView = mController.getCurrentTopWebView();
+ if (currentTopView != null) {
+ currentTopView.clearFormData();
+ }
+ }
+ }
+
+ public void clearPasswords() {
+ // Clear password store maintained by SWE engine
+ WebSettings settings = null;
+ // find a valid settings object
+ Iterator<WeakReference<WebSettings>> iter = mManagedSettings.iterator();
+ while (iter.hasNext()) {
+ WeakReference<WebSettings> ref = iter.next();
+ settings = (WebSettings)ref.get();
+ if (settings != null) {
+ break;
+ }
+ }
+ if (settings != null) {
+ settings.clearPasswords();
+ }
+
+ // Clear passwords in WebView database
+ WebViewDatabase db = WebViewDatabase.getInstance(mContext);
+ db.clearUsernamePassword();
+ db.clearHttpAuthUsernamePassword();
+ }
+
+ public void clearDatabases() {
+ WebStorage.getInstance().deleteAllData();
+ }
+
+ public void clearLocationAccess() {
+ GeolocationPermissions.getInstance().clearAll();
+ }
+
+ public void resetDefaultPreferences() {
+ // Preserve autologin setting
+ long gal = mPrefs.getLong(GoogleAccountLogin.PREF_AUTOLOGIN_TIME, -1);
+ mPrefs.edit()
+ .clear()
+ .putLong(GoogleAccountLogin.PREF_AUTOLOGIN_TIME, gal)
+ .apply();
+ resetCachedValues();
+ syncManagedSettings();
+ }
+
+ private void resetCachedValues() {
+ updateSearchEngine(false);
+ }
+
+ public AutoFillProfile getAutoFillProfile() {
+ // query the profile from components autofill database 524
+ if (mAutofillHandler.mAutoFillProfile == null &&
+ !mAutofillHandler.mAutoFillActiveProfileId.equals("")) {
+ WebSettings settings = null;
+ // find a valid settings object
+ Iterator<WeakReference<WebSettings>> iter = mManagedSettings.iterator();
+ while (iter.hasNext()) {
+ WeakReference<WebSettings> ref = iter.next();
+ settings = (WebSettings)ref.get();
+ if (settings != null) {
+ break;
+ }
+ }
+ if (settings != null) {
+ AutoFillProfile profile =
+ settings.getAutoFillProfile(mAutofillHandler.mAutoFillActiveProfileId);
+ mAutofillHandler.setAutoFillProfile(profile);
+ }
+ }
+ return mAutofillHandler.getAutoFillProfile();
+ }
+
+ public String getAutoFillProfileId() {
+ return mAutofillHandler.getAutoFillProfileId();
+ }
+
+ public void updateAutoFillProfile(AutoFillProfile profile) {
+ syncAutoFillProfile(profile);
+ }
+
+ private void syncAutoFillProfile(AutoFillProfile profile) {
+ synchronized (mManagedSettings) {
+ Iterator<WeakReference<WebSettings>> iter = mManagedSettings.iterator();
+ while (iter.hasNext()) {
+ WeakReference<WebSettings> ref = iter.next();
+ WebSettings settings = (WebSettings)ref.get();
+ if (settings == null) {
+ iter.remove();
+ continue;
+ }
+ // update the profile only once.
+ settings.setAutoFillProfile(profile);
+ // Now we should have the guid
+ mAutofillHandler.setAutoFillProfile(profile);
+ break;
+ }
+ }
+ }
+ public void toggleDebugSettings() {
+ setDebugEnabled(!isDebugEnabled());
+ }
+
+ public boolean hasDesktopUseragent(WebView view) {
+ return view != null && mCustomUserAgents.get(view.getSettings()) != null;
+ }
+
+ public void toggleDesktopUseragent(WebView view) {
+ if (view == null) {
+ return;
+ }
+ WebSettings settings = view.getSettings();
+ if (mCustomUserAgents.get(settings) != null) {
+ mCustomUserAgents.remove(settings);
+ settings.setUserAgentString(USER_AGENTS[getUserAgent()]);
+ } else {
+ mCustomUserAgents.put(settings, DESKTOP_USERAGENT);
+ settings.setUserAgentString(DESKTOP_USERAGENT);
+ }
+ }
+
+ public static int getAdjustedMinimumFontSize(int rawValue) {
+ rawValue++; // Preference starts at 0, min font at 1
+ if (rawValue > 1) {
+ rawValue += (MIN_FONT_SIZE_OFFSET - 2);
+ }
+ return rawValue;
+ }
+
+ public int getAdjustedTextZoom(int rawValue) {
+ rawValue = (rawValue - TEXT_ZOOM_START_VAL) * TEXT_ZOOM_STEP;
+ return (int) ((rawValue + 100) * mFontSizeMult);
+ }
+
+ static int getRawTextZoom(int percent) {
+ return (percent - 100) / TEXT_ZOOM_STEP + TEXT_ZOOM_START_VAL;
+ }
+
+ public int getAdjustedDoubleTapZoom(int rawValue) {
+ rawValue = (rawValue - DOUBLE_TAP_ZOOM_START_VAL) * DOUBLE_TAP_ZOOM_STEP;
+ return (int) ((rawValue + 100) * mFontSizeMult);
+ }
+
+ static int getRawDoubleTapZoom(int percent) {
+ return (percent - 100) / DOUBLE_TAP_ZOOM_STEP + DOUBLE_TAP_ZOOM_START_VAL;
+ }
+
+ public SharedPreferences getPreferences() {
+ return mPrefs;
+ }
+
+ // update connectivity-dependent options
+ public void updateConnectionType() {
+ ConnectivityManager cm = (ConnectivityManager)
+ mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ String linkPrefetchPreference = getLinkPrefetchEnabled();
+ boolean linkPrefetchAllowed = linkPrefetchPreference.
+ equals(getLinkPrefetchAlwaysPreferenceString(mContext));
+ NetworkInfo ni = cm.getActiveNetworkInfo();
+ if (ni != null) {
+ switch (ni.getType()) {
+ case ConnectivityManager.TYPE_WIFI:
+ case ConnectivityManager.TYPE_ETHERNET:
+ case ConnectivityManager.TYPE_BLUETOOTH:
+ linkPrefetchAllowed |= linkPrefetchPreference.
+ equals(getLinkPrefetchOnWifiOnlyPreferenceString(mContext));
+ break;
+ case ConnectivityManager.TYPE_MOBILE:
+ case ConnectivityManager.TYPE_MOBILE_DUN:
+ case ConnectivityManager.TYPE_MOBILE_MMS:
+ case ConnectivityManager.TYPE_MOBILE_SUPL:
+ case ConnectivityManager.TYPE_WIMAX:
+ default:
+ break;
+ }
+ }
+ if (mLinkPrefetchAllowed != linkPrefetchAllowed) {
+ mLinkPrefetchAllowed = linkPrefetchAllowed;
+ syncManagedSettings();
+ }
+ }
+
+ public String getDownloadPath() {
+ return mPrefs.getString(PREF_DOWNLOAD_PATH,
+ DownloadHandler.getDefaultDownloadPath(mContext));
+ }
+ // -----------------------------
+ // getter/setters for accessibility_preferences.xml
+ // -----------------------------
+
+ @Deprecated
+ private TextSize getTextSize() {
+ String textSize = mPrefs.getString(PREF_TEXT_SIZE, "NORMAL");
+ return TextSize.valueOf(textSize);
+ }
+
+ public int getMinimumFontSize() {
+ int minFont = mPrefs.getInt(PREF_MIN_FONT_SIZE, 0);
+ return getAdjustedMinimumFontSize(minFont);
+ }
+
+ public boolean forceEnableUserScalable() {
+ return mPrefs.getBoolean(PREF_FORCE_USERSCALABLE, false);
+ }
+
+ public int getTextZoom() {
+ requireInitialization();
+ int textZoom = mPrefs.getInt(PREF_TEXT_ZOOM, 10);
+ return getAdjustedTextZoom(textZoom);
+ }
+
+ public void setTextZoom(int percent) {
+ mPrefs.edit().putInt(PREF_TEXT_ZOOM, getRawTextZoom(percent)).apply();
+ }
+
+ public int getDoubleTapZoom() {
+ requireInitialization();
+ int doubleTapZoom = mPrefs.getInt(PREF_DOUBLE_TAP_ZOOM, 5);
+ return getAdjustedDoubleTapZoom(doubleTapZoom);
+ }
+
+ public void setDoubleTapZoom(int percent) {
+ mPrefs.edit().putInt(PREF_DOUBLE_TAP_ZOOM, getRawDoubleTapZoom(percent)).apply();
+ }
+
+ // -----------------------------
+ // getter/setters for advanced_preferences.xml
+ // -----------------------------
+
+ public String getSearchEngineName() {
+ String defaultSearchEngineValue = mContext.getString(R.string.default_search_engine_value);
+ if (defaultSearchEngineValue == null) {
+ defaultSearchEngineValue = SearchEngine.GOOGLE;
+ }
+ return mPrefs.getString(PREF_SEARCH_ENGINE, defaultSearchEngineValue);
+ }
+
+ public boolean allowAppTabs() {
+ return mPrefs.getBoolean(PREF_ALLOW_APP_TABS, false);
+ }
+
+ public boolean openInBackground() {
+ return mPrefs.getBoolean(PREF_OPEN_IN_BACKGROUND, false);
+ }
+
+ public boolean enableJavascript() {
+ return mPrefs.getBoolean(PREF_ENABLE_JAVASCRIPT, true);
+ }
+
+ public boolean enableMemoryMonitor() {
+ return mPrefs.getBoolean(PREF_ENABLE_MEMORY_MONITOR, true);
+ }
+
+ // TODO: Cache
+ public PluginState getPluginState() {
+ String state = mPrefs.getString(PREF_PLUGIN_STATE, "ON");
+ return PluginState.valueOf(state);
+ }
+
+ // TODO: Cache
+ public ZoomDensity getDefaultZoom() {
+ String zoom = mPrefs.getString(PREF_DEFAULT_ZOOM, "MEDIUM");
+ return ZoomDensity.valueOf(zoom);
+ }
+
+ public boolean loadPageInOverviewMode() {
+ return mPrefs.getBoolean(PREF_LOAD_PAGE, true);
+ }
+
+ public boolean autofitPages() {
+ return mPrefs.getBoolean(PREF_AUTOFIT_PAGES, true);
+ }
+
+ public boolean blockPopupWindows() {
+ return mPrefs.getBoolean(PREF_BLOCK_POPUP_WINDOWS, true);
+ }
+
+ public boolean loadImages() {
+ return mPrefs.getBoolean(PREF_LOAD_IMAGES, true);
+ }
+
+ public String getDefaultTextEncoding() {
+ return mPrefs.getString(PREF_DEFAULT_TEXT_ENCODING, null);
+ }
+
+ // -----------------------------
+ // getter/setters for general_preferences.xml
+ // -----------------------------
+
+ public String getHomePage() {
+ return mPrefs.getString(PREF_HOMEPAGE, getFactoryResetHomeUrl(mContext));
+ }
+
+ public void setHomePage(String value) {
+ mPrefs.edit().putString(PREF_HOMEPAGE, value).apply();
+ }
+
+ public boolean isAutofillEnabled() {
+ return mPrefs.getBoolean(PREF_AUTOFILL_ENABLED, true);
+ }
+
+ public void setAutofillEnabled(boolean value) {
+ mPrefs.edit().putBoolean(PREF_AUTOFILL_ENABLED, value).apply();
+ }
+
+ // -----------------------------
+ // getter/setters for debug_preferences.xml
+ // -----------------------------
+
+ public boolean isHardwareAccelerated() {
+ if (!isDebugEnabled()) {
+ return true;
+ }
+ return mPrefs.getBoolean(PREF_ENABLE_HARDWARE_ACCEL, true);
+ }
+
+ public boolean isSkiaHardwareAccelerated() {
+ if (!isDebugEnabled()) {
+ return false;
+ }
+ return mPrefs.getBoolean(PREF_ENABLE_HARDWARE_ACCEL_SKIA, false);
+ }
+
+ public int getUserAgent() {
+ if (!isDebugEnabled()) {
+ return 0;
+ }
+ return Integer.parseInt(mPrefs.getString(PREF_USER_AGENT, "0"));
+ }
+
+ // -----------------------------
+ // getter/setters for hidden_debug_preferences.xml
+ // -----------------------------
+
+ public boolean enableVisualIndicator() {
+ if (!isDebugEnabled()) {
+ return false;
+ }
+ return mPrefs.getBoolean(PREF_ENABLE_VISUAL_INDICATOR, false);
+ }
+
+ public boolean enableCpuUploadPath() {
+ if (!isDebugEnabled()) {
+ return false;
+ }
+ return mPrefs.getBoolean(PREF_ENABLE_CPU_UPLOAD_PATH, false);
+ }
+
+ public boolean enableJavascriptConsole() {
+ if (!isDebugEnabled()) {
+ return false;
+ }
+ return mPrefs.getBoolean(PREF_JAVASCRIPT_CONSOLE, true);
+ }
+
+ public boolean isSmallScreen() {
+ if (!isDebugEnabled()) {
+ return false;
+ }
+ return mPrefs.getBoolean(PREF_SMALL_SCREEN, false);
+ }
+
+ public boolean isWideViewport() {
+ if (!isDebugEnabled()) {
+ return true;
+ }
+ return mPrefs.getBoolean(PREF_WIDE_VIEWPORT, true);
+ }
+
+ public boolean isNormalLayout() {
+ if (!isDebugEnabled()) {
+ return false;
+ }
+ return mPrefs.getBoolean(PREF_NORMAL_LAYOUT, false);
+ }
+
+ public boolean isTracing() {
+ if (!isDebugEnabled()) {
+ return false;
+ }
+ return mPrefs.getBoolean(PREF_ENABLE_TRACING, false);
+ }
+
+ public boolean enableLightTouch() {
+ if (!isDebugEnabled()) {
+ return false;
+ }
+ return mPrefs.getBoolean(PREF_ENABLE_LIGHT_TOUCH, false);
+ }
+
+ public boolean enableNavDump() {
+ if (!isDebugEnabled()) {
+ return false;
+ }
+ return mPrefs.getBoolean(PREF_ENABLE_NAV_DUMP, false);
+ }
+
+ public String getJsEngineFlags() {
+ if (!isDebugEnabled()) {
+ return "";
+ }
+ return mPrefs.getString(PREF_JS_ENGINE_FLAGS, "");
+ }
+
+ // -----------------------------
+ // getter/setters for lab_preferences.xml
+ // -----------------------------
+
+ public boolean useQuickControls() {
+ return mPrefs.getBoolean(PREF_ENABLE_QUICK_CONTROLS, false);
+ }
+
+ public boolean useMostVisitedHomepage() {
+ return HomeProvider.MOST_VISITED.equals(getHomePage());
+ }
+
+ public boolean useFullscreen() {
+ return mPrefs.getBoolean(PREF_FULLSCREEN, false);
+ }
+
+ public boolean useInvertedRendering() {
+ return mPrefs.getBoolean(PREF_INVERTED, false);
+ }
+
+ public float getInvertedContrast() {
+ return 1 + (mPrefs.getInt(PREF_INVERTED_CONTRAST, 0) / 10f);
+ }
+
+ // -----------------------------
+ // getter/setters for privacy_security_preferences.xml
+ // -----------------------------
+
+ public boolean showSecurityWarnings() {
+ return mPrefs.getBoolean(PREF_SHOW_SECURITY_WARNINGS, true);
+ }
+
+ public boolean acceptCookies() {
+ return mPrefs.getBoolean(PREF_ACCEPT_COOKIES, true);
+ }
+
+ public boolean saveFormdata() {
+ return mPrefs.getBoolean(PREF_SAVE_FORMDATA, true);
+ }
+
+ public boolean enableGeolocation() {
+ return mPrefs.getBoolean(PREF_ENABLE_GEOLOCATION, true);
+ }
+
+ public boolean rememberPasswords() {
+ return mPrefs.getBoolean(PREF_REMEMBER_PASSWORDS, true);
+ }
+
+ // -----------------------------
+ // getter/setters for bandwidth_preferences.xml
+ // -----------------------------
+
+ public static String getPreloadOnWifiOnlyPreferenceString(Context context) {
+ return context.getResources().getString(R.string.pref_data_preload_value_wifi_only);
+ }
+
+ public static String getPreloadAlwaysPreferenceString(Context context) {
+ return context.getResources().getString(R.string.pref_data_preload_value_always);
+ }
+
+ private static final String DEAULT_PRELOAD_SECURE_SETTING_KEY =
+ "browser_default_preload_setting";
+
+ public String getDefaultPreloadSetting() {
+ String preload = Settings.Secure.getString(mContext.getContentResolver(),
+ DEAULT_PRELOAD_SECURE_SETTING_KEY);
+ if (preload == null) {
+ preload = mContext.getResources().getString(R.string.pref_data_preload_default_value);
+ }
+ return preload;
+ }
+
+ public String getPreloadEnabled() {
+ return mPrefs.getString(PREF_DATA_PRELOAD, getDefaultPreloadSetting());
+ }
+
+ public static String getLinkPrefetchOnWifiOnlyPreferenceString(Context context) {
+ return context.getResources().getString(R.string.pref_link_prefetch_value_wifi_only);
+ }
+
+ public static String getLinkPrefetchAlwaysPreferenceString(Context context) {
+ return context.getResources().getString(R.string.pref_link_prefetch_value_always);
+ }
+
+ private static final String DEFAULT_LINK_PREFETCH_SECURE_SETTING_KEY =
+ "browser_default_link_prefetch_setting";
+
+ public String getDefaultLinkPrefetchSetting() {
+ String preload = Settings.Secure.getString(mContext.getContentResolver(),
+ DEFAULT_LINK_PREFETCH_SECURE_SETTING_KEY);
+ if (preload == null) {
+ preload = mContext.getResources().getString(R.string.pref_link_prefetch_default_value);
+ }
+ return preload;
+ }
+
+ public String getLinkPrefetchEnabled() {
+ return mPrefs.getString(PREF_LINK_PREFETCH, getDefaultLinkPrefetchSetting());
+ }
+
+ // -----------------------------
+ // getter/setters for browser recovery
+ // -----------------------------
+ /**
+ * The last time browser was started.
+ * @return The last browser start time as System.currentTimeMillis. This
+ * can be 0 if this is the first time or the last tab was closed.
+ */
+ public long getLastRecovered() {
+ return mPrefs.getLong(KEY_LAST_RECOVERED, 0);
+ }
+
+ /**
+ * Sets the last browser start time.
+ * @param time The last time as System.currentTimeMillis that the browser
+ * was started. This should be set to 0 if the last tab is closed.
+ */
+ public void setLastRecovered(long time) {
+ mPrefs.edit()
+ .putLong(KEY_LAST_RECOVERED, time)
+ .apply();
+ }
+
+ /**
+ * Used to determine whether or not the previous browser run crashed. Once
+ * the previous state has been determined, the value will be set to false
+ * until a pause is received.
+ * @return true if the last browser run was paused or false if it crashed.
+ */
+ public boolean wasLastRunPaused() {
+ return mPrefs.getBoolean(KEY_LAST_RUN_PAUSED, false);
+ }
+
+ /**
+ * Sets whether or not the last run was a pause or crash.
+ * @param isPaused Set to true When a pause is received or false after
+ * resuming.
+ */
+ public void setLastRunPaused(boolean isPaused) {
+ mPrefs.edit()
+ .putBoolean(KEY_LAST_RUN_PAUSED, isPaused)
+ .apply();
+ }
+}
diff --git a/src/com/android/browser/BrowserSnapshotPage.java b/src/com/android/browser/BrowserSnapshotPage.java
new file mode 100644
index 0000000..5d2453b
--- /dev/null
+++ b/src/com/android/browser/BrowserSnapshotPage.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2011 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.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.app.Fragment;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.ContextMenu;
+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.MeasureSpec;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.GridView;
+import android.widget.ImageView;
+import android.widget.ResourceCursorAdapter;
+import android.widget.TextView;
+
+import com.android.browser.R;
+import com.android.browser.provider.SnapshotProvider.Snapshots;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+public class BrowserSnapshotPage extends Fragment implements
+ LoaderCallbacks<Cursor>, OnItemClickListener {
+
+ public static final String EXTRA_ANIMATE_ID = "animate_id";
+
+ private static final int LOADER_SNAPSHOTS = 1;
+ private static final String[] PROJECTION = new String[] {
+ Snapshots._ID,
+ Snapshots.TITLE,
+ Snapshots.VIEWSTATE_SIZE,
+ Snapshots.THUMBNAIL,
+ Snapshots.FAVICON,
+ Snapshots.URL,
+ Snapshots.DATE_CREATED,
+ };
+ private static final int SNAPSHOT_ID = 0;
+ private static final int SNAPSHOT_TITLE = 1;
+ private static final int SNAPSHOT_VIEWSTATE_SIZE = 2;
+ private static final int SNAPSHOT_THUMBNAIL = 3;
+ private static final int SNAPSHOT_FAVICON = 4;
+ private static final int SNAPSHOT_URL = 5;
+ private static final int SNAPSHOT_DATE_CREATED = 6;
+
+ GridView mGrid;
+ View mEmpty;
+ SnapshotAdapter mAdapter;
+ CombinedBookmarksCallbacks mCallback;
+ long mAnimateId;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mCallback = (CombinedBookmarksCallbacks) getActivity();
+ mAnimateId = getArguments().getLong(EXTRA_ANIMATE_ID);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.snapshots, container, false);
+ mEmpty = view.findViewById(android.R.id.empty);
+ mGrid = (GridView) view.findViewById(R.id.grid);
+ setupGrid(inflater);
+ getLoaderManager().initLoader(LOADER_SNAPSHOTS, null, this);
+ return view;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ getLoaderManager().destroyLoader(LOADER_SNAPSHOTS);
+ if (mAdapter != null) {
+ mAdapter.changeCursor(null);
+ mAdapter = null;
+ }
+ }
+
+ void setupGrid(LayoutInflater inflater) {
+ View item = inflater.inflate(R.layout.snapshot_item, mGrid, false);
+ int mspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ item.measure(mspec, mspec);
+ int width = item.getMeasuredWidth();
+ mGrid.setColumnWidth(width);
+ mGrid.setOnItemClickListener(this);
+ mGrid.setOnCreateContextMenuListener(this);
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ if (id == LOADER_SNAPSHOTS) {
+ return new CursorLoader(getActivity(),
+ Snapshots.CONTENT_URI, PROJECTION,
+ null, null, Snapshots.DATE_CREATED + " DESC");
+ }
+ return null;
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ if (loader.getId() == LOADER_SNAPSHOTS) {
+ if (mAdapter == null) {
+ mAdapter = new SnapshotAdapter(getActivity(), data);
+ mGrid.setAdapter(mAdapter);
+ } else {
+ mAdapter.changeCursor(data);
+ }
+ if (mAnimateId > 0) {
+ mAdapter.animateIn(mAnimateId);
+ mAnimateId = 0;
+ getArguments().remove(EXTRA_ANIMATE_ID);
+ }
+ boolean empty = mAdapter.isEmpty();
+ mGrid.setVisibility(empty ? View.GONE : View.VISIBLE);
+ mEmpty.setVisibility(empty ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v,
+ ContextMenuInfo menuInfo) {
+ MenuInflater inflater = getActivity().getMenuInflater();
+ inflater.inflate(R.menu.snapshots_context, menu);
+ // Create the header, re-use BookmarkItem (has the layout we want)
+ BookmarkItem header = new BookmarkItem(getActivity());
+ header.setEnableScrolling(true);
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
+ populateBookmarkItem(mAdapter.getItem(info.position), header);
+ menu.setHeaderView(header);
+ }
+
+ private void populateBookmarkItem(Cursor cursor, BookmarkItem item) {
+ item.setName(cursor.getString(SNAPSHOT_TITLE));
+ item.setUrl(cursor.getString(SNAPSHOT_URL));
+ item.setFavicon(getBitmap(cursor, SNAPSHOT_FAVICON));
+ }
+
+ static Bitmap getBitmap(Cursor cursor, int columnIndex) {
+ byte[] data = cursor.getBlob(columnIndex);
+ if (data == null) {
+ return null;
+ }
+ return BitmapFactory.decodeByteArray(data, 0, data.length);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ if (!(item.getMenuInfo() instanceof AdapterContextMenuInfo)) {
+ return false;
+ }
+ if (item.getItemId() == R.id.delete_context_menu_id) {
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
+ deleteSnapshot(info.id);
+ return true;
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ void deleteSnapshot(long id) {
+ final Uri uri = ContentUris.withAppendedId(Snapshots.CONTENT_URI, id);
+ final ContentResolver cr = getActivity().getContentResolver();
+ new Thread() {
+ @Override
+ public void run() {
+ cr.delete(uri, null, null);
+ }
+ }.start();
+
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position,
+ long id) {
+ mCallback.openSnapshot(id);
+ }
+
+ private static class SnapshotAdapter extends ResourceCursorAdapter {
+ private long mAnimateId;
+ private AnimatorSet mAnimation;
+ private View mAnimationTarget;
+
+ public SnapshotAdapter(Context context, Cursor c) {
+ super(context, R.layout.snapshot_item, c, 0);
+ mAnimation = new AnimatorSet();
+ mAnimation.playTogether(
+ ObjectAnimator.ofFloat(null, View.SCALE_X, 0f, 1f),
+ ObjectAnimator.ofFloat(null, View.SCALE_Y, 0f, 1f));
+ mAnimation.setStartDelay(100);
+ mAnimation.setDuration(400);
+ mAnimation.addListener(new AnimatorListener() {
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAnimateId = 0;
+ mAnimationTarget = null;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+ });
+ }
+
+ public void animateIn(long id) {
+ mAnimateId = id;
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ long id = cursor.getLong(SNAPSHOT_ID);
+ if (id == mAnimateId) {
+ if (mAnimationTarget != view) {
+ float scale = 0f;
+ if (mAnimationTarget != null) {
+ scale = mAnimationTarget.getScaleX();
+ mAnimationTarget.setScaleX(1f);
+ mAnimationTarget.setScaleY(1f);
+ }
+ view.setScaleX(scale);
+ view.setScaleY(scale);
+ }
+ mAnimation.setTarget(view);
+ mAnimationTarget = view;
+ if (!mAnimation.isRunning()) {
+ mAnimation.start();
+ }
+
+ }
+ ImageView thumbnail = (ImageView) view.findViewById(R.id.thumb);
+ byte[] thumbBlob = cursor.getBlob(SNAPSHOT_THUMBNAIL);
+ if (thumbBlob == null) {
+ thumbnail.setImageResource(R.drawable.browser_thumbnail);
+ } else {
+ Bitmap thumbBitmap = BitmapFactory.decodeByteArray(
+ thumbBlob, 0, thumbBlob.length);
+ thumbnail.setImageBitmap(thumbBitmap);
+ }
+ TextView title = (TextView) view.findViewById(R.id.title);
+ title.setText(cursor.getString(SNAPSHOT_TITLE));
+ TextView size = (TextView) view.findViewById(R.id.size);
+ if (size != null) {
+ int stateLen = cursor.getInt(SNAPSHOT_VIEWSTATE_SIZE);
+ size.setText(String.format("%.2fMB", stateLen / 1024f / 1024f));
+ }
+ long timestamp = cursor.getLong(SNAPSHOT_DATE_CREATED);
+ TextView date = (TextView) view.findViewById(R.id.date);
+ DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.SHORT);
+ date.setText(dateFormat.format(new Date(timestamp)));
+ }
+
+ @Override
+ public Cursor getItem(int position) {
+ return (Cursor) super.getItem(position);
+ }
+ }
+
+}
diff --git a/src/com/android/browser/BrowserUtils.java b/src/com/android/browser/BrowserUtils.java
new file mode 100644
index 0000000..be16ab1
--- /dev/null
+++ b/src/com/android/browser/BrowserUtils.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of The Linux Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.android.browser;
+
+import com.android.browser.R;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.text.InputFilter;
+import android.text.Spanned;
+import android.util.Log;
+import android.widget.EditText;
+
+public class BrowserUtils {
+
+ private static final String LOGTAG = "BrowserUtils";
+ public static final int FILENAME_MAX_LENGTH = 32;
+ public static final int ADDRESS_MAX_LENGTH = 2048;
+ private static AlertDialog.Builder mAlertDialog = null;
+
+ public static void maxLengthFilter(final Context context, final EditText editText,
+ final int max_length) {
+ InputFilter[] contentFilters = new InputFilter[1];
+ contentFilters[0] = new InputFilter.LengthFilter(max_length) {
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend) {
+ int keep = max_length - (dest.length() - (dend - dstart));
+ if (keep <= 0) {
+ showWarningDialog(context, max_length);
+ return "";
+ } else if (keep >= end - start) {
+ return null;
+ } else {
+ if (keep < source.length()) {
+ showWarningDialog(context, max_length);
+ }
+ return source.subSequence(start, start + keep);
+ }
+ }
+ };
+ editText.setFilters(contentFilters);
+ }
+
+ private static void showWarningDialog(final Context context, int max_length) {
+ if (mAlertDialog != null)
+ return;
+
+ mAlertDialog = new AlertDialog.Builder(context);
+ mAlertDialog.setTitle(R.string.browser_max_input_title)
+ .setIcon(android.R.drawable.ic_dialog_info)
+ .setMessage(context.getString(R.string.browser_max_input, max_length))
+ .setPositiveButton(R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ return;
+ }
+ })
+ .show()
+ .setOnDismissListener(new DialogInterface.OnDismissListener() {
+ public void onDismiss(DialogInterface dialog) {
+ Log.w("BrowserUtils", "onDismiss");
+ mAlertDialog = null;
+ return;
+ }
+ });
+ }
+}
diff --git a/src/com/android/browser/BrowserWebView.java b/src/com/android/browser/BrowserWebView.java
new file mode 100644
index 0000000..5d71ce3
--- /dev/null
+++ b/src/com/android/browser/BrowserWebView.java
@@ -0,0 +1,164 @@
+/*
+ * 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.util.AttributeSet;
+import android.view.View;
+import org.codeaurora.swe.WebChromeClient;
+import org.codeaurora.swe.WebView;
+import org.codeaurora.swe.WebViewClient;
+
+import java.util.Map;
+
+/**
+ * Manage WebView scroll events
+ */
+public class BrowserWebView extends WebView implements WebView.TitleBarDelegate {
+
+ public interface OnScrollChangedListener {
+ void onScrollChanged(int l, int t, int oldl, int oldt);
+ }
+
+ private boolean mBackgroundRemoved = false;
+ private TitleBar mTitleBar;
+ private OnScrollChangedListener mOnScrollChangedListener;
+ private WebChromeClient mWebChromeClient;
+ private WebViewClient mWebViewClient;
+
+ /**
+ * @param context
+ * @param attrs
+ * @param defStyle
+ * @param javascriptInterfaces
+ */
+ public BrowserWebView(Context context, AttributeSet attrs, int defStyle,
+ Map<String, Object> javascriptInterfaces, boolean privateBrowsing) {
+ super(context, attrs, defStyle, privateBrowsing);
+ this.setJavascriptInterfaces(javascriptInterfaces);
+ }
+
+ /**
+ * @param context
+ * @param attrs
+ * @param defStyle
+ */
+ public BrowserWebView(
+ Context context, AttributeSet attrs, int defStyle, boolean privateBrowsing) {
+ super(context, attrs, defStyle, privateBrowsing);
+ }
+
+ /**
+ * @param context
+ * @param attrs
+ */
+ public BrowserWebView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /**
+ * @param context
+ */
+ public BrowserWebView(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void setWebChromeClient(WebChromeClient client) {
+ mWebChromeClient = client;
+ super.setWebChromeClient(client);
+ }
+
+ public WebChromeClient getWebChromeClient() {
+ return mWebChromeClient;
+ }
+
+ @Override
+ public void setWebViewClient(WebViewClient client) {
+ mWebViewClient = client;
+ super.setWebViewClient(client);
+ }
+
+ public WebViewClient getWebViewClient() {
+ return mWebViewClient;
+ }
+
+ public void setTitleBar(TitleBar title) {
+ mTitleBar = title;
+ }
+
+ // From TitleBarDelegate
+ @Override
+ public int getTitleHeight() {
+ return (mTitleBar != null) ? mTitleBar.getEmbeddedHeight() : 0;
+ }
+
+ // From TitleBarDelegate
+ @Override
+ public void onSetEmbeddedTitleBar(final View title) {
+ // TODO: Remove this method; it is never invoked.
+ }
+
+ public boolean hasTitleBar() {
+ return (mTitleBar != null);
+ }
+
+ @Override
+ public void onDraw(Canvas c) {
+ super.onDraw(c);
+ if (!mBackgroundRemoved && getRootView().getBackground() != null) {
+ mBackgroundRemoved = true;
+ post(new Runnable() {
+ public void run() {
+ getRootView().setBackgroundDrawable(null);
+ }
+ });
+ }
+ }
+
+ public void drawContent(Canvas c) {
+ //super.drawContent(c);
+ }
+
+ @Override
+ public void onScrollChanged(int l, int t, int oldl, int oldt) {
+ super.onScrollChanged(l, t, oldl, oldt);
+ if (mTitleBar != null) {
+ mTitleBar.onScrollChanged();
+ }
+ if (mOnScrollChangedListener != null) {
+ mOnScrollChangedListener.onScrollChanged(l, t, oldl, oldt);
+ }
+ }
+
+ public void setOnScrollChangedListener(OnScrollChangedListener listener) {
+ mOnScrollChangedListener = listener;
+ }
+
+ @Override
+ public boolean showContextMenuForChild(View originalView) {
+ return false;
+ }
+
+ @Override
+ public void destroy() {
+ BrowserSettings.getInstance().stopManagingSettings(getSettings());
+ super.destroy();
+ }
+
+}
diff --git a/src/com/android/browser/BrowserWebViewFactory.java b/src/com/android/browser/BrowserWebViewFactory.java
new file mode 100644
index 0000000..4364b26
--- /dev/null
+++ b/src/com/android/browser/BrowserWebViewFactory.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2011 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.pm.PackageManager;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.browser.reflect.ReflectHelper;
+
+import org.codeaurora.swe.WebView;
+
+/**
+ * Web view factory class for creating {@link BrowserWebView}'s.
+ */
+public class BrowserWebViewFactory implements WebViewFactory {
+
+ private final Context mContext;
+
+ public BrowserWebViewFactory(Context context) {
+ mContext = context;
+ }
+
+ protected WebView instantiateWebView(AttributeSet attrs, int defStyle,
+ boolean privateBrowsing) {
+ return new BrowserWebView(mContext, attrs, defStyle, privateBrowsing);
+ }
+
+ @Override
+ public WebView createSubWebView(boolean privateBrowsing) {
+ return createWebView(privateBrowsing);
+ }
+
+ @Override
+ public WebView createWebView(boolean privateBrowsing) {
+ WebView w = instantiateWebView(null, android.R.attr.webViewStyle, privateBrowsing);
+ initWebViewSettings(w);
+ return w;
+ }
+
+ protected void initWebViewSettings(WebView w) {
+ w.setScrollbarFadingEnabled(true);
+ w.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
+ w.setMapTrackballToArrowKeys(false); // use trackball directly
+ // Enable the built-in zoom
+ w.getSettings().setBuiltInZoomControls(true);
+ final PackageManager pm = mContext.getPackageManager();
+ boolean supportsMultiTouch =
+ pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH)
+ || pm.hasSystemFeature(PackageManager.FEATURE_FAKETOUCH_MULTITOUCH_DISTINCT);
+ w.getSettings().setDisplayZoomControls(!supportsMultiTouch);
+
+ // add for carrier homepage feature
+ Object[] params = {new String("persist.env.c.browser.resource"),
+ new String("default")};
+ Class[] type = new Class[] {String.class, String.class};
+ String browserRes = (String)ReflectHelper.invokeStaticMethod(
+ "android.os.SystemProperties","get", type, params);
+ if ("ct".equals(browserRes)) {
+ w.getSettings().setJavaScriptEnabled(true);
+ if (mContext instanceof BrowserActivity) {
+ w.addJavascriptInterface(mContext, "default_homepage");
+ }
+ }
+
+ // Add this WebView to the settings observer list and update the
+ // settings
+ final BrowserSettings s = BrowserSettings.getInstance();
+ s.startManagingSettings(w.getSettings());
+ }
+
+}
diff --git a/src/com/android/browser/BrowserYesNoPreference.java b/src/com/android/browser/BrowserYesNoPreference.java
new file mode 100644
index 0000000..f2344b4
--- /dev/null
+++ b/src/com/android/browser/BrowserYesNoPreference.java
@@ -0,0 +1,60 @@
+/*
+ * 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.preference.DialogPreference;
+import android.util.AttributeSet;
+import android.util.Log;
+
+class BrowserYesNoPreference extends DialogPreference {
+
+ // This is the constructor called by the inflater
+ public BrowserYesNoPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ super.onDialogClosed(positiveResult);
+
+ if (positiveResult) {
+ setEnabled(false);
+
+ BrowserSettings settings = BrowserSettings.getInstance();
+ if (PreferenceKeys.PREF_PRIVACY_CLEAR_CACHE.equals(getKey())) {
+ settings.clearCache();
+ settings.clearDatabases();
+ } else if (PreferenceKeys.PREF_PRIVACY_CLEAR_COOKIES.equals(getKey())) {
+ settings.clearCookies();
+ } else if (PreferenceKeys.PREF_PRIVACY_CLEAR_HISTORY.equals(getKey())) {
+ settings.clearHistory();
+ } else if (PreferenceKeys.PREF_PRIVACY_CLEAR_FORM_DATA.equals(getKey())) {
+ settings.clearFormData();
+ } else if (PreferenceKeys.PREF_PRIVACY_CLEAR_PASSWORDS.equals(getKey())) {
+ settings.clearPasswords();
+ } else if (PreferenceKeys.PREF_RESET_DEFAULT_PREFERENCES.equals(
+ getKey())) {
+ settings.resetDefaultPreferences();
+ setEnabled(true);
+ } else if (PreferenceKeys.PREF_PRIVACY_CLEAR_GEOLOCATION_ACCESS.equals(
+ getKey())) {
+ settings.clearLocationAccess();
+ }
+ }
+ }
+}
diff --git a/src/com/android/browser/CombinedBookmarksCallbacks.java b/src/com/android/browser/CombinedBookmarksCallbacks.java
new file mode 100644
index 0000000..cdffb6b
--- /dev/null
+++ b/src/com/android/browser/CombinedBookmarksCallbacks.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2011 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;
+
+public interface CombinedBookmarksCallbacks {
+ void openUrl(String url);
+ void openInNewTab(String... urls);
+ void openSnapshot(long id);
+ void close();
+}
\ No newline at end of file
diff --git a/src/com/android/browser/ComboViewActivity.java b/src/com/android/browser/ComboViewActivity.java
new file mode 100644
index 0000000..4026bdd
--- /dev/null
+++ b/src/com/android/browser/ComboViewActivity.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2011 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.app.Fragment;
+import android.app.FragmentTransaction;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v13.app.FragmentPagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.android.browser.R;
+import com.android.browser.UI.ComboViews;
+
+import java.util.ArrayList;
+
+public class ComboViewActivity extends Activity implements CombinedBookmarksCallbacks {
+
+ private static final String STATE_SELECTED_TAB = "tab";
+ public static final String EXTRA_COMBO_ARGS = "combo_args";
+ public static final String EXTRA_INITIAL_VIEW = "initial_view";
+
+ public static final String EXTRA_OPEN_SNAPSHOT = "snapshot_id";
+ public static final String EXTRA_OPEN_ALL = "open_all";
+ public static final String EXTRA_CURRENT_URL = "url";
+ private ViewPager mViewPager;
+ private TabsAdapter mTabsAdapter;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setResult(RESULT_CANCELED);
+ Bundle extras = getIntent().getExtras();
+ Bundle args = extras.getBundle(EXTRA_COMBO_ARGS);
+ String svStr = extras.getString(EXTRA_INITIAL_VIEW, null);
+ ComboViews startingView = svStr != null
+ ? ComboViews.valueOf(svStr)
+ : ComboViews.Bookmarks;
+ mViewPager = new ViewPager(this);
+ mViewPager.setId(R.id.tab_view);
+ setContentView(mViewPager);
+
+ final ActionBar bar = getActionBar();
+ bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+ if (BrowserActivity.isTablet(this)) {
+ bar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME
+ | ActionBar.DISPLAY_USE_LOGO);
+ bar.setHomeButtonEnabled(true);
+ } else {
+ bar.setDisplayOptions(0);
+ }
+
+ mTabsAdapter = new TabsAdapter(this, mViewPager);
+ mTabsAdapter.addTab(bar.newTab().setText(R.string.tab_bookmarks),
+ BrowserBookmarksPage.class, args);
+ mTabsAdapter.addTab(bar.newTab().setText(R.string.tab_history),
+ BrowserHistoryPage.class, args);
+ mTabsAdapter.addTab(bar.newTab().setText(R.string.tab_snapshots),
+ BrowserSnapshotPage.class, args);
+
+ if (savedInstanceState != null) {
+ bar.setSelectedNavigationItem(
+ savedInstanceState.getInt(STATE_SELECTED_TAB, 0));
+ } else {
+ switch (startingView) {
+ case Bookmarks:
+ mViewPager.setCurrentItem(0);
+ break;
+ case History:
+ mViewPager.setCurrentItem(1);
+ break;
+ case Snapshots:
+ mViewPager.setCurrentItem(2);
+ break;
+ }
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(STATE_SELECTED_TAB,
+ getActionBar().getSelectedNavigationIndex());
+ }
+
+ @Override
+ public void openUrl(String url) {
+ Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ setResult(RESULT_OK, i);
+ finish();
+ }
+
+ @Override
+ public void openInNewTab(String... urls) {
+ Intent i = new Intent();
+ i.putExtra(EXTRA_OPEN_ALL, urls);
+ setResult(RESULT_OK, i);
+ finish();
+ }
+
+ @Override
+ public void close() {
+ finish();
+ }
+
+ @Override
+ public void openSnapshot(long id) {
+ Intent i = new Intent();
+ i.putExtra(EXTRA_OPEN_SNAPSHOT, id);
+ setResult(RESULT_OK, i);
+ finish();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.combined, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ return true;
+ } else if (item.getItemId() == R.id.preferences_menu_id) {
+ String url = getIntent().getStringExtra(EXTRA_CURRENT_URL);
+ Intent intent = new Intent(this, BrowserPreferencesPage.class);
+ intent.putExtra(BrowserPreferencesPage.CURRENT_PAGE, url);
+ startActivityForResult(intent, Controller.PREFERENCES_PAGE);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * This is a helper class that implements the management of tabs and all
+ * details of connecting a ViewPager with associated TabHost. It relies on a
+ * trick. Normally a tab host has a simple API for supplying a View or
+ * Intent that each tab will show. This is not sufficient for switching
+ * between pages. So instead we make the content part of the tab host
+ * 0dp high (it is not shown) and the TabsAdapter supplies its own dummy
+ * view to show as the tab content. It listens to changes in tabs, and takes
+ * care of switch to the correct page in the ViewPager whenever the selected
+ * tab changes.
+ */
+ public static class TabsAdapter extends FragmentPagerAdapter
+ implements ActionBar.TabListener, ViewPager.OnPageChangeListener {
+ private final Context mContext;
+ private final ActionBar mActionBar;
+ private final ViewPager mViewPager;
+ private final ArrayList<TabInfo> mTabs = new ArrayList<TabInfo>();
+
+ static final class TabInfo {
+ private final Class<?> clss;
+ private final Bundle args;
+
+ TabInfo(Class<?> _class, Bundle _args) {
+ clss = _class;
+ args = _args;
+ }
+ }
+
+ public TabsAdapter(Activity activity, ViewPager pager) {
+ super(activity.getFragmentManager());
+ mContext = activity;
+ mActionBar = activity.getActionBar();
+ mViewPager = pager;
+ mViewPager.setAdapter(this);
+ mViewPager.setOnPageChangeListener(this);
+ }
+
+ public void addTab(ActionBar.Tab tab, Class<?> clss, Bundle args) {
+ TabInfo info = new TabInfo(clss, args);
+ tab.setTag(info);
+ tab.setTabListener(this);
+ mTabs.add(info);
+ mActionBar.addTab(tab);
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ return mTabs.size();
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ TabInfo info = mTabs.get(position);
+ return Fragment.instantiate(mContext, info.clss.getName(), info.args);
+ }
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ mActionBar.setSelectedNavigationItem(position);
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ }
+
+ @Override
+ public void onTabSelected(android.app.ActionBar.Tab tab,
+ FragmentTransaction ft) {
+ Object tag = tab.getTag();
+ for (int i=0; i<mTabs.size(); i++) {
+ if (mTabs.get(i) == tag) {
+ mViewPager.setCurrentItem(i);
+ }
+ }
+ }
+
+ @Override
+ public void onTabUnselected(android.app.ActionBar.Tab tab,
+ FragmentTransaction ft) {
+ }
+
+ @Override
+ public void onTabReselected(android.app.ActionBar.Tab tab,
+ FragmentTransaction ft) {
+ }
+ }
+
+ private static String makeFragmentName(int viewId, int index) {
+ return "android:switcher:" + viewId + ":" + index;
+ }
+
+}
diff --git a/src/com/android/browser/Controller.java b/src/com/android/browser/Controller.java
new file mode 100644
index 0000000..27b3620
--- /dev/null
+++ b/src/com/android/browser/Controller.java
@@ -0,0 +1,3368 @@
+/*
+ * 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.Dialog;
+import android.app.DownloadManager;
+import android.app.ProgressDialog;
+import android.content.ClipboardManager;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.net.http.SslError;
+import android.net.wifi.WifiManager;
+import android.net.wifi.ScanResult;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Environment;
+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.ContactsContract;
+import android.provider.ContactsContract.Intents.Insert;
+import android.provider.Settings;
+import android.speech.RecognizerIntent;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Patterns;
+import android.view.ActionMode;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.MotionEvent;
+import android.view.View;
+import android.webkit.MimeTypeMap;
+import android.webkit.ValueCallback;
+import android.webkit.WebChromeClient.CustomViewCallback;
+import android.webkit.WebIconDatabase;
+import android.widget.Toast;
+
+import org.codeaurora.swe.CookieManager;
+import org.codeaurora.swe.CookieSyncManager;
+import org.codeaurora.swe.HttpAuthHandler;
+import org.codeaurora.swe.SslErrorHandler;
+import org.codeaurora.swe.WebSettings;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.R;
+import com.android.browser.IntentHandler.UrlData;
+import com.android.browser.UI.ComboViews;
+import com.android.browser.mynavigation.AddMyNavigationPage;
+import com.android.browser.mynavigation.MyNavigationUtil;
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.WebAddress;
+import com.android.browser.platformsupport.BrowserContract.Images;
+import com.android.browser.provider.BrowserProvider2.Thumbnails;
+import com.android.browser.provider.SnapshotProvider.Snapshots;
+import com.android.browser.reflect.ReflectHelper;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Controller for browser
+ */
+public class Controller
+ implements WebViewController, UiController, ActivityController {
+
+ private static final String LOGTAG = "Controller";
+ private static final String SEND_APP_ID_EXTRA =
+ "android.speech.extras.SEND_APPLICATION_ID_EXTRA";
+ private static final String INCOGNITO_URI = "browser:incognito";
+
+
+ private static final String PROP_NETSWITCH = "persist.env.browser.netswitch";
+ private static final String INTENT_WIFI_SELECTION_DATA_CONNECTION =
+ "android.net.wifi.cmcc.WIFI_SELECTION_DATA_CONNECTION";
+ private static final String OFFLINE_PAGE =
+ "content://com.android.browser.mynavigation/websites";
+ private static final String INTENT_PICK_NETWORK =
+ "android.net.wifi.cmcc.PICK_WIFI_NETWORK_AND_GPRS";
+
+ public final static String EXTRA_SHARE_SCREENSHOT = "share_screenshot";
+ public final static String EXTRA_SHARE_FAVICON = "share_favicon";
+ // 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 OPEN_MENU = 202;
+
+ private static final int EMPTY_MENU = -1;
+
+ // activity requestCode
+ final static int COMBO_VIEW = 1;
+ final static int PREFERENCES_PAGE = 3;
+ final static int FILE_SELECTED = 4;
+ final static int AUTOFILL_SETUP = 5;
+ final static int VOICE_RESULT = 6;
+ final static int MY_NAVIGATION = 7;
+
+ 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";
+
+ // "no-crash-recovery" parameter in intent to suppress crash recovery
+ final static String NO_CRASH_RECOVERY = "no-crash-recovery";
+
+ // A bitmap that is re-used in createScreenshot as scratch space
+ private static Bitmap sThumbnailBitmap;
+
+ 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 boolean mNetworkShouldNotify = true;
+
+ 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;
+
+ 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 mActivityPaused = true;
+ private boolean mLoadStopped;
+
+ private Handler mHandler;
+ // Checks to see when the bookmarks database has changed, and updates the
+ // Tabs' notion of whether they represent bookmarked sites.
+ private ContentObserver mBookmarksObserver;
+ private CrashRecoveryHandler mCrashRecoveryHandler;
+
+ private boolean mBlockEvents;
+
+ private String mVoiceResult;
+ private boolean mUpdateMyNavThumbnail;
+ private String mUpdateMyNavThumbnailUrl;
+
+ public Controller(Activity browser) {
+ mActivity = browser;
+ mSettings = BrowserSettings.getInstance();
+ mTabControl = new TabControl(this);
+ mSettings.setController(this);
+ mCrashRecoveryHandler = CrashRecoveryHandler.initialize(this);
+ mCrashRecoveryHandler.preloadCrashState();
+ mFactory = new BrowserWebViewFactory(browser);
+
+ mUrlHandler = new UrlHandler(this);
+ mIntentHandler = new IntentHandler(mActivity, this);
+ mPageDialogsHandler = new PageDialogsHandler(mActivity, this);
+
+ // Creating dummy Webview for browser to force loading of library;
+ // in order for CookieManager calls to be invoked properly and
+ // awBrowserContext to be initialized
+ (mFactory.createWebView(false)).destroy();
+
+ startHandler();
+ mBookmarksObserver = new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ int size = mTabControl.getTabCount();
+ for (int i = 0; i < size; i++) {
+ mTabControl.getTab(i).updateBookmarkedStatus();
+ }
+ }
+
+ };
+ browser.getContentResolver().registerContentObserver(
+ BrowserContract.Bookmarks.CONTENT_URI, true, mBookmarksObserver);
+
+ mNetworkHandler = new NetworkStateHandler(mActivity, this);
+ // Start watching the default geolocation permissions
+ mSystemAllowGeolocationOrigins =
+ new SystemAllowGeolocationOrigins(mActivity.getApplicationContext());
+ mSystemAllowGeolocationOrigins.start();
+
+ openIconDatabase();
+ }
+
+ @Override
+ public void start(final Intent intent) {
+ WebView.setShouldMonitorWebCoreThread();
+ // mCrashRecoverHandler has any previously saved state.
+ mCrashRecoveryHandler.startRecovery(intent);
+ }
+
+ void doStart(final Bundle icicle, final 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);
+
+ // we dont want to ever recover incognito tabs
+ final boolean restoreIncognitoTabs = false;
+
+ // Find out if we will restore any state and remember the tab.
+ final long currentTabId =
+ mTabControl.canRestoreState(icicle, restoreIncognitoTabs);
+
+ mSettings.initializeCookieSettings();
+ if (currentTabId == -1) {
+ // Not able to restore so we go ahead and clear session cookies. We
+ // must do this before trying to login the user as we don't want to
+ // clear any session cookies set during login.
+ CookieManager.getInstance().removeSessionCookie();
+ }
+
+ GoogleAccountLogin.startLoginIfNeeded(mActivity,
+ new Runnable() {
+ @Override public void run() {
+ onPreloginFinished(icicle, intent, currentTabId,
+ restoreIncognitoTabs);
+ }
+ });
+ }
+
+ private void onPreloginFinished(Bundle icicle, Intent intent, long currentTabId,
+ boolean restoreIncognitoTabs) {
+ if (currentTabId == -1) {
+ BackgroundHandler.execute(new PruneThumbnails(mActivity, null));
+ if (intent == null) {
+ // This won't happen under common scenarios. The icicle is
+ // not null, but there aren't any tabs to restore.
+ openTabToHomePage();
+ } else {
+ 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 = null;
+ if (intent.getData() != null
+ && Intent.ACTION_VIEW.equals(intent.getAction())
+ && intent.getData().toString().startsWith("content://")) {
+ urlData = new UrlData(intent.getData().toString());
+ } else {
+ urlData = IntentHandler.getUrlDataFromIntent(intent);
+ }
+ Tab t = null;
+ if (urlData.isEmpty()) {
+ Object[] params = { new String("persist.env.c.browser.resource"),
+ new String("default")};
+ Class[] type = new Class[] {String.class, String.class};
+ String browserRes = (String)ReflectHelper.invokeStaticMethod(
+ "android.os.SystemProperties", "get",
+ type, params);
+ if (browserRes.equals(
+ "cmcc")) {
+ t = openTab(OFFLINE_PAGE, false, true, true);
+ } else {
+ t = openTabToHomePage();
+ }
+ } else {
+ t = openTab(urlData);
+ }
+ if (t != null) {
+ t.setAppId(intent.getStringExtra(Browser.EXTRA_APPLICATION_ID));
+ }
+ WebView webView = t.getWebView();
+ if (extra != null) {
+ int scale = extra.getInt(Browser.INITIAL_ZOOM_LEVEL, 0);
+ if (scale > 0 && scale <= 1000) {
+ webView.setInitialScale(scale);
+ }
+ }
+ }
+ mUi.updateTabs(mTabControl.getTabs());
+ } else {
+ mTabControl.restoreState(icicle, currentTabId, restoreIncognitoTabs,
+ mUi.needsRestoreAllTabs());
+ List<Tab> tabs = mTabControl.getTabs();
+ ArrayList<Long> restoredTabs = new ArrayList<Long>(tabs.size());
+ for (Tab t : tabs) {
+ restoredTabs.add(t.getId());
+ }
+ BackgroundHandler.execute(new PruneThumbnails(mActivity, restoredTabs));
+ if (tabs.size() == 0) {
+ openTabToHomePage();
+ }
+ mUi.updateTabs(tabs);
+ // TabControl.restoreState() will create a new tab even if
+ // restoring the state fails.
+ setActiveTab(mTabControl.getCurrentTab());
+ // Intent is non-null when framework thinks the browser should be
+ // launching with a new intent (icicle is null).
+ if (intent != null) {
+ mIntentHandler.onNewIntent(intent);
+ }
+ }
+ // Read JavaScript flags if it exists.
+ String jsFlags = getSettings().getJsEngineFlags();
+ if (jsFlags.trim().length() != 0) {
+ getCurrentWebView().setJsFlags(jsFlags);
+ }
+ if (intent != null
+ && BrowserActivity.ACTION_SHOW_BOOKMARKS.equals(intent.getAction())) {
+ bookmarksOrHistoryPicker(ComboViews.Bookmarks);
+ }
+ }
+
+ private static class PruneThumbnails implements Runnable {
+ private Context mContext;
+ private List<Long> mIds;
+
+ PruneThumbnails(Context context, List<Long> preserveIds) {
+ mContext = context.getApplicationContext();
+ mIds = preserveIds;
+ }
+
+ @Override
+ public void run() {
+ ContentResolver cr = mContext.getContentResolver();
+ if (mIds == null || mIds.size() == 0) {
+ cr.delete(Thumbnails.CONTENT_URI, null, null);
+ } else {
+ int length = mIds.size();
+ StringBuilder where = new StringBuilder();
+ where.append(Thumbnails._ID);
+ where.append(" not in (");
+ for (int i = 0; i < length; i++) {
+ where.append(mIds.get(i));
+ if (i < (length - 1)) {
+ where.append(",");
+ }
+ }
+ where.append(")");
+ cr.delete(Thumbnails.CONTENT_URI, where.toString(), null);
+ }
+ }
+
+ }
+
+ @Override
+ public WebViewFactory getWebViewFactory() {
+ return mFactory;
+ }
+
+ @Override
+ public void onSetWebView(Tab tab, WebView view) {
+ if (tab.hasCrashed)
+ tab.showCrashView();
+ else
+ mUi.onSetWebView(tab, view);
+ }
+
+ @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 Context getContext() {
+ return mActivity;
+ }
+
+ @Override
+ public Activity getActivity() {
+ return mActivity;
+ }
+
+ void setUi(UI ui) {
+ mUi = ui;
+ }
+
+ @Override
+ public 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.
+ private void openIconDatabase() {
+ // We have to call getInstance on the UI thread
+ final WebIconDatabase instance = WebIconDatabase.getInstance();
+ BackgroundHandler.execute(new Runnable() {
+
+ @Override
+ public void run() {
+ instance.open(mActivity.getDir("icons", 0).getPath());
+ }
+ });
+ }
+
+ private void startHandler() {
+ mHandler = new Handler() {
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case OPEN_BOOKMARKS:
+ bookmarksOrHistoryPicker(ComboViews.Bookmarks);
+ break;
+ case FOCUS_NODE_HREF:
+ {
+ String url = (String) msg.getData().get("url");
+ String title = (String) msg.getData().get("title");
+ String src = (String) msg.getData().get("src");
+ if (url == "") url = src; // use image if no anchor
+ 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:
+ loadUrlFromContext(url);
+ break;
+ case R.id.view_image_context_menu_id:
+ loadUrlFromContext(src);
+ break;
+ case R.id.open_newtab_context_menu_id:
+ final Tab parent = mTabControl.getCurrentTab();
+ openTab(url, parent,
+ !mSettings.openInBackground(), true);
+ 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, view.getSettings().getUserAgentString(),
+ null, null, null, view.isPrivateBrowsingEnabled(), 0);
+ break;
+ }
+ break;
+ }
+
+ case LOAD_URL:
+ loadUrlFromContext((String) msg.obj);
+ break;
+
+ case STOP_LOAD:
+ stopLoading();
+ break;
+
+ case RELEASE_WAKELOCK:
+ if (mWakeLock != null && 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:
+ Tab tab = (Tab) msg.obj;
+ if (tab != null) {
+ updateScreenshot(tab);
+ }
+ break;
+ case OPEN_MENU:
+ if (!mOptionsMenuOpen && mActivity != null ) {
+ mActivity.openOptionsMenu();
+ }
+ break;
+ }
+ }
+ };
+
+ }
+
+ @Override
+ public Tab getCurrentTab() {
+ return mTabControl.getCurrentTab();
+ }
+
+ @Override
+ public void shareCurrentPage() {
+ shareCurrentPage(mTabControl.getCurrentTab());
+ }
+
+ private void shareCurrentPage(Tab tab) {
+ if (tab != null) {
+ sharePage(mActivity, tab.getTitle(),
+ tab.getUrl(), tab.getFavicon(),
+ createScreenshot(tab.getWebView(),
+ getDesiredThumbnailWidth(mActivity),
+ getDesiredThumbnailHeight(mActivity)));
+ }
+ }
+
+ /**
+ * 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(EXTRA_SHARE_FAVICON, favicon);
+ send.putExtra(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
+
+ @Override
+ public void onConfgurationChanged(Configuration config) {
+ mConfigChanged = true;
+ // update the menu in case of a locale change
+ mActivity.invalidateOptionsMenu();
+ if (mOptionsMenuOpen) {
+ mActivity.closeOptionsMenu();
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(OPEN_MENU), 100);
+ }
+ if (mPageDialogsHandler != null) {
+ mPageDialogsHandler.onConfigurationChanged(config);
+ }
+ mUi.onConfigurationChanged(config);
+ }
+
+ @Override
+ public void handleNewIntent(Intent intent) {
+ if (!mUi.isWebShowing()) {
+ mUi.showWeb(false);
+ }
+ mIntentHandler.onNewIntent(intent);
+ }
+
+ @Override
+ public void onPause() {
+ if (mUi.isCustomViewShowing()) {
+ hideCustomView();
+ }
+ if (mActivityPaused) {
+ Log.e(LOGTAG, "BrowserActivity is already paused.");
+ return;
+ }
+ mActivityPaused = true;
+ Tab tab = mTabControl.getCurrentTab();
+ if (tab != null) {
+ tab.pause();
+ if (!pauseWebViewTimers(tab)) {
+ if (mWakeLock == null) {
+ PowerManager pm = (PowerManager) mActivity
+ .getSystemService(Context.POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Browser");
+ }
+ mWakeLock.acquire();
+ mHandler.sendMessageDelayed(mHandler
+ .obtainMessage(RELEASE_WAKELOCK), WAKELOCK_TIMEOUT);
+ }
+ }
+ mUi.onPause();
+ mNetworkHandler.onPause();
+
+ WebView.disablePlatformNotifications();
+ NfcHandler.unregister(mActivity);
+ if (sThumbnailBitmap != null) {
+ sThumbnailBitmap.recycle();
+ sThumbnailBitmap = null;
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ // Save all the tabs
+ Bundle saveState = createSaveState();
+
+ // crash recovery manages all save & restore state
+ mCrashRecoveryHandler.writeState(saveState);
+ mSettings.setLastRunPaused(true);
+ }
+
+ /**
+ * Save the current state to outState. Does not write the state to
+ * disk.
+ * @return Bundle containing the current state of all tabs.
+ */
+ /* package */ Bundle createSaveState() {
+ Bundle saveState = new Bundle();
+ mTabControl.saveState(saveState);
+ if (!saveState.isEmpty()) {
+ // Save time so that we know how old incognito tabs (if any) are.
+ saveState.putSerializable("lastActiveDate", Calendar.getInstance());
+ }
+ return saveState;
+ }
+
+ @Override
+ public void onResume() {
+ if (!mActivityPaused) {
+ Log.e(LOGTAG, "BrowserActivity is already resumed.");
+ return;
+ }
+ mSettings.setLastRunPaused(false);
+ mActivityPaused = false;
+ Tab current = mTabControl.getCurrentTab();
+ if (current != null) {
+ current.resume();
+ resumeWebViewTimers(current);
+ }
+ releaseWakeLock();
+
+ mUi.onResume();
+ mNetworkHandler.onResume();
+ WebView.enablePlatformNotifications();
+ NfcHandler.register(mActivity, this);
+ if (mVoiceResult != null) {
+ mUi.onVoiceResult(mVoiceResult);
+ mVoiceResult = null;
+ }
+ if (current != null && current.hasCrashed) {
+ current.showCrashView();
+ }
+ }
+
+ private void releaseWakeLock() {
+ if (mWakeLock != null && mWakeLock.isHeld()) {
+ mHandler.removeMessages(RELEASE_WAKELOCK);
+ mWakeLock.release();
+ }
+ }
+
+ /**
+ * resume all WebView timers using the WebView instance of the given tab
+ * @param tab guaranteed non-null
+ */
+ private void resumeWebViewTimers(Tab tab) {
+ boolean inLoad = tab.inPageLoad();
+ if ((!mActivityPaused && !inLoad) || (mActivityPaused && inLoad)) {
+ CookieSyncManager.getInstance().startSync();
+ WebView w = tab.getWebView();
+ WebViewTimersControl.getInstance().onBrowserActivityResume(w);
+ }
+ }
+
+ /**
+ * Pause all WebView timers using the WebView of the given tab
+ * @param tab
+ * @return true if the timers are paused or tab is null
+ */
+ private boolean pauseWebViewTimers(Tab tab) {
+ if (tab == null) {
+ return true;
+ } else if (!tab.inPageLoad()) {
+ CookieSyncManager.getInstance().stopSync();
+ WebViewTimersControl.getInstance().onBrowserActivityPause(getCurrentWebView());
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mUploadHandler != null && !mUploadHandler.handled()) {
+ 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);
+ }
+ mActivity.getContentResolver().unregisterContentObserver(mBookmarksObserver);
+ // Destroy all the tabs
+ mTabControl.destroy();
+ WebIconDatabase.getInstance().close();
+ // Stop watching the default geolocation permissions
+ mSystemAllowGeolocationOrigins.stop();
+ mSystemAllowGeolocationOrigins = null;
+ }
+
+ protected boolean isActivityPaused() {
+ return mActivityPaused;
+ }
+
+ @Override
+ public 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();
+ WebView w = getCurrentTopWebView();
+ if (w != null) {
+ w.stopLoading();
+ mUi.onPageStopped(tab);
+ }
+ }
+
+ boolean didUserStopLoading() {
+ return mLoadStopped;
+ }
+
+ private void handleNetworkNotify(WebView view) {
+ ConnectivityManager conMgr = (ConnectivityManager) this.getContext().getSystemService(
+ Context.CONNECTIVITY_SERVICE);
+ WifiManager wifiMgr = (WifiManager) this.getContext()
+ .getSystemService(Context.WIFI_SERVICE);
+ int networkSwitchTypeOK = this.getContext().getResources()
+ .getInteger(R.integer.netswitch_type_remind);
+
+ if (wifiMgr.isWifiEnabled()) {
+ NetworkInfo mNetworkInfo = conMgr.getActiveNetworkInfo();
+ if (mNetworkInfo == null
+ || (mNetworkInfo != null && (mNetworkInfo.getType() !=
+ ConnectivityManager.TYPE_WIFI))) {
+ List<ScanResult> list = wifiMgr.getScanResults();
+ if (list != null && list.size() == 0) {
+ int isReminder = Settings.System.getInt(
+ mActivity.getContentResolver(),
+ this.getContext().getResources()
+ .getString(R.string.network_switch_remind_type),
+ networkSwitchTypeOK);
+ if (isReminder == networkSwitchTypeOK) {
+ Intent intent = new Intent(
+ INTENT_WIFI_SELECTION_DATA_CONNECTION);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ this.getContext().startActivity(intent);
+ }
+ } else {
+ if ((Boolean)ReflectHelper.invokeStaticMethod(
+ "ActivityManagerNative", "isSystemReady", null, null)) {
+ try {
+ Intent intent = new Intent(INTENT_PICK_NETWORK);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ this.getContext().startActivity(intent);
+ } catch (Exception e) {
+ String err_msg = this.getContext().getString(
+ R.string.acivity_not_found, INTENT_PICK_NETWORK);
+ Toast.makeText(this.getContext(), err_msg, Toast.LENGTH_LONG).show();
+ }
+ } else {
+ Log.e(LOGTAG, "System is not ready!");
+ }
+ }
+ mNetworkShouldNotify = false;
+ }
+ } else {
+ if (!mNetworkHandler.isNetworkUp()) {
+ view.setNetworkAvailable(false);
+ Log.v(LOGTAG, "handleNetworkNotify() Wlan is not enabled.");
+ }
+ }
+ }
+ // WebViewController
+
+ @Override
+ public void onPageStarted(Tab tab, WebView view, 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,
+ tab);
+
+ // reset sync timer to avoid sync starts during loading a page
+ CookieSyncManager.getInstance().resetSync();
+ Object[] params = {new String(PROP_NETSWITCH),
+ new Boolean(false)};
+ Class[] type = new Class[] {String.class, boolean.class};
+ Boolean result = (Boolean) ReflectHelper.invokeStaticMethod(
+ "android.os.SystemProperties", "getBoolean",
+ type, params);
+ if (result) {
+ if (!mNetworkHandler.isNetworkUp()) {
+ Log.d(LOGTAG, "onPageStarted() network unavailable");
+ if (mNetworkShouldNotify) {
+ handleNetworkNotify(view);
+ } else {
+ view.setNetworkAvailable(false);
+ }
+ mNetworkShouldNotify = false;
+ } else {
+ Log.d(LOGTAG, "onPageStarted() network available");
+ if (mNetworkShouldNotify) {
+ handleNetworkNotify(view);
+ }
+ mNetworkShouldNotify = false;
+ }
+ } else {
+ 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(tab);
+ }
+ mLoadStopped = false;
+ endActionMode();
+
+ mUi.onTabDataChanged(tab);
+
+ String url = tab.getUrl();
+ // 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) {
+ mCrashRecoveryHandler.backupState();
+ mUi.onTabDataChanged(tab);
+
+ // Performance probe
+ if (false) {
+ Performance.onPageFinished(tab.getUrl());
+ }
+
+ Performance.tracePageFinished();
+ }
+
+ @Override
+ public void onProgressChanged(Tab tab) {
+ int newProgress = tab.getLoadProgress();
+
+ 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 (tab.inPageLoad()) {
+ updateInLoadMenuItems(mCachedMenu, tab);
+ } else if (mActivityPaused && pauseWebViewTimers(tab)) {
+ // pause the WebView timer and release the wake lock if it is
+ // finished while BrowserActivity is in pause state.
+ releaseWakeLock();
+ }
+ if (!tab.isPrivateBrowsingEnabled()
+ && !TextUtils.isEmpty(tab.getUrl())
+ && !tab.isSnapshot()) {
+ // Only update the bookmark screenshot if the user did not
+ // cancel the load early and there is not already
+ // a pending update for the tab.
+ if (tab.shouldUpdateThumbnail() &&
+ (tab.inForeground() && !didUserStopLoading()
+ || !tab.inForeground())) {
+ if (!mHandler.hasMessages(UPDATE_BOOKMARK_THUMBNAIL, tab)) {
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(
+ UPDATE_BOOKMARK_THUMBNAIL, 0, 0, tab),
+ 1500);
+ }
+ }
+ }
+ } else {
+ if (!tab.inPageLoad()) {
+ // onPageFinished may have already been called but a subframe is
+ // still loading
+ // updating the progress and
+ // update the menu items.
+ updateInLoadMenuItems(mCachedMenu, tab);
+ }
+ }
+ mUi.onProgressChanged(tab);
+ }
+
+ @Override
+ public void onUpdatedSecurityState(Tab tab) {
+ mUi.onTabDataChanged(tab);
+ }
+
+ @Override
+ public void onReceivedTitle(Tab tab, final String title) {
+ mUi.onTabDataChanged(tab);
+ final String pageUrl = tab.getOriginalUrl();
+ if (TextUtils.isEmpty(pageUrl) || 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()) {
+ DataController.getInstance(mActivity).updateHistoryTitle(pageUrl, title);
+ }
+ }
+
+ @Override
+ public void onFavicon(Tab tab, WebView view, Bitmap icon) {
+ mUi.onTabDataChanged(tab);
+ maybeUpdateFavicon(tab, view.getOriginalUrl(), view.getUrl(), icon);
+ }
+
+ @Override
+ public boolean shouldOverrideUrlLoading(Tab tab, WebView view, String url) {
+ return mUrlHandler.shouldOverrideUrlLoading(tab, 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);
+ }
+ int keyCode = event.getKeyCode();
+ // We need to send almost every key to WebKit. However:
+ // 1. We don't want to block the device on the renderer for
+ // some keys like menu, home, call.
+ // 2. There are no WebKit equivalents for some of these keys
+ // (see app/keyboard_codes_win.h)
+ // Note that these are not the same set as KeyEvent.isSystemKey:
+ // for instance, AKEYCODE_MEDIA_* will be dispatched to webkit.
+ if (keyCode == KeyEvent.KEYCODE_MENU ||
+ keyCode == KeyEvent.KEYCODE_HOME ||
+ keyCode == KeyEvent.KEYCODE_BACK ||
+ keyCode == KeyEvent.KEYCODE_CALL ||
+ keyCode == KeyEvent.KEYCODE_ENDCALL ||
+ keyCode == KeyEvent.KEYCODE_POWER ||
+ keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
+ keyCode == KeyEvent.KEYCODE_CAMERA ||
+ keyCode == KeyEvent.KEYCODE_FOCUS ||
+ keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
+ keyCode == KeyEvent.KEYCODE_VOLUME_MUTE ||
+ keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+ return true;
+ }
+
+ // We also have to intercept some shortcuts before we send them to the ContentView.
+ if (event.isCtrlPressed() && (
+ keyCode == KeyEvent.KEYCODE_TAB ||
+ keyCode == KeyEvent.KEYCODE_W ||
+ keyCode == KeyEvent.KEYCODE_F4)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onUnhandledKeyEvent(KeyEvent event) {
+ if (!isActivityPaused()) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ return mActivity.onKeyDown(event.getKeyCode(), event);
+ } else {
+ return mActivity.onKeyUp(event.getKeyCode(), event);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void doUpdateVisitedHistory(Tab tab, boolean isReload) {
+ // Don't save anything in private browsing mode
+ if (tab.isPrivateBrowsingEnabled()) return;
+ String url = tab.getOriginalUrl();
+
+ if (TextUtils.isEmpty(url)
+ || url.regionMatches(true, 0, "about:", 0, 6)) {
+ return;
+ }
+
+ DataController.getInstance(mActivity).updateVisitedHistory(url);
+ mCrashRecoveryHandler.backupState();
+ }
+
+ @Override
+ public void getVisitedHistory(final ValueCallback<String[]> callback) {
+ AsyncTask<Void, Void, String[]> task =
+ new AsyncTask<Void, Void, String[]>() {
+ @Override
+ public String[] doInBackground(Void... unused) {
+ Object[] params = {mActivity.getContentResolver()};
+ Class[] type = new Class[] {ContentResolver.class};
+ return (String[])ReflectHelper.invokeStaticMethod(
+ "android.provider.Browser","getVisitedHistory",
+ type, params);
+ }
+ @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() /*&& !handler.suppressDialog()*/) {
+ mPageDialogsHandler.showHttpAuthentication(tab, handler, host, realm);
+ } else {
+ handler.cancel();
+ }
+ }
+ }
+
+ @Override
+ public void onDownloadStart(Tab tab, String url, String userAgent,
+ String contentDisposition, String mimetype, String referer,
+ long contentLength) {
+ WebView w = tab.getWebView();
+ boolean ret = DownloadHandler.onDownloadStart(mActivity, url, userAgent,
+ contentDisposition, mimetype, referer, w.isPrivateBrowsingEnabled(), contentLength);
+ if (ret == false && w.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);
+ }
+
+ @Override
+ public void showAutoLogin(Tab tab) {
+ assert tab.inForeground();
+ // Update the title bar to show the auto-login request.
+ mUi.showAutoLogin(tab);
+ }
+
+ @Override
+ public void hideAutoLogin(Tab tab) {
+ assert tab.inForeground();
+ mUi.hideAutoLogin(tab);
+ }
+
+ // 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);
+ }
+ }
+
+ @Override
+ public void bookmarkedStatusHasChanged(Tab tab) {
+ // TODO: Switch to using onTabDataChanged after b/3262950 is fixed
+ mUi.bookmarkedStatusHasChanged(tab);
+ }
+
+ // end WebViewController
+
+ protected void pageUp() {
+ getCurrentTopWebView().pageUp(false);
+ }
+
+ protected void pageDown() {
+ getCurrentTopWebView().pageDown(false);
+ }
+
+ // callback from phone title bar
+ @Override
+ public void editUrl() {
+ if (mOptionsMenuOpen) mActivity.closeOptionsMenu();
+ mUi.editUrl(false, true);
+ }
+
+ @Override
+ public void showCustomView(Tab tab, View view, int requestedOrientation,
+ CustomViewCallback callback) {
+ if (tab.inForeground()) {
+ if (mUi.isCustomViewShowing()) {
+ callback.onCustomViewHidden();
+ return;
+ }
+ mUi.showCustomView(view, requestedOrientation, callback);
+ // Save the menu state and set it to empty while the custom
+ // view is showing.
+ mOldMenuState = mMenuState;
+ mMenuState = EMPTY_MENU;
+ mActivity.invalidateOptionsMenu();
+ }
+ }
+
+ @Override
+ public void hideCustomView() {
+ if (mUi.isCustomViewShowing()) {
+ mUi.onHideCustomView();
+ // Reset the old menu state.
+ mMenuState = mOldMenuState;
+ mOldMenuState = EMPTY_MENU;
+ mActivity.invalidateOptionsMenu();
+ }
+ }
+
+ @Override
+ public 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 (PreferenceKeys.PREF_PRIVACY_CLEAR_HISTORY.equals(action)) {
+ mTabControl.removeParentChildRelationShips();
+ }
+ }
+ break;
+ case FILE_SELECTED:
+ // Chose a file from the file picker.
+ if (null == mUploadHandler) break;
+ mUploadHandler.onResult(resultCode, intent);
+ 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;
+ case COMBO_VIEW:
+ if (intent == null || resultCode != Activity.RESULT_OK) {
+ break;
+ }
+ mUi.showWeb(false);
+ if (Intent.ACTION_VIEW.equals(intent.getAction())) {
+ Tab t = getCurrentTab();
+ Uri uri = intent.getData();
+ mUpdateMyNavThumbnail = true;
+ mUpdateMyNavThumbnailUrl = uri.toString();
+ loadUrl(t, uri.toString());
+ } else if (intent.hasExtra(ComboViewActivity.EXTRA_OPEN_ALL)) {
+ String[] urls = intent.getStringArrayExtra(
+ ComboViewActivity.EXTRA_OPEN_ALL);
+ Tab parent = getCurrentTab();
+ for (String url : urls) {
+ if (url != null) {
+ parent = openTab(url, parent,
+ !mSettings.openInBackground(), true);
+ }
+ }
+ } else if (intent.hasExtra(ComboViewActivity.EXTRA_OPEN_SNAPSHOT)) {
+ long id = intent.getLongExtra(
+ ComboViewActivity.EXTRA_OPEN_SNAPSHOT, -1);
+ if (id >= 0) {
+ createNewSnapshotTab(id, true);
+ }
+ }
+ break;
+ case VOICE_RESULT:
+ if (resultCode == Activity.RESULT_OK && intent != null) {
+ ArrayList<String> results = intent.getStringArrayListExtra(
+ RecognizerIntent.EXTRA_RESULTS);
+ if (results.size() >= 1) {
+ mVoiceResult = results.get(0);
+ }
+ }
+ break;
+ case MY_NAVIGATION:
+ if (intent == null || resultCode != Activity.RESULT_OK) {
+ break;
+ }
+
+ if (intent.getBooleanExtra("need_refresh", false) &&
+ getCurrentTopWebView() != null) {
+ getCurrentTopWebView().reload();
+ }
+ 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(ComboViews startView) {
+ if (mTabControl.getCurrentWebView() == null) {
+ return;
+ }
+ // clear action mode
+ if (isInCustomActionMode()) {
+ endActionMode();
+ }
+ 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(startView, extras);
+ }
+
+ // combo view callbacks
+
+ // 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();
+ }
+ }
+ }
+
+ protected boolean onMenuKey() {
+ return mUi.onMenuKey();
+ }
+
+ // menu handling and state
+ // TODO: maybe put into separate handler
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ if (mMenuState == EMPTY_MENU) {
+ return false;
+ }
+ MenuInflater inflater = mActivity.getMenuInflater();
+ inflater.inflate(R.menu.browser, menu);
+ return true;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v,
+ ContextMenuInfo menuInfo) {
+ if (v instanceof TitleBar) {
+ 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();
+ final String navigationUrl = MyNavigationUtil.getMyNavigationUrl(extra);
+ if (extra == null) return;
+ 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);
+ String itemUrl = null;
+ String url = webview.getOriginalUrl();
+ if (url != null && url.equalsIgnoreCase(MyNavigationUtil.MY_NAVIGATION)) {
+ itemUrl = Uri.decode(navigationUrl);
+ if (itemUrl != null && !MyNavigationUtil.isDefaultMyNavigation(itemUrl)) {
+ menu.setGroupVisible(R.id.MY_NAVIGATION_MENU,
+ type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
+ } else {
+ menu.setGroupVisible(R.id.MY_NAVIGATION_MENU, false);
+ }
+ menu.setGroupVisible(R.id.IMAGE_MENU, false);
+ menu.setGroupVisible(R.id.ANCHOR_MENU, false);
+ } else {
+ menu.setGroupVisible(R.id.MY_NAVIGATION_MENU, false);
+
+ 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:
+ menu.setHeaderTitle(extra);
+ // 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.setTitle(getSettings().openInBackground()
+ ? R.string.contextmenu_openlink_newwindow_background
+ : R.string.contextmenu_openlink_newwindow);
+ 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();
+ openTab(extra, parent,
+ !mSettings.openInBackground(),
+ true);
+ return true;
+ }
+ });
+ }
+ }
+ if (url != null && url.equalsIgnoreCase(MyNavigationUtil.MY_NAVIGATION)) {
+ menu.setHeaderTitle(navigationUrl);
+ menu.findItem(R.id.open_newtab_context_menu_id).setVisible(false);
+
+ if (itemUrl != null) {
+ if (!MyNavigationUtil.isDefaultMyNavigation(itemUrl)) {
+ menu.findItem(R.id.edit_my_navigation_context_menu_id)
+ .setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ final Intent intent = new Intent(Controller.this
+ .getContext(),
+ AddMyNavigationPage.class);
+ Bundle bundle = new Bundle();
+ String url = Uri.decode(navigationUrl);
+ bundle.putBoolean("isAdding", false);
+ bundle.putString("url", url);
+ bundle.putString("name", getNameFromUrl(url));
+ intent.putExtra("websites", bundle);
+ mActivity.startActivityForResult(intent, MY_NAVIGATION);
+ return false;
+ }
+ });
+ menu.findItem(R.id.delete_my_navigation_context_menu_id)
+ .setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ showMyNavigationDeleteDialog(Uri.decode(navigationUrl));
+ return false;
+ }
+ });
+ }
+ } else {
+ Log.e(LOGTAG, "mynavigation onCreateContextMenu itemUrl is null!");
+ }
+ }
+ if (type == WebView.HitTestResult.SRC_ANCHOR_TYPE) {
+ break;
+ }
+ // otherwise fall through to handle image part
+ case WebView.HitTestResult.IMAGE_TYPE:
+ MenuItem shareItem = menu.findItem(R.id.share_link_context_menu_id);
+ shareItem.setVisible(type == WebView.HitTestResult.IMAGE_TYPE);
+ if (type == WebView.HitTestResult.IMAGE_TYPE) {
+ menu.setHeaderTitle(extra);
+ shareItem.setOnMenuItemClickListener(
+ new MenuItem.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ sharePage(mActivity, null, extra, null,
+ null);
+ return true;
+ }
+ }
+ );
+ }
+ menu.findItem(R.id.view_image_context_menu_id)
+ .setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ openTab(extra, mTabControl.getCurrentTab(), true, true);
+ return false;
+ }
+ });
+ menu.findItem(R.id.download_context_menu_id).setOnMenuItemClickListener(
+ new Download(mActivity, extra, webview.isPrivateBrowsingEnabled(),
+ webview.getSettings().getUserAgentString()));
+ 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);
+ }
+
+ public void startAddMyNavigation(String url) {
+ final Intent intent = new Intent(Controller.this.getContext(), AddMyNavigationPage.class);
+ Bundle bundle = new Bundle();
+ bundle.putBoolean("isAdding", true);
+ bundle.putString("url", url);
+ bundle.putString("name", getNameFromUrl(url));
+ intent.putExtra("websites", bundle);
+ mActivity.startActivityForResult(intent, MY_NAVIGATION);
+ }
+
+ private void showMyNavigationDeleteDialog(final String itemUrl) {
+ new AlertDialog.Builder(this.getContext())
+ .setTitle(R.string.my_navigation_delete_label)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.my_navigation_delete_msg)
+ .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ deleteMyNavigationItem(itemUrl);
+ }
+ })
+ .setNegativeButton(R.string.cancel, null)
+ .show();
+ }
+
+ private void deleteMyNavigationItem(final String itemUrl) {
+ ContentResolver cr = this.getContext().getContentResolver();
+ Cursor cursor = null;
+
+ try {
+ cursor = cr.query(MyNavigationUtil.MY_NAVIGATION_URI,
+ new String[] {
+ MyNavigationUtil.ID
+ }, "url = ?", new String[] {
+ itemUrl
+ }, null);
+ if (null != cursor && cursor.moveToFirst()) {
+ Uri uri = ContentUris.withAppendedId(MyNavigationUtil.MY_NAVIGATION_URI,
+ cursor.getLong(0));
+
+ ContentValues values = new ContentValues();
+ values.put(MyNavigationUtil.TITLE, "");
+ values.put(MyNavigationUtil.URL, "ae://" + cursor.getLong(0) + "add-fav");
+ values.put(MyNavigationUtil.WEBSITE, 0 + "");
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ Bitmap bm = BitmapFactory.decodeResource(this.getContext().getResources(),
+ R.raw.my_navigation_add);
+ bm.compress(Bitmap.CompressFormat.PNG, 100, os);
+ values.put(MyNavigationUtil.THUMBNAIL, os.toByteArray());
+ Log.d(LOGTAG, "deleteMyNavigationItem uri is : " + uri);
+ cr.update(uri, values, null, null);
+ } else {
+ Log.e(LOGTAG, "deleteMyNavigationItem the item does not exist!");
+ }
+ } catch (IllegalStateException e) {
+ Log.e(LOGTAG, "deleteMyNavigationItem", e);
+ } finally {
+ if (null != cursor) {
+ cursor.close();
+ }
+ }
+
+ if (getCurrentTopWebView() != null) {
+ getCurrentTopWebView().reload();
+ }
+ }
+
+ private String getNameFromUrl(String itemUrl) {
+ ContentResolver cr = this.getContext().getContentResolver();
+ Cursor cursor = null;
+ String name = null;
+
+ try {
+ cursor = cr.query(MyNavigationUtil.MY_NAVIGATION_URI,
+ new String[] {
+ MyNavigationUtil.TITLE
+ }, "url = ?", new String[] {
+ itemUrl
+ }, null);
+ if (null != cursor && cursor.moveToFirst()) {
+ name = cursor.getString(0);
+ } else {
+ Log.e(LOGTAG, "this item does not exist!");
+ }
+ } catch (IllegalStateException e) {
+ Log.e(LOGTAG, "getNameFromUrl", e);
+ } finally {
+ if (null != cursor) {
+ cursor.close();
+ }
+ }
+ return name;
+ }
+
+ private void updateMyNavigationThumbnail(final String itemUrl, WebView webView) {
+ int width = mActivity.getResources().getDimensionPixelOffset(
+ R.dimen.myNavigationThumbnailWidth);
+ int height = mActivity.getResources().getDimensionPixelOffset(
+ R.dimen.myNavigationThumbnailHeight);
+
+ final Bitmap bm = createScreenshot(webView, width, height);
+
+ if (bm == null) {
+ Log.e(LOGTAG, "updateMyNavigationThumbnail bm is null!");
+ return;
+ }
+
+ final ContentResolver cr = mActivity.getContentResolver();
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... unused) {
+ ContentResolver cr = mActivity.getContentResolver();
+ Cursor cursor = null;
+ try {
+ cursor = cr.query(MyNavigationUtil.MY_NAVIGATION_URI,
+ new String[] {
+ MyNavigationUtil.ID
+ }, "url = ?", new String[] {
+ itemUrl
+ }, null);
+ if (null != cursor && cursor.moveToFirst()) {
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ bm.compress(Bitmap.CompressFormat.PNG, 100, os);
+
+ ContentValues values = new ContentValues();
+ values.put(MyNavigationUtil.THUMBNAIL, os.toByteArray());
+ Uri uri = ContentUris.withAppendedId(MyNavigationUtil.MY_NAVIGATION_URI,
+ cursor.getLong(0));
+ Log.d(LOGTAG, "updateMyNavigationThumbnail uri is " + uri);
+ cr.update(uri, values, null, null);
+ os.close();
+ }
+ } catch (IllegalStateException e) {
+ Log.e(LOGTAG, "updateMyNavigationThumbnail", e);
+ } catch (IOException e) {
+ Log.e(LOGTAG, "updateMyNavigationThumbnail", e);
+ } finally {
+ if (null != cursor) {
+ cursor.close();
+ }
+ }
+ return null;
+ }
+ }.execute();
+ }
+ /**
+ * 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, Tab tab) {
+ if (menu == null) {
+ return;
+ }
+ MenuItem dest = menu.findItem(R.id.stop_reload_menu_id);
+ MenuItem src = ((tab != null) && tab.inPageLoad()) ?
+ 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());
+ }
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ updateInLoadMenuItems(menu, getCurrentTab());
+ // hold on to the menu reference here; it is used by the page callbacks
+ // to update the menu based on loading state
+ mCachedMenu = menu;
+ // 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);
+ }
+ updateMenuState(getCurrentTab(), menu);
+ break;
+ }
+ mCurrentMenuState = mMenuState;
+ return mUi.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public void updateMenuState(Tab tab, Menu menu) {
+ boolean canGoBack = false;
+ boolean canGoForward = false;
+ boolean isDesktopUa = false;
+ boolean isLive = false;
+ if (tab != null) {
+ canGoBack = tab.canGoBack();
+ canGoForward = tab.canGoForward();
+ isDesktopUa = mSettings.hasDesktopUseragent(tab.getWebView());
+ isLive = !tab.isSnapshot();
+ }
+ final MenuItem back = menu.findItem(R.id.back_menu_id);
+ back.setEnabled(canGoBack);
+
+ final MenuItem home = menu.findItem(R.id.homepage_menu_id);
+
+ final MenuItem forward = menu.findItem(R.id.forward_menu_id);
+ forward.setEnabled(canGoForward);
+
+ final MenuItem source = menu.findItem(isInLoad() ? R.id.stop_menu_id
+ : R.id.reload_menu_id);
+ final MenuItem dest = menu.findItem(R.id.stop_reload_menu_id);
+ if (source != null && dest != null) {
+ dest.setTitle(source.getTitle());
+ dest.setIcon(source.getIcon());
+ }
+ menu.setGroupVisible(R.id.NAV_MENU, isLive);
+
+ // 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.enableNavDump();
+ final MenuItem nav = menu.findItem(R.id.dump_nav_menu_id);
+ nav.setVisible(isNavDump);
+ nav.setEnabled(isNavDump);
+
+ boolean showDebugSettings = mSettings.isDebugEnabled();
+ final MenuItem uaSwitcher = menu.findItem(R.id.ua_desktop_menu_id);
+ uaSwitcher.setChecked(isDesktopUa);
+ menu.setGroupVisible(R.id.LIVE_MENU, isLive);
+ menu.setGroupVisible(R.id.SNAPSHOT_MENU, !isLive);
+ // history and snapshots item are the members of COMBO menu group,
+ // so if show history item, only make snapshots item invisible.
+ menu.findItem(R.id.snapshots_menu_id).setVisible(false);
+
+ mUi.updateMenuState(tab, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ 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;
+ }
+ if (mUi.onOptionsItemSelected(item)) {
+ // ui callback handled it
+ return true;
+ }
+ 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.close_other_tabs_id:
+ closeOtherTabs();
+ break;
+
+ case R.id.goto_menu_id:
+ editUrl();
+ break;
+
+ case R.id.bookmarks_menu_id:
+ bookmarksOrHistoryPicker(ComboViews.Bookmarks);
+ break;
+
+ case R.id.history_menu_id:
+ bookmarksOrHistoryPicker(ComboViews.History);
+ break;
+
+ case R.id.snapshots_menu_id:
+ bookmarksOrHistoryPicker(ComboViews.Snapshots);
+ break;
+
+ case R.id.add_bookmark_menu_id:
+ bookmarkCurrentPage();
+ break;
+
+ case R.id.stop_reload_menu_id:
+ if (isInLoad()) {
+ stopLoading();
+ } else {
+ Tab currentTab = mTabControl.getCurrentTab();
+ if (currentTab.hasCrashed) {
+ currentTab.replaceCrashView(getCurrentTopWebView(),
+ currentTab.getViewContainer());
+ }
+ getCurrentTopWebView().reload();
+ }
+ break;
+
+ case R.id.back_menu_id:
+ getCurrentTab().goBack();
+ break;
+
+ case R.id.forward_menu_id:
+ getCurrentTab().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.exit_menu_id:
+ Object[] params = { new String("persist.debug.browsermonkeytest")};
+ Class[] type = new Class[] {String.class};
+ String ret = (String)ReflectHelper.invokeStaticMethod(
+ "android.os.SystemProperties","get", type, params);
+ if (ret != null && ret.equals("enable"))
+ break;
+ showExitDialog(mActivity);
+ return true;
+ case R.id.homepage_menu_id:
+ Tab current = mTabControl.getCurrentTab();
+ loadUrl(current, mSettings.getHomePage());
+ break;
+
+ case R.id.preferences_menu_id:
+ openPreferences();
+ break;
+
+ case R.id.find_menu_id:
+ findOnPage();
+ break;
+
+ case R.id.save_snapshot_menu_id:
+ final Tab source = getTabControl().getCurrentTab();
+ if (source == null) break;
+ new SaveSnapshotTask(source).execute();
+ break;
+
+ case R.id.page_info_menu_id:
+ showPageInfo();
+ break;
+
+ case R.id.snapshot_go_live:
+ goLive();
+ return true;
+
+ case R.id.share_page_menu_id:
+ Tab currentTab = mTabControl.getCurrentTab();
+ if (null == currentTab) {
+ return false;
+ }
+ shareCurrentPage(currentTab);
+ break;
+
+ case R.id.dump_nav_menu_id:
+ getCurrentTopWebView().debugDump();
+ 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.ua_desktop_menu_id:
+ toggleUserAgent();
+ 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(desiredTab);
+ }
+ break;
+ }
+ }
+ }
+ break;
+
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ private class SaveSnapshotTask extends AsyncTask<Void, Void, Long>
+ implements OnCancelListener {
+
+ private Tab mTab;
+ private Dialog mProgressDialog;
+ private ContentValues mValues;
+
+ private SaveSnapshotTask(Tab tab) {
+ mTab = tab;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ CharSequence message = mActivity.getText(R.string.saving_snapshot);
+ mProgressDialog = ProgressDialog.show(mActivity, null, message,
+ true, true, this);
+ mValues = mTab.createSnapshotValues();
+ }
+
+ @Override
+ protected Long doInBackground(Void... params) {
+ if (!mTab.saveViewState(mValues)) {
+ return null;
+ }
+ if (isCancelled()) {
+ String path = mValues.getAsString(Snapshots.VIEWSTATE_PATH);
+ File file = mActivity.getFileStreamPath(path);
+ if (!file.delete()) {
+ file.deleteOnExit();
+ }
+ return null;
+ }
+ final ContentResolver cr = mActivity.getContentResolver();
+ Uri result = cr.insert(Snapshots.CONTENT_URI, mValues);
+ if (result == null) {
+ return null;
+ }
+ long id = ContentUris.parseId(result);
+ return id;
+ }
+
+ @Override
+ protected void onPostExecute(Long id) {
+ if (isCancelled()) {
+ return;
+ }
+ mProgressDialog.dismiss();
+ if (id == null) {
+ Toast.makeText(mActivity, R.string.snapshot_failed,
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ Bundle b = new Bundle();
+ b.putLong(BrowserSnapshotPage.EXTRA_ANIMATE_ID, id);
+ mUi.showComboView(ComboViews.Snapshots, b);
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ cancel(true);
+ }
+ }
+
+ @Override
+ public void toggleUserAgent() {
+ WebView web = getCurrentWebView();
+ mSettings.toggleDesktopUseragent(web);
+ web.loadUrl(web.getOriginalUrl());
+ }
+
+ @Override
+ public void findOnPage() {
+ getCurrentTopWebView().showFindDialog(null, true);
+ }
+
+ @Override
+ public void openPreferences() {
+ Intent intent = new Intent(mActivity, BrowserPreferencesPage.class);
+ intent.putExtra(BrowserPreferencesPage.CURRENT_PAGE,
+ getCurrentTopWebView().getUrl());
+ mActivity.startActivityForResult(intent, PREFERENCES_PAGE);
+ }
+
+ @Override
+ public void bookmarkCurrentPage() {
+ Intent bookmarkIntent = createBookmarkCurrentPageIntent(false);
+ if (bookmarkIntent != null) {
+ mActivity.startActivity(bookmarkIntent);
+ }
+ }
+
+ private void goLive() {
+ SnapshotTab t = (SnapshotTab) getCurrentTab();
+ t.loadUrl(t.getLiveUrl(), null);
+ }
+
+ private void showExitDialog(final Activity activity) {
+ new AlertDialog.Builder(activity)
+ .setTitle(R.string.exit_browser_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.exit_browser_msg)
+ .setNegativeButton(R.string.exit_minimize, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ activity.moveTaskToBack(true);
+ dialog.dismiss();
+ }
+ })
+ .setPositiveButton(R.string.exit_quit, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ activity.finish();
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ // TODO Auto-generated method stub
+ mCrashRecoveryHandler.clearState(true);
+ int pid = android.os.Process.myPid();
+ android.os.Process.killProcess(pid);
+ }
+ }, 300);
+ dialog.dismiss();
+ }
+ })
+ .show();
+ }
+ @Override
+ public void showPageInfo() {
+ mPageDialogsHandler.showPageInfo(mTabControl.getCurrentTab(), false, null);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ // Let the History and Bookmark fragments handle menus they created.
+ if (item.getGroupId() == R.id.CONTEXT_MENU) {
+ return false;
+ }
+
+ int id = item.getItemId();
+ boolean result = true;
+ switch (id) {
+ // -- Browser context menu
+ case R.id.open_context_menu_id:
+ case R.id.save_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);
+ }
+ 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();
+ }
+
+ @Override
+ 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(isInLoad());
+ }
+ }
+ } else {
+ // The options menu is closed, so open it, and show the title
+ mOptionsMenuOpen = true;
+ mConfigChanged = false;
+ mExtendedMenuOpen = false;
+ mUi.onOptionsMenuOpened();
+ }
+ return true;
+ }
+
+ @Override
+ public void onOptionsMenuClosed(Menu menu) {
+ mOptionsMenuOpen = false;
+ mUi.onOptionsMenuClosed(isInLoad());
+ }
+
+ @Override
+ public void onContextMenuClosed(Menu menu) {
+ mUi.onContextMenuClosed(menu, isInLoad());
+ }
+
+ // 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);
+ }
+
+ int getActionModeHeight() {
+ TypedArray actionBarSizeTypedArray = mActivity.obtainStyledAttributes(
+ new int[] { android.R.attr.actionBarSize });
+ int size = (int) actionBarSizeTypedArray.getDimension(0, 0f);
+ actionBarSizeTypedArray.recycle();
+ return size;
+ }
+
+ // action mode
+
+ @Override
+ public 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.
+ */
+ @Override
+ public void onActionModeFinished(ActionMode mode) {
+ if (!isInCustomActionMode()) return;
+ mUi.onActionModeFinished(isInLoad());
+ mActionMode = null;
+ }
+
+ boolean isInLoad() {
+ final Tab tab = getCurrentTab();
+ return (tab != null) && tab.inPageLoad();
+ }
+
+ // bookmark handling
+
+ /**
+ * add the current page as a bookmark to the given folder id
+ * @param folderId use -1 for the default folder
+ * @param editExisting If true, check to see whether the site is already
+ * bookmarked, and if it is, edit that bookmark. If false, and
+ * the site is already bookmarked, do not attempt to edit the
+ * existing bookmark.
+ */
+ @Override
+ public Intent createBookmarkCurrentPageIntent(boolean editExisting) {
+ WebView w = getCurrentTopWebView();
+ if (w == null) {
+ return null;
+ }
+ Intent i = new Intent(mActivity,
+ AddBookmarkPage.class);
+ 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());
+ if (editExisting) {
+ i.putExtra(AddBookmarkPage.CHECK_FOR_DUPE, true);
+ }
+ // Put the dialog at the upper right of the screen, covering the
+ // star on the title bar.
+ i.putExtra("gravity", Gravity.RIGHT | Gravity.TOP);
+ return i;
+ }
+
+ // file chooser
+ @Override
+ public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
+ mUploadHandler = new UploadHandler(this);
+ mUploadHandler.openFileChooser(uploadMsg, acceptType, capture);
+ }
+
+ // 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);
+ }
+
+ static Bitmap createScreenshot(WebView view, int width, int height) {
+ if (view == null || width == 0 || height == 0) {
+ return null;
+ }
+
+ Bitmap viewportBitmap = view.getViewportBitmap();
+ if (viewportBitmap == null) {
+ return null;
+ }
+
+ float aspectRatio = (float) width/height;
+ int viewportWidth = viewportBitmap.getWidth();
+ int viewportHeight = viewportBitmap.getHeight();
+
+ //modify the size to attain the same aspect ratio of desired thumbnail size
+ if (viewportHeight > viewportWidth) {
+ viewportHeight= (int)Math.round(viewportWidth * aspectRatio);
+ } else {
+ viewportWidth = (int)Math.round(viewportHeight * aspectRatio);
+ }
+
+ Rect srcRect = new Rect(0, 0, viewportWidth, viewportHeight);
+ Rect dstRect = new Rect(0, 0, width, height);
+
+ if (sThumbnailBitmap == null || sThumbnailBitmap.getWidth() != width
+ || sThumbnailBitmap.getHeight() != height) {
+ if (sThumbnailBitmap != null) {
+ sThumbnailBitmap.recycle();
+ sThumbnailBitmap = null;
+ }
+
+ sThumbnailBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
+ }
+
+ Canvas canvas = new Canvas(sThumbnailBitmap);
+ canvas.drawBitmap(viewportBitmap, srcRect, dstRect, new Paint(Paint.FILTER_BITMAP_FLAG));
+
+ return sThumbnailBitmap;
+ }
+
+ private void updateScreenshot(Tab tab) {
+ // If this is a bookmarked site, add a screenshot to the database.
+ // FIXME: Would like to make sure there is actually something to
+ // draw, but the API for that (WebViewCore.pictureReady()) is not
+ // currently accessible here.
+
+ WebView view = tab.getWebView();
+ if (view == null) {
+ // Tab was destroyed
+ return;
+ }
+ final String url = tab.getUrl();
+ final String originalUrl = view.getOriginalUrl();
+ final String thumbnailUrl = mUpdateMyNavThumbnailUrl;
+ if (TextUtils.isEmpty(url)) {
+ return;
+ }
+
+ //update My Navigation Thumbnails
+ boolean isMyNavigationUrl = MyNavigationUtil.isMyNavigationUrl(mActivity, url);
+ if (isMyNavigationUrl) {
+ updateMyNavigationThumbnail(url, view);
+ }
+ // Only update thumbnails for web urls (http(s)://), not for
+ // about:, javascript:, data:, etc...
+ // Unless it is a bookmarked site, then always update
+ if (!Patterns.WEB_URL.matcher(url).matches() && !tab.isBookmarkedSite()) {
+ return;
+ }
+
+ if (url != null && mUpdateMyNavThumbnailUrl != null
+ && Patterns.WEB_URL.matcher(url).matches()
+ && Patterns.WEB_URL.matcher(mUpdateMyNavThumbnailUrl).matches()) {
+ String urlHost = (new WebAddress(url)).getHost();
+ String bookmarkHost = (new WebAddress(mUpdateMyNavThumbnailUrl)).getHost();
+ if (urlHost == null || urlHost.length() == 0 || bookmarkHost == null
+ || bookmarkHost.length() == 0) {
+ return;
+ }
+ String urlDomain = urlHost.substring(urlHost.indexOf('.'), urlHost.length());
+ String bookmarkDomain = bookmarkHost.substring(bookmarkHost.indexOf('.'),
+ bookmarkHost.length());
+ Log.d(LOGTAG, "addressUrl domain is " + urlDomain);
+ Log.d(LOGTAG, "bookmarkUrl domain is " + bookmarkDomain);
+ if (!bookmarkDomain.equals(urlDomain)) {
+ return;
+ }
+ }
+ final Bitmap bm = createScreenshot(view, getDesiredThumbnailWidth(mActivity),
+ getDesiredThumbnailHeight(mActivity));
+ if (bm == null) {
+ if (!mHandler.hasMessages(UPDATE_BOOKMARK_THUMBNAIL, tab)) {
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(
+ UPDATE_BOOKMARK_THUMBNAIL, 0, 0, tab),
+ 500);
+ }
+ return;
+ }
+
+ final ContentResolver cr = mActivity.getContentResolver();
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... unused) {
+ Cursor cursor = null;
+ try {
+ // TODO: Clean this up
+ cursor = Bookmarks.queryCombinedForUrl(cr, originalUrl,
+ mUpdateMyNavThumbnail ? ((thumbnailUrl != null) ? thumbnailUrl : url)
+ : url);
+ if (mUpdateMyNavThumbnail) {
+ mUpdateMyNavThumbnail = false;
+ mUpdateMyNavThumbnailUrl = null;
+ }
+ 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());
+
+ do {
+ values.put(Images.URL, cursor.getString(0));
+ cr.update(Images.CONTENT_URI, values, null, null);
+ } while (cursor.moveToNext());
+ }
+ } catch (IllegalStateException e) {
+ // Ignore
+ } catch (SQLiteException s) {
+ // Added for possible error when user tries to remove the same bookmark
+ // that is being updated with a screen shot
+ Log.w(LOGTAG, "Error when running updateScreenshot ", s);
+ } finally {
+ if (cursor != null) cursor.close();
+ }
+ return null;
+ }
+ }.execute();
+ }
+
+ private class Copy implements OnMenuItemClickListener {
+ private CharSequence mText;
+
+ @Override
+ 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;
+ private boolean mPrivateBrowsing;
+ private String mUserAgent;
+ private static final String FALLBACK_EXTENSION = "dat";
+ private static final String IMAGE_BASE_FORMAT = "yyyy-MM-dd-HH-mm-ss-";
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ if (DataUri.isDataUri(mText)) {
+ saveDataUri();
+ } else {
+ DownloadHandler.onDownloadStartNoStream(mActivity, mText, mUserAgent,
+ null, null, null, mPrivateBrowsing, 0);
+ }
+ return true;
+ }
+
+ public Download(Activity activity, String toDownload, boolean privateBrowsing,
+ String userAgent) {
+ mActivity = activity;
+ mText = toDownload;
+ mPrivateBrowsing = privateBrowsing;
+ mUserAgent = userAgent;
+ }
+
+ /**
+ * Treats mText as a data URI and writes its contents to a file
+ * based on the current time.
+ */
+ private void saveDataUri() {
+ FileOutputStream outputStream = null;
+ try {
+ DataUri uri = new DataUri(mText);
+ File target = getTarget(uri);
+ outputStream = new FileOutputStream(target);
+ outputStream.write(uri.getData());
+ final DownloadManager manager =
+ (DownloadManager) mActivity.getSystemService(Context.DOWNLOAD_SERVICE);
+ manager.addCompletedDownload(target.getName(),
+ mActivity.getTitle().toString(), false,
+ uri.getMimeType(), target.getAbsolutePath(),
+ uri.getData().length, true);
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Could not save data URL");
+ } finally {
+ if (outputStream != null) {
+ try {
+ outputStream.close();
+ } catch (IOException e) {
+ // ignore close errors
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates a File based on the current time stamp and uses
+ * the mime type of the DataUri to get the extension.
+ */
+ private File getTarget(DataUri uri) throws IOException {
+ File dir = mActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
+ DateFormat format = new SimpleDateFormat(IMAGE_BASE_FORMAT, Locale.US);
+ String nameBase = format.format(new Date());
+ String mimeType = uri.getMimeType();
+ MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
+ String extension = mimeTypeMap.getExtensionFromMimeType(mimeType);
+ if (extension == null) {
+ Log.w(LOGTAG, "Unknown mime type in data URI" + mimeType);
+ extension = FALLBACK_EXTENSION;
+ }
+ extension = "." + extension; // createTempFile needs the '.'
+ File targetFile = File.createTempFile(nameBase, extension, dir);
+ return targetFile;
+ }
+ }
+
+ private static class SelectText implements OnMenuItemClickListener {
+ private WebView mWebView;
+
+ @Override
+ 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);
+ mCrashRecoveryHandler.backupState();
+ }
+
+ @Override
+ public void setActiveTab(Tab tab) {
+ // monkey protection against delayed start
+ if (tab != null) {
+ mTabControl.setCurrentTab(tab);
+ // the tab is guaranteed to have a webview after setCurrentTab
+ mUi.setActiveTab(tab);
+ tab.setTimeStamp();
+ }
+ }
+
+ protected void closeEmptyTab() {
+ Tab current = mTabControl.getCurrentTab();
+ if (current != null
+ && current.getWebView().copyBackForwardList().getSize() == 0) {
+ closeCurrentTab();
+ }
+ }
+
+ protected void reuseTab(Tab appTab, UrlData urlData) {
+ // 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.
+ mTabControl.recreateWebView(appTab);
+ // TODO: analyze why the remove and add are necessary
+ mUi.attachTab(appTab);
+ if (mTabControl.getCurrentTab() != appTab) {
+ switchToTab(appTab);
+ loadUrlDataIn(appTab, urlData);
+ } else {
+ // If the tab was the current tab, we have to attach
+ // it to the view system again.
+ setActiveTab(appTab);
+ 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.
+ @Override
+ public void dismissSubWindow(Tab tab) {
+ removeSubWindow(tab);
+ // dismiss the subwindow. This will destroy the WebView.
+ tab.dismissSubWindow();
+ WebView wv = getCurrentTopWebView();
+ if (wv != null) {
+ wv.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();
+ }
+ }
+
+ private Tab showPreloadedTab(final UrlData urlData) {
+ if (!urlData.isPreloaded()) {
+ return null;
+ }
+ final PreloadedTabControl tabControl = urlData.getPreloadedTab();
+ final String sbQuery = urlData.getSearchBoxQueryToSubmit();
+ if (sbQuery != null) {
+ if (!tabControl.searchBoxSubmit(sbQuery, urlData.mUrl, urlData.mHeaders)) {
+ // Could not submit query. Fallback to regular tab creation
+ tabControl.destroy();
+ return null;
+ }
+ }
+ // check tab count and make room for new tab
+ if (!mTabControl.canCreateNewTab()) {
+ Tab leastUsed = mTabControl.getLeastUsedTab(getCurrentTab());
+ if (leastUsed != null) {
+ closeTab(leastUsed);
+ }
+ }
+ Tab t = tabControl.getTab();
+ t.refreshIdAfterPreload();
+ mTabControl.addPreloadedTab(t);
+ addTab(t);
+ setActiveTab(t);
+ return t;
+ }
+
+ // open a non inconito tab with the given url data
+ // and set as active tab
+ public Tab openTab(UrlData urlData) {
+ Tab tab = showPreloadedTab(urlData);
+ if (tab == null) {
+ tab = createNewTab(false, true, true);
+ if ((tab != null) && !urlData.isEmpty()) {
+ loadUrlDataIn(tab, urlData);
+ }
+ }
+ return tab;
+ }
+
+ @Override
+ public Tab openTabToHomePage() {
+ return openTab(mSettings.getHomePage(), false, true, false);
+ }
+
+ @Override
+ public Tab openIncognitoTab() {
+ return openTab(INCOGNITO_URI, true, true, false);
+ }
+
+ @Override
+ public Tab openTab(String url, boolean incognito, boolean setActive,
+ boolean useCurrent) {
+ return openTab(url, incognito, setActive, useCurrent, null);
+ }
+
+ @Override
+ public Tab openTab(String url, Tab parent, boolean setActive,
+ boolean useCurrent) {
+ return openTab(url, (parent != null) && parent.isPrivateBrowsingEnabled(),
+ setActive, useCurrent, parent);
+ }
+
+ public Tab openTab(String url, boolean incognito, boolean setActive,
+ boolean useCurrent, Tab parent) {
+ Tab tab = createNewTab(incognito, setActive, useCurrent);
+ if (tab != null) {
+ if (parent != null && parent != tab) {
+ parent.addChildTab(tab);
+ }
+ if (url != null) {
+ loadUrl(tab, url);
+ }
+ }
+ return tab;
+ }
+
+ // this method will attempt to create a new tab
+ // incognito: private browsing tab
+ // setActive: ste tab as current tab
+ // useCurrent: if no new tab can be created, return current tab
+ private Tab createNewTab(boolean incognito, boolean setActive,
+ boolean useCurrent) {
+ Tab tab = null;
+ MemoryMonitor memMonitor = null;
+ if (mTabControl.canCreateNewTab()) {
+ if (mSettings.enableMemoryMonitor()) {
+ Log.d(LOGTAG, " Memory Monitor Enabled .");
+ memMonitor = MemoryMonitor.getInstance(mActivity.getApplicationContext(),this);
+ if (memMonitor != null) {
+ //Remove webview associated with the oldest tab
+ memMonitor.destroyLeastRecentlyActiveTab();
+ }
+ } else {
+ Log.d(LOGTAG, " Memory Monitor disabled .");
+ }
+ tab = mTabControl.createNewTab(incognito);
+ addTab(tab);
+ tab.setTimeStamp();
+ if (setActive) {
+ setActiveTab(tab);
+ }
+ } else {
+ if (useCurrent) {
+ tab = mTabControl.getCurrentTab();
+ reuseTab(tab, null);
+ } else {
+ mUi.showMaxTabsWarning();
+ }
+ }
+ return tab;
+ }
+
+ @Override
+ public SnapshotTab createNewSnapshotTab(long snapshotId, boolean setActive) {
+ SnapshotTab tab = null;
+ if (mTabControl.canCreateNewTab()) {
+ tab = mTabControl.createSnapshotTab(snapshotId);
+ addTab(tab);
+ if (setActive) {
+ setActiveTab(tab);
+ }
+ } else {
+ mUi.showMaxTabsWarning();
+ }
+ return tab;
+ }
+
+ /**
+ * @param tab the tab to switch to
+ * @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(Tab tab) {
+ Tab currentTab = mTabControl.getCurrentTab();
+ if (tab == null || tab == currentTab) {
+ return false;
+ }
+ setActiveTab(tab);
+ return true;
+ }
+
+ @Override
+ public void closeCurrentTab() {
+ closeCurrentTab(false);
+ }
+
+ protected void closeCurrentTab(boolean andQuit) {
+ if (mTabControl.getTabCount() == 1) {
+ mCrashRecoveryHandler.clearState();
+ mTabControl.removeTab(getCurrentTab());
+ mActivity.finish();
+ return;
+ }
+ final Tab current = mTabControl.getCurrentTab();
+ final int pos = mTabControl.getCurrentPosition();
+ Tab newTab = current.getParent();
+ if (newTab == null) {
+ newTab = mTabControl.getTab(pos + 1);
+ if (newTab == null) {
+ newTab = mTabControl.getTab(pos - 1);
+ }
+ }
+ if (andQuit) {
+ mTabControl.setCurrentTab(newTab);
+ closeTab(current);
+ } else if (switchToTab(newTab)) {
+ // 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) {
+ if (tab == mTabControl.getCurrentTab()) {
+ closeCurrentTab();
+ } else {
+ removeTab(tab);
+ }
+ }
+
+ /**
+ * Close all tabs except the current one
+ */
+ @Override
+ public void closeOtherTabs() {
+ int inactiveTabs = mTabControl.getTabCount() - 1;
+ for (int i = inactiveTabs; i >= 0; i--) {
+ Tab tab = mTabControl.getTab(i);
+ if (tab != mTabControl.getCurrentTab()) {
+ removeTab(tab);
+ }
+ }
+ }
+
+ // Called when loading from context menu or LOAD_URL message
+ protected void loadUrlFromContext(String url) {
+ Tab tab = getCurrentTab();
+ WebView view = tab != null ? tab.getWebView() : null;
+ // In case the user enters nothing.
+ if (url != null && url.length() != 0 && tab != null && view != null) {
+ url = UrlUtils.smartUrlFilter(url);
+ if (!((BrowserWebView) view).getWebViewClient().
+ shouldOverrideUrlLoading(view, url)) {
+ loadUrl(tab, 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.
+ */
+ @Override
+ public void loadUrl(Tab tab, String url) {
+ loadUrl(tab, url, null);
+ }
+
+ protected void loadUrl(Tab tab, String url, Map<String, String> headers) {
+ if (tab != null) {
+ dismissSubWindow(tab);
+ tab.loadUrl(url, headers);
+ if (tab.hasCrashed) {
+ tab.replaceCrashView(tab.getWebView(), tab.getViewContainer());
+ }
+ mUi.onProgressChanged(tab);
+ }
+ }
+
+ /**
+ * 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) {
+ if (data != null) {
+ if (data.isPreloaded()) {
+ // this isn't called for preloaded tabs
+ } else {
+ if (t != null && data.mDisableUrlOverride) {
+ t.disableUrlOverridingForLoad();
+ }
+ loadUrl(t, data.mUrl, data.mHeaders);
+ }
+ }
+ }
+
+ @Override
+ public void onUserCanceledSsl(Tab tab) {
+ // TODO: Figure out the "right" behavior
+ if (tab.canGoBack()) {
+ tab.goBack();
+ } else {
+ tab.loadUrl(mSettings.getHomePage(), null);
+ }
+ }
+
+ 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().
+ */
+ showExitDialog(mActivity);
+ return;
+ }
+ if (current.canGoBack()) {
+ current.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.getParent();
+ if (parent != null) {
+ switchToTab(parent);
+ // Now we close the other tab
+ closeTab(current);
+ } else {
+ /*
+ * 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().
+ */
+ showExitDialog(mActivity);
+ }
+ }
+ }
+
+ /**
+ * helper method for key handler
+ * returns the current tab if it can't advance
+ */
+ private Tab getNextTab() {
+ int pos = mTabControl.getCurrentPosition() + 1;
+ if (pos >= mTabControl.getTabCount()) {
+ pos = 0;
+ }
+ return mTabControl.getTab(pos);
+ }
+
+ /**
+ * helper method for key handler
+ * returns the current tab if it can't advance
+ */
+ private Tab getPrevTab() {
+ int pos = mTabControl.getCurrentPosition() - 1;
+ if ( pos < 0) {
+ pos = mTabControl.getTabCount() - 1;
+ }
+ return mTabControl.getTab(pos);
+ }
+
+ boolean isMenuOrCtrlKey(int keyCode) {
+ return (KeyEvent.KEYCODE_MENU == keyCode)
+ || (KeyEvent.KEYCODE_CTRL_LEFT == keyCode)
+ || (KeyEvent.KEYCODE_CTRL_RIGHT == keyCode);
+ }
+
+ /**
+ * handle key events in browser
+ *
+ * @param keyCode
+ * @param event
+ * @return true if handled, false to pass to super
+ */
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ boolean noModifiers = event.hasNoModifiers();
+ // Even if MENU is already held down, we need to call to super to open
+ // the IME on long press.
+ if (!noModifiers && isMenuOrCtrlKey(keyCode)) {
+ mMenuIsDown = true;
+ return false;
+ }
+
+ WebView webView = getCurrentTopWebView();
+ Tab tab = getCurrentTab();
+ if (webView == null || tab == null) return false;
+
+ boolean ctrl = event.hasModifiers(KeyEvent.META_CTRL_ON);
+ boolean shift = event.hasModifiers(KeyEvent.META_SHIFT_ON);
+
+ switch(keyCode) {
+ case KeyEvent.KEYCODE_TAB:
+ if (event.isCtrlPressed()) {
+ if (event.isShiftPressed()) {
+ // prev tab
+ switchToTab(getPrevTab());
+ } else {
+ // next tab
+ switchToTab(getNextTab());
+ }
+ return true;
+ }
+ break;
+ 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 (shift) {
+ pageUp();
+ } else if (noModifiers) {
+ pageDown();
+ }
+ return true;
+ case KeyEvent.KEYCODE_BACK:
+ if (!noModifiers) break;
+ event.startTracking();
+ return true;
+ case KeyEvent.KEYCODE_FORWARD:
+ if (!noModifiers) break;
+ tab.goForward();
+ return true;
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (ctrl) {
+ tab.goBack();
+ return true;
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (ctrl) {
+ tab.goForward();
+ return true;
+ }
+ break;
+ case KeyEvent.KEYCODE_A:
+ if (ctrl) {
+ webView.selectAll();
+ return true;
+ }
+ break;
+// case KeyEvent.KEYCODE_B: // menu
+ case KeyEvent.KEYCODE_C:
+ if (ctrl ) {
+ webView.copySelection();
+ return true;
+ }
+ break;
+// case KeyEvent.KEYCODE_D: // menu
+// case KeyEvent.KEYCODE_E: // in Chrome: puts '?' in URL bar
+// case KeyEvent.KEYCODE_F: // menu
+// case KeyEvent.KEYCODE_G: // in Chrome: finds next match
+// case KeyEvent.KEYCODE_H: // menu
+// case KeyEvent.KEYCODE_I: // unused
+// case KeyEvent.KEYCODE_J: // menu
+// case KeyEvent.KEYCODE_K: // in Chrome: puts '?' in URL bar
+// case KeyEvent.KEYCODE_L: // menu
+// case KeyEvent.KEYCODE_M: // unused
+// case KeyEvent.KEYCODE_N: // in Chrome: new window
+// case KeyEvent.KEYCODE_O: // in Chrome: open file
+// case KeyEvent.KEYCODE_P: // in Chrome: print page
+// case KeyEvent.KEYCODE_Q: // unused
+// case KeyEvent.KEYCODE_R:
+// case KeyEvent.KEYCODE_S: // in Chrome: saves page
+ case KeyEvent.KEYCODE_T:
+ // we can't use the ctrl/shift flags, they check for
+ // exclusive use of a modifier
+ if (event.isCtrlPressed()) {
+ if (event.isShiftPressed()) {
+ openIncognitoTab();
+ } else {
+ openTabToHomePage();
+ }
+ return true;
+ }
+ break;
+// case KeyEvent.KEYCODE_U: // in Chrome: opens source of page
+// case KeyEvent.KEYCODE_V: // text view intercepts to paste
+// case KeyEvent.KEYCODE_W: // menu
+// case KeyEvent.KEYCODE_X: // text view intercepts to cut
+// case KeyEvent.KEYCODE_Y: // unused
+// case KeyEvent.KEYCODE_Z: // unused
+ }
+ // it is a regular key and webview is not null
+ return mUi.dispatchKey(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ switch(keyCode) {
+ case KeyEvent.KEYCODE_BACK:
+ if (mUi.isWebShowing()) {
+ bookmarksOrHistoryPicker(ComboViews.History);
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (isMenuOrCtrlKey(keyCode)) {
+ mMenuIsDown = false;
+ if (KeyEvent.KEYCODE_MENU == keyCode
+ && event.isTracking() && !event.isCanceled()) {
+ return onMenuKey();
+ }
+ }
+ if (!event.hasNoModifiers()) return false;
+ switch(keyCode) {
+ case KeyEvent.KEYCODE_BACK:
+ if (event.isTracking() && !event.isCanceled()) {
+ onBackKey();
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+
+ public boolean isMenuDown() {
+ return mMenuIsDown;
+ }
+
+ @Override
+ 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);
+ }
+
+ @Override
+ public boolean onSearchRequested() {
+ mUi.editUrl(false, true);
+ return true;
+ }
+
+ @Override
+ public boolean shouldCaptureThumbnails() {
+ return mUi.shouldCaptureThumbnails();
+ }
+
+ @Override
+ public boolean supportsVoice() {
+ PackageManager pm = mActivity.getPackageManager();
+ List activities = pm.queryIntentActivities(new Intent(
+ RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0);
+ return activities.size() != 0;
+ }
+
+ @Override
+ public void startVoiceRecognizer() {
+ Intent voice = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+ voice.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
+ RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
+ voice.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1);
+ mActivity.startActivityForResult(voice, VOICE_RESULT);
+ }
+
+ @Override
+ public void setBlockEvents(boolean block) {
+ mBlockEvents = block;
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ return mBlockEvents;
+ }
+
+ @Override
+ public boolean dispatchKeyShortcutEvent(KeyEvent event) {
+ return mBlockEvents;
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ return mBlockEvents;
+ }
+
+ @Override
+ public boolean dispatchTrackballEvent(MotionEvent ev) {
+ return mBlockEvents;
+ }
+
+ @Override
+ public boolean dispatchGenericMotionEvent(MotionEvent ev) {
+ return mBlockEvents;
+ }
+
+}
diff --git a/src/com/android/browser/CrashRecoveryHandler.java b/src/com/android/browser/CrashRecoveryHandler.java
new file mode 100644
index 0000000..bcdf8b0
--- /dev/null
+++ b/src/com/android/browser/CrashRecoveryHandler.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2011 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.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Parcel;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+public class CrashRecoveryHandler {
+
+ private static final boolean LOGV_ENABLED = Browser.LOGV_ENABLED;
+ private static final String LOGTAG = "BrowserCrashRecovery";
+ private static final String STATE_FILE = "browser_state.parcel";
+ private static final int BUFFER_SIZE = 4096;
+ private static final long BACKUP_DELAY = 500; // 500ms between writes
+ /* This is the duration for which we will prompt to restore
+ * instead of automatically restoring. The first time the browser crashes,
+ * we will automatically restore. If we then crash again within XX minutes,
+ * we will prompt instead of automatically restoring.
+ */
+ private static final long PROMPT_INTERVAL = 5 * 60 * 1000; // 5 minutes
+
+ private static final int MSG_WRITE_STATE = 1;
+ private static final int MSG_CLEAR_STATE = 2;
+ private static final int MSG_PRELOAD_STATE = 3;
+
+ private static CrashRecoveryHandler sInstance;
+
+ private Controller mController;
+ private Context mContext;
+ private Handler mForegroundHandler;
+ private Handler mBackgroundHandler;
+ private boolean mIsPreloading = false;
+ private boolean mDidPreload = false;
+ private Bundle mRecoveryState = null;
+
+ public static CrashRecoveryHandler initialize(Controller controller) {
+ if (sInstance == null) {
+ sInstance = new CrashRecoveryHandler(controller);
+ } else {
+ sInstance.mController = controller;
+ }
+ return sInstance;
+ }
+
+ public static CrashRecoveryHandler getInstance() {
+ return sInstance;
+ }
+
+ private CrashRecoveryHandler(Controller controller) {
+ mController = controller;
+ mContext = mController.getActivity().getApplicationContext();
+ mForegroundHandler = new Handler();
+ mBackgroundHandler = new Handler(BackgroundHandler.getLooper()) {
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_WRITE_STATE:
+ Bundle saveState = (Bundle) msg.obj;
+ writeState(saveState);
+ break;
+ case MSG_CLEAR_STATE:
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "Clearing crash recovery state");
+ }
+ File state = new File(mContext.getCacheDir(), STATE_FILE);
+ if (state.exists()) {
+ state.delete();
+ }
+ break;
+ case MSG_PRELOAD_STATE:
+ mRecoveryState = loadCrashState();
+ synchronized (CrashRecoveryHandler.this) {
+ mIsPreloading = false;
+ mDidPreload = true;
+ CrashRecoveryHandler.this.notifyAll();
+ }
+ break;
+ }
+ }
+ };
+ }
+
+ public void backupState() {
+ mForegroundHandler.postDelayed(mCreateState, BACKUP_DELAY);
+ }
+
+ private Runnable mCreateState = new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ final Bundle state = mController.createSaveState();
+ Message.obtain(mBackgroundHandler, MSG_WRITE_STATE, state)
+ .sendToTarget();
+ // Remove any queued up saves
+ mForegroundHandler.removeCallbacks(mCreateState);
+ } catch (Throwable t) {
+ Log.w(LOGTAG, "Failed to save state", t);
+ return;
+ }
+ }
+
+ };
+
+ public void clearState() {
+ clearState(false);
+ }
+
+ /**
+ * Clear cached state files.
+ *
+ * @param block If block, clear state files in the caller thread, otherwise
+ * do it in a worker thread.
+ */
+ void clearState(boolean block) {
+ if (block) {
+ if (mContext != null) {
+ File state = new File(mContext.getCacheDir(), STATE_FILE);
+ if (state.exists()) {
+ state.delete();
+ }
+ }
+ } else {
+ mBackgroundHandler.sendEmptyMessage(MSG_CLEAR_STATE);
+ }
+ updateLastRecovered(0);
+ }
+
+ private boolean shouldRestore() {
+ BrowserSettings browserSettings = BrowserSettings.getInstance();
+ long lastRecovered = browserSettings.getLastRecovered();
+ long timeSinceLastRecover = System.currentTimeMillis() - lastRecovered;
+ return (timeSinceLastRecover > PROMPT_INTERVAL)
+ || browserSettings.wasLastRunPaused();
+ }
+
+ private void updateLastRecovered(long time) {
+ BrowserSettings browserSettings = BrowserSettings.getInstance();
+ browserSettings.setLastRecovered(time);
+ }
+
+ synchronized private Bundle loadCrashState() {
+ if (!shouldRestore()) {
+ return null;
+ }
+ BrowserSettings browserSettings = BrowserSettings.getInstance();
+ browserSettings.setLastRunPaused(false);
+ Bundle state = null;
+ Parcel parcel = Parcel.obtain();
+ FileInputStream fin = null;
+ try {
+ File stateFile = new File(mContext.getCacheDir(), STATE_FILE);
+ fin = new FileInputStream(stateFile);
+ ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
+ byte[] buffer = new byte[BUFFER_SIZE];
+ int read;
+ while ((read = fin.read(buffer)) > 0) {
+ dataStream.write(buffer, 0, read);
+ }
+ byte[] data = dataStream.toByteArray();
+ parcel.unmarshall(data, 0, data.length);
+ parcel.setDataPosition(0);
+ state = parcel.readBundle();
+ if (state != null && !state.isEmpty()) {
+ return state;
+ }
+ } catch (FileNotFoundException e) {
+ // No state to recover
+ } catch (Throwable e) {
+ Log.w(LOGTAG, "Failed to recover state!", e);
+ } finally {
+ parcel.recycle();
+ if (fin != null) {
+ try {
+ fin.close();
+ } catch (IOException e) { }
+ }
+ }
+ return null;
+ }
+
+ public void startRecovery(Intent intent) {
+ synchronized (CrashRecoveryHandler.this) {
+ while (mIsPreloading) {
+ try {
+ CrashRecoveryHandler.this.wait();
+ } catch (InterruptedException e) {}
+ }
+ }
+ if (!mDidPreload) {
+ mRecoveryState = loadCrashState();
+ }
+ updateLastRecovered(mRecoveryState != null
+ ? System.currentTimeMillis() : 0);
+ mController.doStart(mRecoveryState, intent);
+ mRecoveryState = null;
+ }
+
+ public void preloadCrashState() {
+ synchronized (CrashRecoveryHandler.this) {
+ if (mIsPreloading) {
+ return;
+ }
+ mIsPreloading = true;
+ }
+ mBackgroundHandler.sendEmptyMessage(MSG_PRELOAD_STATE);
+ }
+
+ /**
+ * Writes the crash recovery state to a file synchronously.
+ * Errors are swallowed, but logged.
+ * @param state The state to write out
+ */
+ synchronized void writeState(Bundle state) {
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "Saving crash recovery state");
+ }
+ Parcel p = Parcel.obtain();
+ try {
+ state.writeToParcel(p, 0);
+ File stateJournal = new File(mContext.getCacheDir(),
+ STATE_FILE + ".journal");
+ FileOutputStream fout = new FileOutputStream(stateJournal);
+ fout.write(p.marshall());
+ fout.close();
+ File stateFile = new File(mContext.getCacheDir(),
+ STATE_FILE);
+ if (!stateJournal.renameTo(stateFile)) {
+ // Failed to rename, try deleting the existing
+ // file and try again
+ stateFile.delete();
+ stateJournal.renameTo(stateFile);
+ }
+ } catch (Throwable e) {
+ Log.i(LOGTAG, "Failed to save persistent state", e);
+ } finally {
+ p.recycle();
+ }
+ }
+}
diff --git a/src/com/android/browser/DataController.java b/src/com/android/browser/DataController.java
new file mode 100644
index 0000000..eb47080
--- /dev/null
+++ b/src/com/android/browser/DataController.java
@@ -0,0 +1,302 @@
+/*
+ * 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.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.BrowserContract.History;
+import com.android.browser.provider.BrowserProvider2.Thumbnails;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+public class DataController {
+ private static final String LOGTAG = "DataController";
+ // Message IDs
+ private static final int HISTORY_UPDATE_VISITED = 100;
+ private static final int HISTORY_UPDATE_TITLE = 101;
+ private static final int QUERY_URL_IS_BOOKMARK = 200;
+ private static final int TAB_LOAD_THUMBNAIL = 201;
+ private static final int TAB_SAVE_THUMBNAIL = 202;
+ private static final int TAB_DELETE_THUMBNAIL = 203;
+ private static DataController sInstance;
+
+ private Context mContext;
+ private DataControllerHandler mDataHandler;
+ private Handler mCbHandler; // To respond on the UI thread
+ private ByteBuffer mBuffer; // to capture thumbnails
+
+ /* package */ static interface OnQueryUrlIsBookmark {
+ void onQueryUrlIsBookmark(String url, boolean isBookmark);
+ }
+ private static class CallbackContainer {
+ Object replyTo;
+ Object[] args;
+ }
+
+ private static class DCMessage {
+ int what;
+ Object obj;
+ Object replyTo;
+ DCMessage(int w, Object o) {
+ what = w;
+ obj = o;
+ }
+ }
+
+ /* package */ static DataController getInstance(Context c) {
+ if (sInstance == null) {
+ sInstance = new DataController(c);
+ }
+ return sInstance;
+ }
+
+ private DataController(Context c) {
+ mContext = c.getApplicationContext();
+ mDataHandler = new DataControllerHandler();
+ mDataHandler.start();
+ mCbHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ CallbackContainer cc = (CallbackContainer) msg.obj;
+ switch (msg.what) {
+ case QUERY_URL_IS_BOOKMARK: {
+ OnQueryUrlIsBookmark cb = (OnQueryUrlIsBookmark) cc.replyTo;
+ String url = (String) cc.args[0];
+ boolean isBookmark = (Boolean) cc.args[1];
+ cb.onQueryUrlIsBookmark(url, isBookmark);
+ break;
+ }
+ }
+ }
+ };
+ }
+
+ public void updateVisitedHistory(String url) {
+ mDataHandler.sendMessage(HISTORY_UPDATE_VISITED, url);
+ }
+
+ public void updateHistoryTitle(String url, String title) {
+ mDataHandler.sendMessage(HISTORY_UPDATE_TITLE, new String[] { url, title });
+ }
+
+ public void queryBookmarkStatus(String url, OnQueryUrlIsBookmark replyTo) {
+ if (url == null || url.trim().length() == 0) {
+ // null or empty url is never a bookmark
+ replyTo.onQueryUrlIsBookmark(url, false);
+ return;
+ }
+ mDataHandler.sendMessage(QUERY_URL_IS_BOOKMARK, url.trim(), replyTo);
+ }
+
+ public void loadThumbnail(Tab tab) {
+ mDataHandler.sendMessage(TAB_LOAD_THUMBNAIL, tab);
+ }
+
+ public void deleteThumbnail(Tab tab) {
+ mDataHandler.sendMessage(TAB_DELETE_THUMBNAIL, tab.getId());
+ }
+
+ public void saveThumbnail(Tab tab) {
+ mDataHandler.sendMessage(TAB_SAVE_THUMBNAIL, tab);
+ }
+
+ // The standard Handler and Message classes don't allow the queue manipulation
+ // we want (such as peeking). So we use our own queue.
+ class DataControllerHandler extends Thread {
+ private BlockingQueue<DCMessage> mMessageQueue
+ = new LinkedBlockingQueue<DCMessage>();
+
+ public DataControllerHandler() {
+ super("DataControllerHandler");
+ }
+
+ @Override
+ public void run() {
+ setPriority(Thread.MIN_PRIORITY);
+ while (true) {
+ try {
+ handleMessage(mMessageQueue.take());
+ } catch (InterruptedException ex) {
+ break;
+ }
+ }
+ }
+
+ void sendMessage(int what, Object obj) {
+ DCMessage m = new DCMessage(what, obj);
+ mMessageQueue.add(m);
+ }
+
+ void sendMessage(int what, Object obj, Object replyTo) {
+ DCMessage m = new DCMessage(what, obj);
+ m.replyTo = replyTo;
+ mMessageQueue.add(m);
+ }
+
+ private void handleMessage(DCMessage msg) {
+ switch (msg.what) {
+ case HISTORY_UPDATE_VISITED:
+ doUpdateVisitedHistory((String) msg.obj);
+ break;
+ case HISTORY_UPDATE_TITLE:
+ String[] args = (String[]) msg.obj;
+ doUpdateHistoryTitle(args[0], args[1]);
+ break;
+ case QUERY_URL_IS_BOOKMARK:
+ // TODO: Look for identical messages in the queue and remove them
+ // TODO: Also, look for partial matches and merge them (such as
+ // multiple callbacks querying the same URL)
+ doQueryBookmarkStatus((String) msg.obj, msg.replyTo);
+ break;
+ case TAB_LOAD_THUMBNAIL:
+ doLoadThumbnail((Tab) msg.obj);
+ break;
+ case TAB_DELETE_THUMBNAIL:
+ ContentResolver cr = mContext.getContentResolver();
+ try {
+ cr.delete(ContentUris.withAppendedId(
+ Thumbnails.CONTENT_URI, (Long)msg.obj),
+ null, null);
+ } catch (Throwable t) {}
+ break;
+ case TAB_SAVE_THUMBNAIL:
+ doSaveThumbnail((Tab)msg.obj);
+ break;
+ }
+ }
+
+ private byte[] getCaptureBlob(Tab tab) {
+ synchronized (tab) {
+ Bitmap capture = tab.getScreenshot();
+ if (capture == null) {
+ return null;
+ }
+ if (mBuffer == null || mBuffer.limit() < capture.getByteCount()) {
+ mBuffer = ByteBuffer.allocate(capture.getByteCount());
+ }
+ capture.copyPixelsToBuffer(mBuffer);
+ mBuffer.rewind();
+ return mBuffer.array();
+ }
+ }
+
+ private void doSaveThumbnail(Tab tab) {
+ byte[] blob = getCaptureBlob(tab);
+ if (blob == null) {
+ return;
+ }
+ ContentResolver cr = mContext.getContentResolver();
+ ContentValues values = new ContentValues();
+ values.put(Thumbnails._ID, tab.getId());
+ values.put(Thumbnails.THUMBNAIL, blob);
+ cr.insert(Thumbnails.CONTENT_URI, values);
+ }
+
+ private void doLoadThumbnail(Tab tab) {
+ ContentResolver cr = mContext.getContentResolver();
+ Cursor c = null;
+ try {
+ Uri uri = ContentUris.withAppendedId(Thumbnails.CONTENT_URI, tab.getId());
+ c = cr.query(uri, new String[] {Thumbnails._ID,
+ Thumbnails.THUMBNAIL}, null, null, null);
+ if (c.moveToFirst()) {
+ byte[] data = c.getBlob(1);
+ if (data != null && data.length > 0) {
+ tab.updateCaptureFromBlob(data);
+ }
+ }
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ private void doUpdateVisitedHistory(String url) {
+ ContentResolver cr = mContext.getContentResolver();
+ Cursor c = null;
+ try {
+ c = cr.query(History.CONTENT_URI, new String[] {History._ID, History.VISITS},
+ History.URL + "=?", new String[] { url }, null);
+ if (c.moveToFirst()) {
+ ContentValues values = new ContentValues();
+ values.put(History.VISITS, c.getInt(1) + 1);
+ values.put(History.DATE_LAST_VISITED, System.currentTimeMillis());
+ cr.update(ContentUris.withAppendedId(History.CONTENT_URI, c.getLong(0)),
+ values, null, null);
+ } else {
+ android.provider.Browser.truncateHistory(cr);
+ ContentValues values = new ContentValues();
+ values.put(History.URL, url);
+ values.put(History.VISITS, 1);
+ values.put(History.DATE_LAST_VISITED, System.currentTimeMillis());
+ values.put(History.TITLE, url);
+ values.put(History.DATE_CREATED, 0);
+ values.put(History.USER_ENTERED, 0);
+ cr.insert(History.CONTENT_URI, values);
+ }
+ } finally {
+ if (c != null) c.close();
+ }
+ }
+
+ private void doQueryBookmarkStatus(String url, Object replyTo) {
+ // Check to see if the site is bookmarked
+ Cursor cursor = null;
+ boolean isBookmark = false;
+ try {
+ cursor = mContext.getContentResolver().query(
+ BookmarkUtils.getBookmarksUri(mContext),
+ new String[] { BrowserContract.Bookmarks.URL },
+ BrowserContract.Bookmarks.URL + " == ?",
+ new String[] { url },
+ null);
+ isBookmark = cursor.moveToFirst();
+ } catch (SQLiteException e) {
+ Log.e(LOGTAG, "Error checking for bookmark: " + e);
+ } finally {
+ if (cursor != null) cursor.close();
+ }
+ CallbackContainer cc = new CallbackContainer();
+ cc.replyTo = replyTo;
+ cc.args = new Object[] { url, isBookmark };
+ mCbHandler.obtainMessage(QUERY_URL_IS_BOOKMARK, cc).sendToTarget();
+ }
+
+ private void doUpdateHistoryTitle(String url, String title) {
+ ContentResolver cr = mContext.getContentResolver();
+ ContentValues values = new ContentValues();
+ values.put(History.TITLE, title);
+ cr.update(History.CONTENT_URI, values, History.URL + "=?",
+ new String[] { url });
+ }
+ }
+}
diff --git a/src/com/android/browser/DataUri.java b/src/com/android/browser/DataUri.java
new file mode 100644
index 0000000..dae3caf
--- /dev/null
+++ b/src/com/android/browser/DataUri.java
@@ -0,0 +1,73 @@
+/*
+ * 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 java.net.MalformedURLException;
+
+import android.util.Base64;
+/**
+ * Class extracts the mime type and data from a data uri.
+ * A data URI is of the form:
+ * <pre>
+ * data:[<MIME-type>][;charset=<encoding>][;base64],<data>
+ * </pre>
+ */
+public class DataUri {
+ private static final String DATA_URI_PREFIX = "data:";
+ private static final String BASE_64_ENCODING = ";base64";
+
+ private String mMimeType;
+ private byte[] mData;
+
+ public DataUri(String uri) throws MalformedURLException {
+ if (!isDataUri(uri)) {
+ throw new MalformedURLException("Not a data URI");
+ }
+
+ int commaIndex = uri.indexOf(',', DATA_URI_PREFIX.length());
+ if (commaIndex < 0) {
+ throw new MalformedURLException("Comma expected in data URI");
+ }
+ String contentType = uri.substring(DATA_URI_PREFIX.length(),
+ commaIndex);
+ mData = uri.substring(commaIndex + 1).getBytes();
+ if (contentType.contains(BASE_64_ENCODING)) {
+ mData = Base64.decode(mData, Base64.DEFAULT);
+ }
+ int semiIndex = contentType.indexOf(';');
+ if (semiIndex > 0) {
+ mMimeType = contentType.substring(0, semiIndex);
+ } else {
+ mMimeType = contentType;
+ }
+ }
+
+ /**
+ * Returns true if the text passed in appears to be a data URI.
+ */
+ public static boolean isDataUri(String text)
+ {
+ return text.startsWith(DATA_URI_PREFIX);
+ }
+
+ public String getMimeType() {
+ return mMimeType;
+ }
+
+ public byte[] getData() {
+ return mData;
+ }
+}
diff --git a/src/com/android/browser/DateSortedExpandableListAdapter.java b/src/com/android/browser/DateSortedExpandableListAdapter.java
new file mode 100644
index 0000000..529e1ed
--- /dev/null
+++ b/src/com/android/browser/DateSortedExpandableListAdapter.java
@@ -0,0 +1,380 @@
+/*
+ * 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.R;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.DateSorter;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.ExpandableListView;
+import android.widget.TextView;
+
+/**
+ * ExpandableListAdapter which separates data into categories based on date.
+ * Used for History and Downloads.
+ */
+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 Cursor mCursor;
+ private DateSorter mDateSorter;
+ private int mDateIndex;
+ private int mIdIndex;
+ private Context mContext;
+
+ boolean mDataValid;
+
+ DataSetObserver mDataSetObserver = new DataSetObserver() {
+ @Override
+ public void onChanged() {
+ mDataValid = true;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ mDataValid = false;
+ notifyDataSetInvalidated();
+ }
+ };
+
+ public DateSortedExpandableListAdapter(Context context, int dateIndex) {
+ mContext = context;
+ mDateSorter = new DateSorter(context);
+ mDateIndex = dateIndex;
+ mDataValid = false;
+ mIdIndex = -1;
+ }
+
+ /**
+ * Set up the bins for determining which items belong to which groups.
+ */
+ private void buildMap() {
+ // The cursor is sorted by date
+ // The ItemMap will store the number of items in each bin.
+ int array[] = new int[DateSorter.DAY_COUNT];
+ // Zero out the array.
+ for (int j = 0; j < DateSorter.DAY_COUNT; j++) {
+ array[j] = 0;
+ }
+ mNumberOfBins = 0;
+ int dateIndex = -1;
+ if (mCursor.moveToFirst() && mCursor.getCount() > 0) {
+ while (!mCursor.isAfterLast()) {
+ long date = getLong(mDateIndex);
+ int index = mDateSorter.getIndex(date);
+ if (index > dateIndex) {
+ mNumberOfBins++;
+ if (index == DateSorter.DAY_COUNT - 1) {
+ // We are already in the last bin, so it will
+ // include all the remaining items
+ array[index] = mCursor.getCount()
+ - mCursor.getPosition();
+ break;
+ }
+ dateIndex = index;
+ }
+ array[dateIndex]++;
+ mCursor.moveToNext();
+ }
+ }
+ mItemMap = array;
+ }
+
+ /**
+ * Get the byte array at cursorIndex from the Cursor. Assumes the Cursor
+ * has already been moved to the correct position. Along with
+ * {@link #getInt} and {@link #getString}, these are provided so the client
+ * does not need to access the Cursor directly
+ * @param cursorIndex Index to query the Cursor.
+ * @return corresponding byte array from the Cursor.
+ */
+ /* package */ byte[] getBlob(int cursorIndex) {
+ if (!mDataValid) return null;
+ return mCursor.getBlob(cursorIndex);
+ }
+
+ /* package */ Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Get the integer at cursorIndex from the Cursor. Assumes the Cursor has
+ * already been moved to the correct position. Along with
+ * {@link #getBlob} and {@link #getString}, these are provided so the client
+ * does not need to access the Cursor directly
+ * @param cursorIndex Index to query the Cursor.
+ * @return corresponding integer from the Cursor.
+ */
+ /* package */ int getInt(int cursorIndex) {
+ if (!mDataValid) return 0;
+ return mCursor.getInt(cursorIndex);
+ }
+
+ /**
+ * Get the long at cursorIndex from the Cursor. Assumes the Cursor has
+ * already been moved to the correct position.
+ */
+ /* package */ long getLong(int cursorIndex) {
+ if (!mDataValid) return 0;
+ return mCursor.getLong(cursorIndex);
+ }
+
+ /**
+ * Get the String at cursorIndex from the Cursor. Assumes the Cursor has
+ * already been moved to the correct position. Along with
+ * {@link #getInt} and {@link #getInt}, these are provided so the client
+ * does not need to access the Cursor directly
+ * @param cursorIndex Index to query the Cursor.
+ * @return corresponding String from the Cursor.
+ */
+ /* package */ String getString(int cursorIndex) {
+ if (!mDataValid) return null;
+ return mCursor.getString(cursorIndex);
+ }
+
+ /**
+ * Determine which group an item belongs to.
+ * @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()) {
+ if (getLong(mIdIndex) == childId) {
+ int bin = mDateSorter.getIndex(getLong(mDateIndex));
+ // bin is the same as the group if the number of bins is the
+ // same as DateSorter
+ 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++;
+ }
+ }
+ break;
+ }
+ }
+ return group;
+ }
+
+ /**
+ * Translates from a group position in the ExpandableList to a bin. This is
+ * necessary because some groups have no history items, so we do not include
+ * those in the ExpandableList.
+ * @param groupPosition Position in the ExpandableList's set of groups
+ * @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");
+ }
+ if (DateSorter.DAY_COUNT == mNumberOfBins || 0 == mNumberOfBins) {
+ // In the first case, we have exactly the same number of bins
+ // as our maximum possible, so there is no need to do a
+ // conversion
+ // The second statement is in case this method gets called when
+ // the array is empty, in which case the provided groupPosition
+ // will do fine.
+ return groupPosition;
+ }
+ int arrayPosition = -1;
+ while (groupPosition > -1) {
+ arrayPosition++;
+ if (mItemMap[arrayPosition] != 0) {
+ groupPosition--;
+ }
+ }
+ return arrayPosition;
+ }
+
+ /**
+ * Move the cursor to the position indicated.
+ * @param packedPosition Position in packed position representation.
+ * @return True on success, false otherwise.
+ */
+ boolean moveCursorToPackedChildPosition(long packedPosition) {
+ if (ExpandableListView.getPackedPositionType(packedPosition) !=
+ ExpandableListView.PACKED_POSITION_TYPE_CHILD) {
+ return false;
+ }
+ int groupPosition = ExpandableListView.getPackedPositionGroup(
+ packedPosition);
+ int childPosition = ExpandableListView.getPackedPositionChild(
+ packedPosition);
+ return moveCursorToChildPosition(groupPosition, childPosition);
+ }
+
+ /**
+ * Move the cursor the the position indicated.
+ * @param groupPosition Index of the group containing the desired item.
+ * @param childPosition Index of the item within the specified group.
+ * @return boolean False if the cursor is closed, so the Cursor was not
+ * moved. True on success.
+ */
+ /* package */ boolean moveCursorToChildPosition(int groupPosition,
+ int childPosition) {
+ if (!mDataValid || mCursor.isClosed()) {
+ return false;
+ }
+ groupPosition = groupPositionToBin(groupPosition);
+ int index = childPosition;
+ for (int i = 0; i < groupPosition; i++) {
+ index += mItemMap[i];
+ }
+ return mCursor.moveToPosition(index);
+ }
+
+ public void changeCursor(Cursor cursor) {
+ if (cursor == mCursor) {
+ return;
+ }
+ 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);
+ item = (TextView) factory.inflate(R.layout.history_header, null);
+ } else {
+ item = (TextView) convertView;
+ }
+ String label = mDateSorter.getLabel(groupPositionToBin(groupPosition));
+ item.setText(label);
+ 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;
+ }
+
+ @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 !mDataValid || mCursor == null || mCursor.isClosed() || mCursor.getCount() == 0;
+ }
+}
diff --git a/src/com/android/browser/DeviceAccountLogin.java b/src/com/android/browser/DeviceAccountLogin.java
new file mode 100644
index 0000000..0638f97
--- /dev/null
+++ b/src/com/android/browser/DeviceAccountLogin.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2011 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.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerCallback;
+import android.accounts.AccountManagerFuture;
+import android.app.Activity;
+import android.os.Bundle;
+import org.codeaurora.swe.WebView;
+
+public class DeviceAccountLogin implements
+ AccountManagerCallback<Bundle> {
+
+ private final Activity mActivity;
+ private final WebView mWebView;
+ private final Tab mTab;
+ private final WebViewController mWebViewController;
+ private final AccountManager mAccountManager;
+ Account[] mAccounts;
+ private AutoLoginCallback mCallback;
+ private String mAuthToken;
+
+ // Current state of the login.
+ private int mState = INITIAL;
+
+ public static final int INITIAL = 0;
+ public static final int FAILED = 1;
+ public static final int PROCESSING = 2;
+
+ public interface AutoLoginCallback {
+ public void loginFailed();
+ }
+
+ public DeviceAccountLogin(Activity activity, WebView view, Tab tab,
+ WebViewController controller) {
+ mActivity = activity;
+ mWebView = view;
+ mTab = tab;
+ mWebViewController = controller;
+ mAccountManager = AccountManager.get(activity);
+ }
+
+ public void handleLogin(String realm, String account, String args) {
+ mAccounts = mAccountManager.getAccountsByType(realm);
+ mAuthToken = "weblogin:" + args;
+
+ // No need to display UI if there are no accounts.
+ if (mAccounts.length == 0) {
+ return;
+ }
+
+ // Verify the account before using it.
+ for (Account a : mAccounts) {
+ if (a.name.equals(account)) {
+ // Handle the automatic login case where the service gave us an
+ // account to use.
+ mAccountManager.getAuthToken(a, mAuthToken, null,
+ mActivity, this, null);
+ return;
+ }
+ }
+
+ displayLoginUi();
+ }
+
+ @Override
+ public void run(AccountManagerFuture<Bundle> value) {
+ try {
+ String result = value.getResult().getString(
+ AccountManager.KEY_AUTHTOKEN);
+ if (result == null) {
+ loginFailed();
+ } else {
+ mWebView.loadUrl(result);
+ mTab.setDeviceAccountLogin(null);
+ if (mTab.inForeground()) {
+ mWebViewController.hideAutoLogin(mTab);
+ }
+ }
+ } catch (Exception e) {
+ loginFailed();
+ }
+ }
+
+ public int getState() {
+ return mState;
+ }
+
+ private void loginFailed() {
+ mState = FAILED;
+ if (mTab.getDeviceAccountLogin() == null) {
+ displayLoginUi();
+ } else {
+ if (mCallback != null) {
+ mCallback.loginFailed();
+ }
+ }
+ }
+
+ private void displayLoginUi() {
+ // Display the account picker.
+ mTab.setDeviceAccountLogin(this);
+ if (mTab.inForeground()) {
+ mWebViewController.showAutoLogin(mTab);
+ }
+ }
+
+ public void cancel() {
+ mTab.setDeviceAccountLogin(null);
+ }
+
+ public void login(int accountIndex, AutoLoginCallback cb) {
+ mState = PROCESSING;
+ mCallback = cb;
+ mAccountManager.getAuthToken(
+ mAccounts[accountIndex], mAuthToken, null,
+ mActivity, this, null);
+ }
+
+ public String[] getAccountNames() {
+ String[] names = new String[mAccounts.length];
+ for (int i = 0; i < mAccounts.length; i++) {
+ names[i] = mAccounts[i].name;
+ }
+ return names;
+ }
+}
diff --git a/src/com/android/browser/DownloadHandler.java b/src/com/android/browser/DownloadHandler.java
new file mode 100644
index 0000000..65c5f85
--- /dev/null
+++ b/src/com/android/browser/DownloadHandler.java
@@ -0,0 +1,629 @@
+/*
+ * 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.app.DownloadManager.Request;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.StatFs;
+import android.os.storage.StorageManager;
+import android.util.Log;
+import org.codeaurora.swe.CookieManager;
+import android.webkit.URLUtil;
+import android.widget.Toast;
+
+import com.android.browser.R;
+import com.android.browser.platformsupport.WebAddress;
+import com.android.browser.reflect.ReflectHelper;
+
+import java.io.File;
+/**
+ * Handle download requests
+ */
+public class DownloadHandler {
+
+ private static final boolean LOGD_ENABLED =
+ com.android.browser.Browser.LOGD_ENABLED;
+
+ private static final String LOGTAG = "DLHandler";
+ private static String mInternalStorage;
+ private static String mExternalStorage;
+ private final static String INVALID_PATH = "/storage";
+
+ public static void startingDownload(Activity activity,
+ String url, String userAgent, String contentDisposition,
+ String mimetype, String referer, boolean privateBrowsing, long contentLength,
+ String filename, String downloadPath) {
+ // 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;
+ try {
+ request = new DownloadManager.Request(uri);
+ } catch (IllegalArgumentException e) {
+ Toast.makeText(activity, R.string.cannot_download, Toast.LENGTH_SHORT).show();
+ return;
+ }
+ request.setMimeType(mimetype);
+ // set downloaded file destination to /sdcard/Download.
+ // or, should it be set to one of several Environment.DIRECTORY* dirs
+ // depending on mimetype?
+ try {
+ setDestinationDir(downloadPath, filename, request);
+ } catch (Exception e) {
+ showNoEnoughMemoryDialog(activity);
+ return;
+ }
+ // 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, privateBrowsing);
+ request.addRequestHeader("cookie", cookies);
+ request.addRequestHeader("User-Agent", userAgent);
+ request.addRequestHeader("Referer", referer);
+ request.setNotificationVisibility(
+ DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
+ final DownloadManager manager = (DownloadManager) activity
+ .getSystemService(Context.DOWNLOAD_SERVICE);
+ new Thread("Browser download") {
+ public void run() {
+ manager.enqueue(request);
+ }
+ }.start();
+ showStartDownloadToast(activity);
+ }
+
+ private static boolean isAudioFileType(int fileType){
+ Object[] params = {Integer.valueOf(fileType)};
+ Class[] type = new Class[] {int.class};
+ Boolean result = (Boolean) ReflectHelper.invokeStaticMethod("android.media.MediaFile",
+ "isAudioFileType", type, params);
+ return result;
+ }
+
+ private static boolean isVideoFileType(int fileType){
+ Object[] params = {Integer.valueOf(fileType)};
+ Class[] type = new Class[] {int.class};
+ Boolean result = (Boolean) ReflectHelper.invokeStaticMethod("android.media.MediaFile",
+ "isVideoFileType", type, params);
+ return result;
+ }
+
+ /**
+ * 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
+ * @param referer The referer associated with the downloaded url
+ * @param privateBrowsing If the request is coming from a private browsing tab.
+ */
+ public static boolean onDownloadStart(final Activity activity, final String url,
+ final String userAgent, final String contentDisposition, final String mimetype,
+ final String referer, final boolean privateBrowsing, final 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)) {
+ // Add for Carrier Feature - When open an audio/video link, prompt a dialog
+ // to let the user choose play or download operation.
+ Uri uri = Uri.parse(url);
+ String scheme = uri.getScheme();
+ Log.v(LOGTAG, "scheme:" + scheme + ", mimetype:" + mimetype);
+ // Some mimetype for audio/video files is not started with "audio" or "video",
+ // such as ogg audio file with mimetype "application/ogg". So we also check
+ // file type by MediaFile.isAudioFileType() and MediaFile.isVideoFileType().
+ // For those file types other than audio or video, download it immediately.
+ Object[] params = {mimetype};
+ Class[] type = new Class[] {String.class};
+ Integer result = (Integer) ReflectHelper.invokeStaticMethod("android.media.MediaFile",
+ "getFileTypeForMimeType", type, params);
+ int fileType = result.intValue();
+ if ("http".equalsIgnoreCase(scheme) &&
+ (mimetype.startsWith("audio/") ||
+ mimetype.startsWith("video/") ||
+ isAudioFileType(fileType) ||
+ isVideoFileType(fileType))) {
+ new AlertDialog.Builder(activity)
+ .setTitle(R.string.application_name)
+ .setIcon(R.drawable.default_video_poster)
+ .setMessage(R.string.http_video_msg)
+ .setPositiveButton(R.string.video_save, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ onDownloadStartNoStream(activity, url, userAgent, contentDisposition,
+ mimetype, referer, privateBrowsing, contentLength);
+ }
+ })
+ .setNegativeButton(R.string.video_play, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(Uri.parse(url), mimetype);
+ try {
+ String title = URLUtil.guessFileName(url, contentDisposition, mimetype);
+ intent.putExtra(Intent.EXTRA_TITLE, title);
+ activity.startActivity(intent);
+ } catch (ActivityNotFoundException ex) {
+ Log.w(LOGTAG, "When http stream play, activity not found for "
+ + mimetype + " over " + Uri.parse(url).getScheme(), ex);
+ }
+ }
+ }).show();
+
+ return true;
+ }
+ // 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 false;
+ } 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, referer, privateBrowsing, contentLength);
+ return false;
+ }
+
+ // 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 == ']' || c == '|') {
+ needed = true;
+ break;
+ }
+ }
+ if (needed == false) {
+ return path;
+ }
+
+ StringBuilder sb = new StringBuilder("");
+ for (char c : chars) {
+ if (c == '[' || 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
+ * @param referer The referer associated with the downloaded url
+ * @param privateBrowsing If the request is coming from a private browsing tab.
+ */
+ /* package */static void onDownloadStartNoStream(Activity activity,
+ String url, String userAgent, String contentDisposition,
+ String mimetype, String referer, boolean privateBrowsing, long contentLength) {
+
+ initStorageDefaultPath(activity);
+ 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)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setMessage(msg)
+ .setPositiveButton(R.string.ok, null)
+ .show();
+ return;
+ }
+
+ 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(activity, url, userAgent, referer,
+ privateBrowsing, filename).start();
+ } else {
+ startDownloadSettings(activity, url, userAgent, contentDisposition, mimetype, referer,
+ privateBrowsing, contentLength, filename);
+ }
+
+ }
+
+ public static void initStorageDefaultPath(Context context) {
+ mExternalStorage = getExternalStorageDirectory(context);
+ if (isPhoneStorageSupported()) {
+ mInternalStorage = Environment.getExternalStorageDirectory().getPath();
+ } else {
+ mInternalStorage = null;
+ }
+ }
+
+ public static void startDownloadSettings(Activity activity,
+ String url, String userAgent, String contentDisposition,
+ String mimetype, String referer, boolean privateBrowsing, long contentLength,
+ String filename) {
+ Bundle fileInfo = new Bundle();
+ fileInfo.putString("url", url);
+ fileInfo.putString("userAgent", userAgent);
+ fileInfo.putString("contentDisposition", contentDisposition);
+ fileInfo.putString("mimetype", mimetype);
+ fileInfo.putString("referer", referer);
+ fileInfo.putLong("contentLength", contentLength);
+ fileInfo.putBoolean("privateBrowsing", privateBrowsing);
+ fileInfo.putString("filename", filename);
+ Intent intent = new Intent("android.intent.action.BROWSERDOWNLOAD");
+ intent.putExtras(fileInfo);
+ activity.startActivity(intent);
+ }
+
+ public static void setAppointedFolder(String downloadPath) {
+ File file = new File(downloadPath);
+ if (file.exists()) {
+ if (!file.isDirectory()) {
+ throw new IllegalStateException(file.getAbsolutePath() +
+ " already exists and is not a directory");
+ }
+ } else {
+ if (!file.mkdir()) {
+ throw new IllegalStateException("Unable to create directory: " +
+ file.getAbsolutePath());
+ }
+ }
+ }
+
+ private static void setDestinationDir(String downloadPath, String filename, Request request) {
+ File file = new File(downloadPath);
+ if (file.exists()) {
+ if (!file.isDirectory()) {
+ throw new IllegalStateException(file.getAbsolutePath() +
+ " already exists and is not a directory");
+ }
+ } else {
+ if (!file.mkdir()) {
+ throw new IllegalStateException("Unable to create directory: " +
+ file.getAbsolutePath());
+ }
+ }
+ setDestinationFromBase(file, filename, request);
+ }
+
+ private static void setDestinationFromBase(File file, String filename, Request request) {
+ if (filename == null) {
+ throw new NullPointerException("filename cannot be null");
+ }
+ request.setDestinationUri(Uri.withAppendedPath(Uri.fromFile(file), filename));
+ }
+
+ public static void fileExistQueryDialog(Activity activity) {
+ new AlertDialog.Builder(activity)
+ .setTitle(R.string.download_file_exist)
+ .setIcon(android.R.drawable.ic_dialog_info)
+ .setMessage(R.string.download_file_exist_msg)
+ // if yes, delete existed file and start new download thread
+ .setPositiveButton(R.string.ok, null)
+ // if no, do nothing at all
+ .show();
+ }
+
+ public static long getAvailableMemory(String root) {
+ StatFs stat = new StatFs(root);
+ final long LEFT10MByte = 2560;
+ long blockSize = stat.getBlockSize();
+ long availableBlocks = stat.getAvailableBlocks() - LEFT10MByte;
+ return availableBlocks * blockSize;
+ }
+
+ public static void showNoEnoughMemoryDialog(Activity mContext) {
+ new AlertDialog.Builder(mContext)
+ .setTitle(R.string.download_no_enough_memory)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setMessage(R.string.download_no_enough_memory)
+ .setPositiveButton(R.string.ok, null)
+ .show();
+ }
+
+ public static boolean manageNoEnoughMemory(long contentLength, String root) {
+ long mAvailableBytes = getAvailableMemory(root);
+ if (mAvailableBytes > 0) {
+ if (contentLength > mAvailableBytes) {
+ return true;
+ }
+ } else {
+ return true;
+ }
+ return false;
+ }
+
+ public static void showStartDownloadToast(Activity activity) {
+ Intent intent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
+ activity.startActivity(intent);
+ Toast.makeText(activity, R.string.download_pending, Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ /**
+ * wheather the storage status OK for download file
+ *
+ * @param activity
+ * @param filename the download file's name
+ * @param downloadPath the download file's path will be in
+ * @return boolean true is ok,and false is not
+ */
+ public static boolean isStorageStatusOK(Activity activity, String filename, String downloadPath) {
+ if (downloadPath.equals(INVALID_PATH)) {
+ new AlertDialog.Builder(activity)
+ .setTitle(R.string.path_wrong)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.invalid_path)
+ .setPositiveButton(R.string.ok, null)
+ .show();
+ return false;
+ }
+
+ if (!(isPhoneStorageSupported() && downloadPath.contains(mInternalStorage))) {
+ String status = getExternalStorageState(activity);
+ 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 false;
+ }
+ } else {
+ String status = Environment.getExternalStorageState();
+ if (!status.equals(Environment.MEDIA_MOUNTED)) {
+ int mTitle = R.string.download_path_unavailable_dlg_title;
+ String mMsg = activity.getString(R.string.download_path_unavailable_dlg_msg);
+ new AlertDialog.Builder(activity)
+ .setTitle(mTitle)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(mMsg)
+ .setPositiveButton(R.string.ok, null)
+ .show();
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * wheather support Phone Storage
+ *
+ * @return boolean true support Phone Storage ,false will be not
+ */
+ public static boolean isPhoneStorageSupported() {
+ return true;
+ }
+
+ /**
+ * show Dialog to warn filename is null
+ *
+ * @param activity
+ */
+ public static void showFilenameEmptyDialog(Activity activity) {
+ new AlertDialog.Builder(activity)
+ .setTitle(R.string.filename_empty_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.filename_empty_msg)
+ .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ }
+ })
+ .show();
+ }
+
+ /**
+ * get the filename except the suffix and dot
+ *
+ * @return String the filename except suffix and dot
+ */
+ public static String getFilenameBase(String filename) {
+ int dotindex = filename.lastIndexOf('.');
+ if (dotindex != -1) {
+ return filename.substring(0, dotindex);
+ } else {
+ return "";
+ }
+ }
+
+ /**
+ * get the filename's extension from filename
+ *
+ * @param filename the download filename, may be the user entered
+ * @return String the filename's extension
+ */
+ public static String getFilenameExtension(String filename) {
+ int dotindex = filename.lastIndexOf('.');
+ if (dotindex != -1) {
+ return filename.substring(dotindex + 1);
+ } else {
+ return "";
+ }
+ }
+
+ public static String getDefaultDownloadPath(Context context) {
+ String defaultDownloadPath;
+
+ String defaultStorage;
+ if (isPhoneStorageSupported()) {
+ defaultStorage = Environment.getExternalStorageDirectory().getPath();
+ } else {
+ defaultStorage = getExternalStorageDirectory(context);
+ }
+
+ defaultDownloadPath = defaultStorage + context.getString(R.string.download_default_path);
+ Log.e(LOGTAG, "defaultStorage directory is : " + defaultDownloadPath);
+ return defaultDownloadPath;
+ }
+
+ /**
+ * translate the directory name into a name which is easy to know for user
+ *
+ * @param activity
+ * @param downloadPath
+ * @return String
+ */
+ public static String getDownloadPathForUser(Activity activity, String downloadPath) {
+ if (downloadPath == null) {
+ return downloadPath;
+ }
+ final String phoneStorageDir;
+ final String sdCardDir = getExternalStorageDirectory(activity);
+ if (isPhoneStorageSupported()) {
+ phoneStorageDir = Environment.getExternalStorageDirectory().getPath();
+ } else {
+ phoneStorageDir = null;
+ }
+
+ if (sdCardDir != null && downloadPath.startsWith(sdCardDir)) {
+ String sdCardLabel = activity.getResources().getString(
+ R.string.download_path_sd_card_label);
+ downloadPath = downloadPath.replace(sdCardDir, sdCardLabel);
+ } else if ((phoneStorageDir != null) && downloadPath.startsWith(phoneStorageDir)) {
+ String phoneStorageLabel = activity.getResources().getString(
+ R.string.download_path_phone_storage_label);
+ downloadPath = downloadPath.replace(phoneStorageDir, phoneStorageLabel);
+ }
+ return downloadPath;
+ }
+
+ private static boolean isRemovable(Object obj) {
+ return (Boolean) ReflectHelper.invokeMethod(obj,
+ "isRemovable", null, null);
+ }
+
+ private static boolean allowMassStorage(Object obj) {
+ return (Boolean) ReflectHelper.invokeMethod(obj,
+ "allowMassStorage", null, null);
+ }
+
+ private static String getPath(Object obj) {
+ return (String) ReflectHelper.invokeMethod(obj,
+ "getPath", null, null);
+ }
+
+ private static String getExternalStorageDirectory(Context context) {
+ String sd = null;
+ StorageManager mStorageManager = (StorageManager) context
+ .getSystemService(Context.STORAGE_SERVICE);
+ Object[] volumes = (Object[]) ReflectHelper.invokeMethod(
+ mStorageManager, "getVolumeList", null, null);
+ for (int i = 0; i < volumes.length; i++) {
+ if (isRemovable(volumes[i]) && allowMassStorage(volumes[i])) {
+ sd = getPath(volumes[i]);
+ }
+ }
+ return sd;
+ }
+
+ private static String getExternalStorageState(Context context) {
+ StorageManager mStorageManager = (StorageManager) context
+ .getSystemService(Context.STORAGE_SERVICE);
+ String path = getExternalStorageDirectory(context);
+ Object[] params = {path};
+ Class[] type = new Class[] {String.class};
+ return (String) ReflectHelper.invokeMethod("android.os.storage.StorageManager",
+ "getVolumeState", type, params);
+ }
+}
diff --git a/src/com/android/browser/DownloadSettings.java b/src/com/android/browser/DownloadSettings.java
new file mode 100644
index 0000000..2b8a848
--- /dev/null
+++ b/src/com/android/browser/DownloadSettings.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of The Linux Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.android.browser;
+
+import java.io.File;
+
+import android.app.Activity;
+import android.content.Intent;
+import java.lang.Thread;
+
+import com.android.browser.R;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.text.format.*;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.view.Window;
+import android.widget.Toast;
+import android.text.TextUtils;
+
+public class DownloadSettings extends Activity {
+
+ private EditText downloadFilenameET;
+ private EditText downloadPathET;
+ private TextView downloadEstimateSize;
+ private TextView downloadEstimateTime;
+ private Button downloadStart;
+ private Button downloadCancel;
+ private String url;
+ private String userAgent;
+ private String contentDisposition;
+ private String mimetype;
+ private String referer;
+ private String filenameBase;
+ private String filename;
+ private String filenameExtension;
+ private boolean privateBrowsing;
+ private long contentLength;
+ private String downloadPath;
+ private String downloadPathForUser;
+ private static final int downloadRate = (1024 * 100 * 60);// Download Rate
+ // 100KB/s
+ private final static String LOGTAG = "DownloadSettings";
+ private final static int DOWNLOAD_PATH = 0;
+ private boolean isDownloadStarted = false;
+
+ private static final String ENV_EMULATED_STORAGE_TARGET = "EMULATED_STORAGE_TARGET";
+
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // initial the DownloadSettings view
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ setContentView(R.layout.download_settings);
+ downloadFilenameET = (EditText) findViewById(R.id.download_filename_edit);
+ downloadPathET = (EditText) findViewById(R.id.download_filepath_selected);
+ downloadEstimateSize = (TextView) findViewById(R.id.download_estimate_size_content);
+ downloadEstimateTime = (TextView) findViewById(R.id.download_estimate_time_content);
+ downloadStart = (Button) findViewById(R.id.download_start);
+ downloadCancel = (Button) findViewById(R.id.download_cancle);
+ downloadPathET.setOnClickListener(downloadPathListener);
+ downloadStart.setOnClickListener(downloadStartListener);
+ downloadCancel.setOnClickListener(downloadCancelListener);
+
+ // get the bundle from Intent
+ Intent intent = getIntent();
+ Bundle fileInfo = intent.getExtras();
+ url = fileInfo.getString("url");
+ userAgent = fileInfo.getString("userAgent");
+ contentDisposition = fileInfo.getString("contentDisposition");
+ mimetype = fileInfo.getString("mimetype");
+ referer = fileInfo.getString("referer");
+ contentLength = fileInfo.getLong("contentLength");
+ privateBrowsing = fileInfo.getBoolean("privateBrowsing");
+ filename = fileInfo.getString("filename");
+
+ // download filenamebase's length is depended on filenameLength's values
+ // if filenamebase.length >= flienameLength, destroy the last string!
+
+ filenameBase = DownloadHandler.getFilenameBase(filename);
+ if (filenameBase.length() >= (BrowserUtils.FILENAME_MAX_LENGTH)) {
+ filenameBase = filenameBase.substring(0, BrowserUtils.FILENAME_MAX_LENGTH);
+ }
+
+ // warring when user enter more over letters into the EditText
+ BrowserUtils.maxLengthFilter(DownloadSettings.this, downloadFilenameET,
+ BrowserUtils.FILENAME_MAX_LENGTH);
+
+ downloadFilenameET.setText(filenameBase);
+ downloadPath = chooseFolderFromMimeType(BrowserSettings.getInstance().getDownloadPath(),
+ mimetype);
+ downloadPathForUser = DownloadHandler.getDownloadPathForUser(DownloadSettings.this,
+ downloadPath);
+ setDownloadPathForUserText(downloadPathForUser);
+ setDownloadFileSizeText();
+ setDownloadFileTimeText();
+
+ }
+
+ private OnClickListener downloadPathListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+
+ // start filemanager for getting download path
+ try {
+ Intent downloadPathIntent = new Intent("com.android.fileexplorer.action.DIR_SEL");
+ DownloadSettings.this.startActivityForResult(downloadPathIntent, DOWNLOAD_PATH);
+ } catch (Exception e) {
+ String err_msg = getString(R.string.activity_not_found,
+ "com.android.fileexplorer.action.DIR_SEL");
+ Toast.makeText(DownloadSettings.this, err_msg, Toast.LENGTH_LONG).show();
+ }
+
+ }
+ };
+
+ private OnClickListener downloadStartListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ filenameBase = getFilenameBaseFromUserEnter();
+ // check the filename user enter is null or not
+ if (filenameBase.length() <= 0) {
+ DownloadHandler.showFilenameEmptyDialog(DownloadSettings.this);
+ return;
+ }
+
+ filenameExtension = DownloadHandler.getFilenameExtension(filename);
+ filename = filenameBase + "." + filenameExtension;
+
+ // check the storage status
+ if (!DownloadHandler.isStorageStatusOK(DownloadSettings.this, filename, downloadPath)) {
+ return;
+ }
+
+ // check the storage memory enough or not
+ try {
+ DownloadHandler.setAppointedFolder(downloadPath);
+ } catch (Exception e) {
+ DownloadHandler.showNoEnoughMemoryDialog(DownloadSettings.this);
+ return;
+ }
+ boolean isNoEnoughMemory = DownloadHandler.manageNoEnoughMemory(contentLength,
+ downloadPath);
+ if (isNoEnoughMemory) {
+ DownloadHandler.showNoEnoughMemoryDialog(DownloadSettings.this);
+ return;
+ }
+
+ // check the download file is exist or not
+ String fullFilename = downloadPath + "/" + filename;
+ if (mimetype != null && new File(fullFilename).exists()) {
+ DownloadHandler.fileExistQueryDialog(DownloadSettings.this);
+ return;
+ }
+
+ // staring downloading
+ DownloadHandler.startingDownload(DownloadSettings.this,
+ url, userAgent, contentDisposition,
+ mimetype, referer, privateBrowsing, contentLength,
+ Uri.encode(filename), downloadPath);
+ isDownloadStarted = true;
+ }
+ };
+
+ private OnClickListener downloadCancelListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+ };
+
+ protected void onDestroy() {
+ super.onDestroy();
+ }
+
+ protected void onPause() {
+ super.onPause();
+ if (isDownloadStarted) {
+ finish();
+ }
+ }
+
+ protected void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+
+ if (DOWNLOAD_PATH == requestCode) {
+ if (resultCode == Activity.RESULT_OK && intent != null) {
+ downloadPath = intent.getStringExtra("result_dir_sel");
+ if (downloadPath != null) {
+ String rawEmulatedStorageTarget = System.getenv(ENV_EMULATED_STORAGE_TARGET);
+ if (!TextUtils.isEmpty(rawEmulatedStorageTarget)) {
+ if (downloadPath.startsWith("/storage/sdcard0"))
+ downloadPath = downloadPath.replace("/storage/sdcard0",
+ "/storage/emulated/0");
+ if (downloadPath.startsWith("/storage/emulated/legacy"))
+ downloadPath = downloadPath.replace("/storage/emulated/legacy",
+ "/storage/emulated/0");
+ }
+ downloadPathForUser = DownloadHandler.getDownloadPathForUser(
+ DownloadSettings.this, downloadPath);
+ setDownloadPathForUserText(downloadPathForUser);
+ }
+ }
+ }
+ }
+
+ // Add for carrier feature - download to related folders by mimetype.
+ private static String chooseFolderFromMimeType(String path, String mimeType) {
+ String destinationFolder = null;
+ if (!path.contains(Environment.DIRECTORY_DOWNLOADS) || null == mimeType)
+ return path;
+ if (mimeType.startsWith("audio"))
+ destinationFolder = Environment.DIRECTORY_MUSIC;
+ else if (mimeType.startsWith("video"))
+ destinationFolder = Environment.DIRECTORY_MOVIES;
+ else if (mimeType.startsWith("image"))
+ destinationFolder = Environment.DIRECTORY_PICTURES;
+ if (null != destinationFolder)
+ path = path.replace(Environment.DIRECTORY_DOWNLOADS, destinationFolder);
+ return path;
+ }
+
+ /**
+ * show download path for user
+ *
+ * @param downloadPath the download path user can see
+ */
+ private void setDownloadPathForUserText(String downloadPathForUser) {
+ downloadPathET.setText(downloadPathForUser);
+ }
+
+ /**
+ * get the filename from user select the download path
+ *
+ * @return String the filename from user selected
+ */
+ private String getFilenameBaseFromUserEnter() {
+ return downloadFilenameET.getText().toString();
+ }
+
+ /**
+ * set the download file size for user to be known
+ */
+ private void setDownloadFileSizeText() {
+ String sizeText;
+ if (contentLength <= 0) {
+ sizeText = getString(R.string.unknow_length);
+ } else {
+ sizeText = getDownloadFileSize();
+ }
+ downloadEstimateSize.setText(sizeText);
+
+ }
+
+ /**
+ * set the time which downloaded this file will be estimately use;
+ */
+ private void setDownloadFileTimeText() {
+ String neededTimeText;
+ if (contentLength <= 0) {
+ neededTimeText = getString(R.string.unknow_length);
+ } else {
+ neededTimeText = getNeededTime() + getString(R.string.time_min);
+ }
+ downloadEstimateTime.setText(neededTimeText);
+ }
+
+ /**
+ * count the download file's size and format the values
+ *
+ * @return String the format values
+ */
+ private String getDownloadFileSize() {
+ String currentSizeText = "";
+ if (contentLength > 0) {
+ currentSizeText = Formatter.formatFileSize(DownloadSettings.this, contentLength);
+ }
+ return currentSizeText;
+ }
+
+ /**
+ * get the time download this file will be use,and format this time values
+ *
+ * @return long the valses of time which download this file will be use
+ */
+ private long getNeededTime() {
+ long timeNeeded = contentLength / downloadRate;
+ if (timeNeeded < 1) {
+ timeNeeded = 1;
+ }
+ Log.e(LOGTAG, "TimeNeeded:" + timeNeeded + "min");
+ // return the time like 5 min, not 5 s;
+ return timeNeeded;
+ }
+}
diff --git a/src/com/android/browser/DownloadTouchIcon.java b/src/com/android/browser/DownloadTouchIcon.java
new file mode 100644
index 0000000..d2c4024
--- /dev/null
+++ b/src/com/android/browser/DownloadTouchIcon.java
@@ -0,0 +1,206 @@
+/*
+ * 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 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.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Proxy;
+import android.net.http.AndroidHttpClient;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Message;
+
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.BrowserContract.Images;
+import com.android.browser.reflect.ReflectHelper;
+
+import org.codeaurora.swe.WebView;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+
+class DownloadTouchIcon extends AsyncTask<String, Void, Void> {
+
+ private final ContentResolver mContentResolver;
+ private Cursor mCursor;
+ private final String mOriginalUrl;
+ private final String mUrl;
+ 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.getApplicationContext();
+ mContentResolver = cr;
+ // Store these in case they change.
+ mOriginalUrl = view.getOriginalUrl();
+ mUrl = view.getUrl();
+ 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.getApplicationContext();
+ mContentResolver = cr;
+ mOriginalUrl = null;
+ mUrl = url;
+ 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.getApplicationContext();
+ mContentResolver = null;
+ mOriginalUrl = null;
+ mUrl = null;
+ mUserAgent = userAgent;
+ }
+
+ @Override
+ public Void doInBackground(String... values) {
+ if (mContentResolver != null) {
+ mCursor = Bookmarks.queryCombinedForUrl(mContentResolver,
+ mOriginalUrl, mUrl);
+ }
+
+ boolean inDatabase = mCursor != null && mCursor.getCount() > 0;
+
+ String url = values[0];
+
+ if (inDatabase || mMessage != null) {
+ AndroidHttpClient client = null;
+ HttpGet request = null;
+
+ try {
+ client = AndroidHttpClient.newInstance(mUserAgent);
+ //HttpHost httpHost = Proxy.getPreferredHttpHost(mContext, url);
+ Object[] params = { mContext, url};
+ Class[] type = new Class[] {android.content.Context.class, String.class};
+ HttpHost httpHost = (HttpHost) ReflectHelper.invokeStaticMethod(
+ "android.net.Proxy", "getPreferredHttpHost",
+ type, params);
+ if (httpHost != null) {
+ ConnRouteParams.setDefaultProxy(client.getParams(), httpHost);
+ }
+
+ request = new HttpGet(url);
+
+ // Follow redirects
+ HttpClientParams.setRedirecting(client.getParams(), true);
+
+ HttpResponse response = client.execute(request);
+ if (response.getStatusLine().getStatusCode() == 200) {
+ HttpEntity entity = response.getEntity();
+ if (entity != null) {
+ InputStream content = entity.getContent();
+ if (content != null) {
+ Bitmap icon = BitmapFactory.decodeStream(
+ content, null, null);
+ if (inDatabase) {
+ storeIcon(icon);
+ } else if (mMessage != null) {
+ Bundle b = mMessage.getData();
+ b.putParcelable(BrowserContract.Bookmarks.TOUCH_ICON, icon);
+ }
+ }
+ }
+ }
+ } catch (Exception ex) {
+ if (request != null) {
+ request.abort();
+ }
+ } finally {
+ if (client != null) {
+ client.close();
+ }
+ }
+ }
+
+ if (mCursor != null) {
+ mCursor.close();
+ }
+
+ if (mMessage != null) {
+ mMessage.sendToTarget();
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onCancelled() {
+ if (mCursor != null) {
+ mCursor.close();
+ }
+ }
+
+ private void storeIcon(Bitmap icon) {
+ // Do this first in case the download failed.
+ if (mTab != null) {
+ // Remove the touch icon loader from the BrowserActivity.
+ mTab.mTouchIconLoader = null;
+ }
+
+ if (icon == null || mCursor == null || isCancelled()) {
+ return;
+ }
+
+ 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());
+
+ do {
+ values.put(Images.URL, mCursor.getString(0));
+ mContentResolver.update(Images.CONTENT_URI, values, null, null);
+ } while (mCursor.moveToNext());
+ }
+ }
+}
diff --git a/src/com/android/browser/ErrorConsoleView.java b/src/com/android/browser/ErrorConsoleView.java
new file mode 100644
index 0000000..bcee7b5
--- /dev/null
+++ b/src/com/android/browser/ErrorConsoleView.java
@@ -0,0 +1,332 @@
+/*
+ * 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.content.Context;
+import android.database.DataSetObserver;
+import android.graphics.Color;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.webkit.ConsoleMessage;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.R;
+
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.TwoLineListItem;
+
+import java.util.Vector;
+
+/* package */ class ErrorConsoleView extends LinearLayout {
+
+ /**
+ * Define some constants to describe the visibility of the error console.
+ */
+ public static final int SHOW_MINIMIZED = 0;
+ public static final int SHOW_MAXIMIZED = 1;
+ public static final int SHOW_NONE = 2;
+
+ private TextView mConsoleHeader;
+ private ErrorConsoleListView mErrorList;
+ private LinearLayout mEvalJsViewGroup;
+ private EditText mEvalEditText;
+ private Button mEvalButton;
+ private WebView mWebView;
+ private int mCurrentShowState = SHOW_NONE;
+
+ private boolean mSetupComplete = false;
+
+ // Before we've been asked to display the console, cache any messages that should
+ // be added to the console. Then when we do display the console, add them to the view
+ // then.
+ private Vector<ConsoleMessage> mErrorMessageCache;
+
+ public ErrorConsoleView(Context context) {
+ super(context);
+ }
+
+ public ErrorConsoleView(Context context, AttributeSet attributes) {
+ super(context, attributes);
+ }
+
+ private void commonSetupIfNeeded() {
+ if (mSetupComplete) {
+ return;
+ }
+
+ LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.error_console, this);
+
+ // Get references to each ui element.
+ mConsoleHeader = (TextView) findViewById(R.id.error_console_header_id);
+ mErrorList = (ErrorConsoleListView) findViewById(R.id.error_console_list_id);
+ mEvalJsViewGroup = (LinearLayout) findViewById(R.id.error_console_eval_view_group_id);
+ mEvalEditText = (EditText) findViewById(R.id.error_console_eval_text_id);
+ mEvalButton = (Button) findViewById(R.id.error_console_eval_button_id);
+
+ mEvalButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ // Send the javascript to be evaluated to webkit as a javascript: url
+ // TODO: Can we expose access to webkit's JS interpreter here and evaluate it that
+ // way? Note that this is called on the UI thread so we will need to post a message
+ // to the WebCore thread to implement this.
+ if (mWebView != null) {
+ mWebView.loadUrl("javascript:" + mEvalEditText.getText());
+ }
+
+ mEvalEditText.setText("");
+ }
+ });
+
+ // Make clicking on the console title bar min/maximse it.
+ mConsoleHeader.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ if (mCurrentShowState == SHOW_MINIMIZED) {
+ showConsole(SHOW_MAXIMIZED);
+ } else {
+ showConsole(SHOW_MINIMIZED);
+ }
+ }
+ });
+
+ // Add any cached messages to the list now that we've assembled the view.
+ if (mErrorMessageCache != null) {
+ for (ConsoleMessage msg : mErrorMessageCache) {
+ mErrorList.addErrorMessage(msg);
+ }
+ mErrorMessageCache.clear();
+ }
+
+ mSetupComplete = true;
+ }
+
+ /**
+ * Adds a message to the set of messages the console uses.
+ */
+ public void addErrorMessage(ConsoleMessage consoleMessage) {
+ if (mSetupComplete) {
+ mErrorList.addErrorMessage(consoleMessage);
+ } else {
+ if (mErrorMessageCache == null) {
+ mErrorMessageCache = new Vector<ConsoleMessage>();
+ }
+ mErrorMessageCache.add(consoleMessage);
+ }
+ }
+
+ /**
+ * Removes all error messages from the console.
+ */
+ public void clearErrorMessages() {
+ if (mSetupComplete) {
+ mErrorList.clearErrorMessages();
+ } else if (mErrorMessageCache != null) {
+ mErrorMessageCache.clear();
+ }
+ }
+
+ /**
+ * Returns the current number of errors displayed in the console.
+ */
+ public int numberOfErrors() {
+ if (mSetupComplete) {
+ return mErrorList.getCount();
+ } else {
+ return (mErrorMessageCache == null) ? 0 : mErrorMessageCache.size();
+ }
+ }
+
+ /**
+ * Sets the webview that this console is associated with. Currently this is used so
+ * we can call into webkit to evaluate JS expressions in the console.
+ */
+ public void setWebView(WebView webview) {
+ mWebView = webview;
+ }
+
+ /**
+ * Sets the visibility state of the console.
+ */
+ public void showConsole(int show_state) {
+ commonSetupIfNeeded();
+ switch (show_state) {
+ case SHOW_MINIMIZED:
+ mConsoleHeader.setVisibility(View.VISIBLE);
+ mConsoleHeader.setText(R.string.error_console_header_text_minimized);
+ mErrorList.setVisibility(View.GONE);
+ mEvalJsViewGroup.setVisibility(View.GONE);
+ break;
+
+ case SHOW_MAXIMIZED:
+ mConsoleHeader.setVisibility(View.VISIBLE);
+ mConsoleHeader.setText(R.string.error_console_header_text_maximized);
+ mErrorList.setVisibility(View.VISIBLE);
+ mEvalJsViewGroup.setVisibility(View.VISIBLE);
+ break;
+
+ case SHOW_NONE:
+ mConsoleHeader.setVisibility(View.GONE);
+ mErrorList.setVisibility(View.GONE);
+ mEvalJsViewGroup.setVisibility(View.GONE);
+ break;
+ }
+ mCurrentShowState = show_state;
+ }
+
+ /**
+ * Returns the current visibility state of the console.
+ */
+ public int getShowState() {
+ if (mSetupComplete) {
+ return mCurrentShowState;
+ } else {
+ return SHOW_NONE;
+ }
+ }
+
+ /**
+ * This class extends ListView to implement the View that will actually display the set of
+ * errors encountered on the current page.
+ */
+ private static class ErrorConsoleListView extends ListView {
+ // An adapter for this View that contains a list of error messages.
+ private ErrorConsoleMessageList mConsoleMessages;
+
+ public ErrorConsoleListView(Context context, AttributeSet attributes) {
+ super(context, attributes);
+ mConsoleMessages = new ErrorConsoleMessageList(context);
+ setAdapter(mConsoleMessages);
+ }
+
+ public void addErrorMessage(ConsoleMessage consoleMessage) {
+ mConsoleMessages.add(consoleMessage);
+ setSelection(mConsoleMessages.getCount());
+ }
+
+ public void clearErrorMessages() {
+ mConsoleMessages.clear();
+ }
+
+ /**
+ * This class is an adapter for ErrorConsoleListView that contains the error console
+ * message data.
+ */
+ private static class ErrorConsoleMessageList extends android.widget.BaseAdapter
+ implements android.widget.ListAdapter {
+
+ private Vector<ConsoleMessage> mMessages;
+ private LayoutInflater mInflater;
+
+ public ErrorConsoleMessageList(Context context) {
+ mMessages = new Vector<ConsoleMessage>();
+ mInflater = (LayoutInflater)context.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ /**
+ * Add a new message to the list and update the View.
+ */
+ public void add(ConsoleMessage consoleMessage) {
+ mMessages.add(consoleMessage);
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Remove all messages from the list and update the view.
+ */
+ public void clear() {
+ mMessages.clear();
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return false;
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public Object getItem(int position) {
+ return mMessages.get(position);
+ }
+
+ public int getCount() {
+ return mMessages.size();
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ /**
+ * Constructs a TwoLineListItem for the error at position.
+ */
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view;
+ ConsoleMessage error = mMessages.get(position);
+
+ if (error == null) {
+ return null;
+ }
+
+ if (convertView == null) {
+ view = mInflater.inflate(android.R.layout.two_line_list_item, parent, false);
+ } else {
+ view = convertView;
+ }
+
+ TextView headline = (TextView) view.findViewById(android.R.id.text1);
+ TextView subText = (TextView) view.findViewById(android.R.id.text2);
+ headline.setText(error.sourceId() + ":" + error.lineNumber());
+ subText.setText(error.message());
+ switch (error.messageLevel()) {
+ case ERROR:
+ subText.setTextColor(Color.RED);
+ break;
+ case WARNING:
+ // Orange
+ subText.setTextColor(Color.rgb(255,192,0));
+ break;
+ case TIP:
+ subText.setTextColor(Color.BLUE);
+ break;
+ default:
+ subText.setTextColor(Color.LTGRAY);
+ break;
+ }
+ return view;
+ }
+
+ }
+ }
+}
diff --git a/src/com/android/browser/EventLogTags.logtags b/src/com/android/browser/EventLogTags.logtags
new file mode 100644
index 0000000..b3834cf
--- /dev/null
+++ b/src/com/android/browser/EventLogTags.logtags
@@ -0,0 +1,15 @@
+# See system/core/logcat/event.logtags for a description of the format of this file.
+
+option java_package com.android.browser
+
+# This event is logged when a user adds a new bookmark. This could just be a boolean,
+# but if lots of users add the same bookmark it could be a default bookmark on the browser.
+# Second parameter is where the bookmark was added from, currently history or bookmarks view.
+70103 browser_bookmark_added (url|3), (where|3)
+
+# This event is logged after a page has finished loading. It is sending back the page url,
+# and how long it took to load the page. Could maybe also tell the kind of connection (2g, 3g, WiFi)?
+70104 browser_page_loaded (url|3), (time|2|3)
+
+# This event is logged when the user navigates to a new page, sending the time spent on the current page.
+70105 browser_timeonpage (url|3), (time|2|3)
diff --git a/src/com/android/browser/FetchUrlMimeType.java b/src/com/android/browser/FetchUrlMimeType.java
new file mode 100644
index 0000000..f42d627
--- /dev/null
+++ b/src/com/android/browser/FetchUrlMimeType.java
@@ -0,0 +1,280 @@
+/*
+ * 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.app.Activity;
+import android.content.Context;
+import android.net.Uri;
+import android.net.http.AndroidHttpClient;
+import android.text.TextUtils;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import com.android.browser.reflect.ReflectHelper;
+
+import java.io.IOException;
+
+import org.apache.http.Header;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpHead;
+import org.apache.http.conn.params.ConnRouteParams;
+
+import org.codeaurora.swe.CookieManager;
+
+/**
+ * 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.
+ * 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 Thread {
+
+ private final static String LOGTAG = "FetchUrlMimeType";
+
+ private Context mContext;
+ private String mUri;
+ private String mUserAgent;
+ private String mFilename;
+ private String mReferer;
+ private Activity mActivity;
+ private boolean mPrivateBrowsing;
+ private long mContentLength;
+
+ public FetchUrlMimeType(Activity activity, String url, String userAgent,
+ String referer, boolean privateBrowsing, String filename) {
+ mActivity = activity;
+ mContext = activity.getApplicationContext();
+ mUri = url;
+ mUserAgent = userAgent;
+ mPrivateBrowsing = privateBrowsing;
+ mFilename = filename;
+ mReferer = referer;
+ }
+
+ @Override
+ public void run() {
+ // User agent is likely to be null, though the AndroidHttpClient
+ // seems ok with that.
+ AndroidHttpClient client = AndroidHttpClient.newInstance(mUserAgent);
+ HttpHost httpHost;
+ try {
+ Class<?> argTypes[] = new Class[]{Context.class, String.class};
+ Object args[] = new Object[]{mContext, mUri};
+ httpHost = (HttpHost) ReflectHelper.invokeStaticMethod("android.net.Proxy",
+ "getPreferredHttpHost", argTypes, args);
+ if (httpHost != null) {
+ ConnRouteParams.setDefaultProxy(client.getParams(), httpHost);
+ }
+ } catch (IllegalArgumentException ex) {
+ Log.e(LOGTAG,"Download failed: " + ex);
+ client.close();
+ return;
+ }
+ HttpHead request = new HttpHead(mUri);
+ String cookies = CookieManager.getInstance().getCookie(mUri, mPrivateBrowsing);
+ if (cookies != null && cookies.length() > 0) {
+ request.addHeader("Cookie", cookies);
+ }
+
+ HttpResponse response;
+ String filename = mFilename;
+ String mimeType = null;
+ String contentDisposition = null;
+ String contentLength = null;
+ try {
+ response = client.execute(request);
+ // We could get a redirect here, but if we do lets let
+ // the download manager take care of it, and thus trust that
+ // the server sends the right mimetype
+ if (response.getStatusLine().getStatusCode() == 200) {
+ Header header = response.getFirstHeader("Content-Type");
+ if (header != null) {
+ mimeType = header.getValue();
+ final int semicolonIndex = mimeType.indexOf(';');
+ if (semicolonIndex != -1) {
+ mimeType = mimeType.substring(0, semicolonIndex);
+ }
+ }
+ Header contentLengthHeader = response.getFirstHeader("Content-Length");
+ if (contentLengthHeader != null) {
+ contentLength = contentLengthHeader.getValue();
+ }
+ Header contentDispositionHeader = response.getFirstHeader("Content-Disposition");
+ if (contentDispositionHeader != null) {
+ contentDisposition = contentDispositionHeader.getValue();
+ }
+ }
+ } catch (IllegalArgumentException ex) {
+ request.abort();
+ } catch (IOException ex) {
+ request.abort();
+ } finally {
+ client.close();
+ }
+
+ if (mimeType != null) {
+ Log.e(LOGTAG, "-----------the mimeType from http header is ------------->" + mimeType);
+ if (mimeType.equalsIgnoreCase("text/plain") ||
+ mimeType.equalsIgnoreCase("application/octet-stream")) {
+ String newMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
+ MimeTypeMap.getFileExtensionFromUrl(mUri));
+ if (newMimeType != null) {
+ mimeType = newMimeType;
+ }
+ }
+
+ String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+ if (fileExtension == null || (fileExtension != null && fileExtension.equals("bin"))) {
+ fileExtension = MimeTypeMap.getFileExtensionFromUrl(mUri);
+ if (fileExtension == null) {
+ fileExtension = "bin";
+ }
+ }
+ filename = DownloadHandler.getFilenameBase(filename) + "." + fileExtension;
+
+ } else {
+ String fileExtension = getFileExtensionFromUrlEx(mUri);
+ if (fileExtension == "") {
+ fileExtension = "bin";
+ }
+ String newMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension);
+ if (newMimeType != null) {
+ mimeType = newMimeType;
+ }
+ filename = guessFileNameEx(mUri, contentDisposition, mimeType);
+
+ }
+
+ if (contentLength != null) {
+ mContentLength = Long.parseLong(contentLength);
+ } else {
+ mContentLength = 0;
+ }
+
+ DownloadHandler.startDownloadSettings(mActivity, mUri, mUserAgent, contentDisposition,
+ mimeType, mReferer, mPrivateBrowsing, mContentLength, filename);
+ }
+
+ /**
+ * when we can not parse MineType and Filename from the header of http body
+ * ,Call the fallowing functions for this matter
+ * getFileExtensionFromUrlEx(String url) : get the file Extension from Url
+ * guessFileNameEx() : get the file name from url Note: this modified for
+ * download http://www.baidu.com girl picture error extension and error
+ * filename
+ */
+ private String getFileExtensionFromUrlEx(String url) {
+ Log.e("FetchUrlMimeType",
+ "--------can not get mimetype from http header, the URL is ---------->" + url);
+ if (!TextUtils.isEmpty(url)) {
+ int fragment = url.lastIndexOf('#');
+ if (fragment > 0) {
+ url = url.substring(0, fragment);
+ }
+
+ int filenamePos = url.lastIndexOf('/');
+ String filename =
+ 0 <= filenamePos ? url.substring(filenamePos + 1) : url;
+ Log.e(LOGTAG,
+ "--------can not get mimetype from http header, the temp filename is----------"
+ + filename);
+ // if the filename contains special characters, we don't
+ // consider it valid for our matching purposes:
+ if (!filename.isEmpty()) {
+ int dotPos = filename.lastIndexOf('.');
+ if (0 <= dotPos) {
+ return filename.substring(dotPos + 1);
+ }
+ }
+ }
+
+ return "";
+ }
+
+ private String guessFileNameEx(String url, String contentDisposition, String mimeType) {
+ String filename = null;
+ String extension = null;
+
+ // If all the other http-related approaches failed, use the plain uri
+ if (filename == null) {
+ String decodedUrl = Uri.decode(url);
+ if (decodedUrl != null) {
+ if (!decodedUrl.endsWith("/")) {
+ int index = decodedUrl.lastIndexOf('/') + 1;
+ if (index > 0) {
+ filename = decodedUrl.substring(index);
+ }
+ }
+ }
+ }
+
+ // Finally, if couldn't get filename from URI, get a generic filename
+ if (filename == null) {
+ filename = "downloadfile";
+ }
+
+ // Split filename between base and extension
+ // Add an extension if filename does not have one
+ int dotIndex = filename.indexOf('.');
+ if (dotIndex < 0) {
+ if (mimeType != null) {
+ extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+ if (extension != null) {
+ extension = "." + extension;
+ }
+ }
+ if (extension == null) {
+ if (mimeType != null && mimeType.toLowerCase().startsWith("text/")) {
+ if (mimeType.equalsIgnoreCase("text/html")) {
+ extension = ".html";
+ } else {
+ extension = ".txt";
+ }
+ } else {
+ extension = ".bin";
+ }
+ }
+ } else {
+ if (mimeType != null) {
+ // Compare the last segment of the extension against the mime
+ // type.
+ // If there's a mismatch, discard the entire extension.
+ int lastDotIndex = filename.lastIndexOf('.');
+ String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
+ filename.substring(lastDotIndex + 1));
+ if (typeFromExt != null && !typeFromExt.equalsIgnoreCase(mimeType)) {
+ extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+ if (extension != null) {
+ extension = "." + extension;
+ }
+ }
+ }
+ if (extension == null) {
+ extension = filename.substring(dotIndex);
+ }
+ filename = filename.substring(0, dotIndex);
+ }
+
+ return filename + extension;
+ }
+
+}
diff --git a/src/com/android/browser/GeolocationPermissionsPrompt.java b/src/com/android/browser/GeolocationPermissionsPrompt.java
new file mode 100755
index 0000000..127107f
--- /dev/null
+++ b/src/com/android/browser/GeolocationPermissionsPrompt.java
@@ -0,0 +1,122 @@
+/*
+ * 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 com.android.browser.R;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.webkit.GeolocationPermissions;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class GeolocationPermissionsPrompt extends RelativeLayout {
+ private TextView mMessage;
+ private Button mShareButton;
+ private Button mDontShareButton;
+ private CheckBox mRemember;
+ private GeolocationPermissions.Callback mCallback;
+ private String mOrigin;
+
+ public GeolocationPermissionsPrompt(Context context) {
+ this(context, null);
+ }
+
+ public GeolocationPermissionsPrompt(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ init();
+ }
+
+ private void init() {
+ mMessage = (TextView) findViewById(R.id.message);
+ mShareButton = (Button) findViewById(R.id.share_button);
+ mDontShareButton = (Button) findViewById(R.id.dont_share_button);
+ mRemember = (CheckBox) findViewById(R.id.remember);
+
+ mShareButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ handleButtonClick(true);
+ }
+ });
+ mDontShareButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ handleButtonClick(false);
+ }
+ });
+ }
+
+ /**
+ * Shows the prompt for the given origin. When the user clicks on one of
+ * the buttons, the supplied callback is be called.
+ */
+ public void show(String origin, GeolocationPermissions.Callback callback) {
+ mOrigin = origin;
+ mCallback = callback;
+ Uri uri = Uri.parse(mOrigin);
+ setMessage("http".equals(uri.getScheme()) ? mOrigin.substring(7) : mOrigin);
+ // The checkbox should always be intially checked.
+ mRemember.setChecked(true);
+ setVisibility(View.VISIBLE);
+ }
+
+ /**
+ * Hides the prompt.
+ */
+ public void hide() {
+ setVisibility(View.GONE);
+ }
+
+ /**
+ * Handles a click on one the buttons by invoking the callback.
+ */
+ private void handleButtonClick(boolean allow) {
+ hide();
+
+ boolean remember = mRemember.isChecked();
+ if (remember) {
+ Toast toast = Toast.makeText(
+ getContext(),
+ allow ? R.string.geolocation_permissions_prompt_toast_allowed :
+ R.string.geolocation_permissions_prompt_toast_disallowed,
+ Toast.LENGTH_LONG);
+ toast.setGravity(Gravity.BOTTOM, 0, 0);
+ toast.show();
+ }
+
+ mCallback.invoke(mOrigin, allow, remember);
+ }
+
+ /**
+ * Sets the prompt's message.
+ */
+ private void setMessage(CharSequence origin) {
+ mMessage.setText(String.format(
+ getResources().getString(R.string.geolocation_permissions_prompt_message),
+ origin));
+ }
+}
diff --git a/src/com/android/browser/GoogleAccountLogin.java b/src/com/android/browser/GoogleAccountLogin.java
new file mode 100644
index 0000000..f605671
--- /dev/null
+++ b/src/com/android/browser/GoogleAccountLogin.java
@@ -0,0 +1,296 @@
+/*
+ * 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.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerCallback;
+import android.accounts.AccountManagerFuture;
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.SharedPreferences.Editor;
+import android.net.Uri;
+import android.net.http.AndroidHttpClient;
+import android.os.Bundle;
+import android.util.Log;
+import org.codeaurora.swe.CookieSyncManager;
+import org.codeaurora.swe.WebView;
+import org.codeaurora.swe.WebViewClient;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.util.EntityUtils;
+
+import com.android.browser.R;
+
+public class GoogleAccountLogin implements Runnable,
+ AccountManagerCallback<Bundle>, OnCancelListener {
+
+ private static final String LOGTAG = "BrowserLogin";
+
+ // Url for issuing the uber token.
+ private Uri ISSUE_AUTH_TOKEN_URL = Uri.parse(
+ "https://www.google.com/accounts/IssueAuthToken?service=gaia&Session=false");
+ // Url for signing into a particular service.
+ private static final Uri TOKEN_AUTH_URL = Uri.parse(
+ "https://www.google.com/accounts/TokenAuth");
+ // Google account type
+ private static final String GOOGLE = "com.google";
+ // Last auto login time
+ public static final String PREF_AUTOLOGIN_TIME = "last_autologin_time";
+
+ private final Activity mActivity;
+ private final Account mAccount;
+ private final WebView mWebView;
+ private Runnable mRunnable;
+ private ProgressDialog mProgressDialog;
+
+ // SID and LSID retrieval process.
+ private String mSid;
+ private String mLsid;
+ private int mState; // {NONE(0), SID(1), LSID(2)}
+ private boolean mTokensInvalidated;
+ private String mUserAgent;
+
+ private GoogleAccountLogin(Activity activity, Account account,
+ Runnable runnable) {
+ mActivity = activity;
+ mAccount = account;
+ mWebView = new WebView(mActivity);
+ mRunnable = runnable;
+ mUserAgent = mWebView.getSettings().getUserAgentString();
+
+ // XXX: Doing pre-login causes onResume to skip calling
+ // resumeWebViewTimers. So to avoid problems with timers not running, we
+ // duplicate the work here using the off-screen WebView.
+ CookieSyncManager.getInstance().startSync();
+ WebViewTimersControl.getInstance().onBrowserActivityResume(mWebView);
+
+ mWebView.setWebViewClient(new WebViewClient() {
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ return false;
+ }
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ done();
+ }
+ });
+ }
+
+ private void saveLoginTime() {
+ Editor ed = BrowserSettings.getInstance().getPreferences().edit();
+ ed.putLong(PREF_AUTOLOGIN_TIME, System.currentTimeMillis());
+ ed.apply();
+ }
+
+ // Runnable
+ @Override
+ public void run() {
+ String url = ISSUE_AUTH_TOKEN_URL.buildUpon()
+ .appendQueryParameter("SID", mSid)
+ .appendQueryParameter("LSID", mLsid)
+ .build().toString();
+ // Intentionally not using Proxy.
+ AndroidHttpClient client = AndroidHttpClient.newInstance(mUserAgent);
+ HttpPost request = new HttpPost(url);
+
+ String result = null;
+ try {
+ HttpResponse response = client.execute(request);
+ int status = response.getStatusLine().getStatusCode();
+ if (status != HttpStatus.SC_OK) {
+ Log.d(LOGTAG, "LOGIN_FAIL: Bad status from auth url "
+ + status + ": "
+ + response.getStatusLine().getReasonPhrase());
+ // Invalidate the tokens once just in case the 403 was for other
+ // reasons.
+ if (status == HttpStatus.SC_FORBIDDEN && !mTokensInvalidated) {
+ Log.d(LOGTAG, "LOGIN_FAIL: Invalidating tokens...");
+ // Need to regenerate the auth tokens and try again.
+ invalidateTokens();
+ // XXX: Do not touch any more member variables from this
+ // thread as a second thread will handle the next login
+ // attempt.
+ return;
+ }
+ done();
+ return;
+ }
+ HttpEntity entity = response.getEntity();
+ if (entity == null) {
+ Log.d(LOGTAG, "LOGIN_FAIL: Null entity in response");
+ done();
+ return;
+ }
+ result = EntityUtils.toString(entity, "UTF-8");
+ } catch (Exception e) {
+ Log.d(LOGTAG, "LOGIN_FAIL: Exception acquiring uber token " + e);
+ request.abort();
+ done();
+ return;
+ } finally {
+ client.close();
+ }
+ final String newUrl = TOKEN_AUTH_URL.buildUpon()
+ .appendQueryParameter("source", "android-browser")
+ .appendQueryParameter("auth", result)
+ .appendQueryParameter("continue",
+ BrowserSettings.getFactoryResetHomeUrl(mActivity))
+ .build().toString();
+ mActivity.runOnUiThread(new Runnable() {
+ @Override public void run() {
+ // Check mRunnable in case the request has been canceled. This
+ // is most likely not necessary as run() is the only non-UI
+ // thread that calls done() but I am paranoid.
+ synchronized (GoogleAccountLogin.this) {
+ if (mRunnable == null) {
+ return;
+ }
+ mWebView.loadUrl(newUrl);
+ }
+ }
+ });
+ }
+
+ private void invalidateTokens() {
+ AccountManager am = AccountManager.get(mActivity);
+ am.invalidateAuthToken(GOOGLE, mSid);
+ am.invalidateAuthToken(GOOGLE, mLsid);
+ mTokensInvalidated = true;
+ mState = 1; // SID
+ am.getAuthToken(mAccount, "SID", null, mActivity, this, null);
+ }
+
+ // AccountManager callbacks.
+ @Override
+ public void run(AccountManagerFuture<Bundle> value) {
+ try {
+ String id = value.getResult().getString(
+ AccountManager.KEY_AUTHTOKEN);
+ switch (mState) {
+ default:
+ case 0:
+ throw new IllegalStateException(
+ "Impossible to get into this state");
+ case 1:
+ mSid = id;
+ mState = 2; // LSID
+ AccountManager.get(mActivity).getAuthToken(
+ mAccount, "LSID", null, mActivity, this, null);
+ break;
+ case 2:
+ mLsid = id;
+ new Thread(this).start();
+ break;
+ }
+ } catch (Exception e) {
+ Log.d(LOGTAG, "LOGIN_FAIL: Exception in state " + mState + " " + e);
+ // For all exceptions load the original signin page.
+ // TODO: toast login failed?
+ done();
+ }
+ }
+
+ // Start the login process if auto-login is enabled and the user is not
+ // already logged in.
+ public static void startLoginIfNeeded(Activity activity,
+ Runnable runnable) {
+ // Already logged in?
+ if (isLoggedIn()) {
+ runnable.run();
+ return;
+ }
+
+ // No account found?
+ Account[] accounts = getAccounts(activity);
+ if (accounts == null || accounts.length == 0) {
+ runnable.run();
+ return;
+ }
+
+ GoogleAccountLogin login =
+ new GoogleAccountLogin(activity, accounts[0], runnable);
+ login.startLogin();
+ }
+
+ private void startLogin() {
+ saveLoginTime();
+ mProgressDialog = ProgressDialog.show(mActivity,
+ mActivity.getString(R.string.pref_autologin_title),
+ mActivity.getString(R.string.pref_autologin_progress,
+ mAccount.name),
+ true /* indeterminate */,
+ true /* cancelable */,
+ this);
+ mState = 1; // SID
+ AccountManager.get(mActivity).getAuthToken(
+ mAccount, "SID", null, mActivity, this, null);
+ }
+
+ private static Account[] getAccounts(Context ctx) {
+ return AccountManager.get(ctx).getAccountsByType(GOOGLE);
+ }
+
+ // Checks if we already did pre-login.
+ private static boolean isLoggedIn() {
+ // See if we last logged in less than a week ago.
+ long lastLogin = BrowserSettings.getInstance().getPreferences()
+ .getLong(PREF_AUTOLOGIN_TIME, -1);
+ if (lastLogin == -1) {
+ return false;
+ }
+ return true;
+ }
+
+ // Used to indicate that the Browser should continue loading the main page.
+ // This can happen on success, error, or timeout.
+ private synchronized void done() {
+ if (mRunnable != null) {
+ Log.d(LOGTAG, "Finished login attempt for " + mAccount.name);
+ mActivity.runOnUiThread(mRunnable);
+
+ try {
+ mProgressDialog.dismiss();
+ } catch (Exception e) {
+ // TODO: Switch to a managed dialog solution (DialogFragment?)
+ // Also refactor this class, it doesn't
+ // play nice with the activity lifecycle, leading to issues
+ // with the dialog it manages
+ Log.w(LOGTAG, "Failed to dismiss mProgressDialog: " + e.getMessage());
+ }
+ mRunnable = null;
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mWebView.destroy();
+ }
+ });
+ }
+ }
+
+ // Called by the progress dialog on startup.
+ public void onCancel(DialogInterface unused) {
+ done();
+ }
+
+}
diff --git a/src/com/android/browser/HistoryItem.java b/src/com/android/browser/HistoryItem.java
new file mode 100644
index 0000000..ceba2cb
--- /dev/null
+++ b/src/com/android/browser/HistoryItem.java
@@ -0,0 +1,92 @@
+/*
+ * 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 com.android.browser.R;
+
+import android.content.Context;
+import android.provider.Browser;
+import android.view.View;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+
+/**
+ * Layout representing a history item in the classic history viewer.
+ */
+/* package */ class HistoryItem extends BookmarkItem
+ implements OnCheckedChangeListener {
+
+ private CompoundButton mStar; // Star for bookmarking
+ /**
+ * Create a new HistoryItem.
+ * @param context Context for this HistoryItem.
+ */
+ /* package */ HistoryItem(Context context) {
+ this(context, true);
+ }
+
+ /* package */ HistoryItem(Context context, boolean showStar) {
+ super(context);
+
+ mStar = (CompoundButton) findViewById(R.id.star);
+ mStar.setOnCheckedChangeListener(this);
+ if (showStar) {
+ mStar.setVisibility(View.VISIBLE);
+ } else {
+ mStar.setVisibility(View.GONE);
+ }
+ }
+
+ /* package */ void copyTo(HistoryItem item) {
+ item.mTextView.setText(mTextView.getText());
+ item.mUrlText.setText(mUrlText.getText());
+ item.setIsBookmark(mStar.isChecked());
+ item.mImageView.setImageDrawable(mImageView.getDrawable());
+ }
+
+ /**
+ * Whether or not this item represents a bookmarked site
+ */
+ /* package */ boolean isBookmark() {
+ return mStar.isChecked();
+ }
+
+ /**
+ * Set whether or not this represents a bookmark, and make sure the star
+ * behaves appropriately.
+ */
+ /* package */ void setIsBookmark(boolean isBookmark) {
+ mStar.setOnCheckedChangeListener(null);
+ mStar.setChecked(isBookmark);
+ mStar.setOnCheckedChangeListener(this);
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView,
+ boolean isChecked) {
+ if (isChecked) {
+ // Uncheck ourseves. When the bookmark is actually added,
+ // we will be notified
+ setIsBookmark(false);
+ Browser.saveBookmark(getContext(), getName(), mUrl);
+ } else {
+ 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 0000000..2981e65
--- /dev/null
+++ b/src/com/android/browser/HttpAuthenticationDialog.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 com.android.browser.R;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+
+/**
+ * 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);
+ mPasswordView.setOnEditorActionListener(new OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ mDialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick();
+ return true;
+ }
+ return false;
+ }
+ });
+
+ String title = mContext.getText(R.string.sign_in_to).toString().replace(
+ "%s1", mHost).replace("%s2", mRealm);
+
+ mDialog = new AlertDialog.Builder(mContext)
+ .setTitle(title)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .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 0000000..ec19246
--- /dev/null
+++ b/src/com/android/browser/IntentHandler.java
@@ -0,0 +1,382 @@
+/*
+ * 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.SearchManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.nfc.NfcAdapter;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.Browser;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.util.Patterns;
+
+import com.android.browser.UI.ComboViews;
+import com.android.browser.search.SearchEngine;
+
+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);
+ }
+ 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;
+ }
+ if (BrowserActivity.ACTION_SHOW_BOOKMARKS.equals(action)) {
+ mController.bookmarksOrHistoryPicker(ComboViews.Bookmarks);
+ return;
+ }
+
+ // In case the SearchDialog is open.
+ ((SearchManager) mActivity.getSystemService(Context.SEARCH_SERVICE))
+ .stopSearch();
+ if (Intent.ACTION_VIEW.equals(action)
+ || NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)
+ || Intent.ACTION_SEARCH.equals(action)
+ || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action)
+ || Intent.ACTION_WEB_SEARCH.equals(action)) {
+ // 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());
+ }
+
+ if (intent.getBooleanExtra(Browser.EXTRA_CREATE_NEW_TAB, false)
+ || urlData.isPreloaded()) {
+ Tab t = mController.openTab(urlData);
+ return;
+ }
+ /*
+ * TODO: Don't allow javascript URIs
+ * 0) If this is a javascript: URI, *always* open a new tab
+ * 1) If the URL is already opened, switch to that tab
+ * 2-phone) Reuse tab with same appId
+ * 2-tablet) Open new tab
+ */
+ final String appId = intent
+ .getStringExtra(Browser.EXTRA_APPLICATION_ID);
+ if (!TextUtils.isEmpty(urlData.mUrl) &&
+ urlData.mUrl.startsWith("javascript:")) {
+ // Always open javascript: URIs in new tabs
+ mController.openTab(urlData);
+ return;
+ }
+ if (Intent.ACTION_VIEW.equals(action)
+ && (appId != null)
+ && appId.startsWith(mActivity.getPackageName())) {
+ Tab appTab = mTabControl.getTabFromAppId(appId);
+ if ((appTab != null) && (appTab == mController.getCurrentTab())) {
+ mController.switchToTab(appTab);
+ mController.loadUrlDataIn(appTab, urlData);
+ return;
+ }
+ }
+ if (Intent.ACTION_VIEW.equals(action)
+ && !mActivity.getPackageName().equals(appId)) {
+ if (!BrowserActivity.isTablet(mActivity)
+ && !mSettings.allowAppTabs()) {
+ Tab appTab = mTabControl.getTabFromAppId(appId);
+ if (appTab != null) {
+ mController.reuseTab(appTab, urlData);
+ return;
+ }
+ }
+ // No matching application tab, try to find a regular tab
+ // with a matching url.
+ Tab appTab = mTabControl.findTabWithUrl(urlData.mUrl);
+ if (appTab != null) {
+ // Transfer ownership
+ appTab.setAppId(appId);
+ if (current != appTab) {
+ mController.switchToTab(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.
+ Tab tab = mController.openTab(urlData);
+ if (tab != null) {
+ tab.setAppId(appId);
+ if ((intent.getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) {
+ tab.setCloseOnBack(true);
+ }
+ }
+ }
+ } 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 ("about:debug.nav".equals(urlData.mUrl)) {
+ current.getWebView().debugDump();
+ } 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 static UrlData getUrlDataFromIntent(Intent intent) {
+ String url = "";
+ Map<String, String> headers = null;
+ PreloadedTabControl preloaded = null;
+ String preloadedSearchBoxQuery = null;
+ if (intent != null
+ && (intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) {
+ final String action = intent.getAction();
+ if (Intent.ACTION_VIEW.equals(action) ||
+ NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
+ url = UrlUtils.smartUrlFilter(intent.getData());
+ 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));
+ }
+ }
+ }
+ if (intent.hasExtra(PreloadRequestReceiver.EXTRA_PRELOAD_ID)) {
+ String id = intent.getStringExtra(PreloadRequestReceiver.EXTRA_PRELOAD_ID);
+ preloadedSearchBoxQuery = intent.getStringExtra(
+ PreloadRequestReceiver.EXTRA_SEARCHBOX_SETQUERY);
+ preloaded = Preloader.getInstance().getPreloadedTab(id);
+ }
+ } 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);
+ 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("source");
+ }
+ if (TextUtils.isEmpty(source)) {
+ source = GOOGLE_SEARCH_SOURCE_UNKNOWN;
+ }
+ url = url.replace(searchSource, "&source=android-"+source+"&");
+ }
+ }
+ }
+ }
+ return new UrlData(url, headers, intent, preloaded, preloadedSearchBoxQuery);
+ }
+
+ /**
+ * 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 (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();
+ if (TextUtils.isEmpty(url)) return false;
+
+ // 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.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 PreloadedTabControl mPreloadedTab;
+ final String mSearchBoxQueryToSubmit;
+ final boolean mDisableUrlOverride;
+
+ UrlData(String url) {
+ this.mUrl = url;
+ this.mHeaders = null;
+ this.mPreloadedTab = null;
+ this.mSearchBoxQueryToSubmit = null;
+ this.mDisableUrlOverride = false;
+ }
+
+ UrlData(String url, Map<String, String> headers, Intent intent) {
+ this(url, headers, intent, null, null);
+ }
+
+ UrlData(String url, Map<String, String> headers, Intent intent,
+ PreloadedTabControl preloaded, String searchBoxQueryToSubmit) {
+ this.mUrl = url;
+ this.mHeaders = headers;
+ this.mPreloadedTab = preloaded;
+ this.mSearchBoxQueryToSubmit = searchBoxQueryToSubmit;
+ if (intent != null) {
+ mDisableUrlOverride = intent.getBooleanExtra(
+ BrowserActivity.EXTRA_DISABLE_URL_OVERRIDE, false);
+ } else {
+ mDisableUrlOverride = false;
+ }
+ }
+
+ boolean isEmpty() {
+ return (mUrl == null || mUrl.length() == 0);
+ }
+
+ boolean isPreloaded() {
+ return mPreloadedTab != null;
+ }
+
+ PreloadedTabControl getPreloadedTab() {
+ return mPreloadedTab;
+ }
+
+ String getSearchBoxQueryToSubmit() {
+ return mSearchBoxQueryToSubmit;
+ }
+ }
+
+}
diff --git a/src/com/android/browser/KeyChainLookup.java b/src/com/android/browser/KeyChainLookup.java
new file mode 100644
index 0000000..5bd86b5
--- /dev/null
+++ b/src/com/android/browser/KeyChainLookup.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 201 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.os.AsyncTask;
+import android.security.KeyChain;
+import android.security.KeyChainException;
+import org.codeaurora.swe.ClientCertRequestHandler;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+
+final class KeyChainLookup extends AsyncTask<Void, Void, Void> {
+ private final Context mContext;
+ private final ClientCertRequestHandler mHandler;
+ private final String mAlias;
+ KeyChainLookup(Context context, ClientCertRequestHandler handler, String alias) {
+ mContext = context.getApplicationContext();
+ mHandler = handler;
+ mAlias = alias;
+ }
+ @Override protected Void doInBackground(Void... params) {
+ PrivateKey privateKey;
+ X509Certificate[] certificateChain;
+ try {
+ privateKey = KeyChain.getPrivateKey(mContext, mAlias);
+ certificateChain = KeyChain.getCertificateChain(mContext, mAlias);
+ } catch (InterruptedException e) {
+ mHandler.ignore();
+ return null;
+ } catch (KeyChainException e) {
+ mHandler.ignore();
+ return null;
+ }
+ mHandler.proceed(privateKey, certificateChain);
+ return null;
+ }
+}
diff --git a/src/com/android/browser/LogTag.java b/src/com/android/browser/LogTag.java
new file mode 100644
index 0000000..b2393c7
--- /dev/null
+++ b/src/com/android/browser/LogTag.java
@@ -0,0 +1,62 @@
+/*
+ * 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.util.EventLog;
+
+public class LogTag {
+
+ public static final int BROWSER_BOOKMARK_ADDED = 70103;
+ public static final int BROWSER_PAGE_LOADED = 70104;
+ public static final int BROWSER_TIMEONPAGE = 70105;
+ /**
+ * Log when the user is adding a new bookmark.
+ *
+ * @param url the url of the new bookmark.
+ * @param where the location from where the bookmark was added
+ */
+ public static void logBookmarkAdded(String url, String where) {
+ EventLog.writeEvent(BROWSER_BOOKMARK_ADDED, url + "|"
+ + where);
+ }
+
+ /**
+ * Log when a page has finished loading with how much
+ * time the browser used to load the page.
+ *
+ * Note that a redirect will restart the timer, so this time is not
+ * always how long it takes for the user to load a page.
+ *
+ * @param url the url of that page that finished loading.
+ * @param duration the time the browser spent loading the page.
+ */
+ public static void logPageFinishedLoading(String url, long duration) {
+ EventLog.writeEvent(BROWSER_PAGE_LOADED, url + "|"
+ + duration);
+ }
+
+ /**
+ * log the time the user has spent on a webpage
+ *
+ * @param url the url of the page that is being logged (old page).
+ * @param duration the time spent on the webpage.
+ */
+ public static void logTimeOnPage(String url, long duration) {
+ EventLog.writeEvent(BROWSER_TIMEONPAGE, url + "|"
+ + duration);
+ }
+}
diff --git a/src/com/android/browser/MemoryMonitor.java b/src/com/android/browser/MemoryMonitor.java
new file mode 100644
index 0000000..a18f698
--- /dev/null
+++ b/src/com/android/browser/MemoryMonitor.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of The Linux Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+package com.android.browser;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.util.Log;
+import java.sql.Timestamp;
+
+public class MemoryMonitor {
+
+ //This number is used with device memory class to calculate max number
+ //of active tabs.
+ private static int sMaxActiveTabs = 0;
+ private static MemoryMonitor sMemoryMonitor;
+ private TabControl mTabControl;
+ private final static String LOGTAG = "MemoryMonitor";
+
+ // Should be called only once
+
+ public static MemoryMonitor getInstance(Context context,
+ Controller controller) {
+ if (sMemoryMonitor == null) {
+ sMemoryMonitor = new MemoryMonitor(context,controller);
+ }
+ return sMemoryMonitor;
+ }
+
+ MemoryMonitor(Context context,Controller controller) {
+ mTabControl = controller.getTabControl();
+ sMaxActiveTabs = getMaxActiveTabs(context);
+ Log.d(LOGTAG,"Max Active Tabs: "+ sMaxActiveTabs);
+ }
+
+ private int getActiveTabs() {
+ int numNativeActiveTab = 0;
+ int size = mTabControl.getTabCount();
+
+ for (int i = 0; i < size; i++) {
+ Tab tab = mTabControl.getTab(i);
+ if (((Tab)tab).isNativeActive()){
+ numNativeActiveTab++;
+ }
+ }
+ return numNativeActiveTab;
+ }
+
+ /**
+ * if number of tabs whose native tab is active, is greater
+ * than MAX_ACTIVE_TABS destroy the nativetab of oldest used Tab
+ */
+
+ public void destroyLeastRecentlyActiveTab() {
+ int numActiveTabs = getActiveTabs();
+ int numActiveTabsToRelease = numActiveTabs - sMaxActiveTabs;
+
+ // The most common case will be that we need to delete one
+ // NativeTab to make room for a new one. So, find the most-stale.
+ if (numActiveTabsToRelease == 1) {
+ Tab mostStaleTab = null;
+ for (Tab t : mTabControl.getTabs()) {
+ if (t.isNativeActive() && !(t.inForeground())) {
+ if (mostStaleTab == null){
+ mostStaleTab = t;
+ }
+ else {
+ if (t.getTimestamp().compareTo(mostStaleTab.
+ getTimestamp()) < 0) {
+ mostStaleTab = t;
+ }
+ }
+ }
+ }
+ if (mostStaleTab != null) {
+ mostStaleTab.destroy();
+ }
+ } else if (numActiveTabsToRelease > 1) {
+ // Since there is more than 1 "extra" tab, just release all
+ // NativeTabs in the background. This would be true when
+ // tracking was turned on after multiple tabs already exists
+ for (Tab t : mTabControl.getTabs()) {
+ if (t.isNativeActive() && !(t.inForeground())) {
+ t.destroy();
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the default max number of active tabs based on device's
+ * memory class.
+ */
+ static int getMaxActiveTabs(Context context) {
+ // We use device memory class to decide number of active tabs
+ // (minimum memory class is 16).
+ ActivityManager am =(ActivityManager)context.
+ getSystemService(Context.ACTIVITY_SERVICE);
+ if (am.getMemoryClass() < 33) {
+ return 1; // only 1 Tab can be active at a time
+ }
+ else {
+ return 2; // atleast 2 Tabs can be active at a time
+ }
+ }
+}
diff --git a/src/com/android/browser/MessagesReceiver.java b/src/com/android/browser/MessagesReceiver.java
new file mode 100644
index 0000000..d59ae84
--- /dev/null
+++ b/src/com/android/browser/MessagesReceiver.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of The Linux Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.android.browser;
+
+import org.w3c.dom.Text;
+
+import com.android.browser.R;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.Toast;
+
+public class MessagesReceiver extends BroadcastReceiver {
+ private static final String TAG = "MessagesReceiver";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "onReceive: " + intent.getAction());
+ if ((intent == null) || TextUtils.isEmpty(intent.getStringExtra("from"))) {
+ return;
+ }
+
+ if (BrowserSettings.getInstance().useFullscreen()) {
+ String from = intent.getStringExtra("from");
+ Log.d(TAG, "the message from: " + from);
+ Toast.makeText(context, context.getString(R.string.received_message_full_screen, from),
+ Toast.LENGTH_LONG).show();
+ }
+ }
+}
diff --git a/src/com/android/browser/NavScreen.java b/src/com/android/browser/NavScreen.java
new file mode 100644
index 0000000..42b35de
--- /dev/null
+++ b/src/com/android/browser/NavScreen.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2011 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.Context;
+import android.content.res.Configuration;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+import android.widget.PopupMenu.OnMenuItemClickListener;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.android.browser.R;
+import com.android.browser.NavTabScroller.OnLayoutListener;
+import com.android.browser.NavTabScroller.OnRemoveListener;
+import com.android.browser.TabControl.OnThumbnailUpdatedListener;
+import com.android.browser.UI.ComboViews;
+
+import java.util.HashMap;
+
+public class NavScreen extends RelativeLayout
+ implements OnClickListener, OnMenuItemClickListener, OnThumbnailUpdatedListener {
+
+
+ UiController mUiController;
+ PhoneUi mUi;
+ Tab mTab;
+ Activity mActivity;
+
+ ImageButton mRefresh;
+ ImageButton mForward;
+ ImageButton mBookmarks;
+ ImageButton mMore;
+ ImageButton mNewTab;
+ FrameLayout mHolder;
+
+ TextView mTitle;
+ ImageView mFavicon;
+ ImageButton mCloseTab;
+
+ NavTabScroller mScroller;
+ TabAdapter mAdapter;
+ int mOrientation;
+ boolean mNeedsMenu;
+ HashMap<Tab, View> mTabViews;
+
+ public NavScreen(Activity activity, UiController ctl, PhoneUi ui) {
+ super(activity);
+ mActivity = activity;
+ mUiController = ctl;
+ mUi = ui;
+ mOrientation = activity.getResources().getConfiguration().orientation;
+ init();
+ }
+
+ protected void showMenu() {
+ PopupMenu popup = new PopupMenu(getContext(), mMore);
+ Menu menu = popup.getMenu();
+ popup.getMenuInflater().inflate(R.menu.browser, menu);
+ mUiController.updateMenuState(mUiController.getCurrentTab(), menu);
+ popup.setOnMenuItemClickListener(this);
+ popup.show();
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ return mUiController.onOptionsItemSelected(item);
+ }
+
+ protected float getToolbarHeight() {
+ return mActivity.getResources().getDimension(R.dimen.toolbar_height);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newconfig) {
+ if (newconfig.orientation != mOrientation) {
+ int sv = mScroller.getScrollValue();
+ removeAllViews();
+ mOrientation = newconfig.orientation;
+ init();
+ mScroller.setScrollValue(sv);
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ public void refreshAdapter() {
+ mScroller.handleDataChanged(
+ mUiController.getTabControl().getTabPosition(mUi.getActiveTab()));
+ }
+
+ private void init() {
+ LayoutInflater.from(getContext()).inflate(R.layout.nav_screen, this);
+ setContentDescription(getContext().getResources().getString(
+ R.string.accessibility_transition_navscreen));
+ mBookmarks = (ImageButton) findViewById(R.id.bookmarks);
+ mNewTab = (ImageButton) findViewById(R.id.newtab);
+ mMore = (ImageButton) findViewById(R.id.more);
+ mBookmarks.setOnClickListener(this);
+ mNewTab.setOnClickListener(this);
+ mMore.setOnClickListener(this);
+ mScroller = (NavTabScroller) findViewById(R.id.scroller);
+ TabControl tc = mUiController.getTabControl();
+ mTabViews = new HashMap<Tab, View>(tc.getTabCount());
+ mAdapter = new TabAdapter(getContext(), tc);
+ mScroller.setOrientation(mOrientation == Configuration.ORIENTATION_LANDSCAPE
+ ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
+ // update state for active tab
+ mScroller.setAdapter(mAdapter,
+ mUiController.getTabControl().getTabPosition(mUi.getActiveTab()));
+ mScroller.setOnRemoveListener(new OnRemoveListener() {
+ public void onRemovePosition(int pos) {
+ Tab tab = mAdapter.getItem(pos);
+ onCloseTab(tab);
+ }
+ });
+ mNeedsMenu = !ViewConfiguration.get(getContext()).hasPermanentMenuKey();
+ if (!mNeedsMenu) {
+ mMore.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mBookmarks == v) {
+ mUiController.bookmarksOrHistoryPicker(ComboViews.Bookmarks);
+ } else if (mNewTab == v) {
+ openNewTab();
+ } else if (mMore == v) {
+ showMenu();
+ }
+ }
+
+ private void onCloseTab(Tab tab) {
+ if (tab != null) {
+ if (tab == mUiController.getCurrentTab()) {
+ mUiController.closeCurrentTab();
+ } else {
+ mUiController.closeTab(tab);
+ }
+ }
+ }
+
+ private void openNewTab() {
+ // need to call openTab explicitely with setactive false
+ final Tab tab = mUiController.openTab(BrowserSettings.getInstance().getHomePage(),
+ false, false, false);
+ if (tab != null) {
+ mUiController.setBlockEvents(true);
+ final int tix = mUi.mTabControl.getTabPosition(tab);
+ mScroller.setOnLayoutListener(new OnLayoutListener() {
+
+ @Override
+ public void onLayout(int l, int t, int r, int b) {
+ mUi.hideNavScreen(tix, true);
+ switchToTab(tab);
+ }
+ });
+ mScroller.handleDataChanged(tix);
+ mUiController.setBlockEvents(false);
+ }
+ }
+
+ private void switchToTab(Tab tab) {
+ if (tab != mUi.getActiveTab()) {
+ mUiController.setActiveTab(tab);
+ }
+ }
+
+ protected void close(int position) {
+ close(position, true);
+ }
+
+ protected void close(int position, boolean animate) {
+ mUi.hideNavScreen(position, animate);
+ }
+
+ protected NavTabView getTabView(int pos) {
+ return mScroller.getTabView(pos);
+ }
+
+ class TabAdapter extends BaseAdapter {
+
+ Context context;
+ TabControl tabControl;
+
+ public TabAdapter(Context ctx, TabControl tc) {
+ context = ctx;
+ tabControl = tc;
+ }
+
+ @Override
+ public int getCount() {
+ return tabControl.getTabCount();
+ }
+
+ @Override
+ public Tab getItem(int position) {
+ return tabControl.getTab(position);
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ final NavTabView tabview = new NavTabView(mActivity);
+ final Tab tab = getItem(position);
+ tabview.setWebView(tab);
+ mTabViews.put(tab, tabview.mImage);
+ tabview.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (tabview.isClose(v)) {
+ mScroller.animateOut(tabview);
+ } else if (tabview.isTitle(v)) {
+ switchToTab(tab);
+ mUi.getTitleBar().setSkipTitleBarAnimations(true);
+ close(position, false);
+ mUi.editUrl(false, true);
+ mUi.getTitleBar().setSkipTitleBarAnimations(false);
+ } else if (tabview.isWebView(v)) {
+ close(position);
+ }
+ }
+ });
+ return tabview;
+ }
+
+ }
+
+ @Override
+ public void onThumbnailUpdated(Tab t) {
+ View v = mTabViews.get(t);
+ if (v != null) {
+ v.invalidate();
+ }
+ }
+
+}
diff --git a/src/com/android/browser/NavTabScroller.java b/src/com/android/browser/NavTabScroller.java
new file mode 100644
index 0000000..a23ebe9
--- /dev/null
+++ b/src/com/android/browser/NavTabScroller.java
@@ -0,0 +1,569 @@
+/*
+ * Copyright (C) 2011 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.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.BaseAdapter;
+import android.widget.LinearLayout;
+
+import com.android.browser.view.ScrollerView;
+
+/**
+ * custom view for displaying tabs in the nav screen
+ */
+public class NavTabScroller extends ScrollerView {
+
+ static final int INVALID_POSITION = -1;
+ static final float[] PULL_FACTOR = { 2.5f, 0.9f };
+
+ interface OnRemoveListener {
+ public void onRemovePosition(int position);
+ }
+
+ interface OnLayoutListener {
+ public void onLayout(int l, int t, int r, int b);
+ }
+
+ private ContentLayout mContentView;
+ private BaseAdapter mAdapter;
+ private OnRemoveListener mRemoveListener;
+ private OnLayoutListener mLayoutListener;
+ private int mGap;
+ private int mGapPosition;
+ private ObjectAnimator mGapAnimator;
+
+ // after drag animation velocity in pixels/sec
+ private static final float MIN_VELOCITY = 1500;
+ private AnimatorSet mAnimator;
+
+ private float mFlingVelocity;
+ private boolean mNeedsScroll;
+ private int mScrollPosition;
+
+ DecelerateInterpolator mCubic;
+ int mPullValue;
+
+ public NavTabScroller(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(context);
+ }
+
+ public NavTabScroller(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public NavTabScroller(Context context) {
+ super(context);
+ init(context);
+ }
+
+ private void init(Context ctx) {
+ mCubic = new DecelerateInterpolator(1.5f);
+ mGapPosition = INVALID_POSITION;
+ setHorizontalScrollBarEnabled(false);
+ setVerticalScrollBarEnabled(false);
+ mContentView = new ContentLayout(ctx, this);
+ mContentView.setOrientation(LinearLayout.HORIZONTAL);
+ addView(mContentView);
+ mContentView.setLayoutParams(
+ new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
+ // ProGuard !
+ setGap(getGap());
+ mFlingVelocity = getContext().getResources().getDisplayMetrics().density
+ * MIN_VELOCITY;
+ }
+
+ protected int getScrollValue() {
+ return mHorizontal ? getScrollX() : getScrollY();
+ }
+
+ protected void setScrollValue(int value) {
+ scrollTo(mHorizontal ? value : 0, mHorizontal ? 0 : value);
+ }
+
+ protected NavTabView getTabView(int pos) {
+ return (NavTabView) mContentView.getChildAt(pos);
+ }
+
+ protected boolean isHorizontal() {
+ return mHorizontal;
+ }
+
+ public void setOrientation(int orientation) {
+ mContentView.setOrientation(orientation);
+ if (orientation == LinearLayout.HORIZONTAL) {
+ mContentView.setLayoutParams(
+ new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
+ } else {
+ mContentView.setLayoutParams(
+ new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
+ }
+ super.setOrientation(orientation);
+ }
+
+ @Override
+ protected void onMeasure(int wspec, int hspec) {
+ super.onMeasure(wspec, hspec);
+ calcPadding();
+ }
+
+ private void calcPadding() {
+ if (mAdapter.getCount() > 0) {
+ View v = mContentView.getChildAt(0);
+ if (mHorizontal) {
+ int pad = (getMeasuredWidth() - v.getMeasuredWidth()) / 2 + 2;
+ mContentView.setPadding(pad, 0, pad, 0);
+ } else {
+ int pad = (getMeasuredHeight() - v.getMeasuredHeight()) / 2 + 2;
+ mContentView.setPadding(0, pad, 0, pad);
+ }
+ }
+ }
+
+ public void setAdapter(BaseAdapter adapter) {
+ setAdapter(adapter, 0);
+ }
+
+
+ public void setOnRemoveListener(OnRemoveListener l) {
+ mRemoveListener = l;
+ }
+
+ public void setOnLayoutListener(OnLayoutListener l) {
+ mLayoutListener = l;
+ }
+
+ protected void setAdapter(BaseAdapter adapter, int selection) {
+ mAdapter = adapter;
+ mAdapter.registerDataSetObserver(new DataSetObserver() {
+
+ @Override
+ public void onChanged() {
+ super.onChanged();
+ handleDataChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ super.onInvalidated();
+ }
+ });
+ handleDataChanged(selection);
+ }
+
+ protected ViewGroup getContentView() {
+ return mContentView;
+ }
+
+ protected int getRelativeChildTop(int ix) {
+ return mContentView.getChildAt(ix).getTop() - getScrollY();
+ }
+
+ protected void handleDataChanged() {
+ handleDataChanged(INVALID_POSITION);
+ }
+
+ void handleDataChanged(int newscroll) {
+ int scroll = getScrollValue();
+ if (mGapAnimator != null) {
+ mGapAnimator.cancel();
+ }
+ mContentView.removeAllViews();
+ for (int i = 0; i < mAdapter.getCount(); i++) {
+ View v = mAdapter.getView(i, null, mContentView);
+ LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+ LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ lp.gravity = (mHorizontal ? Gravity.CENTER_VERTICAL : Gravity.CENTER_HORIZONTAL);
+ mContentView.addView(v, lp);
+ if (mGapPosition > INVALID_POSITION){
+ adjustViewGap(v, i);
+ }
+ }
+ if (newscroll > INVALID_POSITION) {
+ newscroll = Math.min(mAdapter.getCount() - 1, newscroll);
+ mNeedsScroll = true;
+ mScrollPosition = newscroll;
+ requestLayout();
+ } else {
+ setScrollValue(scroll);
+ }
+ }
+
+ protected void finishScroller() {
+ mScroller.forceFinished(true);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+ if (mNeedsScroll) {
+ mScroller.forceFinished(true);
+ snapToSelected(mScrollPosition, false);
+ mNeedsScroll = false;
+ }
+ if (mLayoutListener != null) {
+ mLayoutListener.onLayout(l, t, r, b);
+ mLayoutListener = null;
+ }
+ }
+
+ void clearTabs() {
+ mContentView.removeAllViews();
+ }
+
+ void snapToSelected(int pos, boolean smooth) {
+ if (pos < 0) return;
+ View v = mContentView.getChildAt(pos);
+ if (v == null) return;
+ int sx = 0;
+ int sy = 0;
+ if (mHorizontal) {
+ sx = (v.getLeft() + v.getRight() - getWidth()) / 2;
+ } else {
+ sy = (v.getTop() + v.getBottom() - getHeight()) / 2;
+ }
+ if ((sx != getScrollX()) || (sy != getScrollY())) {
+ if (smooth) {
+ smoothScrollTo(sx,sy);
+ } else {
+ scrollTo(sx, sy);
+ }
+ }
+ }
+
+ protected void animateOut(View v) {
+ if (v == null) return;
+ animateOut(v, -mFlingVelocity);
+ }
+
+ private void animateOut(final View v, float velocity) {
+ float start = mHorizontal ? v.getTranslationY() : v.getTranslationX();
+ animateOut(v, velocity, start);
+ }
+
+ private void animateOut(final View v, float velocity, float start) {
+ if ((v == null) || (mAnimator != null)) return;
+ final int position = mContentView.indexOfChild(v);
+ int target = 0;
+ if (velocity < 0) {
+ target = mHorizontal ? -getHeight() : -getWidth();
+ } else {
+ target = mHorizontal ? getHeight() : getWidth();
+ }
+ int distance = target - (mHorizontal ? v.getTop() : v.getLeft());
+ long duration = (long) (Math.abs(distance) * 1000 / Math.abs(velocity));
+ int scroll = 0;
+ int translate = 0;
+ int gap = mHorizontal ? v.getWidth() : v.getHeight();
+ int centerView = getViewCenter(v);
+ int centerScreen = getScreenCenter();
+ int newpos = INVALID_POSITION;
+ if (centerView < centerScreen - gap / 2) {
+ // top view
+ scroll = - (centerScreen - centerView - gap);
+ translate = (position > 0) ? gap : 0;
+ newpos = position;
+ } else if (centerView > centerScreen + gap / 2) {
+ // bottom view
+ scroll = - (centerScreen + gap - centerView);
+ if (position < mAdapter.getCount() - 1) {
+ translate = -gap;
+ }
+ } else {
+ // center view
+ scroll = - (centerScreen - centerView);
+ if (position < mAdapter.getCount() - 1) {
+ translate = -gap;
+ } else {
+ scroll -= gap;
+ }
+ }
+ mGapPosition = position;
+ final int pos = newpos;
+ ObjectAnimator trans = ObjectAnimator.ofFloat(v,
+ (mHorizontal ? TRANSLATION_Y : TRANSLATION_X), start, target);
+ ObjectAnimator alpha = ObjectAnimator.ofFloat(v, ALPHA, getAlpha(v,start),
+ getAlpha(v,target));
+ AnimatorSet set1 = new AnimatorSet();
+ set1.playTogether(trans, alpha);
+ set1.setDuration(duration);
+ mAnimator = new AnimatorSet();
+ ObjectAnimator trans2 = null;
+ ObjectAnimator scroll1 = null;
+ if (scroll != 0) {
+ if (mHorizontal) {
+ scroll1 = ObjectAnimator.ofInt(this, "scrollX", getScrollX(), getScrollX() + scroll);
+ } else {
+ scroll1 = ObjectAnimator.ofInt(this, "scrollY", getScrollY(), getScrollY() + scroll);
+ }
+ }
+ if (translate != 0) {
+ trans2 = ObjectAnimator.ofInt(this, "gap", 0, translate);
+ }
+ final int duration2 = 200;
+ if (scroll1 != null) {
+ if (trans2 != null) {
+ AnimatorSet set2 = new AnimatorSet();
+ set2.playTogether(scroll1, trans2);
+ set2.setDuration(duration2);
+ mAnimator.playSequentially(set1, set2);
+ } else {
+ scroll1.setDuration(duration2);
+ mAnimator.playSequentially(set1, scroll1);
+ }
+ } else {
+ if (trans2 != null) {
+ trans2.setDuration(duration2);
+ mAnimator.playSequentially(set1, trans2);
+ }
+ }
+ mAnimator.addListener(new AnimatorListenerAdapter() {
+ public void onAnimationEnd(Animator a) {
+ if (mRemoveListener != null) {
+ mRemoveListener.onRemovePosition(position);
+ mAnimator = null;
+ mGapPosition = INVALID_POSITION;
+ mGap = 0;
+ handleDataChanged(pos);
+ }
+ }
+ });
+ mAnimator.start();
+ }
+
+ public void setGap(int gap) {
+ if (mGapPosition != INVALID_POSITION) {
+ mGap = gap;
+ postInvalidate();
+ }
+ }
+
+ public int getGap() {
+ return mGap;
+ }
+
+ void adjustGap() {
+ for (int i = 0; i < mContentView.getChildCount(); i++) {
+ final View child = mContentView.getChildAt(i);
+ adjustViewGap(child, i);
+ }
+ }
+
+ private void adjustViewGap(View view, int pos) {
+ if ((mGap < 0 && pos > mGapPosition)
+ || (mGap > 0 && pos < mGapPosition)) {
+ if (mHorizontal) {
+ view.setTranslationX(mGap);
+ } else {
+ view.setTranslationY(mGap);
+ }
+ }
+ }
+
+ private int getViewCenter(View v) {
+ if (mHorizontal) {
+ return v.getLeft() + v.getWidth() / 2;
+ } else {
+ return v.getTop() + v.getHeight() / 2;
+ }
+ }
+
+ private int getScreenCenter() {
+ if (mHorizontal) {
+ return getScrollX() + getWidth() / 2;
+ } else {
+ return getScrollY() + getHeight() / 2;
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (mGapPosition > INVALID_POSITION) {
+ adjustGap();
+ }
+ super.draw(canvas);
+ }
+
+ @Override
+ protected View findViewAt(int x, int y) {
+ x += getScrollX();
+ y += getScrollY();
+ final int count = mContentView.getChildCount();
+ for (int i = count - 1; i >= 0; i--) {
+ View child = mContentView.getChildAt(i);
+ if (child.getVisibility() == View.VISIBLE) {
+ if ((x >= child.getLeft()) && (x < child.getRight())
+ && (y >= child.getTop()) && (y < child.getBottom())) {
+ return child;
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void onOrthoDrag(View v, float distance) {
+ if ((v != null) && (mAnimator == null)) {
+ offsetView(v, distance);
+ }
+ }
+
+ @Override
+ protected void onOrthoDragFinished(View downView) {
+ if (mAnimator != null) return;
+ if (mIsOrthoDragged && downView != null) {
+ // offset
+ float diff = mHorizontal ? downView.getTranslationY() : downView.getTranslationX();
+ if (Math.abs(diff) > (mHorizontal ? downView.getHeight() : downView.getWidth()) / 2) {
+ // remove it
+ animateOut(downView, Math.signum(diff) * mFlingVelocity, diff);
+ } else {
+ // snap back
+ offsetView(downView, 0);
+ }
+ }
+ }
+
+ @Override
+ protected void onOrthoFling(View v, float velocity) {
+ if (v == null) return;
+ if (mAnimator == null && Math.abs(velocity) > mFlingVelocity / 2) {
+ animateOut(v, velocity);
+ } else {
+ offsetView(v, 0);
+ }
+ }
+
+ private void offsetView(View v, float distance) {
+ v.setAlpha(getAlpha(v, distance));
+ if (mHorizontal) {
+ v.setTranslationY(distance);
+ } else {
+ v.setTranslationX(distance);
+ }
+ }
+
+ private float getAlpha(View v, float distance) {
+ return 1 - (float) Math.abs(distance) / (mHorizontal ? v.getHeight() : v.getWidth());
+ }
+
+ private float ease(DecelerateInterpolator inter, float value, float start,
+ float dist, float duration) {
+ return start + dist * inter.getInterpolation(value / duration);
+ }
+
+ @Override
+ protected void onPull(int delta) {
+ boolean layer = false;
+ int count = 2;
+ if (delta == 0 && mPullValue == 0) return;
+ if (delta == 0 && mPullValue != 0) {
+ // reset
+ for (int i = 0; i < count; i++) {
+ View child = mContentView.getChildAt((mPullValue < 0)
+ ? i
+ : mContentView.getChildCount() - 1 - i);
+ if (child == null) break;
+ ObjectAnimator trans = ObjectAnimator.ofFloat(child,
+ mHorizontal ? "translationX" : "translationY",
+ mHorizontal ? getTranslationX() : getTranslationY(),
+ 0);
+ ObjectAnimator rot = ObjectAnimator.ofFloat(child,
+ mHorizontal ? "rotationY" : "rotationX",
+ mHorizontal ? getRotationY() : getRotationX(),
+ 0);
+ AnimatorSet set = new AnimatorSet();
+ set.playTogether(trans, rot);
+ set.setDuration(100);
+ set.start();
+ }
+ mPullValue = 0;
+ } else {
+ if (mPullValue == 0) {
+ layer = true;
+ }
+ mPullValue += delta;
+ }
+ final int height = mHorizontal ? getWidth() : getHeight();
+ int oscroll = Math.abs(mPullValue);
+ int factor = (mPullValue <= 0) ? 1 : -1;
+ for (int i = 0; i < count; i++) {
+ View child = mContentView.getChildAt((mPullValue < 0)
+ ? i
+ : mContentView.getChildCount() - 1 - i);
+ if (child == null) break;
+ if (layer) {
+ }
+ float k = PULL_FACTOR[i];
+ float rot = -factor * ease(mCubic, oscroll, 0, k * 2, height);
+ int y = factor * (int) ease(mCubic, oscroll, 0, k*20, height);
+ if (mHorizontal) {
+ child.setTranslationX(y);
+ } else {
+ child.setTranslationY(y);
+ }
+ if (mHorizontal) {
+ child.setRotationY(-rot);
+ } else {
+ child.setRotationX(rot);
+ }
+ }
+ }
+
+ static class ContentLayout extends LinearLayout {
+
+ NavTabScroller mScroller;
+
+ public ContentLayout(Context context, NavTabScroller scroller) {
+ super(context);
+ mScroller = scroller;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ if (mScroller.getGap() != 0) {
+ View v = getChildAt(0);
+ if (v != null) {
+ if (mScroller.isHorizontal()) {
+ int total = v.getMeasuredWidth() + getMeasuredWidth();
+ setMeasuredDimension(total, getMeasuredHeight());
+ } else {
+ int total = v.getMeasuredHeight() + getMeasuredHeight();
+ setMeasuredDimension(getMeasuredWidth(), total);
+ }
+ }
+
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/com/android/browser/NavTabView.java b/src/com/android/browser/NavTabView.java
new file mode 100644
index 0000000..3bcd7a2
--- /dev/null
+++ b/src/com/android/browser/NavTabView.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2011 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.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.R;
+
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class NavTabView extends LinearLayout {
+
+ private ViewGroup mContent;
+ private Tab mTab;
+ private ImageView mClose;
+ private TextView mTitle;
+ private View mTitleBar;
+ ImageView mImage;
+ private OnClickListener mClickListener;
+ private boolean mHighlighted;
+
+ public NavTabView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ public NavTabView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public NavTabView(Context context) {
+ super(context);
+ init();
+ }
+
+ private void init() {
+ LayoutInflater.from(getContext()).inflate(R.layout.nav_tab_view, this);
+ mContent = (ViewGroup) findViewById(R.id.main);
+ mClose = (ImageView) findViewById(R.id.closetab);
+ mTitle = (TextView) findViewById(R.id.title);
+ mTitleBar = findViewById(R.id.titlebar);
+ mImage = (ImageView) findViewById(R.id.tab_view);
+ }
+
+ protected boolean isClose(View v) {
+ return v == mClose;
+ }
+
+ protected boolean isTitle(View v) {
+ return v == mTitleBar;
+ }
+
+ protected boolean isWebView(View v) {
+ return v == mImage;
+ }
+
+ private void setTitle() {
+ if (mTab == null) return;
+ if (mHighlighted) {
+ mTitle.setText(mTab.getUrl());
+ } else {
+ String txt = mTab.getTitle();
+ if (txt == null) {
+ txt = mTab.getUrl();
+ }
+ mTitle.setText(txt);
+ }
+ if (mTab.isSnapshot()) {
+ setTitleIcon(R.drawable.ic_history_holo_dark);
+ } else if (mTab.isPrivateBrowsingEnabled()) {
+ setTitleIcon(R.drawable.ic_incognito_holo_dark);
+ } else {
+ setTitleIcon(0);
+ }
+ }
+
+ private void setTitleIcon(int id) {
+ if (id == 0) {
+ mTitle.setPadding(mTitle.getCompoundDrawablePadding(), 0, 0, 0);
+ } else {
+ mTitle.setPadding(0, 0, 0, 0);
+ }
+ mTitle.setCompoundDrawablesWithIntrinsicBounds(id, 0, 0, 0);
+ }
+
+ protected boolean isHighlighted() {
+ return mHighlighted;
+ }
+
+ protected void setWebView(Tab tab) {
+ mTab = tab;
+ setTitle();
+ Bitmap image = tab.getScreenshot();
+ if (image != null) {
+ mImage.setImageBitmap(image);
+ if (tab != null) {
+ mImage.setContentDescription(tab.getTitle());
+ }
+ }
+ }
+
+ @Override
+ public void setOnClickListener(OnClickListener listener) {
+ mClickListener = listener;
+ mTitleBar.setOnClickListener(mClickListener);
+ mClose.setOnClickListener(mClickListener);
+ if (mImage != null) {
+ mImage.setOnClickListener(mClickListener);
+ }
+ }
+
+}
diff --git a/src/com/android/browser/NavigationBarBase.java b/src/com/android/browser/NavigationBarBase.java
new file mode 100644
index 0000000..0cf23ee
--- /dev/null
+++ b/src/com/android/browser/NavigationBarBase.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2011 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.SearchManager;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnFocusChangeListener;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.Toast;
+
+import com.android.browser.R;
+import com.android.browser.UrlInputView.UrlInputListener;
+import com.android.browser.reflect.ReflectHelper;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URISyntaxException;
+
+import org.codeaurora.swe.WebView;
+
+public class NavigationBarBase extends LinearLayout implements
+ OnClickListener, UrlInputListener, OnFocusChangeListener,
+ TextWatcher {
+
+ private final static String TAG = "NavigationBarBase";
+
+ protected BaseUi mBaseUi;
+ protected TitleBar mTitleBar;
+ protected UiController mUiController;
+ protected UrlInputView mUrlInput;
+
+ private ImageView mFavicon;
+ private ImageView mLockIcon;
+
+ public NavigationBarBase(Context context) {
+ super(context);
+ }
+
+ public NavigationBarBase(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public NavigationBarBase(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mLockIcon = (ImageView) findViewById(R.id.lock);
+ mFavicon = (ImageView) findViewById(R.id.favicon);
+ mUrlInput = (UrlInputView) findViewById(R.id.url);
+ mUrlInput.setUrlInputListener(this);
+ mUrlInput.setOnFocusChangeListener(this);
+ mUrlInput.setSelectAllOnFocus(true);
+ mUrlInput.addTextChangedListener(this);
+ }
+
+ public void setTitleBar(TitleBar titleBar) {
+ mTitleBar = titleBar;
+ mBaseUi = mTitleBar.getUi();
+ mUiController = mTitleBar.getUiController();
+ mUrlInput.setController(mUiController);
+ }
+
+ public void setLock(Drawable d) {
+ if (mLockIcon == null) return;
+ if (d == null) {
+ mLockIcon.setVisibility(View.GONE);
+ } else {
+ mLockIcon.setImageDrawable(d);
+ mLockIcon.setVisibility(View.VISIBLE);
+ }
+ }
+
+ public void setFavicon(Bitmap icon) {
+ if (mFavicon == null) return;
+ mFavicon.setImageDrawable(mBaseUi.getFaviconDrawable(icon));
+ }
+
+ @Override
+ public void onClick(View v) {
+ }
+
+ @Override
+ public void onFocusChange(View view, boolean hasFocus) {
+ // if losing focus and not in touch mode, leave as is
+ if (hasFocus || view.isInTouchMode() || mUrlInput.needsUpdate()) {
+ setFocusState(hasFocus);
+ }
+ if (hasFocus) {
+ mBaseUi.showTitleBar();
+ } else if (!mUrlInput.needsUpdate()) {
+ mUrlInput.dismissDropDown();
+ mUrlInput.hideIME();
+ if (mUrlInput.getText().length() == 0) {
+ Tab currentTab = mUiController.getTabControl().getCurrentTab();
+ if (currentTab != null) {
+ setDisplayTitle(currentTab.getUrl());
+ }
+ }
+ mBaseUi.suggestHideTitleBar();
+ }
+ mUrlInput.clearNeedsUpdate();
+ }
+
+ protected void setFocusState(boolean focus) {
+ }
+
+ public boolean isEditingUrl() {
+ return mUrlInput.hasFocus();
+ }
+
+ void stopEditingUrl() {
+ WebView currentTopWebView = mUiController.getCurrentTopWebView();
+ if (currentTopWebView != null) {
+ currentTopWebView.requestFocus();
+ }
+ }
+
+ void setDisplayTitle(String title) {
+ if (!isEditingUrl()) {
+ if (!title.equals(mUrlInput.getText().toString())) {
+ mUrlInput.setText(title, false);
+ }
+ }
+ }
+
+ void setIncognitoMode(boolean incognito) {
+ mUrlInput.setIncognitoMode(incognito);
+ }
+
+ void clearCompletions() {
+ mUrlInput.dismissDropDown();
+ }
+
+ // UrlInputListener implementation
+
+ /**
+ * callback from suggestion dropdown
+ * user selected a suggestion
+ */
+ @Override
+ public void onAction(String text, String extra, String source) {
+ stopEditingUrl();
+ if (UrlInputView.TYPED.equals(source)) {
+ String url = null;
+ Object[] params = {new String("persist.env.browser.wap2estore"),
+ Boolean.valueOf(false)};
+ Class[] type = new Class[] {String.class, boolean.class};
+ Boolean wap2estore = (Boolean) ReflectHelper.invokeStaticMethod(
+ "android.os.SystemProperties", "getBoolean", type, params);
+ if ((wap2estore && isEstoreTypeUrl(text)) || isRtspTypeUrl(text)) {
+ url = text;
+ } else {
+ url = UrlUtils.smartUrlFilter(text, false);
+ }
+
+ Tab t = mBaseUi.getActiveTab();
+ // Only shortcut javascript URIs for now, as there is special
+ // logic in UrlHandler for other schemas
+ if (url != null && t != null && url.startsWith("javascript:")) {
+ mUiController.loadUrl(t, url);
+ setDisplayTitle(text);
+ return;
+ }
+
+ // add for carrier wap2estore feature
+ if (url != null && t != null && wap2estore && isEstoreTypeUrl(url)) {
+ handleEstoreTypeUrl(url);
+ setDisplayTitle(text);
+ return;
+ }
+ // add for rtsp scheme feature
+ if (url != null && t != null && isRtspTypeUrl(url)) {
+ if (handleRtspTypeUrl(url)) {
+ return;
+ }
+ }
+ }
+ Intent i = new Intent();
+ String action = Intent.ACTION_SEARCH;
+ i.setAction(action);
+ i.putExtra(SearchManager.QUERY, text);
+ if (extra != null) {
+ i.putExtra(SearchManager.EXTRA_DATA_KEY, extra);
+ }
+ if (source != null) {
+ Bundle appData = new Bundle();
+ appData.putString("source", source);
+ i.putExtra("source", appData);
+ }
+ mUiController.handleNewIntent(i);
+ setDisplayTitle(text);
+ }
+
+ private boolean isEstoreTypeUrl(String url) {
+ String utf8Url = null;
+ try {
+ utf8Url = new String(url.getBytes("UTF-8"), "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ Log.e(TAG, "err " + e);
+ }
+ if (utf8Url != null && utf8Url.startsWith("estore:")) {
+ return true;
+ }
+ return false;
+ }
+
+ private void handleEstoreTypeUrl(String url) {
+ String utf8Url = null, finalUrl = null;
+ try {
+ utf8Url = new String(url.getBytes("UTF-8"), "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ Log.e(TAG, "err " + e);
+ }
+ if (utf8Url != null) {
+ finalUrl = utf8Url;
+ } else {
+ finalUrl = url;
+ }
+ if (finalUrl.replaceFirst("estore:", "").length() > 256) {
+ Toast.makeText(getContext(), R.string.estore_url_warning, Toast.LENGTH_LONG).show();
+ return;
+ }
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(finalUrl));
+ try {
+ getContext().startActivity(intent);
+ } catch (ActivityNotFoundException ex) {
+ String downloadUrl = getContext().getResources().getString(R.string.estore_homepage);
+ mUiController.loadUrl(mBaseUi.getActiveTab(), downloadUrl);
+ Toast.makeText(getContext(), R.string.download_estore_app, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ private boolean isRtspTypeUrl(String url) {
+ String utf8Url = null;
+ try {
+ utf8Url = new String(url.getBytes("UTF-8"), "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ Log.e(TAG, "err " + e);
+ }
+ if (utf8Url != null && utf8Url.startsWith("rtsp://")) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean handleRtspTypeUrl(String url) {
+ 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;
+ }
+
+ try {
+ getContext().startActivity(intent);
+ } catch (ActivityNotFoundException ex) {
+ Log.w("Browser", "No resolveActivity " + url);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void onDismiss() {
+ final Tab currentTab = mBaseUi.getActiveTab();
+ mBaseUi.hideTitleBar();
+ post(new Runnable() {
+ public void run() {
+ clearFocus();
+ if (currentTab != null) {
+ setDisplayTitle(currentTab.getUrl());
+ }
+ }
+ });
+ }
+
+ /**
+ * callback from the suggestion dropdown
+ * copy text to input field and stay in edit mode
+ */
+ @Override
+ public void onCopySuggestion(String text) {
+ mUrlInput.setText(text, true);
+ if (text != null) {
+ mUrlInput.setSelection(text.length());
+ }
+ }
+
+ public void setCurrentUrlIsBookmark(boolean isBookmark) {
+ }
+
+ @Override
+ public boolean dispatchKeyEventPreIme(KeyEvent evt) {
+ if (evt.getKeyCode() == KeyEvent.KEYCODE_BACK) {
+ // catch back key in order to do slightly more cleanup than usual
+ stopEditingUrl();
+ return true;
+ }
+ return super.dispatchKeyEventPreIme(evt);
+ }
+
+ /**
+ * called from the Ui when the user wants to edit
+ * @param clearInput clear the input field
+ */
+ void startEditingUrl(boolean clearInput, boolean forceIME) {
+ // editing takes preference of progress
+ setVisibility(View.VISIBLE);
+ if (mTitleBar.useQuickControls()) {
+ mTitleBar.getProgressView().setVisibility(View.GONE);
+ }
+ if (!mUrlInput.hasFocus()) {
+ mUrlInput.requestFocus();
+ }
+ if (clearInput) {
+ mUrlInput.setText("");
+ }
+ if (forceIME) {
+ mUrlInput.showIME();
+ }
+ }
+
+ public void onProgressStarted() {
+ }
+
+ public void onProgressStopped() {
+ }
+
+ public boolean isMenuShowing() {
+ return false;
+ }
+
+ public void onTabDataChanged(Tab tab) {
+ }
+
+ public void onVoiceResult(String s) {
+ startEditingUrl(true, true);
+ onCopySuggestion(s);
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) { }
+
+ @Override
+ public void afterTextChanged(Editable s) { }
+
+}
diff --git a/src/com/android/browser/NavigationBarPhone.java b/src/com/android/browser/NavigationBarPhone.java
new file mode 100644
index 0000000..50ddea2
--- /dev/null
+++ b/src/com/android/browser/NavigationBarPhone.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2011 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.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewConfiguration;
+import org.codeaurora.swe.WebView;
+import android.widget.ImageView;
+import android.widget.PopupMenu;
+import android.widget.PopupMenu.OnDismissListener;
+import android.widget.PopupMenu.OnMenuItemClickListener;
+
+import com.android.browser.R;
+import com.android.browser.UrlInputView.StateListener;
+
+public class NavigationBarPhone extends NavigationBarBase implements
+ StateListener, OnMenuItemClickListener, OnDismissListener {
+
+ private ImageView mStopButton;
+ private ImageView mMagnify;
+ private ImageView mClearButton;
+ private ImageView mVoiceButton;
+ private Drawable mStopDrawable;
+ private Drawable mRefreshDrawable;
+ private String mStopDescription;
+ private String mRefreshDescription;
+ private View mTabSwitcher;
+ private View mComboIcon;
+ private View mTitleContainer;
+ private View mMore;
+ private Drawable mTextfieldBgDrawable;
+ private PopupMenu mPopupMenu;
+ private boolean mOverflowMenuShowing;
+ private boolean mNeedsMenu;
+ private View mIncognitoIcon;
+
+ public NavigationBarPhone(Context context) {
+ super(context);
+ }
+
+ public NavigationBarPhone(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public NavigationBarPhone(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mStopButton = (ImageView) findViewById(R.id.stop);
+ mStopButton.setOnClickListener(this);
+ mClearButton = (ImageView) findViewById(R.id.clear);
+ mClearButton.setOnClickListener(this);
+ mVoiceButton = (ImageView) findViewById(R.id.voice);
+ mVoiceButton.setOnClickListener(this);
+ mMagnify = (ImageView) findViewById(R.id.magnify);
+ mTabSwitcher = findViewById(R.id.tab_switcher);
+ mTabSwitcher.setOnClickListener(this);
+ mMore = findViewById(R.id.more);
+ mMore.setOnClickListener(this);
+ mComboIcon = findViewById(R.id.iconcombo);
+ mComboIcon.setOnClickListener(this);
+ mTitleContainer = findViewById(R.id.title_bg);
+ setFocusState(false);
+ Resources res = getContext().getResources();
+ mStopDrawable = res.getDrawable(R.drawable.ic_stop_holo_dark);
+ mRefreshDrawable = res.getDrawable(R.drawable.ic_refresh_holo_dark);
+ mStopDescription = res.getString(R.string.accessibility_button_stop);
+ mRefreshDescription = res.getString(R.string.accessibility_button_refresh);
+ mTextfieldBgDrawable = res.getDrawable(R.drawable.textfield_active_holo_dark);
+ mUrlInput.setContainer(this);
+ mUrlInput.setStateListener(this);
+ mNeedsMenu = !ViewConfiguration.get(getContext()).hasPermanentMenuKey();
+ mIncognitoIcon = findViewById(R.id.incognito_icon);
+ }
+
+ @Override
+ public void onProgressStarted() {
+ super.onProgressStarted();
+ if (mStopButton.getDrawable() != mStopDrawable) {
+ mStopButton.setImageDrawable(mStopDrawable);
+ mStopButton.setContentDescription(mStopDescription);
+ if (mStopButton.getVisibility() != View.VISIBLE) {
+ mComboIcon.setVisibility(View.GONE);
+ mStopButton.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ @Override
+ public void onProgressStopped() {
+ super.onProgressStopped();
+ mStopButton.setImageDrawable(mRefreshDrawable);
+ mStopButton.setContentDescription(mRefreshDescription);
+ if (!isEditingUrl()) {
+ mComboIcon.setVisibility(View.VISIBLE);
+ }
+ onStateChanged(mUrlInput.getState());
+ }
+
+ /**
+ * Update the text displayed in the title bar.
+ * @param title String to display. If null, the new tab string will be
+ * shown.
+ */
+ @Override
+ void setDisplayTitle(String title) {
+ mUrlInput.setTag(title);
+ if (!isEditingUrl()) {
+ if (title == null) {
+ mUrlInput.setText(R.string.new_tab);
+ } else {
+ Tab tab = mUiController.getTabControl().getCurrentTab();
+ if (tab != null && tab.getUrl() != null &&
+ tab.getUrl().startsWith("http://218.206.177.209:8080/waptest/browser15")) {
+ //for cmcc test case, display website title for specified cmcc website,
+ //not url address.
+ mUrlInput.setText(tab.getTitle(), false);
+ } else {
+ mUrlInput.setText(UrlUtils.stripUrl(title), false);
+ }
+ }
+ mUrlInput.setSelection(0);
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == mStopButton) {
+ if (mTitleBar.isInLoad()) {
+ mUiController.stopLoading();
+ } else {
+ WebView web = mBaseUi.getWebView();
+ if (web != null) {
+ stopEditingUrl();
+ Tab currentTab = mUiController.getTabControl().getCurrentTab();
+ if (currentTab.hasCrashed) {
+ currentTab.replaceCrashView(web, currentTab.getViewContainer());
+ }
+ web.reload();
+ }
+ }
+ } else if (v == mTabSwitcher) {
+ ((PhoneUi) mBaseUi).toggleNavScreen();
+ } else if (mMore == v) {
+ showMenu(mMore);
+ } else if (mClearButton == v) {
+ mUrlInput.setText("");
+ } else if (mComboIcon == v) {
+ mUiController.showPageInfo();
+ } else if (mVoiceButton == v) {
+ mUiController.startVoiceRecognizer();
+ } else {
+ super.onClick(v);
+ }
+ }
+
+ @Override
+ public boolean isMenuShowing() {
+ return super.isMenuShowing() || mOverflowMenuShowing;
+ }
+
+ void showMenu(View anchor) {
+ Activity activity = mUiController.getActivity();
+ if (mPopupMenu == null) {
+ mPopupMenu = new PopupMenu(getContext(), anchor);
+ mPopupMenu.setOnMenuItemClickListener(this);
+ mPopupMenu.setOnDismissListener(this);
+ if (!activity.onCreateOptionsMenu(mPopupMenu.getMenu())) {
+ mPopupMenu = null;
+ return;
+ }
+ }
+ Menu menu = mPopupMenu.getMenu();
+ if (activity.onPrepareOptionsMenu(menu)) {
+ mOverflowMenuShowing = true;
+ mPopupMenu.show();
+ }
+ }
+
+ @Override
+ public void onDismiss(PopupMenu menu) {
+ if (menu == mPopupMenu) {
+ onMenuHidden();
+ }
+ }
+
+ private void onMenuHidden() {
+ mOverflowMenuShowing = false;
+ mBaseUi.showTitleBarForDuration();
+ }
+
+ @Override
+ public void onFocusChange(View view, boolean hasFocus) {
+ if (view == mUrlInput) {
+ if (hasFocus && !mUrlInput.getText().toString().equals(mUrlInput.getTag())) {
+ // only change text if different
+ mUrlInput.setText((String) mUrlInput.getTag(), false);
+ mUrlInput.selectAll();
+ } else {
+ setDisplayTitle(mUrlInput.getText().toString());
+ }
+ }
+ super.onFocusChange(view, hasFocus);
+ }
+
+ @Override
+ public void onStateChanged(int state) {
+ mVoiceButton.setVisibility(View.GONE);
+ switch(state) {
+ case StateListener.STATE_NORMAL:
+ mComboIcon.setVisibility(View.VISIBLE);
+ mStopButton.setVisibility(View.GONE);
+ mClearButton.setVisibility(View.GONE);
+ mMagnify.setVisibility(View.GONE);
+ mTabSwitcher.setVisibility(View.VISIBLE);
+ mTitleContainer.setBackgroundDrawable(null);
+ mMore.setVisibility(mNeedsMenu ? View.VISIBLE : View.GONE);
+ break;
+ case StateListener.STATE_HIGHLIGHTED:
+ mComboIcon.setVisibility(View.GONE);
+ mStopButton.setVisibility(View.VISIBLE);
+ mClearButton.setVisibility(View.GONE);
+ if ((mUiController != null) && mUiController.supportsVoice()) {
+ mVoiceButton.setVisibility(View.VISIBLE);
+ }
+ mMagnify.setVisibility(View.GONE);
+ mTabSwitcher.setVisibility(View.GONE);
+ mMore.setVisibility(View.GONE);
+ mTitleContainer.setBackgroundDrawable(mTextfieldBgDrawable);
+ break;
+ case StateListener.STATE_EDITED:
+ mComboIcon.setVisibility(View.GONE);
+ mStopButton.setVisibility(View.GONE);
+ mClearButton.setVisibility(View.VISIBLE);
+ mMagnify.setVisibility(View.VISIBLE);
+ mTabSwitcher.setVisibility(View.GONE);
+ mMore.setVisibility(View.GONE);
+ mTitleContainer.setBackgroundDrawable(mTextfieldBgDrawable);
+ break;
+ }
+ }
+
+ @Override
+ public void onTabDataChanged(Tab tab) {
+ super.onTabDataChanged(tab);
+ mIncognitoIcon.setVisibility(tab.isPrivateBrowsingEnabled()
+ ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ return mUiController.onOptionsItemSelected(item);
+ }
+
+}
diff --git a/src/com/android/browser/NavigationBarTablet.java b/src/com/android/browser/NavigationBarTablet.java
new file mode 100644
index 0000000..ebe40ea
--- /dev/null
+++ b/src/com/android/browser/NavigationBarTablet.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2011 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.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+
+import com.android.browser.R;
+import com.android.browser.UI.ComboViews;
+import com.android.browser.UrlInputView.StateListener;
+
+public class NavigationBarTablet extends NavigationBarBase implements StateListener {
+
+ private Drawable mStopDrawable;
+ private Drawable mReloadDrawable;
+ private String mStopDescription;
+ private String mRefreshDescription;
+
+ private View mUrlContainer;
+ private ImageButton mBackButton;
+ private ImageButton mForwardButton;
+ private ImageView mStar;
+ private ImageView mUrlIcon;
+ private ImageView mSearchButton;
+ private ImageView mStopButton;
+ private View mAllButton;
+ private View mClearButton;
+ private View mVoiceButton;
+ private View mNavButtons;
+ private Drawable mFocusDrawable;
+ private Drawable mUnfocusDrawable;
+ private boolean mHideNavButtons;
+ private Drawable mFaviconDrawable;
+
+ public NavigationBarTablet(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public NavigationBarTablet(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public NavigationBarTablet(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(context);
+ }
+
+ private void init(Context context) {
+ Resources resources = context.getResources();
+ mStopDrawable = resources.getDrawable(R.drawable.ic_stop_holo_dark);
+ mReloadDrawable = resources.getDrawable(R.drawable.ic_refresh_holo_dark);
+ mStopDescription = resources.getString(R.string.accessibility_button_stop);
+ mRefreshDescription = resources.getString(R.string.accessibility_button_refresh);
+ mFocusDrawable = resources.getDrawable(
+ R.drawable.textfield_active_holo_dark);
+ mUnfocusDrawable = resources.getDrawable(
+ R.drawable.textfield_default_holo_dark);
+ mHideNavButtons = resources.getBoolean(R.bool.hide_nav_buttons);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mAllButton = findViewById(R.id.all_btn);
+ // TODO: Change enabled states based on whether you can go
+ // back/forward. Probably should be done inside onPageStarted.
+ mNavButtons = findViewById(R.id.navbuttons);
+ mBackButton = (ImageButton) findViewById(R.id.back);
+ mForwardButton = (ImageButton) findViewById(R.id.forward);
+ mUrlIcon = (ImageView) findViewById(R.id.url_icon);
+ mStar = (ImageView) findViewById(R.id.star);
+ mStopButton = (ImageView) findViewById(R.id.stop);
+ mSearchButton = (ImageView) findViewById(R.id.search);
+ mClearButton = findViewById(R.id.clear);
+ mVoiceButton = findViewById(R.id.voice);
+ mUrlContainer = findViewById(R.id.urlbar_focused);
+ mBackButton.setOnClickListener(this);
+ mForwardButton.setOnClickListener(this);
+ mStar.setOnClickListener(this);
+ mAllButton.setOnClickListener(this);
+ mStopButton.setOnClickListener(this);
+ mSearchButton.setOnClickListener(this);
+ mClearButton.setOnClickListener(this);
+ mVoiceButton.setOnClickListener(this);
+ mUrlInput.setContainer(mUrlContainer);
+ mUrlInput.setStateListener(this);
+ }
+
+ public void onConfigurationChanged(Configuration config) {
+ super.onConfigurationChanged(config);
+ Resources res = getContext().getResources();
+ mHideNavButtons = res.getBoolean(R.bool.hide_nav_buttons);
+ if (mUrlInput.hasFocus()) {
+ if (mHideNavButtons && (mNavButtons.getVisibility() == View.VISIBLE)) {
+ int aw = mNavButtons.getMeasuredWidth();
+ mNavButtons.setVisibility(View.GONE);
+ mNavButtons.setAlpha(0f);
+ mNavButtons.setTranslationX(-aw);
+ } else if (!mHideNavButtons && (mNavButtons.getVisibility() == View.GONE)) {
+ mNavButtons.setVisibility(View.VISIBLE);
+ mNavButtons.setAlpha(1f);
+ mNavButtons.setTranslationX(0);
+ }
+ }
+ }
+
+ @Override
+ public void setTitleBar(TitleBar titleBar) {
+ super.setTitleBar(titleBar);
+ setFocusState(false);
+ }
+
+ void updateNavigationState(Tab tab) {
+ if (tab != null) {
+ mBackButton.setImageResource(tab.canGoBack()
+ ? R.drawable.ic_back_holo_dark
+ : R.drawable.ic_back_disabled_holo_dark);
+ mForwardButton.setImageResource(tab.canGoForward()
+ ? R.drawable.ic_forward_holo_dark
+ : R.drawable.ic_forward_disabled_holo_dark);
+ }
+ updateUrlIcon();
+ }
+
+ @Override
+ public void onTabDataChanged(Tab tab) {
+ super.onTabDataChanged(tab);
+ showHideStar(tab);
+ }
+
+ @Override
+ public void setCurrentUrlIsBookmark(boolean isBookmark) {
+ mStar.setActivated(isBookmark);
+ }
+
+ @Override
+ public void onClick(View v) {
+ if ((mBackButton == v) && (mUiController.getCurrentTab() != null)) {
+ mUiController.getCurrentTab().goBack();
+ } else if ((mForwardButton == v) && (mUiController.getCurrentTab() != null)) {
+ mUiController.getCurrentTab().goForward();
+ } else if (mStar == v) {
+ Intent intent = mUiController.createBookmarkCurrentPageIntent(true);
+ if (intent != null) {
+ getContext().startActivity(intent);
+ }
+ } else if (mAllButton == v) {
+ mUiController.bookmarksOrHistoryPicker(ComboViews.Bookmarks);
+ } else if (mSearchButton == v) {
+ mBaseUi.editUrl(true, true);
+ } else if (mStopButton == v) {
+ stopOrRefresh();
+ } else if (mClearButton == v) {
+ clearOrClose();
+ } else if (mVoiceButton == v) {
+ mUiController.startVoiceRecognizer();
+ } else {
+ super.onClick(v);
+ }
+ }
+
+ private void clearOrClose() {
+ if (TextUtils.isEmpty(mUrlInput.getText())) {
+ // close
+ mUrlInput.clearFocus();
+ } else {
+ // clear
+ mUrlInput.setText("");
+ }
+ }
+
+ @Override
+ public void setFavicon(Bitmap icon) {
+ mFaviconDrawable = mBaseUi.getFaviconDrawable(icon);
+ updateUrlIcon();
+ }
+
+ void updateUrlIcon() {
+ if (mUrlInput.hasFocus()) {
+ mUrlIcon.setImageResource(R.drawable.ic_search_holo_dark);
+ } else {
+ if (mFaviconDrawable == null) {
+ mFaviconDrawable = mBaseUi.getFaviconDrawable(null);
+ }
+ mUrlIcon.setImageDrawable(mFaviconDrawable);
+ }
+ }
+
+ @Override
+ protected void setFocusState(boolean focus) {
+ super.setFocusState(focus);
+ if (focus) {
+ if (mHideNavButtons) {
+ hideNavButtons();
+ }
+ mSearchButton.setVisibility(View.GONE);
+ mStar.setVisibility(View.GONE);
+ mUrlIcon.setImageResource(R.drawable.ic_search_holo_dark);
+ } else {
+ if (mHideNavButtons) {
+ showNavButtons();
+ }
+ showHideStar(mUiController.getCurrentTab());
+ if (mTitleBar.useQuickControls()) {
+ mSearchButton.setVisibility(View.GONE);
+ } else {
+ mSearchButton.setVisibility(View.VISIBLE);
+ }
+ updateUrlIcon();
+ }
+ mUrlContainer.setBackgroundDrawable(focus
+ ? mFocusDrawable : mUnfocusDrawable);
+ }
+
+ private void stopOrRefresh() {
+ if (mUiController == null) return;
+ if (mTitleBar.isInLoad()) {
+ mUiController.stopLoading();
+ } else {
+ if (mUiController.getCurrentTopWebView() != null) {
+ Tab currTab = mUiController.getTabControl().getCurrentTab();
+ if (currTab.hasCrashed) {
+ currTab.replaceCrashView(mUiController.getCurrentTopWebView(),
+ currTab.getViewContainer());
+ }
+ mUiController.getCurrentTopWebView().reload();
+ }
+ }
+ }
+
+ @Override
+ public void onProgressStarted() {
+ mStopButton.setImageDrawable(mStopDrawable);
+ mStopButton.setContentDescription(mStopDescription);
+ }
+
+ @Override
+ public void onProgressStopped() {
+ mStopButton.setImageDrawable(mReloadDrawable);
+ mStopButton.setContentDescription(mRefreshDescription);
+ }
+
+ private AnimatorSet mAnimation;
+
+ private void hideNavButtons() {
+ if (mBaseUi.blockFocusAnimations()) {
+ mNavButtons.setVisibility(View.GONE);
+ return;
+ }
+ int awidth = mNavButtons.getMeasuredWidth();
+ Animator anim1 = ObjectAnimator.ofFloat(mNavButtons, View.TRANSLATION_X, 0, - awidth);
+ Animator anim2 = ObjectAnimator.ofInt(mUrlContainer, "left", mUrlContainer.getLeft(),
+ mUrlContainer.getPaddingLeft());
+ Animator anim3 = ObjectAnimator.ofFloat(mNavButtons, View.ALPHA, 1f, 0f);
+ mAnimation = new AnimatorSet();
+ mAnimation.playTogether(anim1, anim2, anim3);
+ mAnimation.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mNavButtons.setVisibility(View.GONE);
+ mAnimation = null;
+ }
+ });
+ mAnimation.setDuration(150);
+ mAnimation.start();
+ }
+
+ private void showNavButtons() {
+ if (mAnimation != null) {
+ mAnimation.cancel();
+ }
+ mNavButtons.setVisibility(View.VISIBLE);
+ mNavButtons.setTranslationX(0);
+ if (!mBaseUi.blockFocusAnimations()) {
+ int awidth = mNavButtons.getMeasuredWidth();
+ Animator anim1 = ObjectAnimator.ofFloat(mNavButtons,
+ View.TRANSLATION_X, -awidth, 0);
+ Animator anim2 = ObjectAnimator.ofInt(mUrlContainer, "left", 0,
+ awidth);
+ Animator anim3 = ObjectAnimator.ofFloat(mNavButtons, View.ALPHA,
+ 0f, 1f);
+ AnimatorSet combo = new AnimatorSet();
+ combo.playTogether(anim1, anim2, anim3);
+ combo.setDuration(150);
+ combo.start();
+ }
+ }
+
+ private void showHideStar(Tab tab) {
+ // hide the bookmark star for data URLs
+ if (tab != null && tab.inForeground()) {
+ int starVisibility = View.VISIBLE;
+ String url = tab.getUrl();
+ if (DataUri.isDataUri(url)) {
+ starVisibility = View.GONE;
+ }
+ mStar.setVisibility(starVisibility);
+ }
+ }
+
+ @Override
+ public void onStateChanged(int state) {
+ mVoiceButton.setVisibility(View.GONE);
+ switch(state) {
+ case STATE_NORMAL:
+ mClearButton.setVisibility(View.GONE);
+ break;
+ case STATE_HIGHLIGHTED:
+ mClearButton.setVisibility(View.GONE);
+ if ((mUiController != null) && mUiController.supportsVoice()) {
+ mVoiceButton.setVisibility(View.VISIBLE);
+ }
+ break;
+ case STATE_EDITED:
+ mClearButton.setVisibility(View.VISIBLE);
+ break;
+ }
+ }
+
+}
diff --git a/src/com/android/browser/NetworkStateHandler.java b/src/com/android/browser/NetworkStateHandler.java
new file mode 100644
index 0000000..74a355a
--- /dev/null
+++ b/src/com/android/browser/NetworkStateHandler.java
@@ -0,0 +1,120 @@
+/*
+ * 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.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.BrowserSettings;
+
+/**
+ * Handle network state changes
+ */
+public class NetworkStateHandler {
+
+ Activity mActivity;
+ Controller mController;
+
+ // monitor platform changes
+ private IntentFilter mNetworkStateChangedFilter;
+ private BroadcastReceiver mNetworkStateIntentReceiver;
+ private boolean mIsNetworkUp;
+
+ 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() : ""));
+ BrowserSettings.getInstance().updateConnectionType();
+
+ boolean noConnection = intent.getBooleanExtra(
+ ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
+
+ onNetworkToggle(!noConnection);
+ }
+ }
+ };
+
+ }
+
+ void onPause() {
+ // unregister network state listener
+ mActivity.unregisterReceiver(mNetworkStateIntentReceiver);
+ }
+
+ void onResume() {
+ mActivity.registerReceiver(mNetworkStateIntentReceiver,
+ mNetworkStateChangedFilter);
+ BrowserSettings.getInstance().updateConnectionType();
+ }
+
+ /**
+ * 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;
+ }
+ mIsNetworkUp = up;
+ WebView w = mController.getCurrentWebView();
+ if (w != null) {
+ w.setNetworkAvailable(up);
+ }
+ }
+
+ boolean isNetworkUp() {
+ return mIsNetworkUp;
+ }
+
+ 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/NfcHandler.java b/src/com/android/browser/NfcHandler.java
new file mode 100644
index 0000000..0dd8576
--- /dev/null
+++ b/src/com/android/browser/NfcHandler.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2011 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.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcEvent;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import java.util.concurrent.CountDownLatch;
+
+/** This class implements sharing the URL of the currently
+ * shown browser page over NFC. Sharing is only active
+ * when the activity is in the foreground and resumed.
+ * Incognito tabs will not be shared over NFC.
+ */
+public class NfcHandler implements NfcAdapter.CreateNdefMessageCallback {
+ static final String TAG = "BrowserNfcHandler";
+ static final int GET_PRIVATE_BROWSING_STATE_MSG = 100;
+
+ final Controller mController;
+
+ Tab mCurrentTab;
+ boolean mIsPrivate;
+ CountDownLatch mPrivateBrowsingSignal;
+
+ public static void register(Activity activity, Controller controller) {
+ NfcAdapter adapter = NfcAdapter.getDefaultAdapter(activity.getApplicationContext());
+ if (adapter == null) {
+ return; // NFC not available on this device
+ }
+ NfcHandler handler = null;
+ if (controller != null) {
+ handler = new NfcHandler(controller);
+ }
+
+ adapter.setNdefPushMessageCallback(handler, activity);
+ }
+
+ public static void unregister(Activity activity) {
+ // Passing a null controller causes us to disable
+ // the callback and release the ref to out activity.
+ register(activity, null);
+ }
+
+ public NfcHandler(Controller controller) {
+ mController = controller;
+ }
+
+ final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == GET_PRIVATE_BROWSING_STATE_MSG) {
+ mIsPrivate = mCurrentTab.getWebView().isPrivateBrowsingEnabled();
+ mPrivateBrowsingSignal.countDown();
+ }
+ }
+ };
+
+ @Override
+ public NdefMessage createNdefMessage(NfcEvent event) {
+ mCurrentTab = mController.getCurrentTab();
+ if ((mCurrentTab != null) && (mCurrentTab.getWebView() != null)) {
+ // We can only read the WebView state on the UI thread, so post
+ // a message and wait.
+ mPrivateBrowsingSignal = new CountDownLatch(1);
+ mHandler.sendMessage(mHandler.obtainMessage(GET_PRIVATE_BROWSING_STATE_MSG));
+ try {
+ mPrivateBrowsingSignal.await();
+ } catch (InterruptedException e) {
+ return null;
+ }
+ }
+
+ if ((mCurrentTab == null) || mIsPrivate) {
+ return null;
+ }
+
+ String currentUrl = mCurrentTab.getUrl();
+ if (currentUrl != null) {
+ try {
+ return new NdefMessage(NdefRecord.createUri(currentUrl));
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "IllegalArgumentException creating URI NdefRecord", e);
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/browser/OpenDownloadReceiver.java b/src/com/android/browser/OpenDownloadReceiver.java
new file mode 100644
index 0000000..4277ff4
--- /dev/null
+++ b/src/com/android/browser/OpenDownloadReceiver.java
@@ -0,0 +1,94 @@
+/*
+ * 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.DownloadManager;
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+/**
+ * This {@link BroadcastReceiver} handles clicks to notifications that
+ * downloads from the browser are in progress/complete. Clicking on an
+ * in-progress or failed download will open the download manager. Clicking on
+ * a complete, successful download will open the file.
+ */
+public class OpenDownloadReceiver extends BroadcastReceiver {
+ 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);
+ }
+ }
+ }
+
+ /**
+ * 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/OptionsMenuHandler.java b/src/com/android/browser/OptionsMenuHandler.java
new file mode 100644
index 0000000..d602c7d
--- /dev/null
+++ b/src/com/android/browser/OptionsMenuHandler.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2011 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.view.Menu;
+import android.view.MenuItem;
+
+public interface OptionsMenuHandler {
+
+ boolean onCreateOptionsMenu(Menu menu);
+ boolean onPrepareOptionsMenu(Menu menu);
+ boolean onOptionsItemSelected(MenuItem item);
+}
diff --git a/src/com/android/browser/PageDialogsHandler.java b/src/com/android/browser/PageDialogsHandler.java
new file mode 100644
index 0000000..a38b904
--- /dev/null
+++ b/src/com/android/browser/PageDialogsHandler.java
@@ -0,0 +1,428 @@
+/*
+ * 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 java.lang.reflect.Method;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.net.http.SslCertificate;
+import android.net.http.SslError;
+import android.view.LayoutInflater;
+import android.view.View;
+import org.codeaurora.swe.HttpAuthHandler;
+import org.codeaurora.swe.SslErrorHandler;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.R;
+
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/**
+ * Displays page info
+ *
+ */
+public class PageDialogsHandler {
+
+ private Context mContext;
+ private Controller mController;
+ private boolean mPageInfoFromShowSSLCertificateOnError;
+ private String mUrlCertificateOnError;
+ 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,
+ mUrlCertificateOnError);
+ }
+ 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.onUpdatedSecurityState(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.
+ * @param urlCertificateOnError The URL that invokes SSLCertificateError.
+ * Null when fromShowSSLCertificateOnError is false.
+ */
+ void showPageInfo(final Tab tab,
+ final boolean fromShowSSLCertificateOnError,
+ final String urlCertificateOnError) {
+ if (tab == null) return;
+ final LayoutInflater factory = LayoutInflater.from(mContext);
+
+ final View pageInfoView = factory.inflate(R.layout.page_info, null);
+
+ final WebView view = tab.getWebView();
+
+ String url = fromShowSSLCertificateOnError ? urlCertificateOnError : tab.getUrl();
+ String title = tab.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;
+ mUrlCertificateOnError = urlCertificateOnError;
+
+ 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
+ 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) {
+
+ SslCertificate cert = tab.getWebView().getCertificate();
+ if (cert == null) {
+ return;
+ }
+
+ mSSLCertificateView = tab;
+ mSSLCertificateDialog = createSslCertificateDialog(cert, tab.getSslCertificateError())
+ .setPositiveButton(R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int whichButton) {
+ mSSLCertificateDialog = null;
+ mSSLCertificateView = null;
+
+ showPageInfo(tab, false, null);
+ }
+ })
+ .setOnCancelListener(
+ new DialogInterface.OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ mSSLCertificateDialog = null;
+ mSSLCertificateView = null;
+
+ showPageInfo(tab, false, null);
+ }
+ })
+ .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) {
+
+ SslCertificate cert = error.getCertificate();
+ if (cert == null) {
+ return;
+ }
+
+ mSSLCertificateOnErrorHandler = handler;
+ mSSLCertificateOnErrorView = view;
+ mSSLCertificateOnErrorError = error;
+ mSSLCertificateOnErrorDialog = createSslCertificateDialog(cert, error)
+ .setPositiveButton(R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int whichButton) {
+ mSSLCertificateOnErrorDialog = null;
+ mSSLCertificateOnErrorView = null;
+ mSSLCertificateOnErrorHandler = null;
+ mSSLCertificateOnErrorError = null;
+
+ ((BrowserWebView) 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,
+ error.getUrl());
+ }
+ })
+ .setOnCancelListener(
+ new DialogInterface.OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ mSSLCertificateOnErrorDialog = null;
+ mSSLCertificateOnErrorView = null;
+ mSSLCertificateOnErrorHandler = null;
+ mSSLCertificateOnErrorError = null;
+
+ ((BrowserWebView) view).getWebViewClient().
+ onReceivedSslError(view, handler, error);
+ }
+ })
+ .show();
+ }
+
+ private static View inflateCertificateView(SslCertificate certificate, Context ctx) {
+ Class certClass;
+ try {
+ certClass = Class.forName("android.net.http.SslCertificate");
+
+ Class argTypes[] = new Class[1];
+ argTypes[0] = Context.class;
+
+ Method m = certClass.getDeclaredMethod("inflateCertificateView", argTypes);
+ m.setAccessible(true);
+
+ Object args[] = new Object[1];
+ args[0] = ctx;
+ return (View) m.invoke(certificate, args);
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ /*
+ * Creates an AlertDialog to display the given certificate. If error is
+ * null, text is added to state that the certificae is valid and the icon
+ * is set accordingly. If error is non-null, it must relate to the supplied
+ * certificate. In this case, error is used to add text describing the
+ * problems with the certificate and a different icon is used.
+ */
+ private AlertDialog.Builder createSslCertificateDialog(SslCertificate certificate,
+ SslError error) {
+ View certificateView = inflateCertificateView(certificate, mContext);
+ Resources res = Resources.getSystem();
+ int placeholder_id = res.getIdentifier("placeholder", "id", "android");
+ final LinearLayout placeholder =
+ (LinearLayout)certificateView.findViewById(placeholder_id);
+
+ LayoutInflater factory = LayoutInflater.from(mContext);
+ int iconId;
+
+ if (error == null) {
+ iconId = R.drawable.ic_dialog_browser_certificate_secure;
+ LinearLayout table = (LinearLayout)factory.inflate(R.layout.ssl_success, placeholder);
+ TextView successString = (TextView)table.findViewById(R.id.success);
+ successString.setText(R.string.ssl_certificate_is_valid);
+ } else {
+ iconId = R.drawable.ic_dialog_browser_certificate_partially_secure;
+ if (error.hasError(SslError.SSL_UNTRUSTED)) {
+ addError(factory, placeholder, R.string.ssl_untrusted);
+ }
+ if (error.hasError(SslError.SSL_IDMISMATCH)) {
+ addError(factory, placeholder, R.string.ssl_mismatch);
+ }
+ if (error.hasError(SslError.SSL_EXPIRED)) {
+ addError(factory, placeholder, R.string.ssl_expired);
+ }
+ if (error.hasError(SslError.SSL_NOTYETVALID)) {
+ addError(factory, placeholder, R.string.ssl_not_yet_valid);
+ }
+ if (error.hasError(SslError.SSL_DATE_INVALID)) {
+ addError(factory, placeholder, R.string.ssl_date_invalid);
+ }
+ if (error.hasError(SslError.SSL_INVALID)) {
+ addError(factory, placeholder, R.string.ssl_invalid);
+ }
+ // The SslError should always have at least one type of error and we
+ // should explicitly handle every type of error it supports. We
+ // therefore expect the condition below to never be hit. We use it
+ // as as safety net in case a new error type is added to SslError
+ // without the logic above being updated accordingly.
+ if (placeholder.getChildCount() == 0) {
+ addError(factory, placeholder, R.string.ssl_unknown);
+ }
+ }
+
+ return new AlertDialog.Builder(mContext)
+ .setTitle(R.string.ssl_certificate)
+ .setIcon(iconId)
+ .setView(certificateView);
+ }
+
+ private void addError(LayoutInflater inflater, LinearLayout parent, int error) {
+ TextView textView = (TextView) inflater.inflate(R.layout.ssl_warning,
+ parent, false);
+ textView.setText(error);
+ parent.addView(textView);
+ }
+}
diff --git a/src/com/android/browser/PageProgressView.java b/src/com/android/browser/PageProgressView.java
new file mode 100644
index 0000000..f512cef
--- /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 0000000..330f47e
--- /dev/null
+++ b/src/com/android/browser/Performance.java
@@ -0,0 +1,134 @@
+/*
+ * 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.platformsupport.Process;
+import com.android.browser.platformsupport.WebAddress;
+
+import android.os.Debug;
+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/PhoneUi.java b/src/com/android/browser/PhoneUi.java
new file mode 100644
index 0000000..01488e1
--- /dev/null
+++ b/src/com/android/browser/PhoneUi.java
@@ -0,0 +1,560 @@
+/*
+ * 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.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.os.Message;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.ActionMode;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import org.codeaurora.swe.WebView;
+import android.widget.ImageView;
+
+import com.android.browser.R;
+import com.android.browser.UrlInputView.StateListener;
+
+/**
+ * Ui for regular phone screen sizes
+ */
+public class PhoneUi extends BaseUi {
+
+ private static final String LOGTAG = "PhoneUi";
+ private static final int MSG_INIT_NAVSCREEN = 100;
+
+ private NavScreen mNavScreen;
+ private AnimScreen mAnimScreen;
+ private NavigationBarPhone mNavigationBar;
+ private int mActionBarHeight;
+
+ boolean mAnimating;
+ boolean mShowNav = false;
+
+ /**
+ * @param browser
+ * @param controller
+ */
+ public PhoneUi(Activity browser, UiController controller) {
+ super(browser, controller);
+ setUseQuickControls(BrowserSettings.getInstance().useQuickControls());
+ mNavigationBar = (NavigationBarPhone) mTitleBar.getNavigationBar();
+ TypedValue heightValue = new TypedValue();
+ browser.getTheme().resolveAttribute(
+ android.R.attr.actionBarSize, heightValue, true);
+ mActionBarHeight = TypedValue.complexToDimensionPixelSize(heightValue.data,
+ browser.getResources().getDisplayMetrics());
+ }
+
+ @Override
+ public void onDestroy() {
+ hideTitleBar();
+ }
+
+ @Override
+ public void editUrl(boolean clearInput, boolean forceIME) {
+ if (mUseQuickControls) {
+ mTitleBar.setShowProgressOnly(false);
+ }
+ //Do nothing while at Nav show screen.
+ if (mShowNav) return;
+ super.editUrl(clearInput, forceIME);
+ }
+
+ @Override
+ public boolean onBackKey() {
+ if (showingNavScreen()) {
+ mNavScreen.close(mUiController.getTabControl().getCurrentPosition());
+ return true;
+ }
+ return super.onBackKey();
+ }
+
+ private boolean showingNavScreen() {
+ return mNavScreen != null && mNavScreen.getVisibility() == View.VISIBLE;
+ }
+
+ @Override
+ public boolean dispatchKey(int code, KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public void onProgressChanged(Tab tab) {
+ super.onProgressChanged(tab);
+ if (mNavScreen == null && getTitleBar().getHeight() > 0) {
+ mHandler.sendEmptyMessage(MSG_INIT_NAVSCREEN);
+ }
+ }
+
+ @Override
+ protected void handleMessage(Message msg) {
+ super.handleMessage(msg);
+ if (msg.what == MSG_INIT_NAVSCREEN) {
+ if (mNavScreen == null) {
+ mNavScreen = new NavScreen(mActivity, mUiController, this);
+ mCustomViewContainer.addView(mNavScreen, COVER_SCREEN_PARAMS);
+ mNavScreen.setVisibility(View.GONE);
+ }
+ if (mAnimScreen == null) {
+ mAnimScreen = new AnimScreen(mActivity);
+ // initialize bitmaps
+ mAnimScreen.set(getTitleBar(), getWebView());
+ }
+ }
+ }
+
+ @Override
+ public void setActiveTab(final Tab tab) {
+ mTitleBar.cancelTitleBarAnimation(true);
+ mTitleBar.setSkipTitleBarAnimations(true);
+ super.setActiveTab(tab);
+
+ //if at Nav screen show, detach tab like what showNavScreen() do.
+ if (mShowNav) {
+ detachTab(mActiveTab);
+ }
+
+ BrowserWebView view = (BrowserWebView) 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;
+ }
+ // Request focus on the top window.
+ if (mUseQuickControls) {
+ mPieControl.forceToTop(mContentView);
+ view.setTitleBar(null);
+ mTitleBar.setShowProgressOnly(true);
+ } else {
+ view.setTitleBar(mTitleBar);
+ }
+ // update nav bar state
+ mNavigationBar.onStateChanged(StateListener.STATE_NORMAL);
+ updateLockIconToLatest(tab);
+ mTitleBar.setSkipTitleBarAnimations(false);
+ }
+
+ // menu handling callbacks
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ updateMenuState(mActiveTab, menu);
+ return true;
+ }
+
+ @Override
+ public void updateMenuState(Tab tab, Menu menu) {
+ MenuItem bm = menu.findItem(R.id.bookmarks_menu_id);
+ if (bm != null) {
+ bm.setVisible(!showingNavScreen());
+ }
+ MenuItem abm = menu.findItem(R.id.add_bookmark_menu_id);
+ if (abm != null) {
+ abm.setVisible((tab != null) && !tab.isSnapshot() && !showingNavScreen());
+ }
+ MenuItem info = menu.findItem(R.id.page_info_menu_id);
+ if (info != null) {
+ info.setVisible(false);
+ }
+ MenuItem newtab = menu.findItem(R.id.new_tab_menu_id);
+ if (newtab != null && !mUseQuickControls) {
+ newtab.setVisible(false);
+ }
+ MenuItem incognito = menu.findItem(R.id.incognito_menu_id);
+ if (incognito != null) {
+ incognito.setVisible(showingNavScreen() || mUseQuickControls);
+ }
+ MenuItem closeOthers = menu.findItem(R.id.close_other_tabs_id);
+ if (closeOthers != null) {
+ boolean isLastTab = true;
+ if (tab != null) {
+ isLastTab = (mTabControl.getTabCount() <= 1);
+ }
+ closeOthers.setEnabled(!isLastTab);
+ }
+ if (showingNavScreen()) {
+ menu.setGroupVisible(R.id.LIVE_MENU, false);
+ menu.setGroupVisible(R.id.SNAPSHOT_MENU, false);
+ menu.setGroupVisible(R.id.NAV_MENU, false);
+ menu.setGroupVisible(R.id.COMBO_MENU, true);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (showingNavScreen()
+ && (item.getItemId() != R.id.history_menu_id)
+ && (item.getItemId() != R.id.snapshots_menu_id)) {
+ hideNavScreen(mUiController.getTabControl().getCurrentPosition(), false);
+ }
+ return false;
+ }
+
+ @Override
+ public void onContextMenuCreated(Menu menu) {
+ hideTitleBar();
+ }
+
+ @Override
+ public void onContextMenuClosed(Menu menu, boolean inLoad) {
+ if (inLoad) {
+ showTitleBar();
+ }
+ }
+
+ // action mode callbacks
+
+ @Override
+ public void onActionModeStarted(ActionMode mode) {
+ if (!isEditingUrl()) {
+ hideTitleBar();
+ } else {
+ mTitleBar.animate().translationY(mActionBarHeight);
+ }
+ }
+
+ @Override
+ public void onActionModeFinished(boolean inLoad) {
+ mTitleBar.animate().translationY(0);
+ if (inLoad) {
+ if (mUseQuickControls) {
+ mTitleBar.setShowProgressOnly(true);
+ }
+ showTitleBar();
+ }
+ }
+
+ @Override
+ public boolean isWebShowing() {
+ return super.isWebShowing() && !showingNavScreen();
+ }
+
+ @Override
+ public void showWeb(boolean animate) {
+ super.showWeb(animate);
+ hideNavScreen(mUiController.getTabControl().getCurrentPosition(), animate);
+ }
+
+ void showNavScreen() {
+ mShowNav = true;
+ mUiController.setBlockEvents(true);
+ if (mNavScreen == null) {
+ mNavScreen = new NavScreen(mActivity, mUiController, this);
+ mCustomViewContainer.addView(mNavScreen, COVER_SCREEN_PARAMS);
+ } else {
+ mNavScreen.setVisibility(View.VISIBLE);
+ mNavScreen.setAlpha(1f);
+ mNavScreen.refreshAdapter();
+ }
+ mActiveTab.capture();
+ if (mAnimScreen == null) {
+ mAnimScreen = new AnimScreen(mActivity);
+ } else {
+ mAnimScreen.mMain.setAlpha(1f);
+ mAnimScreen.mTitle.setAlpha(1f);
+ mAnimScreen.setScaleFactor(1f);
+ }
+ mAnimScreen.set(getTitleBar(), getWebView());
+ if (mAnimScreen.mMain.getParent() == null) {
+ mCustomViewContainer.addView(mAnimScreen.mMain, COVER_SCREEN_PARAMS);
+ }
+ mCustomViewContainer.setVisibility(View.VISIBLE);
+ mCustomViewContainer.bringToFront();
+ mAnimScreen.mMain.layout(0, 0, mContentView.getWidth(),
+ mContentView.getHeight());
+ int fromLeft = 0;
+ int fromTop = getTitleBar().getHeight();
+ int fromRight = mContentView.getWidth();
+ int fixedTbarHeight = mTitleBar.isFixed() ? mTitleBar.calculateEmbeddedHeight() : 0;
+ int fromBottom = mContentView.getHeight() + fixedTbarHeight;
+ int width = mActivity.getResources().getDimensionPixelSize(R.dimen.nav_tab_width);
+ int height = mActivity.getResources().getDimensionPixelSize(R.dimen.nav_tab_height);
+ int ntth = mActivity.getResources().getDimensionPixelSize(R.dimen.nav_tab_titleheight);
+ int toLeft = (mContentView.getWidth() - width) / 2;
+ int toTop = ((fromBottom - (ntth + height)) / 2 + ntth);
+ int toRight = toLeft + width;
+ int toBottom = toTop + height;
+ float scaleFactor = width / (float) mContentView.getWidth();
+ // SWE: Detaching the active tab results flashing screen with SWE.
+ // Not detaching the tab doesn't seem to have any issues.
+ //detachTab(mActiveTab);
+ mContentView.setVisibility(View.GONE);
+ AnimatorSet set1 = new AnimatorSet();
+ AnimatorSet inanim = new AnimatorSet();
+ ObjectAnimator tx = ObjectAnimator.ofInt(mAnimScreen.mContent, "left",
+ fromLeft, toLeft);
+ ObjectAnimator ty = ObjectAnimator.ofInt(mAnimScreen.mContent, "top",
+ fromTop, toTop);
+ ObjectAnimator tr = ObjectAnimator.ofInt(mAnimScreen.mContent, "right",
+ fromRight, toRight);
+ ObjectAnimator tb = ObjectAnimator.ofInt(mAnimScreen.mContent, "bottom",
+ fromBottom, toBottom);
+ ObjectAnimator title = ObjectAnimator.ofFloat(mAnimScreen.mTitle, "alpha",
+ 1f, 0f);
+ ObjectAnimator sx = ObjectAnimator.ofFloat(mAnimScreen, "scaleFactor",
+ 1f, scaleFactor);
+ ObjectAnimator blend1 = ObjectAnimator.ofFloat(mAnimScreen.mMain,
+ "alpha", 1f, 0f);
+ blend1.setDuration(100);
+
+ inanim.playTogether(tx, ty, tr, tb, sx, title);
+ inanim.setDuration(200);
+ set1.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator anim) {
+ mCustomViewContainer.removeView(mAnimScreen.mMain);
+ finishAnimationIn();
+ mUiController.setBlockEvents(false);
+ }
+ });
+ set1.playSequentially(inanim, blend1);
+ set1.start();
+ }
+
+ private void finishAnimationIn() {
+ if (showingNavScreen()) {
+ // notify accessibility manager about the screen change
+ mNavScreen.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ mTabControl.setOnThumbnailUpdatedListener(mNavScreen);
+ }
+ }
+
+ void hideNavScreen(int position, boolean animate) {
+ mShowNav = false;
+ if (!showingNavScreen()) return;
+ final Tab tab = mUiController.getTabControl().getTab(position);
+ if ((tab == null) || !animate) {
+ if (tab != null) {
+ setActiveTab(tab);
+ } else if (mTabControl.getTabCount() > 0) {
+ // use a fallback tab
+ setActiveTab(mTabControl.getCurrentTab());
+ }
+ mContentView.setVisibility(View.VISIBLE);
+ finishAnimateOut();
+ return;
+ }
+ NavTabView tabview = (NavTabView) mNavScreen.getTabView(position);
+ if (tabview == null) {
+ if (mTabControl.getTabCount() > 0) {
+ // use a fallback tab
+ setActiveTab(mTabControl.getCurrentTab());
+ }
+ mContentView.setVisibility(View.VISIBLE);
+ finishAnimateOut();
+ return;
+ }
+ mUiController.setBlockEvents(true);
+ mUiController.setActiveTab(tab);
+ mContentView.setVisibility(View.VISIBLE);
+ if (mAnimScreen == null) {
+ mAnimScreen = new AnimScreen(mActivity);
+ }
+ mAnimScreen.set(tab.getScreenshot());
+ if (mAnimScreen.mMain.getParent() == null) {
+ mCustomViewContainer.addView(mAnimScreen.mMain, COVER_SCREEN_PARAMS);
+ }
+ int fixedTbarHeight = mTitleBar.isFixed() ? mTitleBar.calculateEmbeddedHeight() : 0;
+ mAnimScreen.mMain.layout(0, 0, mContentView.getWidth(),
+ mContentView.getHeight() + fixedTbarHeight);
+ mNavScreen.mScroller.finishScroller();
+ ImageView target = tabview.mImage;
+ int toLeft = 0;
+ int toTop = 0;
+ if (mTitleBar.isFixed()) {
+ toTop = fixedTbarHeight;
+ } else {
+ toTop = (tab.getWebView() != null) ? tab.getWebView().getVisibleTitleHeight() : 0;
+ }
+ int toRight = mContentView.getWidth();
+ int width = target.getDrawable().getIntrinsicWidth();
+ int height = target.getDrawable().getIntrinsicHeight();
+ int fromLeft = tabview.getLeft() + target.getLeft() - mNavScreen.mScroller.getScrollX();
+ int fromTop = tabview.getTop() + target.getTop() - mNavScreen.mScroller.getScrollY();
+ int fromRight = fromLeft + width;
+ int fromBottom = fromTop + height;
+ float scaleFactor = mContentView.getWidth() / (float) width;
+ int toBottom = toTop + (int) (height * scaleFactor);
+ mAnimScreen.mContent.setLeft(fromLeft);
+ mAnimScreen.mContent.setTop(fromTop);
+ mAnimScreen.mContent.setRight(fromRight);
+ mAnimScreen.mContent.setBottom(fromBottom);
+ mAnimScreen.setScaleFactor(1f);
+ AnimatorSet set1 = new AnimatorSet();
+ ObjectAnimator fade2 = ObjectAnimator.ofFloat(mAnimScreen.mMain, "alpha", 0f, 1f);
+ ObjectAnimator fade1 = ObjectAnimator.ofFloat(mNavScreen, "alpha", 1f, 0f);
+ set1.playTogether(fade1, fade2);
+ set1.setDuration(100);
+ AnimatorSet set2 = new AnimatorSet();
+ ObjectAnimator l = ObjectAnimator.ofInt(mAnimScreen.mContent, "left",
+ fromLeft, toLeft);
+ ObjectAnimator t = ObjectAnimator.ofInt(mAnimScreen.mContent, "top",
+ fromTop, toTop);
+ ObjectAnimator r = ObjectAnimator.ofInt(mAnimScreen.mContent, "right",
+ fromRight, toRight);
+ ObjectAnimator b = ObjectAnimator.ofInt(mAnimScreen.mContent, "bottom",
+ fromBottom, toBottom);
+ ObjectAnimator scale = ObjectAnimator.ofFloat(mAnimScreen, "scaleFactor",
+ 1f, scaleFactor);
+ ObjectAnimator otheralpha = ObjectAnimator.ofFloat(mCustomViewContainer, "alpha", 1f, 0f);
+ otheralpha.setDuration(100);
+ set2.playTogether(l, t, r, b, scale);
+ set2.setDuration(200);
+ AnimatorSet combo = new AnimatorSet();
+ combo.playSequentially(set1, set2, otheralpha);
+ combo.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator anim) {
+ mCustomViewContainer.removeView(mAnimScreen.mMain);
+ finishAnimateOut();
+ mUiController.setBlockEvents(false);
+ }
+ });
+ combo.start();
+ }
+
+ private void finishAnimateOut() {
+ mTabControl.setOnThumbnailUpdatedListener(null);
+ mNavScreen.setVisibility(View.GONE);
+ mCustomViewContainer.setAlpha(1f);
+ mCustomViewContainer.setVisibility(View.GONE);
+ }
+
+ @Override
+ public boolean needsRestoreAllTabs() {
+ return false;
+ }
+
+ public void toggleNavScreen() {
+ if (!showingNavScreen()) {
+ showNavScreen();
+ } else {
+ hideNavScreen(mUiController.getTabControl().getCurrentPosition(), false);
+ }
+ }
+
+ @Override
+ public boolean shouldCaptureThumbnails() {
+ return true;
+ }
+
+ static class AnimScreen {
+
+ private View mMain;
+ private ImageView mTitle;
+ private ImageView mContent;
+ private float mScale;
+ private Bitmap mTitleBarBitmap;
+ private Bitmap mContentBitmap;
+
+ public AnimScreen(Context ctx) {
+ mMain = LayoutInflater.from(ctx).inflate(R.layout.anim_screen,
+ null);
+ mTitle = (ImageView) mMain.findViewById(R.id.title);
+ mContent = (ImageView) mMain.findViewById(R.id.content);
+ mContent.setScaleType(ImageView.ScaleType.MATRIX);
+ mContent.setImageMatrix(new Matrix());
+ mScale = 1.0f;
+ setScaleFactor(getScaleFactor());
+ }
+
+ public void set(TitleBar tbar, WebView web) {
+ if (tbar == null || web == null) {
+ return;
+ }
+ int embTbarHeight = tbar.getEmbeddedHeight();
+ int tbarHeight = tbar.isFixed() ? tbar.calculateEmbeddedHeight() : embTbarHeight;
+ if (tbar.getWidth() > 0 && tbarHeight > 0) {
+ if (mTitleBarBitmap == null
+ || mTitleBarBitmap.getWidth() != tbar.getWidth()
+ || mTitleBarBitmap.getHeight() != tbarHeight) {
+ mTitleBarBitmap = safeCreateBitmap(tbar.getWidth(),
+ tbarHeight);
+ }
+ if (mTitleBarBitmap != null) {
+ Canvas c = new Canvas(mTitleBarBitmap);
+ tbar.draw(c);
+ c.setBitmap(null);
+ }
+ } else {
+ mTitleBarBitmap = null;
+ }
+ mTitle.setImageBitmap(mTitleBarBitmap);
+ mTitle.setVisibility(View.VISIBLE);
+ // SWE: WebView.draw() wouldn't draw anything if SurfaceView is enabled.
+ mContentBitmap = web.getViewportBitmap();
+ if (mContentBitmap == null) {
+ int h = web.getHeight() - embTbarHeight;
+ if (mContentBitmap == null
+ || mContentBitmap.getWidth() != web.getWidth()
+ || mContentBitmap.getHeight() != h) {
+ mContentBitmap = safeCreateBitmap(web.getWidth(), h);
+ }
+ if (mContentBitmap != null) {
+ Canvas c = new Canvas(mContentBitmap);
+ int tx = web.getScrollX();
+ int ty = web.getScrollY();
+ c.translate(-tx, -ty - embTbarHeight);
+ web.draw(c);
+ c.setBitmap(null);
+ }
+ }
+ mContent.setImageBitmap(mContentBitmap);
+ }
+
+ private Bitmap safeCreateBitmap(int width, int height) {
+ if (width <= 0 || height <= 0) {
+ Log.w(LOGTAG, "safeCreateBitmap failed! width: " + width
+ + ", height: " + height);
+ return null;
+ }
+ return Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
+ }
+
+ public void set(Bitmap image) {
+ mTitle.setVisibility(View.GONE);
+ mContent.setImageBitmap(image);
+ }
+
+ private void setScaleFactor(float sf) {
+ mScale = sf;
+ Matrix m = new Matrix();
+ m.postScale(sf,sf);
+ mContent.setImageMatrix(m);
+ }
+
+ private float getScaleFactor() {
+ return mScale;
+ }
+
+ }
+
+}
diff --git a/src/com/android/browser/PieControl.java b/src/com/android/browser/PieControl.java
new file mode 100644
index 0000000..68f7983
--- /dev/null
+++ b/src/com/android/browser/PieControl.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright (C) 2011 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.Context;
+import android.graphics.Bitmap;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import org.codeaurora.swe.WebView;
+import android.widget.BaseAdapter;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.TextView;
+
+import com.android.browser.R;
+import com.android.browser.UI.ComboViews;
+import com.android.browser.view.PieItem;
+import com.android.browser.view.PieMenu;
+import com.android.browser.view.PieStackView;
+import com.android.browser.view.PieMenu.PieView.OnLayoutListener;
+import com.android.browser.view.PieStackView.OnCurrentListener;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Controller for Quick Controls pie menu
+ */
+public class PieControl implements PieMenu.PieController, OnClickListener {
+
+ protected Activity mActivity;
+ protected UiController mUiController;
+ protected PieMenu mPie;
+ protected int mItemSize;
+ protected TextView mTabsCount;
+ private BaseUi mUi;
+ private PieItem mBack;
+ private PieItem mForward;
+ private PieItem mRefresh;
+ private PieItem mUrl;
+ private PieItem mOptions;
+ private PieItem mBookmarks;
+ private PieItem mHistory;
+ private PieItem mAddBookmark;
+ private PieItem mNewTab;
+ private PieItem mIncognito;
+ private PieItem mClose;
+ private PieItem mShowTabs;
+ private PieItem mInfo;
+ private PieItem mFind;
+ private PieItem mShare;
+ private PieItem mRDS;
+ private TabAdapter mTabAdapter;
+
+ public PieControl(Activity activity, UiController controller, BaseUi ui) {
+ mActivity = activity;
+ mUiController = controller;
+ mItemSize = (int) activity.getResources().getDimension(R.dimen.qc_item_size);
+ mUi = ui;
+ }
+
+ public void stopEditingUrl() {
+ mUi.stopEditingUrl();
+ }
+
+ protected void attachToContainer(FrameLayout container) {
+ if (mPie == null) {
+ mPie = new PieMenu(mActivity);
+ LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT);
+ mPie.setLayoutParams(lp);
+ populateMenu();
+ mPie.setController(this);
+ }
+ container.addView(mPie);
+ }
+
+ protected void removeFromContainer(FrameLayout container) {
+ container.removeView(mPie);
+ }
+
+ protected void forceToTop(FrameLayout container) {
+ if (mPie.getParent() != null) {
+ container.removeView(mPie);
+ container.addView(mPie);
+ }
+ }
+
+ protected void setClickListener(OnClickListener listener, PieItem... items) {
+ for (PieItem item : items) {
+ item.getView().setOnClickListener(listener);
+ }
+ }
+
+ @Override
+ public boolean onOpen() {
+ int n = mUiController.getTabControl().getTabCount();
+ mTabsCount.setText(Integer.toString(n));
+ Tab tab = mUiController.getCurrentTab();
+ if (tab != null) {
+ mForward.setEnabled(tab.canGoForward());
+ }
+ WebView view = mUiController.getCurrentWebView();
+ if (view != null) {
+ ImageView icon = (ImageView) mRDS.getView();
+ if (mUiController.getSettings().hasDesktopUseragent(view)) {
+ icon.setImageResource(R.drawable.ic_mobile);
+ } else {
+ icon.setImageResource(R.drawable.ic_desktop_holo_dark);
+ }
+ }
+ return true;
+ }
+
+ protected void populateMenu() {
+ mBack = makeItem(R.drawable.ic_back_holo_dark, 1);
+ mUrl = makeItem(R.drawable.ic_web_holo_dark, 1);
+ mBookmarks = makeItem(R.drawable.ic_bookmarks_holo_dark, 1);
+ mHistory = makeItem(R.drawable.ic_history_holo_dark, 1);
+ mAddBookmark = makeItem(R.drawable.ic_bookmark_on_holo_dark, 1);
+ mRefresh = makeItem(R.drawable.ic_refresh_holo_dark, 1);
+ mForward = makeItem(R.drawable.ic_forward_holo_dark, 1);
+ mNewTab = makeItem(R.drawable.ic_new_window_holo_dark, 1);
+ mIncognito = makeItem(R.drawable.ic_new_incognito_holo_dark, 1);
+ mClose = makeItem(R.drawable.ic_close_window_holo_dark, 1);
+ mInfo = makeItem(android.R.drawable.ic_menu_info_details, 1);
+ mFind = makeItem(R.drawable.ic_search_holo_dark, 1);
+ mShare = makeItem(R.drawable.ic_share_holo_dark, 1);
+ View tabs = makeTabsView();
+ mShowTabs = new PieItem(tabs, 1);
+ mOptions = makeItem(R.drawable.ic_settings_holo_dark, 1);
+ mRDS = makeItem(R.drawable.ic_desktop_holo_dark, 1);
+ mTabAdapter = new TabAdapter(mActivity, mUiController);
+ PieStackView stack = new PieStackView(mActivity);
+ stack.setLayoutListener(new OnLayoutListener() {
+ @Override
+ public void onLayout(int ax, int ay, boolean left) {
+ buildTabs();
+ }
+ });
+ stack.setOnCurrentListener(mTabAdapter);
+ stack.setAdapter(mTabAdapter);
+ mShowTabs.setPieView(stack);
+ setClickListener(this, mBack, mRefresh, mForward, mUrl, mFind, mInfo,
+ mShare, mBookmarks, mNewTab, mIncognito, mClose, mHistory,
+ mAddBookmark, mOptions, mRDS);
+ if (!BrowserActivity.isTablet(mActivity)) {
+ mShowTabs.getView().setOnClickListener(this);
+ }
+ // level 1
+ mPie.addItem(mOptions);
+ mOptions.addItem(mRDS);
+ mOptions.addItem(makeFiller());
+ mOptions.addItem(makeFiller());
+ mOptions.addItem(makeFiller());
+ mPie.addItem(mBack);
+ mBack.addItem(mRefresh);
+ mBack.addItem(mForward);
+ mBack.addItem(makeFiller());
+ mBack.addItem(makeFiller());
+ mPie.addItem(mUrl);
+ mUrl.addItem(mFind);
+ mUrl.addItem(mShare);
+ mUrl.addItem(makeFiller());
+ mUrl.addItem(makeFiller());
+ mPie.addItem(mShowTabs);
+ mShowTabs.addItem(mClose);
+ mShowTabs.addItem(mIncognito);
+ mShowTabs.addItem(mNewTab);
+ mShowTabs.addItem(makeFiller());
+ mPie.addItem(mBookmarks);
+ mBookmarks.addItem(makeFiller());
+ mBookmarks.addItem(makeFiller());
+ mBookmarks.addItem(mAddBookmark);
+ mBookmarks.addItem(mHistory);
+ }
+
+ @Override
+ public void onClick(View v) {
+ Tab tab = mUiController.getTabControl().getCurrentTab();
+ WebView web = tab.getWebView();
+ if (mBack.getView() == v) {
+ tab.goBack();
+ } else if (mForward.getView() == v) {
+ tab.goForward();
+ } else if (mRefresh.getView() == v) {
+ if (tab.inPageLoad()) {
+ web.stopLoading();
+ } else {
+ web.reload();
+ }
+ } else if (mUrl.getView() == v) {
+ mUi.editUrl(false, true);
+ } else if (mBookmarks.getView() == v) {
+ mUiController.bookmarksOrHistoryPicker(ComboViews.Bookmarks);
+ } else if (mHistory.getView() == v) {
+ mUiController.bookmarksOrHistoryPicker(ComboViews.History);
+ } else if (mAddBookmark.getView() == v) {
+ mUiController.bookmarkCurrentPage();
+ } else if (mNewTab.getView() == v) {
+ mUiController.openTabToHomePage();
+ mUi.editUrl(false, true);
+ } else if (mIncognito.getView() == v) {
+ mUiController.openIncognitoTab();
+ mUi.editUrl(false, true);
+ } else if (mClose.getView() == v) {
+ mUiController.closeCurrentTab();
+ } else if (mOptions.getView() == v) {
+ mUiController.openPreferences();
+ } else if (mShare.getView() == v) {
+ mUiController.shareCurrentPage();
+ } else if (mInfo.getView() == v) {
+ mUiController.showPageInfo();
+ } else if (mFind.getView() == v) {
+ mUiController.findOnPage();
+ } else if (mRDS.getView() == v) {
+ mUiController.toggleUserAgent();
+ } else if (mShowTabs.getView() == v) {
+ ((PhoneUi) mUi).showNavScreen();
+ }
+ }
+
+ private void buildTabs() {
+ final List<Tab> tabs = mUiController.getTabs();
+ mUi.getActiveTab().capture();
+ mTabAdapter.setTabs(tabs);
+ PieStackView sym = (PieStackView) mShowTabs.getPieView();
+ sym.setCurrent(mUiController.getTabControl().getCurrentPosition());
+ }
+
+ protected PieItem makeItem(int image, int l) {
+ ImageView view = new ImageView(mActivity);
+ view.setImageResource(image);
+ view.setMinimumWidth(mItemSize);
+ view.setMinimumHeight(mItemSize);
+ view.setScaleType(ScaleType.CENTER);
+ LayoutParams lp = new LayoutParams(mItemSize, mItemSize);
+ view.setLayoutParams(lp);
+ return new PieItem(view, l);
+ }
+
+ protected PieItem makeFiller() {
+ return new PieItem(null, 1);
+ }
+
+ protected View makeTabsView() {
+ View v = mActivity.getLayoutInflater().inflate(R.layout.qc_tabs_view, null);
+ mTabsCount = (TextView) v.findViewById(R.id.label);
+ mTabsCount.setText("1");
+ ImageView image = (ImageView) v.findViewById(R.id.icon);
+ image.setImageResource(R.drawable.ic_windows_holo_dark);
+ image.setScaleType(ScaleType.CENTER);
+ LayoutParams lp = new LayoutParams(mItemSize, mItemSize);
+ v.setLayoutParams(lp);
+ return v;
+ }
+
+ static class TabAdapter extends BaseAdapter implements OnCurrentListener {
+
+ LayoutInflater mInflater;
+ UiController mUiController;
+ private List<Tab> mTabs;
+ private int mCurrent;
+
+ public TabAdapter(Context ctx, UiController ctl) {
+ mInflater = LayoutInflater.from(ctx);
+ mUiController = ctl;
+ mTabs = new ArrayList<Tab>();
+ mCurrent = -1;
+ }
+
+ public void setTabs(List<Tab> tabs) {
+ mTabs = tabs;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ return mTabs.size();
+ }
+
+ @Override
+ public Tab getItem(int position) {
+ return mTabs.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final Tab tab = mTabs.get(position);
+ View view = mInflater.inflate(R.layout.qc_tab,
+ null);
+ ImageView thumb = (ImageView) view.findViewById(R.id.thumb);
+ TextView title1 = (TextView) view.findViewById(R.id.title1);
+ TextView title2 = (TextView) view.findViewById(R.id.title2);
+ Bitmap b = tab.getScreenshot();
+ if (b != null) {
+ thumb.setImageBitmap(b);
+ }
+ if (position > mCurrent) {
+ title1.setVisibility(View.GONE);
+ title2.setText(tab.getTitle());
+ } else {
+ title2.setVisibility(View.GONE);
+ title1.setText(tab.getTitle());
+ }
+ view.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mUiController.switchToTab(tab);
+ }
+ });
+ return view;
+ }
+
+ @Override
+ public void onSetCurrent(int index) {
+ mCurrent = index;
+ }
+
+ }
+
+}
diff --git a/src/com/android/browser/PreferenceKeys.java b/src/com/android/browser/PreferenceKeys.java
new file mode 100644
index 0000000..8620053
--- /dev/null
+++ b/src/com/android/browser/PreferenceKeys.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2011 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;
+
+public interface PreferenceKeys {
+
+ static final String PREF_AUTOFILL_ACTIVE_PROFILE_ID = "autofill_active_profile_id";
+ static final String PREF_DEBUG_MENU = "debug_menu";
+
+ // ----------------------
+ // Keys for accessibility_preferences.xml
+ // ----------------------
+ static final String PREF_MIN_FONT_SIZE = "min_font_size";
+ static final String PREF_TEXT_SIZE = "text_size";
+ static final String PREF_TEXT_ZOOM = "text_zoom";
+ static final String PREF_DOUBLE_TAP_ZOOM = "double_tap_zoom";
+ static final String PREF_FORCE_USERSCALABLE = "force_userscalable";
+ static final String PREF_INVERTED = "inverted";
+ static final String PREF_INVERTED_CONTRAST = "inverted_contrast";
+
+ // ----------------------
+ // Keys for advanced_preferences.xml
+ // ----------------------
+ static final String PREF_AUTOFIT_PAGES = "autofit_pages";
+ static final String PREF_BLOCK_POPUP_WINDOWS = "block_popup_windows";
+ static final String PREF_DEFAULT_TEXT_ENCODING = "default_text_encoding";
+ static final String PREF_DEFAULT_ZOOM = "default_zoom";
+ static final String PREF_ENABLE_JAVASCRIPT = "enable_javascript";
+ static final String PREF_ENABLE_MEMORY_MONITOR = "enable_memory_monitor";
+ static final String PREF_LOAD_PAGE = "load_page";
+ static final String PREF_OPEN_IN_BACKGROUND = "open_in_background";
+ static final String PREF_PLUGIN_STATE = "plugin_state";
+ static final String PREF_RESET_DEFAULT_PREFERENCES = "reset_default_preferences";
+ static final String PREF_SEARCH_ENGINE = "search_engine";
+ static final String PREF_WEBSITE_SETTINGS = "website_settings";
+ static final String PREF_ALLOW_APP_TABS = "allow_apptabs";
+ // Keys for download path settings
+ static final String PREF_DOWNLOAD_PATH = "download_path_setting_screen";
+ // ----------------------
+ // Keys for debug_preferences.xml
+ // ----------------------
+ static final String PREF_ENABLE_HARDWARE_ACCEL = "enable_hardware_accel";
+ static final String PREF_ENABLE_HARDWARE_ACCEL_SKIA = "enable_hardware_accel_skia";
+ static final String PREF_USER_AGENT = "user_agent";
+
+ // ----------------------
+ // Keys for general_preferences.xml
+ // ----------------------
+ static final String PREF_AUTOFILL_ENABLED = "autofill_enabled";
+ static final String PREF_AUTOFILL_PROFILE = "autofill_profile";
+ static final String PREF_HOMEPAGE = "homepage";
+ static final String PREF_SYNC_WITH_CHROME = "sync_with_chrome";
+
+ // ----------------------
+ // Keys for hidden_debug_preferences.xml
+ // ----------------------
+ static final String PREF_ENABLE_LIGHT_TOUCH = "enable_light_touch";
+ static final String PREF_ENABLE_NAV_DUMP = "enable_nav_dump";
+ static final String PREF_ENABLE_TRACING = "enable_tracing";
+ static final String PREF_ENABLE_VISUAL_INDICATOR = "enable_visual_indicator";
+ static final String PREF_ENABLE_CPU_UPLOAD_PATH = "enable_cpu_upload_path";
+ static final String PREF_JAVASCRIPT_CONSOLE = "javascript_console";
+ static final String PREF_JS_ENGINE_FLAGS = "js_engine_flags";
+ static final String PREF_NORMAL_LAYOUT = "normal_layout";
+ static final String PREF_SMALL_SCREEN = "small_screen";
+ static final String PREF_WIDE_VIEWPORT = "wide_viewport";
+ static final String PREF_RESET_PRELOGIN = "reset_prelogin";
+
+ // ----------------------
+ // Keys for lab_preferences.xml
+ // ----------------------
+ static final String PREF_ENABLE_QUICK_CONTROLS = "enable_quick_controls";
+ static final String PREF_FULLSCREEN = "fullscreen";
+
+ // ----------------------
+ // Keys for privacy_security_preferences.xml
+ // ----------------------
+ static final String PREF_ACCEPT_COOKIES = "accept_cookies";
+ static final String PREF_ENABLE_GEOLOCATION = "enable_geolocation";
+ static final String PREF_PRIVACY_CLEAR_CACHE = "privacy_clear_cache";
+ static final String PREF_PRIVACY_CLEAR_COOKIES = "privacy_clear_cookies";
+ static final String PREF_PRIVACY_CLEAR_FORM_DATA = "privacy_clear_form_data";
+ static final String PREF_PRIVACY_CLEAR_GEOLOCATION_ACCESS = "privacy_clear_geolocation_access";
+ static final String PREF_PRIVACY_CLEAR_HISTORY = "privacy_clear_history";
+ static final String PREF_PRIVACY_CLEAR_PASSWORDS = "privacy_clear_passwords";
+ static final String PREF_REMEMBER_PASSWORDS = "remember_passwords";
+ static final String PREF_SAVE_FORMDATA = "save_formdata";
+ static final String PREF_SHOW_SECURITY_WARNINGS = "show_security_warnings";
+
+ // ----------------------
+ // Keys for bandwidth_preferences.xml
+ // ----------------------
+ static final String PREF_DATA_PRELOAD = "preload_when";
+ static final String PREF_LINK_PREFETCH = "link_prefetch_when";
+ static final String PREF_LOAD_IMAGES = "load_images";
+
+ // ----------------------
+ // Keys for browser recovery
+ // ----------------------
+ /**
+ * The last time recovery was started as System.currentTimeMillis.
+ * 0 if not set.
+ */
+ static final String KEY_LAST_RECOVERED = "last_recovered";
+
+ /**
+ * Key for whether or not the last run was paused.
+ */
+ static final String KEY_LAST_RUN_PAUSED = "last_paused";
+}
diff --git a/src/com/android/browser/PreloadController.java b/src/com/android/browser/PreloadController.java
new file mode 100644
index 0000000..b564318
--- /dev/null
+++ b/src/com/android/browser/PreloadController.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2011 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.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.net.http.SslError;
+import android.os.Message;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import org.codeaurora.swe.HttpAuthHandler;
+import org.codeaurora.swe.SslErrorHandler;
+import android.webkit.ValueCallback;
+import android.webkit.WebChromeClient.CustomViewCallback;
+import org.codeaurora.swe.WebView;
+
+public class PreloadController implements WebViewController {
+
+ private static final boolean LOGD_ENABLED = false;
+ private static final String LOGTAG = "PreloadController";
+
+ private Context mContext;
+
+ public PreloadController(Context ctx) {
+ mContext = ctx.getApplicationContext();
+
+ }
+
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+ @Override
+ public Activity getActivity() {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "getActivity()");
+ return null;
+ }
+
+ @Override
+ public TabControl getTabControl() {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "getTabControl()");
+ return null;
+ }
+
+ @Override
+ public WebViewFactory getWebViewFactory() {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "getWebViewFactory()");
+ return null;
+ }
+
+ @Override
+ public void onSetWebView(Tab tab, WebView view) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "onSetWebView()");
+ }
+
+ @Override
+ public void createSubWindow(Tab tab) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "createSubWindow()");
+ }
+
+ @Override
+ public void onPageStarted(Tab tab, WebView view, Bitmap favicon) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "onPageStarted()");
+ if (view != null) {
+ // Clear history of all previously visited pages. When the
+ // user visits a preloaded tab, the only item in the history
+ // list should the currently viewed page.
+ view.clearHistory();
+ }
+ }
+
+ @Override
+ public void onPageFinished(Tab tab) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "onPageFinished()");
+ if (tab != null) {
+ final WebView view = tab.getWebView();
+ if (view != null) {
+ // Clear history of all previously visited pages. When the
+ // user visits a preloaded tab.
+ view.clearHistory();
+ }
+ }
+ }
+
+ @Override
+ public void onProgressChanged(Tab tab) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "onProgressChanged()");
+ }
+
+ @Override
+ public void onReceivedTitle(Tab tab, String title) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "onReceivedTitle()");
+ }
+
+ @Override
+ public void onFavicon(Tab tab, WebView view, Bitmap icon) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "onFavicon()");
+ }
+
+ @Override
+ public boolean shouldOverrideUrlLoading(Tab tab, WebView view, String url) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "shouldOverrideUrlLoading()");
+ return false;
+ }
+
+ @Override
+ public boolean shouldOverrideKeyEvent(KeyEvent event) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "shouldOverrideKeyEvent()");
+ return false;
+ }
+
+ @Override
+ public boolean onUnhandledKeyEvent(KeyEvent event) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "onUnhandledKeyEvent()");
+ return false;
+ }
+
+ @Override
+ public void doUpdateVisitedHistory(Tab tab, boolean isReload) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "doUpdateVisitedHistory()");
+ }
+
+ @Override
+ public void getVisitedHistory(ValueCallback<String[]> callback) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "getVisitedHistory()");
+ }
+
+ @Override
+ public void onReceivedHttpAuthRequest(Tab tab, WebView view,
+ HttpAuthHandler handler, String host,
+ String realm) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "onReceivedHttpAuthRequest()");
+ }
+
+ @Override
+ public void onDownloadStart(Tab tab, String url, String useragent,
+ String contentDisposition, String mimeType,
+ String referer, long contentLength) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "onDownloadStart()");
+ }
+
+ @Override
+ public void showCustomView(Tab tab, View view, int requestedOrientation,
+ CustomViewCallback callback) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "showCustomView()");
+ }
+
+ @Override
+ public void hideCustomView() {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "hideCustomView()");
+ }
+
+ @Override
+ public Bitmap getDefaultVideoPoster() {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "getDefaultVideoPoster()");
+ return null;
+ }
+
+ @Override
+ public View getVideoLoadingProgressView() {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "getVideoLoadingProgressView()");
+ return null;
+ }
+
+ @Override
+ public void showSslCertificateOnError(WebView view,
+ SslErrorHandler handler, SslError error) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "showSslCertificateOnError()");
+ }
+
+ @Override
+ public void onUserCanceledSsl(Tab tab) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "onUserCanceledSsl()");
+ }
+
+ @Override
+ public boolean shouldShowErrorConsole() {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "shouldShowErrorConsole()");
+ return false;
+ }
+
+ @Override
+ public void onUpdatedSecurityState(Tab tab) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "onUpdatedSecurityState()");
+ }
+
+ @Override
+ public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "openFileChooser()");
+ }
+
+ @Override
+ public void endActionMode() {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "endActionMode()");
+ }
+
+ @Override
+ public void attachSubWindow(Tab tab) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "attachSubWindow()");
+ }
+
+ @Override
+ public void dismissSubWindow(Tab tab) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "dismissSubWindow()");
+ }
+
+ @Override
+ public Tab openTab(String url, boolean incognito, boolean setActive, boolean useCurrent) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "openTab()");
+ return null;
+ }
+
+ @Override
+ public Tab openTab(String url, Tab parent, boolean setActive, boolean useCurrent) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "openTab()");
+ return null;
+ }
+
+ @Override
+ public boolean switchToTab(Tab tab) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "switchToTab()");
+ return false;
+ }
+
+ @Override
+ public void closeTab(Tab tab) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "closeTab()");
+ }
+
+ @Override
+ public void setupAutoFill(Message message) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "setupAutoFill()");
+ }
+
+ @Override
+ public void bookmarkedStatusHasChanged(Tab tab) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "bookmarkedStatusHasChanged()");
+ }
+
+ @Override
+ public void showAutoLogin(Tab tab) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "showAutoLogin()");
+ }
+
+ @Override
+ public void hideAutoLogin(Tab tab) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "hideAutoLogin()");
+ }
+
+ @Override
+ public boolean shouldCaptureThumbnails() {
+ return false;
+ }
+
+}
diff --git a/src/com/android/browser/PreloadRequestReceiver.java b/src/com/android/browser/PreloadRequestReceiver.java
new file mode 100644
index 0000000..c654037
--- /dev/null
+++ b/src/com/android/browser/PreloadRequestReceiver.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2011 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.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Bundle;
+import android.provider.Browser;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * Broadcast receiver for receiving browser preload requests
+ */
+public class PreloadRequestReceiver extends BroadcastReceiver {
+
+ private final static String LOGTAG = "browser.preloader";
+ private final static boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED;
+
+ private static final String ACTION_PRELOAD = "android.intent.action.PRELOAD";
+ static final String EXTRA_PRELOAD_ID = "preload_id";
+ static final String EXTRA_PRELOAD_DISCARD = "preload_discard";
+ static final String EXTRA_SEARCHBOX_CANCEL = "searchbox_cancel";
+ static final String EXTRA_SEARCHBOX_SETQUERY = "searchbox_query";
+
+ private ConnectivityManager mConnectivityManager;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "received intent " + intent);
+ if (isPreloadEnabledOnCurrentNetwork(context) &&
+ intent.getAction().equals(ACTION_PRELOAD)) {
+ handlePreload(context, intent);
+ }
+ }
+
+ private boolean isPreloadEnabledOnCurrentNetwork(Context context) {
+ String preload = BrowserSettings.getInstance().getPreloadEnabled();
+ if (LOGD_ENABLED) Log.d(LOGTAG, "Preload setting: " + preload);
+ if (BrowserSettings.getPreloadAlwaysPreferenceString(context).equals(preload)) {
+ return true;
+ } else if (BrowserSettings.getPreloadOnWifiOnlyPreferenceString(context).equals(preload)) {
+ boolean onWifi = isOnWifi(context);
+ if (LOGD_ENABLED) Log.d(LOGTAG, "on wifi:" + onWifi);
+ return onWifi;
+ } else {
+ return false;
+ }
+ }
+
+ private boolean isOnWifi(Context context) {
+ if (mConnectivityManager == null) {
+ mConnectivityManager = (ConnectivityManager)
+ context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ }
+ NetworkInfo ni = mConnectivityManager.getActiveNetworkInfo();
+ if (ni == null) {
+ return false;
+ }
+ switch (ni.getType()) {
+ case ConnectivityManager.TYPE_MOBILE:
+ case ConnectivityManager.TYPE_MOBILE_DUN:
+ case ConnectivityManager.TYPE_MOBILE_MMS:
+ case ConnectivityManager.TYPE_MOBILE_SUPL:
+ case ConnectivityManager.TYPE_MOBILE_HIPRI:
+ case ConnectivityManager.TYPE_WIMAX: // separate case for this?
+ return false;
+ case ConnectivityManager.TYPE_WIFI:
+ case ConnectivityManager.TYPE_ETHERNET:
+ case ConnectivityManager.TYPE_BLUETOOTH:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private void handlePreload(Context context, Intent i) {
+ String url = UrlUtils.smartUrlFilter(i.getData());
+ String id = i.getStringExtra(EXTRA_PRELOAD_ID);
+ Map<String, String> headers = null;
+ if (id == null) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "Preload request has no " + EXTRA_PRELOAD_ID);
+ return;
+ }
+ if (i.getBooleanExtra(EXTRA_PRELOAD_DISCARD, false)) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "Got " + id + " preload discard request");
+ Preloader.getInstance().discardPreload(id);
+ } else if (i.getBooleanExtra(EXTRA_SEARCHBOX_CANCEL, false)) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "Got " + id + " searchbox cancel request");
+ Preloader.getInstance().cancelSearchBoxPreload(id);
+ } else {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "Got " + id + " preload request for " + url);
+ if (url != null && url.startsWith("http")) {
+ final Bundle pairs = i.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));
+ }
+ }
+ }
+ String sbQuery = i.getStringExtra(EXTRA_SEARCHBOX_SETQUERY);
+ if (url != null) {
+ if (LOGD_ENABLED){
+ Log.d(LOGTAG, "Preload request(" + id + ", " + url + ", " +
+ headers + ", " + sbQuery + ")");
+ }
+ Preloader.getInstance().handlePreloadRequest(id, url, headers, sbQuery);
+ }
+ }
+ }
+
+}
diff --git a/src/com/android/browser/PreloadedTabControl.java b/src/com/android/browser/PreloadedTabControl.java
new file mode 100644
index 0000000..21dafa9
--- /dev/null
+++ b/src/com/android/browser/PreloadedTabControl.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2011 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.text.TextUtils;
+import android.util.Log;
+
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * Class to manage the controlling of preloaded tab.
+ */
+public class PreloadedTabControl {
+ private static final boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED;
+ private static final String LOGTAG = "PreloadedTabControl";
+
+ final Tab mTab;
+ private String mLastQuery;
+ private boolean mDestroyed;
+
+ public PreloadedTabControl(Tab t) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "PreloadedTabControl.<init>");
+ mTab = t;
+ }
+
+ public void setQuery(String query) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "Cannot set query: no searchbox interface");
+ }
+
+ public boolean searchBoxSubmit(final String query,
+ final String fallbackUrl, final Map<String, String> fallbackHeaders) {
+ return false;
+ }
+
+ public void searchBoxCancel() {
+ }
+
+ public void loadUrlIfChanged(String url, Map<String, String> headers) {
+ String currentUrl = mTab.getUrl();
+ if (!TextUtils.isEmpty(currentUrl)) {
+ try {
+ // remove fragment:
+ currentUrl = Uri.parse(currentUrl).buildUpon().fragment(null).build().toString();
+ } catch (UnsupportedOperationException e) {
+ // carry on
+ }
+ }
+ if (LOGD_ENABLED) Log.d(LOGTAG, "loadUrlIfChanged\nnew: " + url + "\nold: " +currentUrl);
+ if (!TextUtils.equals(url, currentUrl)) {
+ loadUrl(url, headers);
+ }
+ }
+
+ public void loadUrl(String url, Map<String, String> headers) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "Preloading " + url);
+ mTab.loadUrl(url, headers);
+ }
+
+ public void destroy() {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "PreloadedTabControl.destroy");
+ mDestroyed = true;
+ mTab.destroy();
+ }
+
+ public Tab getTab() {
+ return mTab;
+ }
+
+}
diff --git a/src/com/android/browser/Preloader.java b/src/com/android/browser/Preloader.java
new file mode 100644
index 0000000..7d8c367
--- /dev/null
+++ b/src/com/android/browser/Preloader.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2011 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.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import org.codeaurora.swe.WebView;
+
+import java.util.Map;
+
+/**
+ * Singleton class for handling preload requests.
+ */
+public class Preloader {
+
+ private final static String LOGTAG = "browser.preloader";
+ private final static boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED;
+
+ private static final int PRERENDER_TIMEOUT_MILLIS = 30 * 1000; // 30s
+
+ private static Preloader sInstance;
+
+ private final Context mContext;
+ private final Handler mHandler;
+ private final BrowserWebViewFactory mFactory;
+ private volatile PreloaderSession mSession;
+
+ public static void initialize(Context context) {
+ sInstance = new Preloader(context);
+ }
+
+ public static Preloader getInstance() {
+ return sInstance;
+ }
+
+ private Preloader(Context context) {
+ mContext = context.getApplicationContext();
+ mHandler = new Handler(Looper.getMainLooper());
+ mSession = null;
+ mFactory = new BrowserWebViewFactory(context);
+
+ }
+
+ private PreloaderSession getSession(String id) {
+ if (mSession == null) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "Create new preload session " + id);
+ mSession = new PreloaderSession(id);
+ WebViewTimersControl.getInstance().onPrerenderStart(
+ mSession.getWebView());
+ return mSession;
+ } else if (mSession.mId.equals(id)) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "Returning existing preload session " + id);
+ return mSession;
+ }
+
+ if (LOGD_ENABLED) Log.d(LOGTAG, "Existing session in progress : " + mSession.mId +
+ " returning null.");
+ return null;
+ }
+
+ private PreloaderSession takeSession(String id) {
+ PreloaderSession s = null;
+ if (mSession != null && mSession.mId.equals(id)) {
+ s = mSession;
+ mSession = null;
+ }
+
+ if (s != null) {
+ s.cancelTimeout();
+ }
+
+ return s;
+ }
+
+ public void handlePreloadRequest(String id, String url, Map<String, String> headers,
+ String searchBoxQuery) {
+ PreloaderSession s = getSession(id);
+ if (s == null) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "Discarding preload request, existing"
+ + " session in progress");
+ return;
+ }
+
+ s.touch(); // reset timer
+ PreloadedTabControl tab = s.getTabControl();
+ if (searchBoxQuery != null) {
+ tab.loadUrlIfChanged(url, headers);
+ tab.setQuery(searchBoxQuery);
+ } else {
+ tab.loadUrl(url, headers);
+ }
+ }
+
+ public void cancelSearchBoxPreload(String id) {
+ PreloaderSession s = getSession(id);
+ if (s != null) {
+ s.touch(); // reset timer
+ PreloadedTabControl tab = s.getTabControl();
+ tab.searchBoxCancel();
+ }
+ }
+
+ public void discardPreload(String id) {
+ PreloaderSession s = takeSession(id);
+ if (s != null) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "Discard preload session " + id);
+ WebViewTimersControl.getInstance().onPrerenderDone(s == null ? null : s.getWebView());
+ PreloadedTabControl t = s.getTabControl();
+ t.destroy();
+ } else {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "Ignored discard request " + id);
+ }
+ }
+
+ /**
+ * Return a preloaded tab, and remove it from the preloader. This is used when the
+ * view is about to be displayed.
+ */
+ public PreloadedTabControl getPreloadedTab(String id) {
+ PreloaderSession s = takeSession(id);
+ if (LOGD_ENABLED) Log.d(LOGTAG, "Showing preload session " + id + "=" + s);
+ return s == null ? null : s.getTabControl();
+ }
+
+ private class PreloaderSession {
+ private final String mId;
+ private final PreloadedTabControl mTabControl;
+
+ private final Runnable mTimeoutTask = new Runnable(){
+ @Override
+ public void run() {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "Preload session timeout " + mId);
+ discardPreload(mId);
+ }};
+
+ public PreloaderSession(String id) {
+ mId = id;
+ mTabControl = new PreloadedTabControl(
+ new Tab(new PreloadController(mContext), mFactory.createWebView(false)));
+ touch();
+ }
+
+ public void cancelTimeout() {
+ mHandler.removeCallbacks(mTimeoutTask);
+ }
+
+ public void touch() {
+ cancelTimeout();
+ mHandler.postDelayed(mTimeoutTask, PRERENDER_TIMEOUT_MILLIS);
+ }
+
+ public PreloadedTabControl getTabControl() {
+ return mTabControl;
+ }
+
+ public WebView getWebView() {
+ Tab t = mTabControl.getTab();
+ return t == null? null : t.getWebView();
+ }
+
+ }
+
+}
diff --git a/src/com/android/browser/ShortcutActivity.java b/src/com/android/browser/ShortcutActivity.java
new file mode 100644
index 0000000..dcc176f
--- /dev/null
+++ b/src/com/android/browser/ShortcutActivity.java
@@ -0,0 +1,74 @@
+/*
+ * 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.R;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+
+public class ShortcutActivity extends Activity
+ implements BookmarksPageCallbacks, OnClickListener {
+
+ private BrowserBookmarksPage mBookmarks;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setTitle(R.string.shortcut_bookmark_title);
+ setContentView(R.layout.pick_bookmark);
+ mBookmarks = (BrowserBookmarksPage) getFragmentManager()
+ .findFragmentById(R.id.bookmarks);
+ mBookmarks.setEnableContextMenu(false);
+ mBookmarks.setCallbackListener(this);
+ View cancel = findViewById(R.id.cancel);
+ if (cancel != null) {
+ cancel.setOnClickListener(this);
+ }
+ }
+
+ // 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(String... urls) {
+ return false;
+ }
+
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.cancel:
+ finish();
+ break;
+ }
+ }
+}
diff --git a/src/com/android/browser/SnapshotBar.java b/src/com/android/browser/SnapshotBar.java
new file mode 100644
index 0000000..42f9fba
--- /dev/null
+++ b/src/com/android/browser/SnapshotBar.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2011 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.os.Handler;
+import android.os.Message;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewConfiguration;
+import android.view.ViewPropertyAnimator;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu.OnMenuItemClickListener;
+import android.widget.TextView;
+
+import com.android.browser.R;
+import com.android.browser.UI.ComboViews;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+public class SnapshotBar extends LinearLayout implements OnClickListener {
+
+ private static final int MSG_SHOW_TITLE = 1;
+ private static final long DURATION_SHOW_DATE = BaseUi.HIDE_TITLEBAR_DELAY;
+
+ private ImageView mFavicon;
+ private TextView mDate;
+ private TextView mTitle;
+ private View mBookmarks;
+ private TitleBar mTitleBar;
+ private View mTabSwitcher;
+ private View mOverflowMenu;
+ private View mToggleContainer;
+ private boolean mIsAnimating;
+ private ViewPropertyAnimator mTitleAnimator, mDateAnimator;
+ private float mAnimRadius = 20f;
+
+ public SnapshotBar(Context context) {
+ super(context);
+ }
+
+ public SnapshotBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public SnapshotBar(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public void setTitleBar(TitleBar titleBar) {
+ mTitleBar = titleBar;
+ setFavicon(null);
+ }
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_SHOW_TITLE) {
+ mIsAnimating = false;
+ showTitle();
+ mTitleBar.getUi().showTitleBarForDuration();
+ }
+ }
+ };
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mFavicon = (ImageView) findViewById(R.id.favicon);
+ mDate = (TextView) findViewById(R.id.date);
+ mTitle = (TextView) findViewById(R.id.title);
+ mBookmarks = findViewById(R.id.all_btn);
+ mTabSwitcher = findViewById(R.id.tab_switcher);
+ mOverflowMenu = findViewById(R.id.more);
+ mToggleContainer = findViewById(R.id.toggle_container);
+
+ if (mBookmarks != null) {
+ mBookmarks.setOnClickListener(this);
+ }
+ if (mTabSwitcher != null) {
+ mTabSwitcher.setOnClickListener(this);
+ }
+ if (mOverflowMenu != null) {
+ mOverflowMenu.setOnClickListener(this);
+ boolean showMenu = !ViewConfiguration.get(getContext())
+ .hasPermanentMenuKey();
+ mOverflowMenu.setVisibility(showMenu ? VISIBLE : GONE);
+ }
+ if (mToggleContainer != null) {
+ mToggleContainer.setOnClickListener(this);
+ resetAnimation();
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+ if (mToggleContainer != null) {
+ mAnimRadius = mToggleContainer.getHeight() / 2f;
+ }
+ }
+
+ void resetAnimation() {
+ if (mToggleContainer == null) {
+ // No animation needed/used
+ return;
+ }
+ if (mTitleAnimator != null) {
+ mTitleAnimator.cancel();
+ mTitleAnimator = null;
+ }
+ if (mDateAnimator != null) {
+ mDateAnimator.cancel();
+ mDateAnimator = null;
+ }
+ mIsAnimating = false;
+ mHandler.removeMessages(MSG_SHOW_TITLE);
+ mTitle.setAlpha(1f);
+ mTitle.setTranslationY(0f);
+ mTitle.setRotationX(0f);
+ mDate.setAlpha(0f);
+ mDate.setTranslationY(-mAnimRadius);
+ mDate.setRotationX(90f);
+ }
+
+ private void showDate() {
+ mTitleAnimator = mTitle.animate()
+ .alpha(0f)
+ .translationY(mAnimRadius)
+ .rotationX(-90f);
+ mDateAnimator = mDate.animate()
+ .alpha(1f)
+ .translationY(0f)
+ .rotationX(0f);
+ }
+
+ private void showTitle() {
+ mTitleAnimator = mTitle.animate()
+ .alpha(1f)
+ .translationY(0f)
+ .rotationX(0f);
+ mDateAnimator = mDate.animate()
+ .alpha(0f)
+ .translationY(-mAnimRadius)
+ .rotationX(90f);
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mBookmarks == v) {
+ mTitleBar.getUiController().bookmarksOrHistoryPicker(ComboViews.Bookmarks);
+ } else if (mTabSwitcher == v) {
+ ((PhoneUi) mTitleBar.getUi()).toggleNavScreen();
+ } else if (mOverflowMenu == v) {
+ NavigationBarBase navBar = mTitleBar.getNavigationBar();
+ if (navBar instanceof NavigationBarPhone) {
+ ((NavigationBarPhone)navBar).showMenu(mOverflowMenu);
+ }
+ } else if (mToggleContainer == v && !mIsAnimating) {
+ mIsAnimating = true;
+ showDate();
+ mTitleBar.getUi().showTitleBar();
+ Message m = mHandler.obtainMessage(MSG_SHOW_TITLE);
+ mHandler.sendMessageDelayed(m, DURATION_SHOW_DATE);
+ }
+ }
+
+ public void onTabDataChanged(Tab tab) {
+ if (!tab.isSnapshot()) return;
+ SnapshotTab snapshot = (SnapshotTab) tab;
+ DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.LONG);
+ mDate.setText(dateFormat.format(new Date(snapshot.getDateCreated())));
+ String title = snapshot.getTitle();
+ if (TextUtils.isEmpty(title)) {
+ title = UrlUtils.stripUrl(snapshot.getUrl());
+ }
+ mTitle.setText(title);
+ setFavicon(tab.getFavicon());
+ resetAnimation();
+ }
+
+ public void setFavicon(Bitmap icon) {
+ if (mFavicon == null) return;
+ mFavicon.setImageDrawable(mTitleBar.getUi().getFaviconDrawable(icon));
+ }
+
+ public boolean isAnimating() {
+ return mIsAnimating;
+ }
+
+}
diff --git a/src/com/android/browser/SnapshotTab.java b/src/com/android/browser/SnapshotTab.java
new file mode 100644
index 0000000..e403dbc
--- /dev/null
+++ b/src/com/android/browser/SnapshotTab.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2011 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.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.provider.SnapshotProvider.Snapshots;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.zip.GZIPInputStream;
+
+
+public class SnapshotTab extends Tab {
+
+ private static final String LOGTAG = "SnapshotTab";
+
+ private long mSnapshotId;
+ private LoadData mLoadTask;
+ private WebViewFactory mWebViewFactory;
+ private int mBackgroundColor;
+ private long mDateCreated;
+ private boolean mIsLive;
+ private String mLiveUrl;
+
+ public SnapshotTab(WebViewController wvcontroller, long snapshotId) {
+ super(wvcontroller, null, null);
+ mSnapshotId = snapshotId;
+ mWebViewFactory = mWebViewController.getWebViewFactory();
+ WebView web = mWebViewFactory.createWebView(false);
+ setWebView(web);
+ loadData();
+ }
+
+ @Override
+ void putInForeground() {
+ if (getWebView() == null) {
+ WebView web = mWebViewFactory.createWebView(false);
+ if (mBackgroundColor != 0) {
+ web.setBackgroundColor(mBackgroundColor);
+ }
+ setWebView(web);
+ loadData();
+ }
+ super.putInForeground();
+ }
+
+ @Override
+ void putInBackground() {
+ if (getWebView() == null) return;
+ super.putInBackground();
+ }
+
+ void loadData() {
+ if (mLoadTask == null) {
+ mLoadTask = new LoadData(this, mContext);
+ mLoadTask.execute();
+ }
+ }
+
+ @Override
+ void addChildTab(Tab child) {
+ if (mIsLive) {
+ super.addChildTab(child);
+ } else {
+ throw new IllegalStateException("Snapshot tabs cannot have child tabs!");
+ }
+ }
+
+ @Override
+ public boolean isSnapshot() {
+ return !mIsLive;
+ }
+
+ public long getSnapshotId() {
+ return mSnapshotId;
+ }
+
+ @Override
+ public ContentValues createSnapshotValues() {
+ if (mIsLive) {
+ return super.createSnapshotValues();
+ }
+ return null;
+ }
+
+ @Override
+ public Bundle saveState() {
+ if (mIsLive) {
+ return super.saveState();
+ }
+ return null;
+ }
+
+ public long getDateCreated() {
+ return mDateCreated;
+ }
+
+ public String getLiveUrl() {
+ return mLiveUrl;
+ }
+
+ @Override
+ public void loadUrl(String url, Map<String, String> headers) {
+ if (!mIsLive) {
+ mIsLive = true;
+ getWebView().clearViewState();
+ }
+ super.loadUrl(url, headers);
+ }
+
+ @Override
+ public boolean canGoBack() {
+ return super.canGoBack() || mIsLive;
+ }
+
+ @Override
+ public boolean canGoForward() {
+ return mIsLive && super.canGoForward();
+ }
+
+ @Override
+ public void goBack() {
+ if (super.canGoBack()) {
+ super.goBack();
+ } else {
+ mIsLive = false;
+ getWebView().stopLoading();
+ loadData();
+ }
+ }
+
+ static class LoadData extends AsyncTask<Void, Void, Cursor> {
+
+ static final String[] PROJECTION = new String[] {
+ Snapshots._ID, // 0
+ Snapshots.URL, // 1
+ Snapshots.TITLE, // 2
+ Snapshots.FAVICON, // 3
+ Snapshots.VIEWSTATE, // 4
+ Snapshots.BACKGROUND, // 5
+ Snapshots.DATE_CREATED, // 6
+ Snapshots.VIEWSTATE_PATH, // 7
+ };
+ static final int SNAPSHOT_ID = 0;
+ static final int SNAPSHOT_URL = 1;
+ static final int SNAPSHOT_TITLE = 2;
+ static final int SNAPSHOT_FAVICON = 3;
+ static final int SNAPSHOT_VIEWSTATE = 4;
+ static final int SNAPSHOT_BACKGROUND = 5;
+ static final int SNAPSHOT_DATE_CREATED = 6;
+ static final int SNAPSHOT_VIEWSTATE_PATH = 7;
+
+ private SnapshotTab mTab;
+ private ContentResolver mContentResolver;
+ private Context mContext;
+
+ public LoadData(SnapshotTab t, Context context) {
+ mTab = t;
+ mContentResolver = context.getContentResolver();
+ mContext = context;
+ }
+
+ @Override
+ protected Cursor doInBackground(Void... params) {
+ long id = mTab.mSnapshotId;
+ Uri uri = ContentUris.withAppendedId(Snapshots.CONTENT_URI, id);
+ return mContentResolver.query(uri, PROJECTION, null, null, null);
+ }
+
+ private InputStream getInputStream(Cursor c) throws FileNotFoundException {
+ byte[] data = c.getBlob(SNAPSHOT_VIEWSTATE);
+ ByteArrayInputStream bis = new ByteArrayInputStream(data);
+ return bis;
+ }
+
+ @Override
+ protected void onPostExecute(Cursor result) {
+ try {
+ if (result.moveToFirst()) {
+ mTab.mCurrentState.mTitle = result.getString(SNAPSHOT_TITLE);
+ mTab.mCurrentState.mUrl = result.getString(SNAPSHOT_URL);
+ mTab.mLiveUrl = result.getString(SNAPSHOT_URL);
+ byte[] favicon = result.getBlob(SNAPSHOT_FAVICON);
+ if (favicon != null) {
+ mTab.mCurrentState.mFavicon = BitmapFactory
+ .decodeByteArray(favicon, 0, favicon.length);
+ }
+ WebView web = mTab.getWebView();
+ if (web != null) {
+ String path = result.getString(SNAPSHOT_VIEWSTATE_PATH);
+ if (!TextUtils.isEmpty(path)) {
+ web.loadViewState(path);
+ } else {
+ InputStream ins = getInputStream(result);
+ GZIPInputStream stream = new GZIPInputStream(ins);
+ web.loadViewState(stream);
+ }
+ }
+ mTab.mBackgroundColor = result.getInt(SNAPSHOT_BACKGROUND);
+ mTab.mDateCreated = result.getLong(SNAPSHOT_DATE_CREATED);
+ mTab.mWebViewController.onPageFinished(mTab);
+ }
+ } catch (Exception e) {
+ Log.w(LOGTAG, "Failed to load view state, closing tab", e);
+ mTab.mWebViewController.closeTab(mTab);
+ } finally {
+ if (result != null) {
+ result.close();
+ }
+ mTab.mLoadTask = null;
+ }
+ }
+
+ }
+
+ @Override
+ protected void persistThumbnail() {
+ if (mIsLive) {
+ super.persistThumbnail();
+ }
+ }
+}
diff --git a/src/com/android/browser/SuggestionsAdapter.java b/src/com/android/browser/SuggestionsAdapter.java
new file mode 100644
index 0000000..41d2b74
--- /dev/null
+++ b/src/com/android/browser/SuggestionsAdapter.java
@@ -0,0 +1,572 @@
+/*
+ * 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.SearchManager;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+
+import com.android.browser.R;
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.provider.BrowserProvider2.OmniboxSuggestions;
+import com.android.browser.search.SearchEngine;
+
+import android.text.Html;
+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 {
+
+ public static final int TYPE_BOOKMARK = 0;
+ public static final int TYPE_HISTORY = 1;
+ public static final int TYPE_SUGGEST_URL = 2;
+ public static final int TYPE_SEARCH = 3;
+ public static final int TYPE_SUGGEST = 4;
+
+ private static final String[] COMBINED_PROJECTION = {
+ OmniboxSuggestions._ID,
+ OmniboxSuggestions.TITLE,
+ OmniboxSuggestions.URL,
+ OmniboxSuggestions.IS_BOOKMARK
+ };
+
+ private static final String COMBINED_SELECTION =
+ "(url LIKE ? OR url LIKE ? OR url LIKE ? OR url LIKE ? OR title LIKE ?)";
+
+ final Context mContext;
+ final Filter mFilter;
+ SuggestionResults mMixedResults;
+ List<SuggestItem> mSuggestResults, mFilterResults;
+ List<CursorSource> mSources;
+ boolean mLandscapeMode;
+ final CompletionListener mListener;
+ final int mLinesPortrait;
+ final int mLinesLandscape;
+ final Object mResultsLock = new Object();
+ boolean mIncognitoMode;
+ BrowserSettings mSettings;
+
+ interface CompletionListener {
+
+ public void onSearch(String txt);
+
+ public void onSelect(String txt, int type, String extraData);
+
+ }
+
+ public SuggestionsAdapter(Context ctx, CompletionListener listener) {
+ mContext = ctx;
+ mSettings = BrowserSettings.getInstance();
+ 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 CombinedCursor());
+ }
+
+ public void setLandscapeMode(boolean mode) {
+ mLandscapeMode = mode;
+ notifyDataSetChanged();
+ }
+
+ public void addSource(CursorSource c) {
+ if (mSources == null) {
+ mSources = new ArrayList<CursorSource>(5);
+ }
+ mSources.add(c);
+ }
+
+ @Override
+ public void onClick(View v) {
+ SuggestItem item = (SuggestItem) ((View) v.getParent()).getTag();
+
+ if (R.id.icon2 == v.getId()) {
+ // replace input field text with suggestion text
+ mListener.onSearch(getSuggestionUrl(item));
+ } else {
+ mListener.onSelect(getSuggestionUrl(item), item.type, 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;
+ }
+ return mMixedResults.items.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @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_item, parent, false);
+ }
+ bindView(view, 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 ic2 = view.findViewById(R.id.icon2);
+ View div = view.findViewById(R.id.divider);
+ tv1.setText(Html.fromHtml(item.title));
+ if (TextUtils.isEmpty(item.url)) {
+ tv2.setVisibility(View.GONE);
+ tv1.setMaxLines(2);
+ } else {
+ tv2.setVisibility(View.VISIBLE);
+ tv2.setText(item.url);
+ tv1.setMaxLines(1);
+ }
+ 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());
+ ic2.setOnClickListener(this);
+ view.findViewById(R.id.suggestion).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();
+ }
+ }
+
+ 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) {
+ if (!mIncognitoMode) {
+ new SlowFilterTask().execute(constraint);
+ }
+ }
+
+ private boolean shouldProcessEmptyQuery() {
+ final SearchEngine searchEngine = mSettings.getSearchEngine();
+ return searchEngine.wantsEmptyQuery();
+ }
+
+ @Override
+ protected FilterResults performFiltering(CharSequence constraint) {
+ FilterResults res = new FilterResults();
+ if (TextUtils.isEmpty(constraint) && !shouldProcessEmptyQuery()) {
+ 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 = getMaxLines();
+ 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) {
+ if (fresults.values instanceof SuggestionResults) {
+ mMixedResults = (SuggestionResults) fresults.values;
+ notifyDataSetChanged();
+ }
+ }
+ }
+
+ private int getMaxLines() {
+ int maxLines = mLandscapeMode ? mLinesLandscape : mLinesPortrait;
+ maxLines = (int) Math.ceil(maxLines / 2.0);
+ return maxLines;
+ }
+
+ /**
+ * 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() {
+ return Math.min((mLandscapeMode ? mLinesLandscape : mLinesPortrait), items.size());
+ }
+
+ @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
+ */
+ public class SuggestItem {
+ public String title;
+ public String url;
+ public int type;
+ public 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 = OmniboxSuggestions.CONTENT_URI.buildUpon();
+ ub.appendQueryParameter(BrowserContract.PARAM_LIMIT,
+ Integer.toString(Math.max(mLinesLandscape, mLinesPortrait)));
+ mCursor =
+ mContext.getContentResolver().query(ub.build(), COMBINED_PROJECTION,
+ selection, (constraint != null) ? args : null, null);
+ 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 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();
+ }
+ SearchEngine searchEngine = mSettings.getSearchEngine();
+ if (!TextUtils.isEmpty(constraint)) {
+ if (searchEngine != null && searchEngine.supportsSuggestions()) {
+ mCursor = searchEngine.getSuggestions(mContext, constraint.toString());
+ if (mCursor != null) {
+ mCursor.moveToFirst();
+ }
+ }
+ } else {
+ if (searchEngine.wantsEmptyQuery()) {
+ mCursor = searchEngine.getSuggestions(mContext, "");
+ }
+ mCursor = null;
+ }
+ }
+
+ }
+
+ public void clearCache() {
+ mFilterResults = null;
+ mSuggestResults = null;
+ notifyDataSetInvalidated();
+ }
+
+ public void setIncognitoMode(boolean incognito) {
+ mIncognitoMode = incognito;
+ clearCache();
+ }
+
+ static String getSuggestionTitle(SuggestItem item) {
+ // There must be a better way to strip HTML from things.
+ // This method is used in multiple places. It is also more
+ // expensive than a standard html escaper.
+ return (item.title != null) ? Html.fromHtml(item.title).toString() : null;
+ }
+
+ static String getSuggestionUrl(SuggestItem item) {
+ final String title = SuggestionsAdapter.getSuggestionTitle(item);
+
+ if (TextUtils.isEmpty(item.url)) {
+ return title;
+ }
+
+ return item.url;
+ }
+}
diff --git a/src/com/android/browser/SystemAllowGeolocationOrigins.java b/src/com/android/browser/SystemAllowGeolocationOrigins.java
new file mode 100644
index 0000000..f4b5835
--- /dev/null
+++ b/src/com/android/browser/SystemAllowGeolocationOrigins.java
@@ -0,0 +1,202 @@
+/*
+ * 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.SharedPreferences;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.webkit.ValueCallback;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.codeaurora.swe.GeolocationPermissions;
+
+/**
+ * Manages the interaction between the secure system setting for default geolocation
+ * permissions and the browser.
+ */
+class SystemAllowGeolocationOrigins {
+
+ // Preference key for the value of the system setting last read by the browser
+ private final static String LAST_READ_ALLOW_GEOLOCATION_ORIGINS =
+ "last_read_allow_geolocation_origins";
+
+ // The application context
+ private final Context mContext;
+
+ // The observer used to listen to the system setting.
+ private final SettingObserver mSettingObserver;
+
+ public SystemAllowGeolocationOrigins(Context context) {
+ mContext = context.getApplicationContext();
+ mSettingObserver = new SettingObserver();
+ }
+
+ /**
+ * Checks whether the setting has changed and installs an observer to listen for
+ * future changes. Must be called on the application main thread.
+ */
+ public void start() {
+ // Register to receive notifications when the system settings change.
+ Uri uri = Settings.Secure.getUriFor(Settings.Secure.ALLOWED_GEOLOCATION_ORIGINS);
+ mContext.getContentResolver().registerContentObserver(uri, false, mSettingObserver);
+
+ // Read and apply the setting if needed.
+ maybeApplySettingAsync();
+ }
+
+ /**
+ * Stops the manager.
+ */
+ public void stop() {
+ mContext.getContentResolver().unregisterContentObserver(mSettingObserver);
+ }
+
+ void maybeApplySettingAsync() {
+ BackgroundHandler.execute(mMaybeApplySetting);
+ }
+
+ /**
+ * Checks to see if the system setting has changed and if so,
+ * updates the Geolocation permissions accordingly.
+ */
+ private Runnable mMaybeApplySetting = new Runnable() {
+
+ @Override
+ public void run() {
+ // Get the new value
+ String newSetting = getSystemSetting();
+
+ // Get the last read value
+ SharedPreferences preferences = BrowserSettings.getInstance()
+ .getPreferences();
+ String lastReadSetting =
+ preferences.getString(LAST_READ_ALLOW_GEOLOCATION_ORIGINS, "");
+
+ // If the new value is the same as the last one we read, we're done.
+ if (TextUtils.equals(lastReadSetting, newSetting)) {
+ return;
+ }
+
+ // Save the new value as the last read value
+ preferences.edit()
+ .putString(LAST_READ_ALLOW_GEOLOCATION_ORIGINS, newSetting)
+ .apply();
+
+ Set<String> oldOrigins = parseAllowGeolocationOrigins(lastReadSetting);
+ Set<String> newOrigins = parseAllowGeolocationOrigins(newSetting);
+ Set<String> addedOrigins = setMinus(newOrigins, oldOrigins);
+ Set<String> removedOrigins = setMinus(oldOrigins, newOrigins);
+
+ // Remove the origins in the last read value
+ removeOrigins(removedOrigins);
+
+ // Add the origins in the new value
+ addOrigins(addedOrigins);
+ }
+ };
+
+ /**
+ * Parses the value of the default geolocation permissions setting.
+ *
+ * @param setting A space-separated list of origins.
+ * @return A mutable set of origins.
+ */
+ private static HashSet<String> parseAllowGeolocationOrigins(String setting) {
+ HashSet<String> origins = new HashSet<String>();
+ if (!TextUtils.isEmpty(setting)) {
+ for (String origin : setting.split("\\s+")) {
+ if (!TextUtils.isEmpty(origin)) {
+ origins.add(origin);
+ }
+ }
+ }
+ return origins;
+ }
+
+ /**
+ * Gets the difference between two sets. Does not modify any of the arguments.
+ *
+ * @return A set containing all elements in {@code x} that are not in {@code y}.
+ */
+ private <A> Set<A> setMinus(Set<A> x, Set<A> y) {
+ HashSet<A> z = new HashSet<A>(x.size());
+ for (A a : x) {
+ if (!y.contains(a)) {
+ z.add(a);
+ }
+ }
+ return z;
+ }
+
+ /**
+ * Gets the current system setting for default allowed geolocation origins.
+ *
+ * @return The default allowed origins. Returns {@code ""} if not set.
+ */
+ private String getSystemSetting() {
+ String value = Settings.Secure.getString(mContext.getContentResolver(),
+ Settings.Secure.ALLOWED_GEOLOCATION_ORIGINS);
+ return value == null ? "" : value;
+ }
+
+ /**
+ * Adds geolocation permissions for the given origins.
+ */
+ private void addOrigins(Set<String> origins) {
+ for (String origin : origins) {
+ GeolocationPermissions.getInstance().allow(origin);
+ }
+ }
+
+ /**
+ * Removes geolocation permissions for the given origins, if they are allowed.
+ * If they are denied or not set, nothing is done.
+ */
+ private void removeOrigins(Set<String> origins) {
+ for (final String origin : origins) {
+ GeolocationPermissions.getInstance().getAllowed(origin, new ValueCallback<Boolean>() {
+ public void onReceiveValue(Boolean value) {
+ if (value != null && value.booleanValue()) {
+ GeolocationPermissions.getInstance().clear(origin);
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Listens for changes to the system setting.
+ */
+ private class SettingObserver extends ContentObserver {
+
+ SettingObserver() {
+ super(new Handler());
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ maybeApplySettingAsync();
+ }
+ }
+
+}
diff --git a/src/com/android/browser/Tab.java b/src/com/android/browser/Tab.java
new file mode 100644
index 0000000..6d36d9c
--- /dev/null
+++ b/src/com/android/browser/Tab.java
@@ -0,0 +1,2108 @@
+/*
+ * 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.AlertDialog;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Picture;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.net.Uri;
+import android.net.http.SslError;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.security.KeyChain;
+import android.security.KeyChainAliasCallback;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewStub;
+import android.view.View.OnClickListener;
+import android.webkit.ConsoleMessage;
+import android.webkit.GeolocationPermissions;
+import android.webkit.URLUtil;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebStorage;
+import android.webkit.WebChromeClient.CustomViewCallback;
+import android.webkit.ValueCallback;
+import android.widget.CheckBox;
+import android.widget.Toast;
+import android.widget.FrameLayout;
+import android.widget.Button;
+
+import com.android.browser.R;
+import com.android.browser.TabControl.OnThumbnailUpdatedListener;
+import com.android.browser.homepages.HomeProvider;
+import com.android.browser.mynavigation.MyNavigationUtil;
+import com.android.browser.provider.MyNavigationProvider;
+import com.android.browser.provider.SnapshotProvider.Snapshots;
+
+import org.codeaurora.swe.BrowserDownloadListener;
+import org.codeaurora.swe.ClientCertRequestHandler;
+import org.codeaurora.swe.HttpAuthHandler;
+import org.codeaurora.swe.SslErrorHandler;
+import org.codeaurora.swe.WebBackForwardList;
+import org.codeaurora.swe.WebBackForwardListClient;
+import org.codeaurora.swe.WebChromeClient;
+import org.codeaurora.swe.WebHistoryItem;
+import org.codeaurora.swe.WebView;
+import org.codeaurora.swe.WebView.PictureListener;
+import org.codeaurora.swe.WebViewClient;
+
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.UUID;
+import java.util.Vector;
+import java.util.regex.Pattern;
+import java.sql.Timestamp;
+import java.util.Date;
+
+/**
+ * Class for maintaining Tabs with a main WebView and a subwindow.
+ */
+class Tab implements PictureListener {
+
+ // Log Tag
+ private static final String LOGTAG = "Tab";
+ private static final boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED;
+ // Special case the logtag for messages for the Console to make it easier to
+ // filter them and match the logtag used for these messages in older versions
+ // of the browser.
+ private static final String CONSOLE_LOGTAG = "browser";
+
+ private static final int MSG_CAPTURE = 42;
+ private static final int CAPTURE_DELAY = 1000;
+ private static final int INITIAL_PROGRESS = 5;
+
+ private static Bitmap sDefaultFavicon;
+ protected boolean hasCrashed = false;
+
+ private static Paint sAlphaPaint = new Paint();
+ static {
+ sAlphaPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+ sAlphaPaint.setColor(Color.TRANSPARENT);
+ }
+
+ public enum SecurityState {
+ // The page's main resource does not use SSL. Note that we use this
+ // state irrespective of the SSL authentication state of sub-resources.
+ SECURITY_STATE_NOT_SECURE,
+ // The page's main resource uses SSL and the certificate is good. The
+ // same is true of all sub-resources.
+ SECURITY_STATE_SECURE,
+ // The page's main resource uses SSL and the certificate is good, but
+ // some sub-resources either do not use SSL or have problems with their
+ // certificates.
+ SECURITY_STATE_MIXED,
+ // The page's main resource uses SSL but there is a problem with its
+ // certificate.
+ SECURITY_STATE_BAD_CERTIFICATE,
+ }
+
+ Context mContext;
+ protected WebViewController mWebViewController;
+
+ // The tab ID
+ private long mId = -1;
+
+ // The Geolocation permissions prompt
+ private GeolocationPermissionsPrompt mGeolocationPermissionsPrompt;
+ // Main WebView wrapper
+ private View mContainer;
+ // Main WebView
+ private WebView mMainView;
+ // Subwindow container
+ private View mSubViewContainer;
+ // Subwindow WebView
+ private WebView mSubView;
+ // Saved bundle for when we are running low on memory. It contains the
+ // information needed to restore the WebView if the user goes back to the
+ // tab.
+ private Bundle mSavedState;
+ // Parent Tab. This is the Tab that created this Tab, or null if the Tab was
+ // created by the UI
+ private Tab mParent;
+ // Tab that constructed by this Tab. This is used when this Tab is
+ // destroyed, it clears all mParentTab values in the children.
+ private Vector<Tab> mChildren;
+ // If true, the tab is in the foreground of the current activity.
+ private boolean mInForeground;
+ // If true, the tab is in page loading state (after onPageStarted,
+ // before onPageFinsihed)
+ private boolean mInPageLoad;
+ private boolean mDisableOverrideUrlLoading;
+ // The last reported progress of the current page
+ private int mPageLoadProgress;
+ // The time the load started, used to find load page time
+ private long mLoadStartTime;
+ // Application identifier used to find tabs that another application wants
+ // to reuse.
+ private String mAppId;
+ // flag to indicate if tab should be closed on back
+ private boolean mCloseOnBack;
+ // Keep the original url around to avoid killing the old WebView if the url
+ // has not changed.
+ // Error console for the tab
+ private ErrorConsoleView mErrorConsole;
+ // The listener that gets invoked when a download is started from the
+ // mMainView
+ private final BrowserDownloadListener mDownloadListener;
+ // Listener used to know when we move forward or back in the history list.
+ private final WebBackForwardListClient mWebBackForwardListClient;
+ private DataController mDataController;
+ // State of the auto-login request.
+ private DeviceAccountLogin mDeviceAccountLogin;
+
+ // AsyncTask for downloading touch icons
+ DownloadTouchIcon mTouchIconLoader;
+
+ private BrowserSettings mSettings;
+ private int mCaptureWidth;
+ private int mCaptureHeight;
+ private Bitmap mCapture;
+ private Handler mHandler;
+ private boolean mUpdateThumbnail;
+ private Timestamp timestamp;
+
+ /**
+ * See {@link #clearBackStackWhenItemAdded(String)}.
+ */
+ private Pattern mClearHistoryUrlPattern;
+
+ private static synchronized Bitmap getDefaultFavicon(Context context) {
+ if (sDefaultFavicon == null) {
+ sDefaultFavicon = BitmapFactory.decodeResource(
+ context.getResources(), R.drawable.app_web_browser_sm);
+ }
+ return sDefaultFavicon;
+ }
+
+ // All the state needed for a page
+ protected static class PageState {
+ String mUrl;
+ String mOriginalUrl;
+ String mTitle;
+ SecurityState mSecurityState;
+ // This is non-null only when mSecurityState is SECURITY_STATE_BAD_CERTIFICATE.
+ SslError mSslCertificateError;
+ Bitmap mFavicon;
+ boolean mIsBookmarkedSite;
+ boolean mIncognito;
+
+ PageState(Context c, boolean incognito) {
+ mIncognito = incognito;
+ if (mIncognito) {
+ mOriginalUrl = mUrl = "browser:incognito";
+ mTitle = c.getString(R.string.new_incognito_tab);
+ } else {
+ mOriginalUrl = mUrl = "";
+ mTitle = c.getString(R.string.new_tab);
+ }
+ mSecurityState = SecurityState.SECURITY_STATE_NOT_SECURE;
+ }
+
+ PageState(Context c, boolean incognito, String url, Bitmap favicon) {
+ mIncognito = incognito;
+ mOriginalUrl = mUrl = url;
+ if (URLUtil.isHttpsUrl(url)) {
+ mSecurityState = SecurityState.SECURITY_STATE_SECURE;
+ } else {
+ mSecurityState = SecurityState.SECURITY_STATE_NOT_SECURE;
+ }
+ mFavicon = favicon;
+ }
+
+ }
+
+ // The current/loading page's state
+ protected PageState mCurrentState;
+
+ // Used for saving and restoring each Tab
+ static final String ID = "ID";
+ static final String CURRURL = "currentUrl";
+ static final String CURRTITLE = "currentTitle";
+ static final String PARENTTAB = "parentTab";
+ static final String APPID = "appid";
+ static final String INCOGNITO = "privateBrowsingEnabled";
+ static final String USERAGENT = "useragent";
+ static final String CLOSEFLAG = "closeOnBack";
+
+ // Container class for the next error dialog that needs to be displayed
+ private class ErrorDialog {
+ public final int mTitle;
+ public final String mDescription;
+ public final int mError;
+ ErrorDialog(int title, String desc, int error) {
+ mTitle = title;
+ mDescription = desc;
+ mError = error;
+ }
+ }
+
+ private void processNextError() {
+ if (mQueuedErrors == null) {
+ return;
+ }
+ // The first one is currently displayed so just remove it.
+ mQueuedErrors.removeFirst();
+ if (mQueuedErrors.size() == 0) {
+ mQueuedErrors = null;
+ return;
+ }
+ showError(mQueuedErrors.getFirst());
+ }
+
+ private DialogInterface.OnDismissListener mDialogListener =
+ new DialogInterface.OnDismissListener() {
+ public void onDismiss(DialogInterface d) {
+ processNextError();
+ }
+ };
+ private LinkedList<ErrorDialog> mQueuedErrors;
+
+ private void queueError(int err, String desc) {
+ if (mQueuedErrors == null) {
+ mQueuedErrors = new LinkedList<ErrorDialog>();
+ }
+ for (ErrorDialog d : mQueuedErrors) {
+ if (d.mError == err) {
+ // Already saw a similar error, ignore the new one.
+ return;
+ }
+ }
+ ErrorDialog errDialog = new ErrorDialog(
+ err == WebViewClient.ERROR_FILE_NOT_FOUND ?
+ R.string.browserFrameFileErrorLabel :
+ R.string.browserFrameNetworkErrorLabel,
+ desc, err);
+ mQueuedErrors.addLast(errDialog);
+
+ // Show the dialog now if the queue was empty and it is in foreground
+ if (mQueuedErrors.size() == 1 && mInForeground) {
+ showError(errDialog);
+ }
+ }
+
+ private void showError(ErrorDialog errDialog) {
+ if (mInForeground) {
+ AlertDialog d = new AlertDialog.Builder(mContext)
+ .setTitle(errDialog.mTitle)
+ .setMessage(errDialog.mDescription)
+ .setPositiveButton(R.string.ok, null)
+ .create();
+ d.setOnDismissListener(mDialogListener);
+ d.show();
+ }
+ }
+
+ protected void replaceCrashView(View view, View container) {
+ if (hasCrashed && (view == mMainView)) {
+ final FrameLayout wrapper = (FrameLayout) container.findViewById(R.id.webview_wrapper);
+ wrapper.removeAllViewsInLayout();
+ wrapper.addView(view);
+ hasCrashed = false;
+ }
+ }
+
+ protected void showCrashView() {
+ if (hasCrashed) {
+ LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ final View crashLayout = inflater.inflate(R.layout.browser_tab_crash, null);
+ final FrameLayout wrapper =
+ (FrameLayout) mContainer.findViewById(R.id.webview_wrapper);
+ wrapper.removeAllViewsInLayout();
+ wrapper.addView(crashLayout);
+ mContainer.requestFocus();
+ Button reloadBtn = (Button) crashLayout.findViewById(R.id.browser_crash_reload_btn);
+ reloadBtn.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View arg0) {
+ replaceCrashView(mMainView, mContainer);
+ mMainView.reload();
+ }
+ });
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // WebViewClient implementation for the main WebView
+ // -------------------------------------------------------------------------
+
+ private final WebViewClient mWebViewClient = new WebViewClient() {
+ private Message mDontResend;
+ private Message mResend;
+
+ private boolean providersDiffer(String url, String otherUrl) {
+ Uri uri1 = Uri.parse(url);
+ Uri uri2 = Uri.parse(otherUrl);
+ return !uri1.getEncodedAuthority().equals(uri2.getEncodedAuthority());
+ }
+
+ @Override
+ public void onPageStarted(WebView view, String url, Bitmap favicon) {
+ mInPageLoad = true;
+ mUpdateThumbnail = true;
+ mPageLoadProgress = INITIAL_PROGRESS;
+ mCurrentState = new PageState(mContext,
+ view.isPrivateBrowsingEnabled(), url, favicon);
+ mLoadStartTime = SystemClock.uptimeMillis();
+
+ // 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
+ // create a new one when the touch icon url is known.
+ if (mTouchIconLoader != null) {
+ mTouchIconLoader.mTab = null;
+ mTouchIconLoader = null;
+ }
+
+ // reset the error console
+ if (mErrorConsole != null) {
+ mErrorConsole.clearErrorMessages();
+ if (mWebViewController.shouldShowErrorConsole()) {
+ mErrorConsole.showConsole(ErrorConsoleView.SHOW_NONE);
+ }
+ }
+
+ // Cancel the auto-login process.
+ if (mDeviceAccountLogin != null) {
+ mDeviceAccountLogin.cancel();
+ mDeviceAccountLogin = null;
+ mWebViewController.hideAutoLogin(Tab.this);
+ }
+
+ // finally update the UI in the activity if it is in the foreground
+ mWebViewController.onPageStarted(Tab.this, view, favicon);
+
+ updateBookmarkedStatus();
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ mDisableOverrideUrlLoading = false;
+ if (!isPrivateBrowsingEnabled()) {
+ LogTag.logPageFinishedLoading(
+ url, SystemClock.uptimeMillis() - mLoadStartTime);
+ }
+ syncCurrentState(view, url);
+ mWebViewController.onPageFinished(Tab.this);
+ }
+
+ // return true if want to hijack the url to let another app to handle it
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ if (!mDisableOverrideUrlLoading && mInForeground) {
+ return mWebViewController.shouldOverrideUrlLoading(Tab.this,
+ view, url);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Updates the security state. This method is called when we discover
+ * another resource to be loaded for this page (for example,
+ * javascript). While we update the security state, we do not update
+ * the lock icon until we are done loading, as it is slightly more
+ * secure this way.
+ */
+ @Override
+ public void onLoadResource(WebView view, String url) {
+ if (url != null && url.length() > 0) {
+ // It is only if the page claims to be secure that we may have
+ // to update the security state:
+ if (mCurrentState.mSecurityState == SecurityState.SECURITY_STATE_SECURE) {
+ // If NOT a 'safe' url, change the state to mixed content!
+ if (!(URLUtil.isHttpsUrl(url) || URLUtil.isDataUrl(url)
+ || URLUtil.isAboutUrl(url))) {
+ mCurrentState.mSecurityState = SecurityState.SECURITY_STATE_MIXED;
+ }
+ }
+ }
+ }
+
+ /**
+ * Show a dialog informing the user of the network error reported by
+ * WebCore if it is in the foreground.
+ */
+ @Override
+ public void onReceivedError(WebView view, int errorCode,
+ String description, String failingUrl) {
+ if (errorCode != WebViewClient.ERROR_HOST_LOOKUP &&
+ errorCode != WebViewClient.ERROR_CONNECT &&
+ errorCode != WebViewClient.ERROR_BAD_URL &&
+ errorCode != WebViewClient.ERROR_UNSUPPORTED_SCHEME &&
+ errorCode != WebViewClient.ERROR_FILE) {
+ queueError(errorCode, description);
+
+ // Don't log URLs when in private browsing mode
+ if (!isPrivateBrowsingEnabled()) {
+ Log.e(LOGTAG, "onReceivedError " + errorCode + " " + failingUrl
+ + " " + description);
+ }
+ }
+ }
+
+ /**
+ * Check with the user if it is ok to resend POST data as the page they
+ * are trying to navigate to is the result of a POST.
+ */
+ @Override
+ public void onFormResubmission(WebView view, final Message dontResend,
+ final Message resend) {
+ if (!mInForeground) {
+ dontResend.sendToTarget();
+ return;
+ }
+ if (mDontResend != null) {
+ Log.w(LOGTAG, "onFormResubmission should not be called again "
+ + "while dialog is still up");
+ dontResend.sendToTarget();
+ return;
+ }
+ mDontResend = dontResend;
+ mResend = resend;
+ new AlertDialog.Builder(mContext).setTitle(
+ R.string.browserFrameFormResubmitLabel).setMessage(
+ R.string.browserFrameFormResubmitMessage)
+ .setPositiveButton(R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+ if (mResend != null) {
+ mResend.sendToTarget();
+ mResend = null;
+ mDontResend = null;
+ }
+ }
+ }).setNegativeButton(R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+ if (mDontResend != null) {
+ mDontResend.sendToTarget();
+ mResend = null;
+ mDontResend = null;
+ }
+ }
+ }).setOnCancelListener(new OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ if (mDontResend != null) {
+ mDontResend.sendToTarget();
+ mResend = null;
+ mDontResend = null;
+ }
+ }
+ }).show();
+ }
+
+ /**
+ * Insert the url into the visited history database.
+ * @param url The url to be inserted.
+ * @param isReload True if this url is being reloaded.
+ * FIXME: Not sure what to do when reloading the page.
+ */
+ @Override
+ public void doUpdateVisitedHistory(WebView view, String url,
+ boolean isReload) {
+ mWebViewController.doUpdateVisitedHistory(Tab.this, isReload);
+ }
+
+ /**
+ * Displays SSL error(s) dialog to the user.
+ */
+ @Override
+ public void onReceivedSslError(final WebView view,
+ final SslErrorHandler handler, final SslError error) {
+ if (!mInForeground) {
+ handler.cancel();
+ setSecurityState(SecurityState.SECURITY_STATE_NOT_SECURE);
+ return;
+ }
+ if (mSettings.showSecurityWarnings()) {
+ new AlertDialog.Builder(mContext)
+ .setTitle(R.string.security_warning)
+ .setMessage(R.string.ssl_warnings_header)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setPositiveButton(R.string.ssl_continue,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog,
+ int whichButton) {
+ handler.proceed();
+ handleProceededAfterSslError(error);
+ }
+ })
+ .setNeutralButton(R.string.view_certificate,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog,
+ int whichButton) {
+ mWebViewController.showSslCertificateOnError(
+ view, handler, error);
+ }
+ })
+ .setNegativeButton(R.string.ssl_go_back,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog,
+ int whichButton) {
+ dialog.cancel();
+ }
+ })
+ .setOnCancelListener(
+ new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ handler.cancel();
+ setSecurityState(SecurityState.SECURITY_STATE_NOT_SECURE);
+ mWebViewController.onUserCanceledSsl(Tab.this);
+ }
+ })
+ .show();
+ } else {
+ handler.proceed();
+ }
+ }
+
+ /**
+ * Called when an SSL error occurred while loading a resource, but the
+ * WebView but chose to proceed anyway based on a decision retained
+ * from a previous response to onReceivedSslError(). We update our
+ * security state to reflect this.
+ */
+ @Override
+ public void onProceededAfterSslError(WebView view, SslError error) {
+ handleProceededAfterSslError(error);
+ }
+
+ /**
+ * Displays client certificate request to the user.
+ */
+ @Override
+ public void onReceivedClientCertRequest(final WebView view,
+ final ClientCertRequestHandler handler, final String host_and_port) {
+ if (!mInForeground) {
+ handler.ignore();
+ return;
+ }
+ int colon = host_and_port.lastIndexOf(':');
+ String host;
+ int port;
+ if (colon == -1) {
+ host = host_and_port;
+ port = -1;
+ } else {
+ String portString = host_and_port.substring(colon + 1);
+ try {
+ port = Integer.parseInt(portString);
+ host = host_and_port.substring(0, colon);
+ } catch (NumberFormatException e) {
+ host = host_and_port;
+ port = -1;
+ }
+ }
+ KeyChain.choosePrivateKeyAlias(
+ mWebViewController.getActivity(), new KeyChainAliasCallback() {
+ @Override public void alias(String alias) {
+ if (alias == null) {
+ handler.cancel();
+ return;
+ }
+ new KeyChainLookup(mContext, handler, alias).execute();
+ }
+ }, null, null, host, port, null);
+ }
+
+ @Override
+ public void onRendererCrash(WebView view, boolean crashedWhileOomProtected) {
+ Log.e(LOGTAG, "Tab Crashed");
+ hasCrashed = true;
+ showCrashView();
+ }
+
+ /**
+ * Handles an HTTP authentication request.
+ *
+ * @param handler The authentication handler
+ * @param host The host
+ * @param realm The realm
+ */
+ @Override
+ public void onReceivedHttpAuthRequest(WebView view,
+ final HttpAuthHandler handler, final String host,
+ final String realm) {
+ mWebViewController.onReceivedHttpAuthRequest(Tab.this, view, handler, host, realm);
+ }
+
+ @Override
+ public WebResourceResponse shouldInterceptRequest(WebView view,
+ String url) {
+ //intercept if opening a new incognito tab - show the incognito welcome page
+ if (url.startsWith("browser:incognito")) {
+ Resources resourceHandle = mContext.getResources();
+ InputStream inStream = resourceHandle.openRawResource(
+ com.android.browser.R.raw.incognito_mode_start_page);
+ return new WebResourceResponse("text/html", "utf8", inStream);
+ }
+ WebResourceResponse res;
+ if (MyNavigationUtil.MY_NAVIGATION.equals(url)) {
+ res = MyNavigationProvider.shouldInterceptRequest(mContext, url);
+ } else {
+ res = HomeProvider.shouldInterceptRequest(mContext, url);
+ }
+ return res;
+ }
+
+ @Override
+ public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
+ if (!mInForeground) {
+ return false;
+ }
+ return mWebViewController.shouldOverrideKeyEvent(event);
+ }
+
+ @Override
+ public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
+ if (!mInForeground) {
+ return;
+ }
+ if (!mWebViewController.onUnhandledKeyEvent(event)) {
+ super.onUnhandledKeyEvent(view, event);
+ }
+ }
+
+ @Override
+ public void onReceivedLoginRequest(WebView view, String realm,
+ String account, String args) {
+ new DeviceAccountLogin(mWebViewController.getActivity(), view, Tab.this, mWebViewController)
+ .handleLogin(realm, account, args);
+ }
+
+ };
+
+ private void syncCurrentState(WebView view, String url) {
+ // Sync state (in case of stop/timeout)
+ mCurrentState.mUrl = view.getUrl();
+ if (mCurrentState.mUrl == null) {
+ mCurrentState.mUrl = "";
+ }
+ mCurrentState.mOriginalUrl = view.getOriginalUrl();
+ mCurrentState.mTitle = view.getTitle();
+ mCurrentState.mFavicon = view.getFavicon();
+ if (!URLUtil.isHttpsUrl(mCurrentState.mUrl)) {
+ // In case we stop when loading an HTTPS page from an HTTP page
+ // but before a provisional load occurred
+ mCurrentState.mSecurityState = SecurityState.SECURITY_STATE_NOT_SECURE;
+ mCurrentState.mSslCertificateError = null;
+ }
+ mCurrentState.mIncognito = view.isPrivateBrowsingEnabled();
+ }
+
+ // Called by DeviceAccountLogin when the Tab needs to have the auto-login UI
+ // displayed.
+ void setDeviceAccountLogin(DeviceAccountLogin login) {
+ mDeviceAccountLogin = login;
+ }
+
+ // Returns non-null if the title bar should display the auto-login UI.
+ DeviceAccountLogin getDeviceAccountLogin() {
+ return mDeviceAccountLogin;
+ }
+
+ // -------------------------------------------------------------------------
+ // WebChromeClient implementation for the main WebView
+ // -------------------------------------------------------------------------
+
+ private final WebChromeClient mWebChromeClient = new WebChromeClient() {
+ // Helper method to create a new tab or sub window.
+ private void createWindow(final boolean dialog, final Message msg) {
+ WebView.WebViewTransport transport =
+ (WebView.WebViewTransport) msg.obj;
+ if (dialog) {
+ createSubWindow();
+ mWebViewController.attachSubWindow(Tab.this);
+ transport.setWebView(mSubView);
+ } else {
+ final Tab newTab = mWebViewController.openTab(null,
+ Tab.this, true, true);
+ transport.setWebView(newTab.getWebView());
+ }
+ msg.sendToTarget();
+ }
+
+ @Override
+ public boolean onCreateWindow(WebView view, final boolean dialog,
+ final boolean userGesture, final Message resultMsg) {
+ // only allow new window or sub window for the foreground case
+ if (!mInForeground) {
+ return false;
+ }
+ // Short-circuit if we can't create any more tabs or sub windows.
+ if (dialog && mSubView != null) {
+ new AlertDialog.Builder(mContext)
+ .setTitle(R.string.too_many_subwindows_dialog_title)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setMessage(R.string.too_many_subwindows_dialog_message)
+ .setPositiveButton(R.string.ok, null)
+ .show();
+ return false;
+ } else if (!mWebViewController.getTabControl().canCreateNewTab()) {
+ new AlertDialog.Builder(mContext)
+ .setTitle(R.string.too_many_windows_dialog_title)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setMessage(R.string.too_many_windows_dialog_message)
+ .setPositiveButton(R.string.ok, null)
+ .show();
+ return false;
+ }
+
+ // Short-circuit if this was a user gesture.
+ if (userGesture || !mSettings.blockPopupWindows()) {
+ createWindow(dialog, resultMsg);
+ return true;
+ }
+
+ // Allow the popup and create the appropriate window.
+ final AlertDialog.OnClickListener allowListener =
+ new AlertDialog.OnClickListener() {
+ public void onClick(DialogInterface d,
+ int which) {
+ createWindow(dialog, resultMsg);
+ }
+ };
+
+ // Block the popup by returning a null WebView.
+ final AlertDialog.OnClickListener blockListener =
+ new AlertDialog.OnClickListener() {
+ public void onClick(DialogInterface d, int which) {
+ resultMsg.sendToTarget();
+ }
+ };
+
+ // Build a confirmation dialog to display to the user.
+ final AlertDialog d =
+ new AlertDialog.Builder(mContext)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setMessage(R.string.popup_window_attempt)
+ .setPositiveButton(R.string.allow, allowListener)
+ .setNegativeButton(R.string.block, blockListener)
+ .setCancelable(false)
+ .create();
+
+ // Show the confirmation dialog.
+ d.show();
+ return true;
+ }
+
+ @Override
+ public void onRequestFocus(WebView view) {
+ if (!mInForeground) {
+ mWebViewController.switchToTab(Tab.this);
+ }
+ }
+
+ @Override
+ public void onCloseWindow(WebView window) {
+ if (mParent != null) {
+ // JavaScript can only close popup window.
+ if (mInForeground) {
+ mWebViewController.switchToTab(mParent);
+ }
+ mWebViewController.closeTab(Tab.this);
+ }
+ }
+
+ @Override
+ public void onProgressChanged(WebView view, int newProgress) {
+ mPageLoadProgress = newProgress;
+ if (newProgress == 100) {
+ Log.i(CONSOLE_LOGTAG, "SWE Pageload Progress = 100");
+ mInPageLoad = false;
+ }
+ mWebViewController.onProgressChanged(Tab.this);
+ if (mUpdateThumbnail && newProgress == 100) {
+ mUpdateThumbnail = false;
+ }
+ }
+
+ @Override
+ public void onReceivedTitle(WebView view, final String title) {
+ mCurrentState.mTitle = title;
+ mWebViewController.onReceivedTitle(Tab.this, title);
+ }
+
+ @Override
+ public void onReceivedIcon(WebView view, Bitmap icon) {
+ mCurrentState.mFavicon = icon;
+ mWebViewController.onFavicon(Tab.this, view, icon);
+ }
+
+ @Override
+ public void onReceivedTouchIconUrl(WebView view, String url,
+ boolean precomposed) {
+ final ContentResolver cr = mContext.getContentResolver();
+ // Let precomposed icons take precedence over non-composed
+ // icons.
+ if (precomposed && mTouchIconLoader != null) {
+ mTouchIconLoader.cancel(false);
+ mTouchIconLoader = null;
+ }
+ // Have only one async task at a time.
+ if (mTouchIconLoader == null) {
+ mTouchIconLoader = new DownloadTouchIcon(Tab.this,
+ mContext, cr, view);
+ mTouchIconLoader.execute(url);
+ }
+ }
+
+ @Override
+ public void onShowCustomView(View view,
+ CustomViewCallback callback) {
+ Activity activity = mWebViewController.getActivity();
+ if (activity != null) {
+ onShowCustomView(view, activity.getRequestedOrientation(), callback);
+ }
+ }
+
+ @Override
+ public void onShowCustomView(View view, int requestedOrientation,
+ CustomViewCallback callback) {
+ if (mInForeground) mWebViewController.showCustomView(Tab.this, view,
+ requestedOrientation, callback);
+ }
+
+ @Override
+ public void onHideCustomView() {
+ if (mInForeground) mWebViewController.hideCustomView();
+ }
+
+ /**
+ * The origin has exceeded its database quota.
+ * @param url the URL that exceeded the quota
+ * @param databaseIdentifier the identifier of the database on which the
+ * transaction that caused the quota overflow was run
+ * @param currentQuota the current quota for the origin.
+ * @param estimatedSize the estimated size of the database.
+ * @param totalUsedQuota is the sum of all origins' quota.
+ * @param quotaUpdater The callback to run when a decision to allow or
+ * deny quota has been made. Don't forget to call this!
+ */
+ @Override
+ public void onExceededDatabaseQuota(String url,
+ String databaseIdentifier, long currentQuota, long estimatedSize,
+ long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) {
+ mSettings.getWebStorageSizeManager()
+ .onExceededDatabaseQuota(url, databaseIdentifier,
+ currentQuota, estimatedSize, totalUsedQuota,
+ quotaUpdater);
+ }
+
+ /**
+ * The Application Cache has exceeded its max size.
+ * @param spaceNeeded is the amount of disk space that would be needed
+ * in order for the last appcache operation to succeed.
+ * @param totalUsedQuota is the sum of all origins' quota.
+ * @param quotaUpdater A callback to inform the WebCore thread that a
+ * new app cache size is available. This callback must always
+ * be executed at some point to ensure that the sleeping
+ * WebCore thread is woken up.
+ */
+ @Override
+ public void onReachedMaxAppCacheSize(long spaceNeeded,
+ long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) {
+ mSettings.getWebStorageSizeManager()
+ .onReachedMaxAppCacheSize(spaceNeeded, totalUsedQuota,
+ quotaUpdater);
+ }
+
+ /**
+ * Instructs the browser to show a prompt to ask the user to set the
+ * Geolocation permission state for the specified origin.
+ * @param origin The origin for which Geolocation permissions are
+ * requested.
+ * @param callback The callback to call once the user has set the
+ * Geolocation permission state.
+ */
+ @Override
+ public void onGeolocationPermissionsShowPrompt(String origin,
+ GeolocationPermissions.Callback callback) {
+ if (mInForeground) {
+ getGeolocationPermissionsPrompt().show(origin, callback);
+ }
+ }
+
+ /**
+ * Instructs the browser to hide the Geolocation permissions prompt.
+ */
+ @Override
+ public void onGeolocationPermissionsHidePrompt() {
+ if (mInForeground && mGeolocationPermissionsPrompt != null) {
+ mGeolocationPermissionsPrompt.hide();
+ }
+ }
+
+ /* Adds a JavaScript error message to the system log and if the JS
+ * console is enabled in the about:debug options, to that console
+ * also.
+ * @param consoleMessage the message object.
+ */
+ @Override
+ public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
+ if (mInForeground) {
+ // call getErrorConsole(true) so it will create one if needed
+ ErrorConsoleView errorConsole = getErrorConsole(true);
+ errorConsole.addErrorMessage(consoleMessage);
+ 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();
+
+ switch (consoleMessage.messageLevel()) {
+ case TIP:
+ Log.v(CONSOLE_LOGTAG, message);
+ break;
+ case LOG:
+ Log.i(CONSOLE_LOGTAG, message);
+ break;
+ case WARNING:
+ Log.w(CONSOLE_LOGTAG, message);
+ break;
+ case ERROR:
+ Log.e(CONSOLE_LOGTAG, message);
+ break;
+ case DEBUG:
+ Log.d(CONSOLE_LOGTAG, message);
+ break;
+ }
+
+ return true;
+ }
+
+ /**
+ * Ask the browser for an icon to represent a <video> element.
+ * This icon will be used if the Web page did not specify a poster attribute.
+ * @return Bitmap The icon or null if no such icon is available.
+ */
+ @Override
+ public Bitmap getDefaultVideoPoster() {
+ if (mInForeground) {
+ return mWebViewController.getDefaultVideoPoster();
+ }
+ return null;
+ }
+
+ /**
+ * Ask the host application for a custom progress view to show while
+ * a <video> is loading.
+ * @return View The progress view.
+ */
+ @Override
+ public View getVideoLoadingProgressView() {
+ if (mInForeground) {
+ return mWebViewController.getVideoLoadingProgressView();
+ }
+ return null;
+ }
+
+ @Override
+ public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
+ if (mInForeground) {
+ mWebViewController.openFileChooser(uploadMsg, acceptType, capture);
+ } else {
+ uploadMsg.onReceiveValue(null);
+ }
+ }
+
+ /**
+ * Deliver a list of already-visited URLs
+ */
+ @Override
+ public void getVisitedHistory(final ValueCallback<String[]> callback) {
+ 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(mContext);
+ LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ final View layout = inflater.inflate(R.layout.setup_autofill_dialog, null);
+
+ builder.setView(layout)
+ .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ CheckBox disableAutoFill = (CheckBox) layout.findViewById(
+ R.id.setup_autofill_dialog_disable_autofill);
+
+ if (disableAutoFill.isChecked()) {
+ // Disable autofill and show a toast with how to turn it on again.
+ mSettings.setAutofillEnabled(false);
+ Toast.makeText(mContext,
+ R.string.autofill_setup_dialog_negative_toast,
+ Toast.LENGTH_LONG).show();
+ } else {
+ // 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, null)
+ .show();
+ }
+ };
+
+ // -------------------------------------------------------------------------
+ // WebViewClient implementation for the sub window
+ // -------------------------------------------------------------------------
+
+ // Subclass of WebViewClient used in subwindows to notify the main
+ // WebViewClient of certain WebView activities.
+ private static class SubWindowClient extends WebViewClient {
+ // The main WebViewClient.
+ private final WebViewClient mClient;
+ private final WebViewController mController;
+
+ SubWindowClient(WebViewClient client, WebViewController controller) {
+ mClient = client;
+ 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.
+ mController.endActionMode();
+ }
+ @Override
+ public void doUpdateVisitedHistory(WebView view, String url,
+ boolean isReload) {
+ mClient.doUpdateVisitedHistory(view, url, isReload);
+ }
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ return mClient.shouldOverrideUrlLoading(view, url);
+ }
+ @Override
+ public void onReceivedSslError(WebView view, SslErrorHandler handler,
+ SslError error) {
+ mClient.onReceivedSslError(view, handler, error);
+ }
+ @Override
+ public void onReceivedClientCertRequest(WebView view,
+ ClientCertRequestHandler handler, String host_and_port) {
+ mClient.onReceivedClientCertRequest(view, handler, host_and_port);
+ }
+ @Override
+ public void onReceivedHttpAuthRequest(WebView view,
+ HttpAuthHandler handler, String host, String realm) {
+ mClient.onReceivedHttpAuthRequest(view, handler, host, realm);
+ }
+ @Override
+ public void onFormResubmission(WebView view, Message dontResend,
+ Message resend) {
+ mClient.onFormResubmission(view, dontResend, resend);
+ }
+ @Override
+ public void onReceivedError(WebView view, int errorCode,
+ String description, String failingUrl) {
+ mClient.onReceivedError(view, errorCode, description, failingUrl);
+ }
+ @Override
+ public boolean shouldOverrideKeyEvent(WebView view,
+ android.view.KeyEvent event) {
+ return mClient.shouldOverrideKeyEvent(view, event);
+ }
+ @Override
+ public void onUnhandledKeyEvent(WebView view,
+ android.view.KeyEvent event) {
+ mClient.onUnhandledKeyEvent(view, event);
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // WebChromeClient implementation for the sub window
+ // -------------------------------------------------------------------------
+
+ private class SubWindowChromeClient extends WebChromeClient {
+ // The main WebChromeClient.
+ private final WebChromeClient mClient;
+
+ SubWindowChromeClient(WebChromeClient client) {
+ mClient = client;
+ }
+ @Override
+ public void onProgressChanged(WebView view, int newProgress) {
+ mClient.onProgressChanged(view, newProgress);
+ }
+ @Override
+ public boolean onCreateWindow(WebView view, boolean dialog,
+ boolean userGesture, android.os.Message resultMsg) {
+ return mClient.onCreateWindow(view, dialog, userGesture, resultMsg);
+ }
+ @Override
+ public void onCloseWindow(WebView window) {
+ if (window != mSubView) {
+ Log.e(LOGTAG, "Can't close the window");
+ }
+ mWebViewController.dismissSubWindow(Tab.this);
+ }
+ }
+
+ // -------------------------------------------------------------------------
+
+ // Construct a new tab
+ Tab(WebViewController wvcontroller, WebView w) {
+ this(wvcontroller, w, null);
+ }
+
+ Tab(WebViewController wvcontroller, Bundle state) {
+ this(wvcontroller, null, state);
+ }
+
+ Tab(WebViewController wvcontroller, WebView w, Bundle state) {
+ mWebViewController = wvcontroller;
+ mContext = mWebViewController.getContext();
+ mSettings = BrowserSettings.getInstance();
+ mDataController = DataController.getInstance(mContext);
+ mCurrentState = new PageState(mContext, w != null
+ ? w.isPrivateBrowsingEnabled() : false);
+ mInPageLoad = false;
+ mInForeground = false;
+
+ mDownloadListener = new BrowserDownloadListener() {
+ public void onDownloadStart(String url, String userAgent,
+ String contentDisposition, String mimetype, String referer,
+ long contentLength) {
+ mWebViewController.onDownloadStart(Tab.this, url, userAgent, contentDisposition,
+ mimetype, referer, contentLength);
+ }
+ };
+ mWebBackForwardListClient = new WebBackForwardListClient() {
+ @Override
+ public void onNewHistoryItem(WebHistoryItem item) {
+ if (mClearHistoryUrlPattern != null) {
+ boolean match =
+ mClearHistoryUrlPattern.matcher(item.getOriginalUrl()).matches();
+ if (LOGD_ENABLED) {
+ Log.d(LOGTAG, "onNewHistoryItem: match=" + match + "\n\t"
+ + item.getUrl() + "\n\t"
+ + mClearHistoryUrlPattern);
+ }
+ if (match) {
+ if (mMainView != null) {
+ mMainView.clearHistory();
+ }
+ }
+ mClearHistoryUrlPattern = null;
+ }
+ }
+ };
+
+ mCaptureWidth = mContext.getResources().getDimensionPixelSize(
+ R.dimen.tab_thumbnail_width);
+ mCaptureHeight = mContext.getResources().getDimensionPixelSize(
+ R.dimen.tab_thumbnail_height);
+ updateShouldCaptureThumbnails();
+ restoreState(state);
+ if (getId() == -1) {
+ mId = TabControl.getNextId();
+ }
+ setWebView(w);
+ mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message m) {
+ switch (m.what) {
+ case MSG_CAPTURE:
+ capture();
+ break;
+ }
+ }
+ };
+ }
+
+ public boolean shouldUpdateThumbnail() {
+ return mUpdateThumbnail;
+ }
+
+ /**
+ * This is used to get a new ID when the tab has been preloaded, before it is displayed and
+ * added to TabControl. Preloaded tabs can be created before restoreInstanceState, leading
+ * to overlapping IDs between the preloaded and restored tabs.
+ */
+ public void refreshIdAfterPreload() {
+ mId = TabControl.getNextId();
+ }
+
+ public void updateShouldCaptureThumbnails() {
+ if (mWebViewController.shouldCaptureThumbnails()) {
+ synchronized (Tab.this) {
+ if (mCapture == null) {
+ mCapture = Bitmap.createBitmap(mCaptureWidth, mCaptureHeight,
+ Bitmap.Config.RGB_565);
+ mCapture.eraseColor(Color.WHITE);
+ if (mInForeground) {
+ postCapture();
+ }
+ }
+ }
+ } else {
+ synchronized (Tab.this) {
+ mCapture = null;
+ deleteThumbnail();
+ }
+ }
+ }
+
+ public void setController(WebViewController ctl) {
+ mWebViewController = ctl;
+ updateShouldCaptureThumbnails();
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ void setWebView(WebView w) {
+ setWebView(w, true);
+ }
+
+ public boolean isNativeActive(){
+ if (mMainView == null)
+ return false;
+ return true;
+ }
+
+ public void setTimeStamp(){
+ Date d = new Date();
+ timestamp = (new Timestamp(d.getTime()));
+ }
+
+ public Timestamp getTimestamp() {
+ return timestamp;
+ }
+ /**
+ * Sets the WebView for this tab, correctly removing the old WebView from
+ * the container view.
+ */
+ void setWebView(WebView w, boolean restore) {
+ if (mMainView == w) {
+ return;
+ }
+
+ // If the WebView is changing, the page will be reloaded, so any ongoing
+ // Geolocation permission requests are void.
+ if (mGeolocationPermissionsPrompt != null) {
+ mGeolocationPermissionsPrompt.hide();
+ }
+
+ mWebViewController.onSetWebView(this, w);
+
+ if (mMainView != null) {
+ mMainView.setPictureListener(null);
+ if (w != null) {
+ syncCurrentState(w, null);
+ } else {
+ mCurrentState = new PageState(mContext, mMainView.isPrivateBrowsingEnabled());
+ }
+ }
+ // set the new one
+ mMainView = w;
+ // attach the WebViewClient, WebChromeClient and DownloadListener
+ if (mMainView != null) {
+ mMainView.setWebViewClient(mWebViewClient);
+ mMainView.setWebChromeClient(mWebChromeClient);
+ // Attach DownloadManager so that downloads can start in an active
+ // or a non-active window. This can happen when going to a site that
+ // does a redirect after a period of time. The user could have
+ // switched to another tab while waiting for the download to start.
+ mMainView.setDownloadListener(mDownloadListener);
+ getWebView().setWebBackForwardListClient(mWebBackForwardListClient);
+ TabControl tc = mWebViewController.getTabControl();
+ if (tc != null /*&& tc.getOnThumbnailUpdatedListener() != null*/) {
+ mMainView.setPictureListener(this);
+ }
+ if (restore && (mSavedState != null)) {
+ restoreUserAgent();
+ WebBackForwardList restoredState
+ = mMainView.restoreState(mSavedState);
+ if (restoredState == null || restoredState.getSize() == 0) {
+ Log.w(LOGTAG, "Failed to restore WebView state!");
+ loadUrl(mCurrentState.mOriginalUrl, null);
+ }
+ mSavedState = null;
+ }
+ }
+ }
+
+ /**
+ * Destroy the tab's main WebView and subWindow if any
+ */
+ void destroy() {
+ if (mMainView != null) {
+ dismissSubWindow();
+ // save the WebView to call destroy() after detach it from the tab
+ WebView webView = mMainView;
+ setWebView(null);
+ webView.destroy();
+ }
+ }
+
+ /**
+ * Remove the tab from the parent
+ */
+ void removeFromTree() {
+ // detach the children
+ if (mChildren != null) {
+ for(Tab t : mChildren) {
+ t.setParent(null);
+ }
+ }
+ // remove itself from the parent list
+ if (mParent != null) {
+ mParent.mChildren.remove(this);
+ }
+ deleteThumbnail();
+ }
+
+ /**
+ * Create a new subwindow unless a subwindow already exists.
+ * @return True if a new subwindow was created. False if one already exists.
+ */
+ boolean createSubWindow() {
+ if (mSubView == null) {
+ mWebViewController.createSubWindow(this);
+ mSubView.setWebViewClient(new SubWindowClient(mWebViewClient,
+ mWebViewController));
+ mSubView.setWebChromeClient(new SubWindowChromeClient(
+ mWebChromeClient));
+ // Set a different DownloadListener for the mSubView, since it will
+ // just need to dismiss the mSubView, rather than close the Tab
+ mSubView.setDownloadListener(new BrowserDownloadListener() {
+ public void onDownloadStart(String url, String userAgent,
+ String contentDisposition, String mimetype, String referer,
+ long contentLength) {
+ mWebViewController.onDownloadStart(Tab.this, url, userAgent,
+ contentDisposition, mimetype, referer, contentLength);
+ if (mSubView.copyBackForwardList().getSize() == 0) {
+ // This subwindow was opened for the sole purpose of
+ // downloading a file. Remove it.
+ mWebViewController.dismissSubWindow(Tab.this);
+ }
+ }
+ });
+ mSubView.setOnCreateContextMenuListener(mWebViewController.getActivity());
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Dismiss the subWindow for the tab.
+ */
+ void dismissSubWindow() {
+ if (mSubView != null) {
+ mWebViewController.endActionMode();
+ mSubView.destroy();
+ mSubView = null;
+ mSubViewContainer = null;
+ }
+ }
+
+
+ /**
+ * Set the parent tab of this tab.
+ */
+ void setParent(Tab parent) {
+ if (parent == this) {
+ throw new IllegalStateException("Cannot set parent to self!");
+ }
+ mParent = parent;
+ // This tab may have been freed due to low memory. If that is the case,
+ // the parent tab id is already saved. If we are changing that id
+ // (most likely due to removing the parent tab) we must update the
+ // parent tab id in the saved Bundle.
+ if (mSavedState != null) {
+ if (parent == null) {
+ mSavedState.remove(PARENTTAB);
+ } else {
+ mSavedState.putLong(PARENTTAB, parent.getId());
+ }
+ }
+
+ // Sync the WebView useragent with the parent
+ if (parent != null && mSettings.hasDesktopUseragent(parent.getWebView())
+ != mSettings.hasDesktopUseragent(getWebView())) {
+ mSettings.toggleDesktopUseragent(getWebView());
+ }
+
+ if (parent != null && parent.getId() == getId()) {
+ throw new IllegalStateException("Parent has same ID as child!");
+ }
+ }
+
+ /**
+ * If this Tab was created through another Tab, then this method returns
+ * that Tab.
+ * @return the Tab parent or null
+ */
+ public Tab getParent() {
+ return mParent;
+ }
+
+ /**
+ * When a Tab is created through the content of another Tab, then we
+ * associate the Tabs.
+ * @param child the Tab that was created from this Tab
+ */
+ void addChildTab(Tab child) {
+ if (mChildren == null) {
+ mChildren = new Vector<Tab>();
+ }
+ mChildren.add(child);
+ child.setParent(this);
+ }
+
+ Vector<Tab> getChildren() {
+ return mChildren;
+ }
+
+ void resume() {
+ if (mMainView != null) {
+ setupHwAcceleration(mMainView);
+ mMainView.onResume();
+ if (mSubView != null) {
+ mSubView.onResume();
+ }
+ }
+ }
+
+ private void setupHwAcceleration(View web) {
+ if (web == null) return;
+ BrowserSettings settings = BrowserSettings.getInstance();
+ if (settings.isHardwareAccelerated()) {
+ web.setLayerType(View.LAYER_TYPE_NONE, null);
+ } else {
+ web.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+ }
+ }
+
+ void pause() {
+ if (mMainView != null) {
+ mMainView.onPause();
+ if (mSubView != null) {
+ mSubView.onPause();
+ }
+ }
+ }
+
+ void putInForeground() {
+ if (mInForeground) {
+ return;
+ }
+ mInForeground = true;
+ resume();
+ Activity activity = mWebViewController.getActivity();
+ mMainView.setOnCreateContextMenuListener(activity);
+ if (mSubView != null) {
+ mSubView.setOnCreateContextMenuListener(activity);
+ }
+ // Show the pending error dialog if the queue is not empty
+ if (mQueuedErrors != null && mQueuedErrors.size() > 0) {
+ showError(mQueuedErrors.getFirst());
+ }
+ mWebViewController.bookmarkedStatusHasChanged(this);
+ }
+
+ void putInBackground() {
+ if (!mInForeground) {
+ return;
+ }
+ capture();
+ mInForeground = false;
+ pause();
+ mMainView.setOnCreateContextMenuListener(null);
+ if (mSubView != null) {
+ mSubView.setOnCreateContextMenuListener(null);
+ }
+ }
+
+ boolean inForeground() {
+ return mInForeground;
+ }
+
+ /**
+ * Return the top window of this tab; either the subwindow if it is not
+ * null or the main window.
+ * @return The top window of this tab.
+ */
+ WebView getTopWindow() {
+ if (mSubView != null) {
+ return mSubView;
+ }
+ return mMainView;
+ }
+
+ /**
+ * Return the main window of this tab. Note: if a tab is freed in the
+ * background, this can return null. It is only guaranteed to be
+ * non-null for the current tab.
+ * @return The main WebView of this tab.
+ */
+ WebView getWebView() {
+ return mMainView;
+ }
+
+ void setViewContainer(View container) {
+ mContainer = container;
+ }
+
+ 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() {
+ return mCurrentState.mIncognito;
+ }
+
+ /**
+ * Return the subwindow of this tab or null if there is no subwindow.
+ * @return The subwindow of this tab or null.
+ */
+ WebView getSubWebView() {
+ 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.
+ */
+ GeolocationPermissionsPrompt getGeolocationPermissionsPrompt() {
+ if (mGeolocationPermissionsPrompt == null) {
+ ViewStub stub = (ViewStub) mContainer
+ .findViewById(R.id.geolocation_permissions_prompt);
+ mGeolocationPermissionsPrompt = (GeolocationPermissionsPrompt) stub
+ .inflate();
+ }
+ return mGeolocationPermissionsPrompt;
+ }
+
+ /**
+ * @return The application id string
+ */
+ String getAppId() {
+ return mAppId;
+ }
+
+ /**
+ * Set the application id string
+ * @param id
+ */
+ void setAppId(String id) {
+ mAppId = id;
+ }
+
+ boolean closeOnBack() {
+ return mCloseOnBack;
+ }
+
+ void setCloseOnBack(boolean close) {
+ mCloseOnBack = close;
+ }
+
+ String getUrl() {
+ return UrlUtils.filteredUrl(mCurrentState.mUrl);
+ }
+
+ String getOriginalUrl() {
+ if (mCurrentState.mOriginalUrl == null) {
+ return getUrl();
+ }
+ return UrlUtils.filteredUrl(mCurrentState.mOriginalUrl);
+ }
+
+ /**
+ * Get the title of this tab.
+ */
+ String getTitle() {
+ if (mCurrentState.mTitle == null && mInPageLoad) {
+ return mContext.getString(R.string.title_bar_loading);
+ }
+ return mCurrentState.mTitle;
+ }
+
+ /**
+ * Get the favicon of this tab.
+ */
+ Bitmap getFavicon() {
+ if (mCurrentState.mFavicon != null) {
+ return mCurrentState.mFavicon;
+ }
+ return getDefaultFavicon(mContext);
+ }
+
+ public boolean isBookmarkedSite() {
+ return mCurrentState.mIsBookmarkedSite;
+ }
+
+ /**
+ * Return the tab's error console. Creates the console if createIfNEcessary
+ * is true and we haven't already created the console.
+ * @param createIfNecessary Flag to indicate if the console should be
+ * created if it has not been already.
+ * @return The tab's error console, or null if one has not been created and
+ * createIfNecessary is false.
+ */
+ ErrorConsoleView getErrorConsole(boolean createIfNecessary) {
+ if (createIfNecessary && mErrorConsole == null) {
+ mErrorConsole = new ErrorConsoleView(mContext);
+ mErrorConsole.setWebView(mMainView);
+ }
+ return mErrorConsole;
+ }
+
+ /**
+ * Sets the security state, clears the SSL certificate error and informs
+ * the controller.
+ */
+ private void setSecurityState(SecurityState securityState) {
+ mCurrentState.mSecurityState = securityState;
+ mCurrentState.mSslCertificateError = null;
+ mWebViewController.onUpdatedSecurityState(this);
+ }
+
+ /**
+ * @return The tab's security state.
+ */
+ SecurityState getSecurityState() {
+ return mCurrentState.mSecurityState;
+ }
+
+ /**
+ * Gets the SSL certificate error, if any, for the page's main resource.
+ * This is only non-null when the security state is
+ * SECURITY_STATE_BAD_CERTIFICATE.
+ */
+ SslError getSslCertificateError() {
+ return mCurrentState.mSslCertificateError;
+ }
+
+ int getLoadProgress() {
+ if (mInPageLoad) {
+ return mPageLoadProgress;
+ }
+ return 100;
+ }
+
+ /**
+ * @return TRUE if onPageStarted is called while onPageFinished is not
+ * called yet.
+ */
+ boolean inPageLoad() {
+ return mInPageLoad;
+ }
+
+ /**
+ * @return The Bundle with the tab's state if it can be saved, otherwise null
+ */
+ public Bundle saveState() {
+ // If the WebView is null it means we ran low on memory and we already
+ // stored the saved state in mSavedState.
+ if (mMainView == null) {
+ return mSavedState;
+ }
+
+ if (TextUtils.isEmpty(mCurrentState.mUrl)) {
+ return null;
+ }
+
+ mSavedState = new Bundle();
+ WebBackForwardList savedList = mMainView.saveState(mSavedState);
+ if (savedList == null || savedList.getSize() == 0) {
+ Log.w(LOGTAG, "Failed to save back/forward list for "
+ + mCurrentState.mUrl);
+ }
+
+ mSavedState.putLong(ID, mId);
+ mSavedState.putString(CURRURL, mCurrentState.mUrl);
+ mSavedState.putString(CURRTITLE, mCurrentState.mTitle);
+ mSavedState.putBoolean(INCOGNITO, mMainView.isPrivateBrowsingEnabled());
+ if (mAppId != null) {
+ mSavedState.putString(APPID, mAppId);
+ }
+ mSavedState.putBoolean(CLOSEFLAG, mCloseOnBack);
+ // Remember the parent tab so the relationship can be restored.
+ if (mParent != null) {
+ mSavedState.putLong(PARENTTAB, mParent.mId);
+ }
+ mSavedState.putBoolean(USERAGENT,
+ mSettings.hasDesktopUseragent(getWebView()));
+ return mSavedState;
+ }
+
+ /*
+ * Restore the state of the tab.
+ */
+ private void restoreState(Bundle b) {
+ mSavedState = b;
+ if (mSavedState == null) {
+ return;
+ }
+ // Restore the internal state even if the WebView fails to restore.
+ // This will maintain the app id, original url and close-on-exit values.
+ mId = b.getLong(ID);
+ mAppId = b.getString(APPID);
+ mCloseOnBack = b.getBoolean(CLOSEFLAG);
+ restoreUserAgent();
+ String url = b.getString(CURRURL);
+ String title = b.getString(CURRTITLE);
+ boolean incognito = b.getBoolean(INCOGNITO);
+ mCurrentState = new PageState(mContext, incognito, url, null);
+ mCurrentState.mTitle = title;
+ synchronized (Tab.this) {
+ if (mCapture != null) {
+ DataController.getInstance(mContext).loadThumbnail(this);
+ }
+ }
+ }
+
+ private void restoreUserAgent() {
+ if (mMainView == null || mSavedState == null) {
+ return;
+ }
+ if (mSavedState.getBoolean(USERAGENT)
+ != mSettings.hasDesktopUseragent(mMainView)) {
+ mSettings.toggleDesktopUseragent(mMainView);
+ }
+ }
+
+ public void updateBookmarkedStatus() {
+ mDataController.queryBookmarkStatus(getUrl(), mIsBookmarkCallback);
+ }
+
+ private DataController.OnQueryUrlIsBookmark mIsBookmarkCallback
+ = new DataController.OnQueryUrlIsBookmark() {
+ @Override
+ public void onQueryUrlIsBookmark(String url, boolean isBookmark) {
+ if (mCurrentState.mUrl.equals(url)) {
+ mCurrentState.mIsBookmarkedSite = isBookmark;
+ mWebViewController.bookmarkedStatusHasChanged(Tab.this);
+ }
+ }
+ };
+
+ public Bitmap getScreenshot() {
+ synchronized (Tab.this) {
+ return mCapture;
+ }
+ }
+
+ public boolean isSnapshot() {
+ return false;
+ }
+
+ private static class SaveCallback implements ValueCallback<String> {
+ boolean onReceiveValueCalled = false;
+ private String mPath;
+
+ @Override
+ public void onReceiveValue(String path) {
+ this.onReceiveValueCalled = true;
+ this.mPath = path;
+ synchronized (this) {
+ notifyAll();
+ }
+ }
+
+ public String getPath() {
+ return mPath;
+ }
+ }
+
+ /**
+ * Must be called on the UI thread
+ */
+ public ContentValues createSnapshotValues() {
+ WebView web = getWebView();
+ if (web == null) return null;
+ ContentValues values = new ContentValues();
+ values.put(Snapshots.TITLE, mCurrentState.mTitle);
+ values.put(Snapshots.URL, mCurrentState.mUrl);
+ values.put(Snapshots.BACKGROUND, web.getPageBackgroundColor());
+ values.put(Snapshots.DATE_CREATED, System.currentTimeMillis());
+ values.put(Snapshots.FAVICON, compressBitmap(getFavicon()));
+ Bitmap screenshot = web.getViewportBitmap();
+ values.put(Snapshots.THUMBNAIL, compressBitmap(screenshot));
+ return values;
+ }
+
+ /**
+ * Probably want to call this on a background thread
+ */
+ public boolean saveViewState(ContentValues values) {
+ WebView web = getWebView();
+ if (web == null) return false;
+ String filename = UUID.randomUUID().toString();
+ SaveCallback callback = new SaveCallback();
+ try {
+ synchronized (callback) {
+ web.saveViewState(filename, callback);
+ callback.wait();
+ }
+ } catch (Exception e) {
+ Log.w(LOGTAG, "Failed to save view state", e);
+ String path = callback.getPath();
+ if (path != null) {
+ File file = mContext.getFileStreamPath(path);
+ if (file.exists() && !file.delete()) {
+ file.deleteOnExit();
+ }
+ }
+ return false;
+ }
+ String path = callback.getPath();
+ File savedFile = new File(path);
+ if (!savedFile.exists()) {
+ return false;
+ }
+ values.put(Snapshots.VIEWSTATE_PATH, path.substring(path.lastIndexOf('/') + 1));
+ values.put(Snapshots.VIEWSTATE_SIZE, savedFile.length());
+ return true;
+ }
+
+ public byte[] compressBitmap(Bitmap bitmap) {
+ if (bitmap == null) {
+ return null;
+ }
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ bitmap.compress(CompressFormat.PNG, 100, stream);
+ return stream.toByteArray();
+ }
+
+ public void loadUrl(String url, Map<String, String> headers) {
+ if (mMainView != null) {
+ mPageLoadProgress = INITIAL_PROGRESS;
+ mInPageLoad = true;
+ mCurrentState = new PageState(mContext, false, url, null);
+ mWebViewController.onPageStarted(this, mMainView, null);
+ mMainView.loadUrl(url, headers);
+ }
+ }
+
+ public void disableUrlOverridingForLoad() {
+ mDisableOverrideUrlLoading = true;
+ }
+
+ protected void capture() {
+ if (mMainView == null || mCapture == null) return;
+ if (mMainView.getContentWidth() <= 0 || mMainView.getContentHeight() <= 0) {
+ return;
+ }
+ Canvas c = new Canvas(mCapture);
+ int state = c.save();
+ Bitmap screenShot = mMainView.getViewportBitmap();
+ if (screenShot != null) {
+ mCapture.eraseColor(Color.WHITE);
+ float scale = (float) mCaptureWidth / screenShot.getWidth();
+ c.scale(scale, scale);
+ c.drawBitmap(screenShot, 0, 0, null);
+ } else {
+ final int left = mMainView.getViewScrollX();
+ final int top = mMainView.getViewScrollY() + mMainView.getVisibleTitleHeight();
+ c.translate(-left, -top);
+ float scale = mCaptureWidth / (float) mMainView.getWidth();
+ c.scale(scale, scale, left, top);
+ if (mMainView instanceof BrowserWebView) {
+ ((BrowserWebView)mMainView).drawContent(c);
+ } else {
+ mMainView.draw(c);
+ }
+ }
+ c.restoreToCount(state);
+ // manually anti-alias the edges for the tilt
+ c.drawRect(0, 0, 1, mCapture.getHeight(), sAlphaPaint);
+ c.drawRect(mCapture.getWidth() - 1, 0, mCapture.getWidth(),
+ mCapture.getHeight(), sAlphaPaint);
+ c.drawRect(0, 0, mCapture.getWidth(), 1, sAlphaPaint);
+ c.drawRect(0, mCapture.getHeight() - 1, mCapture.getWidth(),
+ mCapture.getHeight(), sAlphaPaint);
+ c.setBitmap(null);
+ mHandler.removeMessages(MSG_CAPTURE);
+ persistThumbnail();
+ TabControl tc = mWebViewController.getTabControl();
+ if (tc != null) {
+ OnThumbnailUpdatedListener updateListener
+ = tc.getOnThumbnailUpdatedListener();
+ if (updateListener != null) {
+ updateListener.onThumbnailUpdated(this);
+ }
+ }
+ }
+
+ @Override
+ public void onNewPicture(WebView view, Picture picture) {
+ postCapture();
+ }
+
+ private void postCapture() {
+ if (!mHandler.hasMessages(MSG_CAPTURE)) {
+ mHandler.sendEmptyMessageDelayed(MSG_CAPTURE, CAPTURE_DELAY);
+ }
+ }
+
+ public boolean canGoBack() {
+ return mMainView != null ? mMainView.canGoBack() : false;
+ }
+
+ public boolean canGoForward() {
+ return mMainView != null ? mMainView.canGoForward() : false;
+ }
+
+ public void goBack() {
+ if (mMainView != null) {
+ mMainView.goBack();
+ }
+ }
+
+ public void goForward() {
+ if (mMainView != null) {
+ mMainView.goForward();
+ }
+ }
+
+ /**
+ * Causes the tab back/forward stack to be cleared once, if the given URL is the next URL
+ * to be added to the stack.
+ *
+ * This is used to ensure that preloaded URLs that are not subsequently seen by the user do
+ * not appear in the back stack.
+ */
+ public void clearBackStackWhenItemAdded(Pattern urlPattern) {
+ mClearHistoryUrlPattern = urlPattern;
+ }
+
+ protected void persistThumbnail() {
+ DataController.getInstance(mContext).saveThumbnail(this);
+ }
+
+ protected void deleteThumbnail() {
+ DataController.getInstance(mContext).deleteThumbnail(this);
+ }
+
+ void updateCaptureFromBlob(byte[] blob) {
+ synchronized (Tab.this) {
+ if (mCapture == null) {
+ return;
+ }
+ ByteBuffer buffer = ByteBuffer.wrap(blob);
+ try {
+ mCapture.copyPixelsFromBuffer(buffer);
+ } catch (RuntimeException rex) {
+ Log.e(LOGTAG, "Load capture has mismatched sizes; buffer: "
+ + buffer.capacity() + " blob: " + blob.length
+ + "capture: " + mCapture.getByteCount());
+ throw rex;
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder(100);
+ builder.append(mId);
+ builder.append(") has parent: ");
+ if (getParent() != null) {
+ builder.append("true[");
+ builder.append(getParent().getId());
+ builder.append("]");
+ } else {
+ builder.append("false");
+ }
+ builder.append(", incog: ");
+ builder.append(isPrivateBrowsingEnabled());
+ if (!isPrivateBrowsingEnabled()) {
+ builder.append(", title: ");
+ builder.append(getTitle());
+ builder.append(", url: ");
+ builder.append(getUrl());
+ }
+ return builder.toString();
+ }
+
+ private void handleProceededAfterSslError(SslError error) {
+ if (error.getUrl().equals(mCurrentState.mUrl)) {
+ // The security state should currently be SECURITY_STATE_SECURE.
+ setSecurityState(SecurityState.SECURITY_STATE_BAD_CERTIFICATE);
+ mCurrentState.mSslCertificateError = error;
+ } else if (getSecurityState() == SecurityState.SECURITY_STATE_SECURE) {
+ // The page's main resource is secure and this error is for a
+ // sub-resource.
+ setSecurityState(SecurityState.SECURITY_STATE_MIXED);
+ }
+ }
+}
diff --git a/src/com/android/browser/TabBar.java b/src/com/android/browser/TabBar.java
new file mode 100644
index 0000000..4078ba4
--- /dev/null
+++ b/src/com/android/browser/TabBar.java
@@ -0,0 +1,536 @@
+/*
+ * 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.AnimatorSet;
+import android.animation.ObjectAnimator;
+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.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Shader;
+import android.graphics.drawable.Drawable;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+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.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.android.browser.R;
+
+/**
+ * tabbed title bar for xlarge screen browser
+ */
+public class TabBar extends LinearLayout implements OnClickListener {
+
+ private static final int PROGRESS_MAX = 100;
+
+ private Activity mActivity;
+ private UiController mUiController;
+ private TabControl mTabControl;
+ private XLargeUi mUi;
+
+ private int mTabWidth;
+
+ private TabScrollView mTabs;
+
+ private ImageButton mNewTab;
+ private int mButtonWidth;
+
+ private Map<Tab, TabView> mTabMap;
+
+ private int mCurrentTextureWidth = 0;
+ private int mCurrentTextureHeight = 0;
+
+ private Drawable mActiveDrawable;
+ private Drawable mInactiveDrawable;
+
+ private final Paint mActiveShaderPaint = new Paint();
+ private final Paint mInactiveShaderPaint = new Paint();
+ private final Paint mFocusPaint = new Paint();
+ private final Matrix mActiveMatrix = new Matrix();
+ private final Matrix mInactiveMatrix = new Matrix();
+
+ private BitmapShader mActiveShader;
+ private BitmapShader mInactiveShader;
+
+ private int mTabOverlap;
+ private int mAddTabOverlap;
+ private int mTabSliceWidth;
+ private boolean mUseQuickControls;
+
+ public TabBar(Activity activity, UiController controller, XLargeUi ui) {
+ super(activity);
+ mActivity = activity;
+ mUiController = controller;
+ mTabControl = mUiController.getTabControl();
+ mUi = ui;
+ Resources res = activity.getResources();
+ mTabWidth = (int) res.getDimension(R.dimen.tab_width);
+ mActiveDrawable = res.getDrawable(R.drawable.bg_urlbar);
+ mInactiveDrawable = res.getDrawable(R.drawable.browsertab_inactive);
+
+ mTabMap = new HashMap<Tab, TabView>();
+ LayoutInflater factory = LayoutInflater.from(activity);
+ factory.inflate(R.layout.tab_bar, this);
+ setPadding(0, (int) res.getDimension(R.dimen.tab_padding_top), 0, 0);
+ mTabs = (TabScrollView) findViewById(R.id.tabs);
+ mNewTab = (ImageButton) findViewById(R.id.newtab);
+ mNewTab.setOnClickListener(this);
+
+ updateTabs(mUiController.getTabs());
+ mButtonWidth = -1;
+ // tab dimensions
+ mTabOverlap = (int) res.getDimension(R.dimen.tab_overlap);
+ mAddTabOverlap = (int) res.getDimension(R.dimen.tab_addoverlap);
+ mTabSliceWidth = (int) res.getDimension(R.dimen.tab_slice);
+
+ mActiveShaderPaint.setStyle(Paint.Style.FILL);
+ mActiveShaderPaint.setAntiAlias(true);
+
+ mInactiveShaderPaint.setStyle(Paint.Style.FILL);
+ mInactiveShaderPaint.setAntiAlias(true);
+
+ mFocusPaint.setStyle(Paint.Style.STROKE);
+ mFocusPaint.setStrokeWidth(res.getDimension(R.dimen.tab_focus_stroke));
+ mFocusPaint.setAntiAlias(true);
+ mFocusPaint.setColor(res.getColor(R.color.tabFocusHighlight));
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration config) {
+ super.onConfigurationChanged(config);
+ Resources res = mActivity.getResources();
+ mTabWidth = (int) res.getDimension(R.dimen.tab_width);
+ // force update of tab bar
+ mTabs.updateLayout();
+ }
+
+ void setUseQuickControls(boolean useQuickControls) {
+ mUseQuickControls = useQuickControls;
+ mNewTab.setVisibility(mUseQuickControls ? View.GONE
+ : View.VISIBLE);
+ }
+
+ int getTabCount() {
+ return mTabMap.size();
+ }
+
+ void updateTabs(List<Tab> tabs) {
+ mTabs.clearTabs();
+ mTabMap.clear();
+ for (Tab tab : tabs) {
+ TabView tv = buildTabView(tab);
+ mTabs.addTab(tv);
+ }
+ mTabs.setSelectedTab(mTabControl.getCurrentPosition());
+ }
+
+ @Override
+ protected void onMeasure(int hspec, int vspec) {
+ super.onMeasure(hspec, vspec);
+ int w = getMeasuredWidth();
+ // adjust for new tab overlap
+ if (!mUseQuickControls) {
+ w -= mAddTabOverlap;
+ }
+ setMeasuredDimension(w, getMeasuredHeight());
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ // use paddingLeft and paddingTop
+ int pl = getPaddingLeft();
+ int pt = getPaddingTop();
+ int sw = mTabs.getMeasuredWidth();
+ int w = right - left - pl;
+ if (mUseQuickControls) {
+ mButtonWidth = 0;
+ } else {
+ mButtonWidth = mNewTab.getMeasuredWidth() - mAddTabOverlap;
+ if (w-sw < mButtonWidth) {
+ sw = w - mButtonWidth;
+ }
+ }
+ mTabs.layout(pl, pt, pl + sw, bottom - top);
+ // adjust for overlap
+ if (!mUseQuickControls) {
+ mNewTab.layout(pl + sw - mAddTabOverlap, pt,
+ pl + sw + mButtonWidth - mAddTabOverlap, bottom - top);
+ }
+ }
+
+ public void onClick(View view) {
+ if (mNewTab == view) {
+ mUiController.openTabToHomePage();
+ } else if (mTabs.getSelectedTab() == view) {
+ if (mUseQuickControls) {
+ if (mUi.isTitleBarShowing() && !isLoading()) {
+ mUi.stopEditingUrl();
+ mUi.hideTitleBar();
+ } else {
+ mUi.stopWebViewScrolling();
+ mUi.editUrl(false, false);
+ }
+ } else if (mUi.isTitleBarShowing() && !isLoading()) {
+ mUi.stopEditingUrl();
+ mUi.hideTitleBar();
+ } else {
+ showUrlBar();
+ }
+ } else if (view instanceof TabView) {
+ final Tab tab = ((TabView) view).mTab;
+ int ix = mTabs.getChildIndex(view);
+ if (ix >= 0) {
+ mTabs.setSelectedTab(ix);
+ mUiController.switchToTab(tab);
+ }
+ }
+ }
+
+ private void showUrlBar() {
+ mUi.stopWebViewScrolling();
+ mUi.showTitleBar();
+ }
+
+ private TabView buildTabView(Tab tab) {
+ TabView tabview = new TabView(mActivity, tab);
+ mTabMap.put(tab, tabview);
+ tabview.setOnClickListener(this);
+ return tabview;
+ }
+
+ private static Bitmap getDrawableAsBitmap(Drawable drawable, int width, int height) {
+ Bitmap b = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas c = new Canvas(b);
+ drawable.setBounds(0, 0, width, height);
+ drawable.draw(c);
+ c.setBitmap(null);
+ return b;
+ }
+
+ /**
+ * View used in the tab bar
+ */
+ class TabView extends LinearLayout implements OnClickListener {
+
+ Tab mTab;
+ View mTabContent;
+ TextView mTitle;
+ View mIncognito;
+ View mSnapshot;
+ ImageView mIconView;
+ ImageView mLock;
+ ImageView mClose;
+ boolean mSelected;
+ Path mPath;
+ Path mFocusPath;
+ int[] mWindowPos;
+
+ /**
+ * @param context
+ */
+ public TabView(Context context, Tab tab) {
+ super(context);
+ setWillNotDraw(false);
+ mPath = new Path();
+ mFocusPath = new Path();
+ mWindowPos = new int[2];
+ mTab = tab;
+ setGravity(Gravity.CENTER_VERTICAL);
+ setOrientation(LinearLayout.HORIZONTAL);
+ setPadding(mTabOverlap, 0, mTabSliceWidth, 0);
+ 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);
+ mSnapshot = mTabContent.findViewById(R.id.snapshot);
+ mSelected = false;
+ // update the status
+ updateFromTab();
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == mClose) {
+ closeTab();
+ }
+ }
+
+ private void updateFromTab() {
+ String displayTitle = mTab.getTitle();
+ if (displayTitle == null) {
+ displayTitle = mTab.getUrl();
+ }
+ setDisplayTitle(displayTitle);
+ if (mTab.getFavicon() != null) {
+ setFavicon(mUi.getFaviconDrawable(mTab.getFavicon()));
+ }
+ updateTabIcons();
+ }
+
+ private void updateTabIcons() {
+ mIncognito.setVisibility(
+ mTab.isPrivateBrowsingEnabled() ?
+ View.VISIBLE : View.GONE);
+ mSnapshot.setVisibility(mTab.isSnapshot()
+ ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public void setActivated(boolean selected) {
+ mSelected = selected;
+ mClose.setVisibility(mSelected ? View.VISIBLE : View.GONE);
+ mIconView.setVisibility(mSelected ? View.GONE : View.VISIBLE);
+ mTitle.setTextAppearance(mActivity, mSelected ?
+ R.style.TabTitleSelected : R.style.TabTitleUnselected);
+ setHorizontalFadingEdgeEnabled(!mSelected);
+ super.setActivated(selected);
+ updateLayoutParams();
+ setFocusable(!selected);
+ postInvalidate();
+ }
+
+ public void updateLayoutParams() {
+ LayoutParams lp = (LinearLayout.LayoutParams) getLayoutParams();
+ lp.width = mTabWidth;
+ lp.height = LayoutParams.MATCH_PARENT;
+ setLayoutParams(lp);
+ }
+
+ 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);
+ }
+ }
+
+ private void closeTab() {
+ if (mTab == mTabControl.getCurrentTab()) {
+ mUiController.closeCurrentTab();
+ } else {
+ mUiController.closeTab(mTab);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+ setTabPath(mPath, 0, 0, r - l, b - t);
+ setFocusPath(mFocusPath, 0, 0, r - l, b - t);
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ if (mCurrentTextureWidth != mUi.getContentWidth() ||
+ mCurrentTextureHeight != getHeight()) {
+ mCurrentTextureWidth = mUi.getContentWidth();
+ mCurrentTextureHeight = getHeight();
+
+ if (mCurrentTextureWidth > 0 && mCurrentTextureHeight > 0) {
+ Bitmap activeTexture = getDrawableAsBitmap(mActiveDrawable,
+ mCurrentTextureWidth, mCurrentTextureHeight);
+ Bitmap inactiveTexture = getDrawableAsBitmap(mInactiveDrawable,
+ mCurrentTextureWidth, mCurrentTextureHeight);
+
+ mActiveShader = new BitmapShader(activeTexture,
+ Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
+ mActiveShaderPaint.setShader(mActiveShader);
+
+ mInactiveShader = new BitmapShader(inactiveTexture,
+ Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
+ mInactiveShaderPaint.setShader(mInactiveShader);
+ }
+ }
+ // add some monkey protection
+ if ((mActiveShader != null) && (mInactiveShader != null)) {
+ int state = canvas.save();
+ getLocationInWindow(mWindowPos);
+ Paint paint = mSelected ? mActiveShaderPaint : mInactiveShaderPaint;
+ drawClipped(canvas, paint, mPath, mWindowPos[0]);
+ canvas.restoreToCount(state);
+ }
+ super.dispatchDraw(canvas);
+ }
+
+ private void drawClipped(Canvas canvas, Paint paint, Path clipPath, int left) {
+ // TODO: We should change the matrix/shader only when needed
+ final Matrix matrix = mSelected ? mActiveMatrix : mInactiveMatrix;
+ matrix.setTranslate(-left, 0.0f);
+ (mSelected ? mActiveShader : mInactiveShader).setLocalMatrix(matrix);
+ canvas.drawPath(clipPath, paint);
+ if (isFocused()) {
+ canvas.drawPath(mFocusPath, mFocusPaint);
+ }
+ }
+
+ private void setTabPath(Path path, int l, int t, int r, int b) {
+ path.reset();
+ path.moveTo(l, b);
+ path.lineTo(l, t);
+ path.lineTo(r - mTabSliceWidth, t);
+ path.lineTo(r, b);
+ path.close();
+ }
+
+ private void setFocusPath(Path path, int l, int t, int r, int b) {
+ path.reset();
+ path.moveTo(l, b);
+ path.lineTo(l, t);
+ path.lineTo(r - mTabSliceWidth, t);
+ path.lineTo(r, b);
+ }
+
+ }
+
+ private void animateTabOut(final Tab tab, final TabView tv) {
+ ObjectAnimator scalex = ObjectAnimator.ofFloat(tv, "scaleX", 1.0f, 0.0f);
+ ObjectAnimator scaley = ObjectAnimator.ofFloat(tv, "scaleY", 1.0f, 0.0f);
+ ObjectAnimator alpha = ObjectAnimator.ofFloat(tv, "alpha", 1.0f, 0.0f);
+ AnimatorSet animator = new AnimatorSet();
+ animator.playTogether(scalex, scaley, alpha);
+ animator.setDuration(150);
+ animator.addListener(new AnimatorListener() {
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mTabs.removeTab(tv);
+ mTabMap.remove(tab);
+ mUi.onRemoveTabCompleted(tab);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ });
+ animator.start();
+ }
+
+ private void animateTabIn(final Tab tab, final TabView tv) {
+ ObjectAnimator scalex = ObjectAnimator.ofFloat(tv, "scaleX", 0.0f, 1.0f);
+ scalex.setDuration(150);
+ scalex.addListener(new AnimatorListener() {
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mUi.onAddTabCompleted(tab);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mTabs.addTab(tv);
+ }
+
+ });
+ scalex.start();
+ }
+
+ // TabChangeListener implementation
+
+ public void onSetActiveTab(Tab tab) {
+ mTabs.setSelectedTab(mTabControl.getTabPosition(tab));
+ }
+
+ public void onFavicon(Tab tab, Bitmap favicon) {
+ TabView tv = mTabMap.get(tab);
+ if (tv != null) {
+ tv.setFavicon(mUi.getFaviconDrawable(favicon));
+ }
+ }
+
+ public void onNewTab(Tab tab) {
+ TabView tv = buildTabView(tab);
+ animateTabIn(tab, tv);
+ }
+
+ public void onRemoveTab(Tab tab) {
+ TabView tv = mTabMap.get(tab);
+ if (tv != null) {
+ animateTabOut(tab, tv);
+ } else {
+ mTabMap.remove(tab);
+ }
+ }
+
+ public void onUrlAndTitle(Tab tab, String url, String title) {
+ TabView tv = mTabMap.get(tab);
+ if (tv != null) {
+ if (title != null) {
+ tv.setDisplayTitle(title);
+ } else if (url != null) {
+ tv.setDisplayTitle(UrlUtils.stripUrl(url));
+ }
+ tv.updateTabIcons();
+ }
+ }
+
+ private boolean isLoading() {
+ Tab tab = mTabControl.getCurrentTab();
+ if (tab != null) {
+ return tab.inPageLoad();
+ } else {
+ return false;
+ }
+ }
+
+}
diff --git a/src/com/android/browser/TabControl.java b/src/com/android/browser/TabControl.java
new file mode 100644
index 0000000..66736cb
--- /dev/null
+++ b/src/com/android/browser/TabControl.java
@@ -0,0 +1,721 @@
+/*
+ * 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.os.Bundle;
+import android.util.Log;
+
+import com.android.browser.reflect.ReflectHelper;
+
+import org.codeaurora.swe.WebView;
+
+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";
+
+ // next Tab ID, starting at 1
+ private static long sNextId = 1;
+
+ private static final String POSITIONS = "positions";
+ private static final String CURRENT = "current";
+
+ public static interface OnThumbnailUpdatedListener {
+ void onThumbnailUpdated(Tab t);
+ }
+
+ // Maximum number of tabs.
+ private int mMaxTabs;
+ // Private array of WebViews that are used as tabs.
+ private ArrayList<Tab> mTabs;
+ // Queue of most recently viewed tabs.
+ private ArrayList<Tab> mTabQueue;
+ // Current position in mTabs.
+ private int mCurrentTab = -1;
+ // the main browser controller
+ private final Controller mController;
+
+ private OnThumbnailUpdatedListener mOnThumbnailUpdatedListener;
+
+ /**
+ * Construct a new TabControl object
+ */
+ TabControl(Controller controller) {
+ mController = controller;
+ mMaxTabs = mController.getMaxTabs();
+ mTabs = new ArrayList<Tab>(mMaxTabs);
+ mTabQueue = new ArrayList<Tab>(mMaxTabs);
+ }
+
+ synchronized static long getNextId() {
+ return sNextId++;
+ }
+
+ /**
+ * Return the current tab's main WebView. This will always return the main
+ * WebView for a given tab and not a subwindow.
+ * @return The current tab's WebView.
+ */
+ WebView getCurrentWebView() {
+ Tab t = getTab(mCurrentTab);
+ if (t == null) {
+ return null;
+ }
+ return t.getWebView();
+ }
+
+ /**
+ * Return the current tab's top-level WebView. This can return a subwindow
+ * if one exists.
+ * @return The top-level WebView of the current tab.
+ */
+ WebView getCurrentTopWebView() {
+ Tab t = getTab(mCurrentTab);
+ if (t == null) {
+ return null;
+ }
+ return t.getTopWindow();
+ }
+
+ /**
+ * Return the current tab's subwindow if it exists.
+ * @return The subwindow of the current tab or null if it doesn't exist.
+ */
+ WebView getCurrentSubWindow() {
+ Tab t = getTab(mCurrentTab);
+ if (t == null) {
+ return null;
+ }
+ return t.getSubWebView();
+ }
+
+ /**
+ * return the list of tabs
+ */
+ List<Tab> getTabs() {
+ return mTabs;
+ }
+
+ /**
+ * Return the tab at the specified position.
+ * @return The Tab for the specified position or null if the tab does not
+ * exist.
+ */
+ Tab getTab(int position) {
+ if (position >= 0 && position < mTabs.size()) {
+ return mTabs.get(position);
+ }
+ return null;
+ }
+
+ /**
+ * Return the current tab.
+ * @return The current tab.
+ */
+ Tab getCurrentTab() {
+ return getTab(mCurrentTab);
+ }
+
+ /**
+ * Return the current tab position.
+ * @return The current tab position
+ */
+ int getCurrentPosition() {
+ return mCurrentTab;
+ }
+
+ /**
+ * Given a Tab, find it's position
+ * @param Tab to find
+ * @return position of Tab or -1 if not found
+ */
+ int getTabPosition(Tab tab) {
+ if (tab == null) {
+ return -1;
+ }
+ return mTabs.indexOf(tab);
+ }
+
+ boolean canCreateNewTab() {
+ 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;
+ }
+
+ void addPreloadedTab(Tab tab) {
+ for (Tab current : mTabs) {
+ if (current != null && current.getId() == tab.getId()) {
+ throw new IllegalStateException("Tab with id " + tab.getId() + " already exists: "
+ + current.toString());
+ }
+ }
+ mTabs.add(tab);
+ tab.setController(mController);
+ mController.onSetWebView(tab, tab.getWebView());
+ tab.putInBackground();
+ }
+
+ /**
+ * Create a new tab.
+ * @return The newly createTab or null if we have reached the maximum
+ * number of open tabs.
+ */
+ Tab createNewTab(boolean privateBrowsing) {
+ return createNewTab(null, privateBrowsing);
+ }
+
+ Tab createNewTab(Bundle state, boolean privateBrowsing) {
+ int size = mTabs.size();
+ // Return false if we have maxed out on tabs
+ if (!canCreateNewTab()) {
+ return null;
+ }
+
+ final WebView w = createNewWebView(privateBrowsing);
+
+ // Create a new tab and add it to the tab list
+ Tab t = new Tab(mController, w, state);
+ mTabs.add(t);
+ // Initially put the tab in the background.
+ t.putInBackground();
+ return t;
+ }
+
+ /**
+ * Create a new tab with default values for closeOnExit(false),
+ * appId(null), url(null), and privateBrowsing(false).
+ */
+ Tab createNewTab() {
+ return createNewTab(false);
+ }
+
+ SnapshotTab createSnapshotTab(long snapshotId) {
+ SnapshotTab t = new SnapshotTab(mController, snapshotId);
+ mTabs.add(t);
+ return t;
+ }
+
+ /**
+ * Remove the parent child relationships from all tabs.
+ */
+ void removeParentChildRelationShips() {
+ for (Tab tab : mTabs) {
+ tab.removeFromTree();
+ }
+ }
+
+ /**
+ * Remove the tab from the list. If the tab is the current tab shown, the
+ * last created tab will be shown.
+ * @param t The tab to be removed.
+ */
+ boolean removeTab(Tab t) {
+ if (t == null) {
+ return false;
+ }
+
+ // Grab the current tab before modifying the list.
+ Tab current = getCurrentTab();
+
+ // Remove t from our list of tabs.
+ mTabs.remove(t);
+
+ // Put the tab in the background only if it is the current one.
+ if (current == t) {
+ t.putInBackground();
+ mCurrentTab = -1;
+ } else {
+ // If a tab that is earlier in the list gets removed, the current
+ // index no longer points to the correct tab.
+ mCurrentTab = getTabPosition(current);
+ }
+
+ // destroy the tab
+ t.destroy();
+ // clear it's references to parent and children
+ t.removeFromTree();
+
+ // Remove it from the queue of viewed tabs.
+ mTabQueue.remove(t);
+ return true;
+ }
+
+ /**
+ * Destroy all the tabs and subwindows
+ */
+ void destroy() {
+ for (Tab t : mTabs) {
+ t.destroy();
+ }
+ mTabs.clear();
+ mTabQueue.clear();
+ }
+
+ /**
+ * Returns the number of tabs created.
+ * @return The number of tabs created.
+ */
+ int getTabCount() {
+ return mTabs.size();
+ }
+
+ /**
+ * save the tab state:
+ * current position
+ * position sorted array of tab ids
+ * for each tab id, save the tab state
+ * @param outState
+ * @param saveImages
+ */
+ void saveState(Bundle outState) {
+ final int numTabs = getTabCount();
+ if (numTabs == 0) {
+ return;
+ }
+ long[] ids = new long[numTabs];
+ int i = 0;
+ for (Tab tab : mTabs) {
+ Bundle tabState = tab.saveState();
+ if (tabState != null && tab.getWebView() != null
+ && tab.getWebView().isPrivateBrowsingEnabled() == false) {
+ ids[i++] = tab.getId();
+ String key = Long.toString(tab.getId());
+ if (outState.containsKey(key)) {
+ // Dump the tab state for debugging purposes
+ for (Tab dt : mTabs) {
+ Log.e(LOGTAG, dt.toString());
+ }
+ throw new IllegalStateException(
+ "Error saving state, duplicate tab ids!");
+ }
+ outState.putBundle(key, tabState);
+ } else {
+ ids[i++] = -1;
+ // Since we won't be restoring the thumbnail, delete it
+ tab.deleteThumbnail();
+ }
+ }
+ if (!outState.isEmpty()) {
+ outState.putLongArray(POSITIONS, ids);
+ Tab current = getCurrentTab();
+ long cid = -1;
+ if (current != null) {
+ cid = current.getId();
+ }
+ outState.putLong(CURRENT, cid);
+ }
+ }
+
+ /**
+ * Check if the state can be restored. If the state can be restored, the
+ * current tab id is returned. This can be passed to restoreState below
+ * in order to restore the correct tab. Otherwise, -1 is returned and the
+ * state cannot be restored.
+ */
+ long canRestoreState(Bundle inState, boolean restoreIncognitoTabs) {
+ final long[] ids = (inState == null) ? null : inState.getLongArray(POSITIONS);
+ if (ids == null) {
+ return -1;
+ }
+ final long oldcurrent = inState.getLong(CURRENT);
+ long current = -1;
+ if (restoreIncognitoTabs || (hasState(oldcurrent, inState) && !isIncognito(oldcurrent, inState))) {
+ current = oldcurrent;
+ } else {
+ // pick first non incognito tab
+ for (long id : ids) {
+ if (hasState(id, inState) && !isIncognito(id, inState)) {
+ current = id;
+ break;
+ }
+ }
+ }
+ return current;
+ }
+
+ private boolean hasState(long id, Bundle state) {
+ if (id == -1) return false;
+ Bundle tab = state.getBundle(Long.toString(id));
+ return ((tab != null) && !tab.isEmpty());
+ }
+
+ private boolean isIncognito(long id, Bundle state) {
+ Bundle tabstate = state.getBundle(Long.toString(id));
+ if ((tabstate != null) && !tabstate.isEmpty()) {
+ return tabstate.getBoolean(Tab.INCOGNITO);
+ }
+ return false;
+ }
+
+ /**
+ * Restore the state of all the tabs.
+ * @param currentId The tab id to restore.
+ * @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)
+ */
+ void restoreState(Bundle inState, long currentId,
+ boolean restoreIncognitoTabs, boolean restoreAll) {
+ if (currentId == -1) {
+ return;
+ }
+ long[] ids = inState.getLongArray(POSITIONS);
+ long maxId = -Long.MAX_VALUE;
+ HashMap<Long, Tab> tabMap = new HashMap<Long, Tab>();
+ for (long id : ids) {
+ if (id > maxId) {
+ maxId = id;
+ }
+ final String idkey = Long.toString(id);
+ Bundle state = inState.getBundle(idkey);
+ if (state == null || state.isEmpty()) {
+ // Skip tab
+ continue;
+ } else if (!restoreIncognitoTabs
+ && state.getBoolean(Tab.INCOGNITO)) {
+ // ignore tab
+ } else if (id == currentId || restoreAll) {
+ Tab t = createNewTab(state, false);
+ if (t == null) {
+ // We could "break" at this point, but we want
+ // sNextId to be set correctly.
+ continue;
+ }
+
+ // add for carrier homepage feature
+ // If the webview restore successfully, add javascript interface again.
+ WebView view = t.getWebView();
+ if (view != null) {
+ Object[] params = { new String("persist.env.c.browser.resource"),
+ new String("default")};
+ Class[] type = new Class[] {String.class, String.class};
+ String browserRes = (String)ReflectHelper.invokeStaticMethod(
+ "android.os.SystemProperties","get",
+ type, params);
+ if ("ct".equals(browserRes)) {
+ view.getSettings().setJavaScriptEnabled(true);
+ if (mController.getActivity() instanceof BrowserActivity) {
+ view.addJavascriptInterface(mController.getActivity(),
+ "default_homepage");
+ }
+ }
+ }
+
+ tabMap.put(id, t);
+ // Me must set the current tab before restoring the state
+ // so that all the client classes are set.
+ if (id == currentId) {
+ setCurrentTab(t);
+ }
+ } else {
+ // Create a new tab and don't restore the state yet, add it
+ // to the tab list
+ Tab t = new Tab(mController, state);
+ tabMap.put(id, t);
+ mTabs.add(t);
+ // added the tab to the front as they are not current
+ mTabQueue.add(0, t);
+ }
+ }
+
+ // make sure that there is no id overlap between the restored
+ // and new tabs
+ sNextId = maxId + 1;
+
+ if (mCurrentTab == -1) {
+ if (getTabCount() > 0) {
+ setCurrentTab(getTab(0));
+ }
+ }
+ // restore parent/child relationships
+ for (long id : ids) {
+ final Tab tab = tabMap.get(id);
+ final Bundle b = inState.getBundle(Long.toString(id));
+ if ((b != null) && (tab != null)) {
+ final long parentId = b.getLong(Tab.PARENTTAB, -1);
+ if (parentId != -1) {
+ final Tab parent = tabMap.get(parentId);
+ if (parent != null) {
+ parent.addChildTab(tab);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Free the memory in this order, 1) free the background tabs; 2) free the
+ * WebView cache;
+ */
+ void freeMemory() {
+ if (getTabCount() == 0) return;
+
+ // free the least frequently used background tabs
+ Vector<Tab> tabs = getHalfLeastUsedTabs(getCurrentTab());
+ if (tabs.size() > 0) {
+ Log.w(LOGTAG, "Free " + tabs.size() + " tabs in the browser");
+ for (Tab t : tabs) {
+ // store the WebView's state.
+ t.saveState();
+ // destroy the tab
+ t.destroy();
+ }
+ return;
+ }
+
+ // free the WebView's unused memory (this includes the cache)
+ Log.w(LOGTAG, "Free WebView's unused memory and cache");
+ WebView view = getCurrentWebView();
+ if (view != null) {
+ view.freeMemory();
+ }
+ }
+
+ private Vector<Tab> getHalfLeastUsedTabs(Tab current) {
+ Vector<Tab> tabsToGo = new Vector<Tab>();
+
+ // Don't do anything if we only have 1 tab or if the current tab is
+ // null.
+ if (getTabCount() == 1 || current == null) {
+ return tabsToGo;
+ }
+
+ if (mTabQueue.size() == 0) {
+ return tabsToGo;
+ }
+
+ // Rip through the queue starting at the beginning and tear down half of
+ // available tabs which are not the current tab or the parent of the
+ // current tab.
+ int openTabCount = 0;
+ for (Tab t : mTabQueue) {
+ if (t != null && t.getWebView() != null) {
+ openTabCount++;
+ if (t != current && t != current.getParent()) {
+ tabsToGo.add(t);
+ }
+ }
+ }
+
+ openTabCount /= 2;
+ if (tabsToGo.size() > openTabCount) {
+ tabsToGo.setSize(openTabCount);
+ }
+
+ return tabsToGo;
+ }
+
+ Tab getLeastUsedTab(Tab current) {
+ if (getTabCount() == 1 || current == null) {
+ return null;
+ }
+ if (mTabQueue.size() == 0) {
+ return null;
+ }
+ // find a tab which is not the current tab or the parent of the
+ // current tab
+ for (Tab t : mTabQueue) {
+ if (t != null && t.getWebView() != null) {
+ if (t != current && t != current.getParent()) {
+ return t;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Show the tab that contains the given WebView.
+ * @param view The WebView used to find the tab.
+ */
+ Tab getTabFromView(WebView view) {
+ for (Tab t : mTabs) {
+ if (t.getSubWebView() == view || t.getWebView() == view) {
+ return t;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return the tab with the matching application id.
+ * @param id The application identifier.
+ */
+ Tab getTabFromAppId(String id) {
+ if (id == null) {
+ return null;
+ }
+ for (Tab t : mTabs) {
+ if (id.equals(t.getAppId())) {
+ return t;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Stop loading in all opened WebView including subWindows.
+ */
+ void stopAllLoading() {
+ for (Tab t : mTabs) {
+ final WebView webview = t.getWebView();
+ if (webview != null) {
+ webview.stopLoading();
+ }
+ final WebView subview = t.getSubWebView();
+ if (subview != null) {
+ subview.stopLoading();
+ }
+ }
+ }
+
+ // This method checks if a tab matches the given url.
+ private boolean tabMatchesUrl(Tab t, String url) {
+ return url.equals(t.getUrl()) || url.equals(t.getOriginalUrl());
+ }
+
+ /**
+ * Return the tab that matches the given url.
+ * @param url The url to search for.
+ */
+ Tab findTabWithUrl(String url) {
+ if (url == null) {
+ return null;
+ }
+ // Check the current tab first.
+ Tab currentTab = getCurrentTab();
+ if (currentTab != null && tabMatchesUrl(currentTab, url)) {
+ return currentTab;
+ }
+ // Now check all the rest.
+ for (Tab tab : mTabs) {
+ if (tabMatchesUrl(tab, url)) {
+ return tab;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Recreate the main WebView of the given tab.
+ */
+ void recreateWebView(Tab t) {
+ final WebView w = t.getWebView();
+ if (w != null) {
+ t.destroy();
+ }
+ // Create a new WebView. If this tab is the current tab, we need to put
+ // back all the clients so force it to be the current tab.
+ t.setWebView(createNewWebView(t.isPrivateBrowsingEnabled()), false);
+ if (getCurrentTab() == t) {
+ setCurrentTab(t, true);
+ }
+ }
+
+ /**
+ * Creates a new WebView and registers it with the global settings.
+ */
+ private WebView createNewWebView() {
+ 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);
+ }
+
+ /**
+ * Put the current tab in the background and set newTab as the current tab.
+ * @param newTab The new tab. If newTab is null, the current tab is not
+ * set.
+ */
+ boolean setCurrentTab(Tab newTab) {
+ return setCurrentTab(newTab, false);
+ }
+
+ /**
+ * If force is true, this method skips the check for newTab == current.
+ */
+ private boolean setCurrentTab(Tab newTab, boolean force) {
+ Tab current = getTab(mCurrentTab);
+ if (current == newTab && !force) {
+ return true;
+ }
+ if (current != null) {
+ current.putInBackground();
+ mCurrentTab = -1;
+ }
+ if (newTab == null) {
+ return false;
+ }
+
+ // Move the newTab to the end of the queue
+ int index = mTabQueue.indexOf(newTab);
+ if (index != -1) {
+ mTabQueue.remove(index);
+ }
+ mTabQueue.add(newTab);
+
+ // Display the new current tab
+ mCurrentTab = mTabs.indexOf(newTab);
+ WebView mainView = newTab.getWebView();
+ boolean needRestore = !newTab.isSnapshot() && (mainView == null);
+ if (needRestore) {
+ // Same work as in createNewTab() except don't do new Tab()
+ mainView = createNewWebView(newTab.isPrivateBrowsingEnabled());
+ newTab.setWebView(mainView);
+ }
+ newTab.putInForeground();
+ return true;
+ }
+
+ public void setOnThumbnailUpdatedListener(OnThumbnailUpdatedListener listener) {
+ mOnThumbnailUpdatedListener = listener;
+ for (Tab t : mTabs) {
+ WebView web = t.getWebView();
+ if (web != null) {
+ web.setPictureListener(listener != null ? t : null);
+ }
+ }
+ }
+
+ public OnThumbnailUpdatedListener getOnThumbnailUpdatedListener() {
+ return mOnThumbnailUpdatedListener;
+ }
+
+}
diff --git a/src/com/android/browser/TabScrollView.java b/src/com/android/browser/TabScrollView.java
new file mode 100644
index 0000000..1df88cc
--- /dev/null
+++ b/src/com/android/browser/TabScrollView.java
@@ -0,0 +1,263 @@
+/*
+ * 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.R;
+import com.android.browser.TabBar.TabView;
+
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.HorizontalScrollView;
+import android.widget.LinearLayout;
+
+/**
+ * custom view for displaying tabs in the tabbed title bar
+ */
+public class TabScrollView extends HorizontalScrollView {
+
+ private LinearLayout mContentView;
+ private int mSelected;
+ private int mAnimationDuration;
+ private int mTabOverlap;
+
+ /**
+ * @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) {
+ mAnimationDuration = ctx.getResources().getInteger(
+ R.integer.tab_animation_duration);
+ mTabOverlap = (int) ctx.getResources().getDimension(R.dimen.tab_overlap);
+ setHorizontalScrollBarEnabled(false);
+ setOverScrollMode(OVER_SCROLL_NEVER);
+ mContentView = new TabLayout(ctx);
+ mContentView.setOrientation(LinearLayout.HORIZONTAL);
+ mContentView.setLayoutParams(
+ new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
+ mContentView.setPadding(
+ (int) ctx.getResources().getDimension(R.dimen.tab_first_padding_left),
+ 0, 0, 0);
+ addView(mContentView);
+ mSelected = -1;
+ // 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());
+ }
+
+ // in case of a configuration change, adjust tab width
+ protected void updateLayout() {
+ final int count = mContentView.getChildCount();
+ for (int i = 0; i < count; i++) {
+ final TabView tv = (TabView) mContentView.getChildAt(i);
+ tv.updateLayoutParams();
+ }
+ 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);
+ tab.setActivated(false);
+ }
+
+ void removeTab(View tab) {
+ int ix = mContentView.indexOfChild(tab);
+ if (ix == mSelected) {
+ mSelected = -1;
+ } else if (ix < mSelected) {
+ mSelected--;
+ }
+ mContentView.removeView(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);
+ }
+ }
+ }
+
+// TODO: These animations are broken and don't work correctly, removing for now
+// as animateOut is actually causing issues
+// 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();
+ }
+
+ @Override
+ protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+ super.onScrollChanged(l, t, oldl, oldt);
+
+ // TabViews base their drawing based on their absolute position within the
+ // window. When hardware accelerated, we need to recreate their display list
+ // when they scroll
+ if (isHardwareAccelerated()) {
+ int count = mContentView.getChildCount();
+ for (int i = 0; i < count; i++) {
+ mContentView.getChildAt(i).invalidate();
+ }
+ }
+ }
+
+ class TabLayout extends LinearLayout {
+
+ public TabLayout(Context context) {
+ super(context);
+ setChildrenDrawingOrderEnabled(true);
+ }
+
+ @Override
+ protected void onMeasure(int hspec, int vspec) {
+ super.onMeasure(hspec, vspec);
+ int w = getMeasuredWidth();
+ w -= Math.max(0, mContentView.getChildCount() - 1) * mTabOverlap;
+ setMeasuredDimension(w, getMeasuredHeight());
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (getChildCount() > 1) {
+ int nextLeft = getChildAt(0).getRight() - mTabOverlap;
+ for (int i = 1; i < getChildCount(); i++) {
+ View tab = getChildAt(i);
+ int w = tab.getRight() - tab.getLeft();
+ tab.layout(nextLeft, tab.getTop(), nextLeft + w, tab.getBottom());
+ nextLeft += w - mTabOverlap;
+ }
+ }
+ }
+
+ @Override
+ protected int getChildDrawingOrder(int count, int i) {
+ int next = -1;
+ if ((i == (count - 1)) && (mSelected >= 0) && (mSelected < count)) {
+ next = mSelected;
+ } else {
+ next = count - i - 1;
+ if (next <= mSelected && next > 0) {
+ next--;
+ }
+ }
+ return next;
+ }
+
+ }
+
+}
diff --git a/src/com/android/browser/TitleBar.java b/src/com/android/browser/TitleBar.java
new file mode 100644
index 0000000..e33a05c
--- /dev/null
+++ b/src/com/android/browser/TitleBar.java
@@ -0,0 +1,454 @@
+/*
+ * 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.ObjectAnimator;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.view.accessibility.AccessibilityManager;
+import android.view.animation.Animation;
+import android.view.animation.Animation.AnimationListener;
+import android.view.animation.AnimationUtils;
+import android.view.animation.DecelerateInterpolator;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.R;
+
+import android.widget.FrameLayout;
+import android.widget.RelativeLayout;
+
+
+/**
+ * Base class for a title bar used by the browser.
+ */
+public class TitleBar extends RelativeLayout {
+
+ private static final int PROGRESS_MAX = 100;
+ private static final float ANIM_TITLEBAR_DECELERATE = 2.5f;
+
+ private UiController mUiController;
+ private BaseUi mBaseUi;
+ private FrameLayout mContentView;
+ private PageProgressView mProgress;
+ private AccessibilityManager mAccessibilityManager;
+
+ private AutologinBar mAutoLogin;
+ private NavigationBarBase mNavBar;
+ private boolean mUseQuickControls;
+ private SnapshotBar mSnapshotBar;
+
+ //state
+ private boolean mShowing;
+ private boolean mInLoad;
+ private boolean mSkipTitleBarAnimations;
+ private Animator mTitleBarAnimator;
+ private boolean mIsFixedTitleBar;
+
+ public TitleBar(Context context, UiController controller, BaseUi ui,
+ FrameLayout contentView) {
+ super(context, null);
+ mUiController = controller;
+ mBaseUi = ui;
+ mContentView = contentView;
+ mAccessibilityManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ initLayout(context);
+ setFixedTitleBar();
+ }
+
+ private void initLayout(Context context) {
+ LayoutInflater factory = LayoutInflater.from(context);
+ factory.inflate(R.layout.title_bar, this);
+ mProgress = (PageProgressView) findViewById(R.id.progress);
+ mNavBar = (NavigationBarBase) findViewById(R.id.taburlbar);
+ mNavBar.setTitleBar(this);
+ }
+
+ private void inflateAutoLoginBar() {
+ if (mAutoLogin != null) {
+ return;
+ }
+
+ ViewStub stub = (ViewStub) findViewById(R.id.autologin_stub);
+ mAutoLogin = (AutologinBar) stub.inflate();
+ mAutoLogin.setTitleBar(this);
+ }
+
+ private void inflateSnapshotBar() {
+ if (mSnapshotBar != null) {
+ return;
+ }
+
+ ViewStub stub = (ViewStub) findViewById(R.id.snapshotbar_stub);
+ mSnapshotBar = (SnapshotBar) stub.inflate();
+ mSnapshotBar.setTitleBar(this);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration config) {
+ super.onConfigurationChanged(config);
+ setFixedTitleBar();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ if (mIsFixedTitleBar) {
+ int margin = getMeasuredHeight() - calculateEmbeddedHeight();
+ mBaseUi.setContentViewMarginTop(-margin);
+ } else {
+ mBaseUi.setContentViewMarginTop(0);
+ }
+ }
+
+ private void setFixedTitleBar() {
+ boolean isFixed = !mUseQuickControls
+ && !getContext().getResources().getBoolean(R.bool.hide_title);
+ isFixed |= mAccessibilityManager.isEnabled();
+ // If getParent() returns null, we are initializing
+ ViewGroup parent = (ViewGroup)getParent();
+ if (mIsFixedTitleBar == isFixed && parent != null) return;
+ mIsFixedTitleBar = isFixed;
+ setSkipTitleBarAnimations(true);
+ show();
+ setSkipTitleBarAnimations(false);
+ if (parent != null) {
+ parent.removeView(this);
+ }
+ if (mIsFixedTitleBar) {
+ mBaseUi.addFixedTitleBar(this);
+ } else {
+ mContentView.addView(this, makeLayoutParams());
+ mBaseUi.setContentViewMarginTop(0);
+ }
+ }
+
+ public BaseUi getUi() {
+ return mBaseUi;
+ }
+
+ public UiController getUiController() {
+ return mUiController;
+ }
+
+ public void setUseQuickControls(boolean use) {
+ mUseQuickControls = use;
+ setFixedTitleBar();
+ if (use) {
+ this.setVisibility(View.GONE);
+ } else {
+ this.setVisibility(View.VISIBLE);
+ }
+ }
+
+ void setShowProgressOnly(boolean progress) {
+ if (progress && !wantsToBeVisible()) {
+ mNavBar.setVisibility(View.GONE);
+ } else {
+ mNavBar.setVisibility(View.VISIBLE);
+ }
+ }
+
+ void setSkipTitleBarAnimations(boolean skip) {
+ mSkipTitleBarAnimations = skip;
+ }
+
+ void setupTitleBarAnimator(Animator animator) {
+ Resources res = getContext().getResources();
+ int duration = res.getInteger(R.integer.titlebar_animation_duration);
+ animator.setInterpolator(new DecelerateInterpolator(
+ ANIM_TITLEBAR_DECELERATE));
+ animator.setDuration(duration);
+ }
+
+ void show() {
+ cancelTitleBarAnimation(false);
+ if (mUseQuickControls || mSkipTitleBarAnimations) {
+ this.setVisibility(View.VISIBLE);
+ this.setTranslationY(0);
+ } else {
+ int visibleHeight = getVisibleTitleHeight();
+ float startPos = (-getEmbeddedHeight() + visibleHeight);
+ if (getTranslationY() != 0) {
+ startPos = Math.max(startPos, getTranslationY());
+ }
+ mTitleBarAnimator = ObjectAnimator.ofFloat(this,
+ "translationY",
+ startPos, 0);
+ setupTitleBarAnimator(mTitleBarAnimator);
+ mTitleBarAnimator.start();
+ }
+ mShowing = true;
+ }
+
+ void hide() {
+ if (mUseQuickControls) {
+ this.setVisibility(View.GONE);
+ } else {
+ if (mIsFixedTitleBar) return;
+ if (!mSkipTitleBarAnimations) {
+ cancelTitleBarAnimation(false);
+ int visibleHeight = getVisibleTitleHeight();
+ mTitleBarAnimator = ObjectAnimator.ofFloat(this,
+ "translationY", getTranslationY(),
+ (-getEmbeddedHeight() + visibleHeight));
+ mTitleBarAnimator.addListener(mHideTileBarAnimatorListener);
+ setupTitleBarAnimator(mTitleBarAnimator);
+ mTitleBarAnimator.start();
+ } else {
+ onScrollChanged();
+ }
+ }
+ mShowing = false;
+ }
+
+ boolean isShowing() {
+ return mShowing;
+ }
+
+ void cancelTitleBarAnimation(boolean reset) {
+ if (mTitleBarAnimator != null) {
+ mTitleBarAnimator.cancel();
+ mTitleBarAnimator = null;
+ }
+ if (reset) {
+ setTranslationY(0);
+ }
+ }
+
+ private AnimatorListener mHideTileBarAnimatorListener = new AnimatorListener() {
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ // update position
+ onScrollChanged();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+ };
+
+ private int getVisibleTitleHeight() {
+ Tab tab = mBaseUi.getActiveTab();
+ WebView webview = tab != null ? tab.getWebView() : null;
+ return webview != null ? webview.getVisibleTitleHeight() : 0;
+ }
+
+ /**
+ * Update the progress, from 0 to 100.
+ */
+ public void setProgress(int newProgress) {
+ if (newProgress >= PROGRESS_MAX) {
+ mProgress.setProgress(PageProgressView.MAX_PROGRESS);
+ mProgress.setVisibility(View.GONE);
+ mInLoad = false;
+ mNavBar.onProgressStopped();
+ // check if needs to be hidden
+ if (!isEditingUrl() && !wantsToBeVisible()) {
+ if (mUseQuickControls) {
+ hide();
+ } else {
+ mBaseUi.showTitleBarForDuration();
+ }
+ }
+ } else {
+ if (!mInLoad) {
+ mProgress.setVisibility(View.VISIBLE);
+ mInLoad = true;
+ mNavBar.onProgressStarted();
+ }
+ mProgress.setProgress(newProgress * PageProgressView.MAX_PROGRESS
+ / PROGRESS_MAX);
+ if (mUseQuickControls && !isEditingUrl()) {
+ setShowProgressOnly(true);
+ }
+ if (!mShowing) {
+ show();
+ }
+ }
+ }
+
+ public int getEmbeddedHeight() {
+ if (mUseQuickControls || mIsFixedTitleBar) return 0;
+ return calculateEmbeddedHeight();
+ }
+
+ public boolean isFixed() {
+ return mIsFixedTitleBar;
+ }
+
+ int calculateEmbeddedHeight() {
+ int height = mNavBar.getHeight();
+ if (mAutoLogin != null && mAutoLogin.getVisibility() == View.VISIBLE) {
+ height += mAutoLogin.getHeight();
+ }
+ return height;
+ }
+
+ public void updateAutoLogin(Tab tab, boolean animate) {
+ if (mAutoLogin == null) {
+ if (tab.getDeviceAccountLogin() == null) {
+ return;
+ }
+ inflateAutoLoginBar();
+ }
+ mAutoLogin.updateAutoLogin(tab, animate);
+ }
+
+ public void showAutoLogin(boolean animate) {
+ if (mUseQuickControls) {
+ mBaseUi.showTitleBar();
+ }
+ if (mAutoLogin == null) {
+ inflateAutoLoginBar();
+ }
+ mAutoLogin.setVisibility(View.VISIBLE);
+ if (animate) {
+ mAutoLogin.startAnimation(AnimationUtils.loadAnimation(
+ getContext(), R.anim.autologin_enter));
+ }
+ }
+
+ public void hideAutoLogin(boolean animate) {
+ if (mUseQuickControls) {
+ mBaseUi.hideTitleBar();
+ mAutoLogin.setVisibility(View.GONE);
+ mBaseUi.refreshWebView();
+ } else {
+ if (animate) {
+ Animation anim = AnimationUtils.loadAnimation(getContext(),
+ R.anim.autologin_exit);
+ anim.setAnimationListener(new AnimationListener() {
+ @Override
+ public void onAnimationEnd(Animation a) {
+ mAutoLogin.setVisibility(View.GONE);
+ mBaseUi.refreshWebView();
+ }
+
+ @Override
+ public void onAnimationStart(Animation a) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation a) {
+ }
+ });
+ mAutoLogin.startAnimation(anim);
+ } else if (mAutoLogin.getAnimation() == null) {
+ mAutoLogin.setVisibility(View.GONE);
+ mBaseUi.refreshWebView();
+ }
+ }
+ }
+
+ public boolean wantsToBeVisible() {
+ return inAutoLogin()
+ || (mSnapshotBar != null && mSnapshotBar.getVisibility() == View.VISIBLE
+ && mSnapshotBar.isAnimating());
+ }
+
+ private boolean inAutoLogin() {
+ return mAutoLogin != null && mAutoLogin.getVisibility() == View.VISIBLE;
+ }
+
+ public boolean isEditingUrl() {
+ return mNavBar.isEditingUrl();
+ }
+
+ public WebView getCurrentWebView() {
+ Tab t = mBaseUi.getActiveTab();
+ if (t != null) {
+ return t.getWebView();
+ } else {
+ return null;
+ }
+ }
+
+ public PageProgressView getProgressView() {
+ return mProgress;
+ }
+
+ public NavigationBarBase getNavigationBar() {
+ return mNavBar;
+ }
+
+ public boolean useQuickControls() {
+ return mUseQuickControls;
+ }
+
+ public boolean isInLoad() {
+ return mInLoad;
+ }
+
+ private ViewGroup.LayoutParams makeLayoutParams() {
+ return new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
+ LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ public View focusSearch(View focused, int dir) {
+ WebView web = getCurrentWebView();
+ if (FOCUS_DOWN == dir && hasFocus() && web != null
+ && web.hasFocusable() && web.getParent() != null) {
+ return web;
+ }
+ return super.focusSearch(focused, dir);
+ }
+
+ public void onTabDataChanged(Tab tab) {
+ if (mSnapshotBar != null) {
+ mSnapshotBar.onTabDataChanged(tab);
+ }
+
+ if (tab.isSnapshot()) {
+ inflateSnapshotBar();
+ mSnapshotBar.setVisibility(VISIBLE);
+ mNavBar.setVisibility(GONE);
+ } else {
+ if (mSnapshotBar != null) {
+ mSnapshotBar.setVisibility(GONE);
+ }
+ mNavBar.setVisibility(VISIBLE);
+ }
+ }
+
+ public void onScrollChanged() {
+ if (!mShowing && !mIsFixedTitleBar) {
+ setTranslationY(getVisibleTitleHeight() - getEmbeddedHeight());
+ }
+ }
+
+ public void onResume() {
+ setFixedTitleBar();
+ }
+
+}
diff --git a/src/com/android/browser/UI.java b/src/com/android/browser/UI.java
new file mode 100644
index 0000000..00dacdb
--- /dev/null
+++ b/src/com/android/browser/UI.java
@@ -0,0 +1,152 @@
+/*
+ * 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.res.Configuration;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.view.ActionMode;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.webkit.WebChromeClient.CustomViewCallback;
+import org.codeaurora.swe.WebView;
+
+import java.util.List;
+
+/**
+ * UI interface definitions
+ */
+public interface UI {
+
+ public static enum ComboViews {
+ History,
+ Bookmarks,
+ Snapshots,
+ }
+
+ public void onPause();
+
+ public void onResume();
+
+ public void onDestroy();
+
+ public void onConfigurationChanged(Configuration config);
+
+ public boolean onBackKey();
+
+ public boolean onMenuKey();
+
+ 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 onSetWebView(Tab tab, WebView view);
+
+ public void createSubWindow(Tab tab, WebView subWebView);
+
+ public void attachSubWindow(View subContainer);
+
+ public void removeSubWindow(View subContainer);
+
+ public void onTabDataChanged(Tab tab);
+
+ public void onPageStopped(Tab tab);
+
+ public void onProgressChanged(Tab tab);
+
+ public void showActiveTabsPage();
+
+ public void removeActiveTabsPage();
+
+ public void showComboView(ComboViews startingView, Bundle extra);
+
+ public void showCustomView(View view, int requestedOrientation,
+ CustomViewCallback callback);
+
+ public void onHideCustomView();
+
+ public boolean isCustomViewShowing();
+
+ public boolean onPrepareOptionsMenu(Menu menu);
+
+ public void updateMenuState(Tab tab, Menu menu);
+
+ public void onOptionsMenuOpened();
+
+ public void onExtendedMenuOpened();
+
+ public boolean onOptionsItemSelected(MenuItem item);
+
+ 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 isWebShowing();
+
+ public void showWeb(boolean animate);
+
+ Bitmap getDefaultVideoPoster();
+
+ View getVideoLoadingProgressView();
+
+ void bookmarkedStatusHasChanged(Tab tab);
+
+ void showMaxTabsWarning();
+
+ void editUrl(boolean clearInput, boolean forceIME);
+
+ boolean isEditingUrl();
+
+ boolean dispatchKey(int code, KeyEvent event);
+
+ void showAutoLogin(Tab tab);
+
+ void hideAutoLogin(Tab tab);
+
+ void setFullscreen(boolean enabled);
+
+ void setUseQuickControls(boolean enabled);
+
+ public boolean shouldCaptureThumbnails();
+
+ boolean blockFocusAnimations();
+
+ void onVoiceResult(String result);
+}
diff --git a/src/com/android/browser/UiController.java b/src/com/android/browser/UiController.java
new file mode 100644
index 0000000..36ee452
--- /dev/null
+++ b/src/com/android/browser/UiController.java
@@ -0,0 +1,116 @@
+/*
+ * 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.view.Menu;
+import android.view.MenuItem;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.UI.ComboViews;
+
+import java.util.List;
+
+
+/**
+ * UI aspect of the controller
+ */
+public interface UiController {
+
+ UI getUi();
+
+ WebView getCurrentWebView();
+
+ WebView getCurrentTopWebView();
+
+ Tab getCurrentTab();
+
+ TabControl getTabControl();
+
+ List<Tab> getTabs();
+
+ Tab openTabToHomePage();
+
+ Tab openIncognitoTab();
+
+ Tab openTab(String url, boolean incognito, boolean setActive,
+ boolean useCurrent);
+
+ void setActiveTab(Tab tab);
+
+ boolean switchToTab(Tab tab);
+
+ void closeCurrentTab();
+
+ void closeTab(Tab tab);
+
+ void closeOtherTabs();
+
+ void stopLoading();
+
+ Intent createBookmarkCurrentPageIntent(boolean canBeAnEdit);
+
+ void bookmarksOrHistoryPicker(ComboViews startView);
+
+ void bookmarkCurrentPage();
+
+ void editUrl();
+
+ void handleNewIntent(Intent intent);
+
+ boolean shouldShowErrorConsole();
+
+ void hideCustomView();
+
+ void attachSubWindow(Tab tab);
+
+ void removeSubWindow(Tab tab);
+
+ boolean isInCustomActionMode();
+
+ void endActionMode();
+
+ void shareCurrentPage();
+
+ void updateMenuState(Tab tab, Menu menu);
+
+ boolean onOptionsItemSelected(MenuItem item);
+
+ SnapshotTab createNewSnapshotTab(long snapshotId, boolean setActive);
+
+ void loadUrl(Tab tab, String url);
+
+ void setBlockEvents(boolean block);
+
+ Activity getActivity();
+
+ void showPageInfo();
+
+ void openPreferences();
+
+ void findOnPage();
+
+ void toggleUserAgent();
+
+ BrowserSettings getSettings();
+
+ boolean supportsVoice();
+
+ void startVoiceRecognizer();
+
+}
diff --git a/src/com/android/browser/UploadHandler.java b/src/com/android/browser/UploadHandler.java
new file mode 100644
index 0000000..8dec49c
--- /dev/null
+++ b/src/com/android/browser/UploadHandler.java
@@ -0,0 +1,310 @@
+/*
+ * 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.database.Cursor;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.webkit.ValueCallback;
+import android.widget.Toast;
+
+import com.android.browser.R;
+import com.android.browser.reflect.ReflectHelper;
+
+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 boolean mHandled;
+ private boolean mCaughtActivityNotFoundException;
+
+ private Controller mController;
+
+ public UploadHandler(Controller controller) {
+ mController = controller;
+ }
+
+ String getFilePath() {
+ return mCameraFilePath;
+ }
+
+ boolean handled() {
+ return mHandled;
+ }
+
+ private boolean isDrmFileUpload(Uri uri) {
+ if (uri == null) return false;
+
+ String path = null;
+ String scheme = uri.getScheme();
+ if ("content".equals(scheme)) {
+ String[] proj = null;
+ if (uri.toString().contains("/images/")) {
+ proj = new String[]{MediaStore.Images.Media.DATA};
+ } else if (uri.toString().contains("/audio/")) {
+ proj = new String[]{MediaStore.Audio.Media.DATA};
+ } else if (uri.toString().contains("/video/")) {
+ proj = new String[]{MediaStore.Video.Media.DATA};
+ }
+ Cursor cursor = mController.getActivity().managedQuery(uri, proj, null, null, null);
+ if (cursor != null && cursor.moveToFirst() && proj != null) {
+ path = cursor.getString(0);
+ }
+ } else if ("file".equals(scheme)) {
+ path = uri.getPath();
+ }
+ if (path != null) {
+ if (path.endsWith(".fl") || path.endsWith(".dm")
+ || path.endsWith(".dcf") || path.endsWith(".dr") || path.endsWith(".dd")) {
+ Toast.makeText(mController.getContext(), R.string.drm_file_unsupported,
+ Toast.LENGTH_LONG).show();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void onResult(int resultCode, Intent intent) {
+
+ if (resultCode == Activity.RESULT_CANCELED && mCaughtActivityNotFoundException) {
+ // Couldn't resolve an activity, we are going to try again so skip
+ // this result.
+ mCaughtActivityNotFoundException = false;
+ return;
+ }
+
+ 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));
+ }
+ }
+
+ // add unsupport uploading drm file feature for carrier.
+ Object[] params = {new String("persist.env.browser.drmupload"),
+ Boolean.valueOf(false)};
+ Class[] type = new Class[] {String.class, boolean.class};
+ Boolean drmUpload = (Boolean) ReflectHelper.invokeStaticMethod(
+ "android.os.SystemProperties", "getBoolean", type, params);
+ if (drmUpload && isDrmFileUpload(result)) {
+ mUploadMessage.onReceiveValue(null);
+ } else {
+ mUploadMessage.onReceiveValue(result);
+ }
+
+ mHandled = true;
+ mCaughtActivityNotFoundException = false;
+ }
+
+ void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
+
+ 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";
+
+ // According to the spec, media source can be 'filesystem' or 'camera' or 'camcorder'
+ // or 'microphone' and the default value should be 'filesystem'.
+ String mediaSource = mediaSourceValueFileSystem;
+
+ 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];
+
+ if (capture.length() > 0) {
+ mediaSource = capture;
+ }
+
+ if (capture.equals(mediaSourceValueFileSystem)) {
+ // To maintain backwards compatibility with the previous implementation
+ // of the media capture API, if the value of the 'capture' attribute is
+ // "filesystem", we should examine the accept-type for a MIME type that
+ // may specify a different capture value.
+ for (String p : params) {
+ String[] keyValue = p.split("=");
+ if (keyValue.length == 2) {
+ // Process key=value parameters.
+ if (mediaSourceKey.equals(keyValue[0])) {
+ mediaSource = keyValue[1];
+ }
+ }
+ }
+ }
+
+ //Ensure it is not still set from a previous upload.
+ mCameraFilePath = null;
+
+ if (mimeType.equals(imageMimeType)) {
+ if (mediaSource.equals(mediaSourceValueCamera)) {
+ // Specified 'image/*' and requested the camera, so go ahead and launch the
+ // camera directly.
+ startActivity(createCameraIntent());
+ return;
+ } else {
+ // Specified just 'image/*', capture=filesystem, or an invalid capture parameter.
+ // In all these cases we show a traditional picker filetered on accept type
+ // so launch an intent for both the Camera and image/* OPENABLE.
+ Intent chooser = createChooserIntent(createCameraIntent());
+ chooser.putExtra(Intent.EXTRA_INTENT, createOpenableIntent(imageMimeType));
+ startActivity(chooser);
+ return;
+ }
+ } else if (mimeType.equals(videoMimeType)) {
+ if (mediaSource.equals(mediaSourceValueCamcorder)) {
+ // Specified 'video/*' and requested the camcorder, so go ahead and launch the
+ // camcorder directly.
+ startActivity(createCamcorderIntent());
+ return;
+ } else {
+ // Specified just 'video/*', capture=filesystem or an invalid capture parameter.
+ // In all these cases we show an intent for the traditional file picker, filtered
+ // on accept type so launch an intent for both camcorder and video/* OPENABLE.
+ Intent chooser = createChooserIntent(createCamcorderIntent());
+ chooser.putExtra(Intent.EXTRA_INTENT, createOpenableIntent(videoMimeType));
+ startActivity(chooser);
+ return;
+ }
+ } else if (mimeType.equals(audioMimeType)) {
+ if (mediaSource.equals(mediaSourceValueMicrophone)) {
+ // Specified 'audio/*' and requested microphone, so go ahead and launch the sound
+ // recorder.
+ startActivity(createSoundRecorderIntent());
+ return;
+ } else {
+ // Specified just 'audio/*', capture=filesystem of an invalid capture parameter.
+ // In all these cases so go ahead and launch an intent for both the sound
+ // recorder and audio/* OPENABLE.
+ Intent chooser = createChooserIntent(createSoundRecorderIntent());
+ chooser.putExtra(Intent.EXTRA_INTENT, createOpenableIntent(audioMimeType));
+ startActivity(chooser);
+ return;
+ }
+ }
+
+ // No special handling based on the accept type was necessary, so trigger the default
+ // file upload chooser.
+ startActivity(createDefaultOpenableIntent());
+ }
+
+ private void startActivity(Intent intent) {
+ try {
+ mController.getActivity().startActivityForResult(intent, Controller.FILE_SELECTED);
+ } catch (ActivityNotFoundException e) {
+ // No installed app was able to handle the intent that
+ // we sent, so fallback to the default file upload control.
+ try {
+ mCaughtActivityNotFoundException = true;
+ mController.getActivity().startActivityForResult(createDefaultOpenableIntent(),
+ Controller.FILE_SELECTED);
+ } catch (ActivityNotFoundException e2) {
+ // Nothing can return us a file, so file upload is effectively disabled.
+ Toast.makeText(mController.getActivity(), R.string.uploads_disabled,
+ Toast.LENGTH_LONG).show();
+ }
+ }
+ }
+
+ private Intent createDefaultOpenableIntent() {
+ // Create and return a chooser with the default OPENABLE
+ // actions including the camera, camcorder and sound
+ // recorder where available.
+ Intent i = new Intent(Intent.ACTION_GET_CONTENT);
+ i.addCategory(Intent.CATEGORY_OPENABLE);
+ i.setType("*/*");
+
+ Intent chooser = createChooserIntent(createCameraIntent(), createCamcorderIntent(),
+ createSoundRecorderIntent());
+ chooser.putExtra(Intent.EXTRA_INTENT, i);
+ return chooser;
+ }
+
+ private Intent createChooserIntent(Intent... intents) {
+ Intent chooser = new Intent(Intent.ACTION_CHOOSER);
+ chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents);
+ chooser.putExtra(Intent.EXTRA_TITLE,
+ mController.getActivity().getResources()
+ .getString(R.string.choose_upload));
+ return chooser;
+ }
+
+ private Intent createOpenableIntent(String type) {
+ Intent i = new Intent(Intent.ACTION_GET_CONTENT);
+ i.addCategory(Intent.CATEGORY_OPENABLE);
+ i.setType(type);
+ return i;
+ }
+
+ private Intent createCameraIntent() {
+ 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)));
+ return cameraIntent;
+ }
+
+ private Intent createCamcorderIntent() {
+ return new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
+ }
+
+ private Intent createSoundRecorderIntent() {
+ return new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
+ }
+
+}
diff --git a/src/com/android/browser/UrlBarAutoShowManager.java b/src/com/android/browser/UrlBarAutoShowManager.java
new file mode 100644
index 0000000..4ef1765
--- /dev/null
+++ b/src/com/android/browser/UrlBarAutoShowManager.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2011 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.os.SystemClock;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.BrowserWebView.OnScrollChangedListener;
+
+/**
+ * Helper class to manage when to show the URL bar based off of touch
+ * input, and when to begin the hide timer.
+ */
+public class UrlBarAutoShowManager implements OnTouchListener,
+ OnScrollChangedListener {
+
+ private static float V_TRIGGER_ANGLE = .9f;
+ private static long SCROLL_TIMEOUT_DURATION = 150;
+ private static long IGNORE_INTERVAL = 250;
+
+ private BrowserWebView mTarget;
+ private BaseUi mUi;
+
+ private int mSlop;
+
+ private float mStartTouchX;
+ private float mStartTouchY;
+ private boolean mIsTracking;
+ private boolean mHasTriggered;
+ private long mLastScrollTime;
+ private long mTriggeredTime;
+ private boolean mIsScrolling;
+
+ public UrlBarAutoShowManager(BaseUi ui) {
+ mUi = ui;
+ ViewConfiguration config = ViewConfiguration.get(mUi.getActivity());
+ mSlop = config.getScaledTouchSlop() * 2;
+ }
+
+ public void setTarget(BrowserWebView v) {
+ if (mTarget == v) return;
+
+ if (mTarget != null) {
+ mTarget.setOnTouchListener(null);
+ mTarget.setOnScrollChangedListener(null);
+ }
+ mTarget = v;
+ if (mTarget != null) {
+ mTarget.setOnTouchListener(this);
+ mTarget.setOnScrollChangedListener(this);
+ }
+ }
+
+ @Override
+ public void onScrollChanged(int l, int t, int oldl, int oldt) {
+ mLastScrollTime = SystemClock.uptimeMillis();
+ mIsScrolling = true;
+ if (t != 0) {
+ // If it is showing, extend it
+ if (mUi.isTitleBarShowing()) {
+ long remaining = mLastScrollTime - mTriggeredTime;
+ remaining = Math.max(BaseUi.HIDE_TITLEBAR_DELAY - remaining,
+ SCROLL_TIMEOUT_DURATION);
+ mUi.showTitleBarForDuration(remaining);
+ }
+ } else {
+ mUi.suggestHideTitleBar();
+ }
+ }
+
+ void stopTracking() {
+ if (mIsTracking) {
+ mIsTracking = false;
+ mIsScrolling = false;
+ if (mUi.isTitleBarShowing()) {
+ mUi.showTitleBarForDuration();
+ }
+ }
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getPointerCount() > 1) {
+ stopTracking();
+ }
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ if (!mIsTracking && event.getPointerCount() == 1) {
+ long sinceLastScroll =
+ SystemClock.uptimeMillis() - mLastScrollTime;
+ if (sinceLastScroll < IGNORE_INTERVAL) {
+ break;
+ }
+ mStartTouchY = event.getY();
+ mStartTouchX = event.getX();
+ mIsTracking = true;
+ mHasTriggered = false;
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (mIsTracking && !mHasTriggered) {
+ WebView web = (WebView) v;
+ float dy = event.getY() - mStartTouchY;
+ float ady = Math.abs(dy);
+ float adx = Math.abs(event.getX() - mStartTouchX);
+ if (ady > mSlop) {
+ mHasTriggered = true;
+ float angle = (float) Math.atan2(ady, adx);
+ if (dy > mSlop && angle > V_TRIGGER_ANGLE
+ && !mUi.isTitleBarShowing()
+ && (web.getVisibleTitleHeight() == 0
+ || (!mIsScrolling && web.getScrollY() > 0))) {
+ mTriggeredTime = SystemClock.uptimeMillis();
+ mUi.showTitleBar();
+ }
+ }
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ stopTracking();
+ break;
+ }
+ return false;
+ }
+
+}
diff --git a/src/com/android/browser/UrlHandler.java b/src/com/android/browser/UrlHandler.java
new file mode 100755
index 0000000..783f11a
--- /dev/null
+++ b/src/com/android/browser/UrlHandler.java
@@ -0,0 +1,275 @@
+/*
+ * 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.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.provider.Browser;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.android.browser.R;
+import com.android.browser.reflect.ReflectHelper;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URISyntaxException;
+import java.util.List;
+import java.util.regex.Matcher;
+
+import org.codeaurora.swe.WebView;
+
+public class UrlHandler {
+
+ private final static String TAG = "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;
+
+ public UrlHandler(Controller controller) {
+ mController = controller;
+ mActivity = mController.getActivity();
+ }
+
+ boolean shouldOverrideUrlLoading(Tab tab, WebView view, String url) {
+ if (view.isPrivateBrowsingEnabled()) {
+ // Don't allow urls to leave the browser app when in
+ // private browsing mode
+ return false;
+ }
+
+ 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.closeEmptyTab();
+ 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 (url.startsWith("ae://") && url.endsWith("add-fav")) {
+ mController.startAddMyNavigation(url);
+ return true;
+ }
+
+ // add for carrier wap2estore feature
+ Object[] params = {new String("persist.env.browser.wap2estore"),
+ Boolean.valueOf(false)};
+ Class[] type = new Class[] {String.class, boolean.class};
+ Boolean wap2estore = (Boolean)ReflectHelper.invokeStaticMethod(
+ "android.os.SystemProperties", "getBoolean", type, params);
+ if (wap2estore && isEstoreTypeUrl(url)) {
+ handleEstoreTypeUrl(url);
+ return true;
+ }
+
+ if (startActivityForUrl(tab, url)) {
+ return true;
+ }
+
+ if (handleMenuClick(tab, url)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean isEstoreTypeUrl(String url) {
+ String utf8Url = null;
+ try {
+ utf8Url = new String(url.getBytes("UTF-8"), "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ Log.e(TAG, "err " + e);
+ }
+ if (utf8Url != null && utf8Url.startsWith("estore:")) {
+ return true;
+ }
+ return false;
+ }
+
+ private void handleEstoreTypeUrl(String url) {
+ String utf8Url = null, finalUrl = null;
+ try {
+ utf8Url = new String(url.getBytes("UTF-8"), "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ Log.e(TAG, "err " + e);
+ }
+ if (utf8Url != null) {
+ finalUrl = utf8Url;
+ } else {
+ finalUrl = url;
+ }
+ if (finalUrl.replaceFirst("estore:", "").length() > 256) {
+ Toast.makeText(mActivity, R.string.estore_url_warning, Toast.LENGTH_LONG).show();
+ return;
+ }
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(finalUrl));
+ try {
+ mActivity.startActivity(intent);
+ } catch (ActivityNotFoundException ex) {
+ String downloadUrl = mActivity.getResources().getString(R.string.estore_homepage);
+ mController.loadUrl(mController.getCurrentTab(), downloadUrl);
+ Toast.makeText(mActivity, R.string.download_estore_app, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ boolean startActivityForUrl(Tab tab, String url) {
+ 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.closeEmptyTab();
+ 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);
+ // Re-use the existing tab if the intent comes back to us
+ if (tab != null) {
+ if (tab.getAppId() == null) {
+ tab.setAppId(mActivity.getPackageName() + "-" + tab.getId());
+ }
+ intent.putExtra(Browser.EXTRA_APPLICATION_ID, tab.getAppId());
+ }
+ // Make sure webkit can handle it internally before checking for specialized
+ // handlers. If webkit can't handle it internally, we need to call
+ // startActivityIfNeeded
+ Matcher m = UrlUtils.ACCEPTED_URI_SCHEMA.matcher(url);
+ if (m.matches() && !isSpecializedHandlerAvailable(intent)) {
+ return false;
+ }
+ try {
+ intent.putExtra(BrowserActivity.EXTRA_DISABLE_URL_OVERRIDE, true);
+ 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.closeEmptyTab();
+ return true;
+ }
+ } catch (ActivityNotFoundException ex) {
+ // ignore the error. If no application can handle the URL,
+ // eg about:blank, assume the browser can handle it.
+ }
+
+ return false;
+ }
+
+ /**
+ * Search for intent handlers that are specific to this URL
+ * aka, specialized apps like google maps or youtube
+ */
+ private boolean isSpecializedHandlerAvailable(Intent intent) {
+ PackageManager pm = mActivity.getPackageManager();
+ List<ResolveInfo> handlers = pm.queryIntentActivities(intent,
+ PackageManager.GET_RESOLVED_FILTER);
+ if (handlers == null || handlers.size() == 0) {
+ return false;
+ }
+ for (ResolveInfo resolveInfo : handlers) {
+ IntentFilter filter = resolveInfo.filter;
+ if (filter == null) {
+ // No intent filter matches this intent?
+ // Error on the side of staying in the browser, ignore
+ continue;
+ }
+ if (filter.countDataAuthorities() == 0 && filter.countDataPaths() == 0) {
+ // Generic handler, skip
+ continue;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ // In case a physical keyboard is attached, handle clicks with the menu key
+ // depressed by opening in a new tab
+ boolean handleMenuClick(Tab tab, String url) {
+ if (mController.isMenuDown()) {
+ mController.openTab(url,
+ (tab != null) && tab.isPrivateBrowsingEnabled(),
+ !BrowserSettings.getInstance().openInBackground(), true);
+ mActivity.closeOptionsMenu();
+ 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 0000000..1359000
--- /dev/null
+++ b/src/com/android/browser/UrlInputView.java
@@ -0,0 +1,362 @@
+/*
+ * 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.res.Configuration;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.util.Patterns;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AutoCompleteTextView;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+
+import com.android.browser.SuggestionsAdapter.CompletionListener;
+import com.android.browser.SuggestionsAdapter.SuggestItem;
+import com.android.browser.reflect.ReflectHelper;
+import com.android.browser.search.SearchEngine;
+import com.android.browser.search.SearchEngineInfo;
+import com.android.browser.search.SearchEngines;
+
+
+/**
+ * url/search input view
+ * handling suggestions
+ */
+public class UrlInputView extends AutoCompleteTextView
+ implements OnEditorActionListener,
+ CompletionListener, OnItemClickListener, TextWatcher {
+
+ static final String TYPED = "browser-type";
+ static final String SUGGESTED = "browser-suggest";
+
+ static final int POST_DELAY = 100;
+
+ static interface StateListener {
+ static final int STATE_NORMAL = 0;
+ static final int STATE_HIGHLIGHTED = 1;
+ static final int STATE_EDITED = 2;
+
+ public void onStateChanged(int state);
+ }
+
+ private UrlInputListener mListener;
+ private InputMethodManager mInputManager;
+ private SuggestionsAdapter mAdapter;
+ private View mContainer;
+ private boolean mLandscape;
+ private boolean mIncognitoMode;
+ private boolean mNeedsUpdate;
+
+ private int mState;
+ private StateListener mStateListener;
+ private Rect mPopupPadding;
+
+ public UrlInputView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ // SWE_TODO : HARDCODED a random background - clean up
+ /*
+ TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.PopupWindow,
+ R.attr.autoCompleteTextViewStyle, 0);
+
+ Drawable popupbg = a.getDrawable(R.styleable.PopupWindow_popupBackground);
+ a.recycle(); */
+ Drawable popupbg = context.getResources().getDrawable(android.R.drawable.editbox_background);
+ mPopupPadding = new Rect();
+ popupbg.getPadding(mPopupPadding);
+ init(context);
+ }
+
+ public UrlInputView(Context context, AttributeSet attrs) {
+ // SWE_TODO : Needs Fix
+ //this(context, attrs, R.attr.autoCompleteTextViewStyle);
+ this(context, attrs, 0);
+ }
+
+ public UrlInputView(Context context) {
+ this(context, null);
+ }
+
+ private void init(Context ctx) {
+ mInputManager = (InputMethodManager) ctx.getSystemService(Context.INPUT_METHOD_SERVICE);
+ setOnEditorActionListener(this);
+ mAdapter = new SuggestionsAdapter(ctx, this);
+ setAdapter(mAdapter);
+ setSelectAllOnFocus(true);
+ onConfigurationChanged(ctx.getResources().getConfiguration());
+ setThreshold(1);
+ setOnItemClickListener(this);
+ mNeedsUpdate = false;
+ addTextChangedListener(this);
+
+ mState = StateListener.STATE_NORMAL;
+ }
+
+ protected void onFocusChanged(boolean focused, int direction, Rect prevRect) {
+ super.onFocusChanged(focused, direction, prevRect);
+ int state = -1;
+ if (focused) {
+ if (hasSelection()) {
+ state = StateListener.STATE_HIGHLIGHTED;
+ } else {
+ state = StateListener.STATE_EDITED;
+ }
+ } else {
+ // reset the selection state
+ state = StateListener.STATE_NORMAL;
+ }
+ final int s = state;
+ post(new Runnable() {
+ public void run() {
+ changeState(s);
+ }
+ });
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent evt) {
+ boolean hasSelection = hasSelection();
+ boolean res = super.onTouchEvent(evt);
+ if ((MotionEvent.ACTION_DOWN == evt.getActionMasked())
+ && hasSelection) {
+ postDelayed(new Runnable() {
+ public void run() {
+ changeState(StateListener.STATE_EDITED);
+ }}, POST_DELAY);
+ }
+ return res;
+ }
+
+ /**
+ * check if focus change requires a title bar update
+ */
+ boolean needsUpdate() {
+ return mNeedsUpdate;
+ }
+
+ /**
+ * clear the focus change needs title bar update flag
+ */
+ void clearNeedsUpdate() {
+ mNeedsUpdate = false;
+ }
+
+ void setController(UiController controller) {
+ UrlSelectionActionMode urlSelectionMode
+ = new UrlSelectionActionMode(controller);
+ setCustomSelectionActionModeCallback(urlSelectionMode);
+ }
+
+ void setContainer(View container) {
+ mContainer = container;
+ }
+
+ public void setUrlInputListener(UrlInputListener listener) {
+ mListener = listener;
+ }
+
+ public void setStateListener(StateListener listener) {
+ mStateListener = listener;
+ // update listener
+ changeState(mState);
+ }
+
+ private void changeState(int newState) {
+ mState = newState;
+ if (mStateListener != null) {
+ mStateListener.onStateChanged(mState);
+ }
+ }
+
+ int getState() {
+ return mState;
+ }
+
+ @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();
+ performFiltering(getText(), 0);
+ }
+ }
+
+ @Override
+ public void showDropDown() {
+ setupDropDown();
+ super.showDropDown();
+ }
+
+ @Override
+ public void dismissDropDown() {
+ super.dismissDropDown();
+ mAdapter.clearCache();
+ }
+
+ private void setupDropDown() {
+ int width = mContainer != null ? mContainer.getWidth() : getWidth();
+ width += mPopupPadding.left + mPopupPadding.right;
+ if (width != getDropDownWidth()) {
+ setDropDownWidth(width);
+ }
+ int left = getLeft();
+ left += mPopupPadding.left;
+ if (left != -getDropDownHorizontalOffset()) {
+ setDropDownHorizontalOffset(-left);
+ }
+ }
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ finishInput(getText().toString(), null, TYPED);
+ return true;
+ }
+
+ void forceFilter() {
+ showDropDown();
+ }
+
+ void hideIME() {
+ mInputManager.hideSoftInputFromWindow(getWindowToken(), 0);
+ }
+
+ void showIME() {
+ //mInputManager.focusIn(this);
+ Object[] params = {this};
+ Class[] type = new Class[] {View.class};
+ ReflectHelper.invokeMethod(mInputManager, "focusIn", type, params);
+ mInputManager.showSoftInput(this, 0);
+ }
+
+ private void finishInput(String url, String extra, String source) {
+ mNeedsUpdate = true;
+ dismissDropDown();
+ mInputManager.hideSoftInputFromWindow(getWindowToken(), 0);
+ if (TextUtils.isEmpty(url)) {
+ mListener.onDismiss();
+ } else {
+ if (mIncognitoMode && isSearch(url)) {
+ // To prevent logging, intercept this request
+ // TODO: This is a quick hack, refactor this
+ SearchEngine searchEngine = BrowserSettings.getInstance()
+ .getSearchEngine();
+ if (searchEngine == null) return;
+ SearchEngineInfo engineInfo = SearchEngines
+ .getSearchEngineInfo(getContext(), searchEngine.getName());
+ if (engineInfo == null) return;
+ url = engineInfo.getSearchUriForQuery(url);
+ // mLister.onAction can take it from here without logging
+ }
+ mListener.onAction(url, extra, source);
+ }
+ }
+
+ boolean isSearch(String inUrl) {
+ String url = UrlUtils.fixUrl(inUrl).trim();
+ if (TextUtils.isEmpty(url)) return false;
+
+ if (Patterns.WEB_URL.matcher(url).matches()
+ || UrlUtils.ACCEPTED_URI_SCHEMA.matcher(url).matches()) {
+ return false;
+ }
+ return true;
+ }
+
+ // Completion Listener
+
+ @Override
+ public void onSearch(String search) {
+ mListener.onCopySuggestion(search);
+ }
+
+ @Override
+ public void onSelect(String url, int type, String extra) {
+ finishInput(url, extra, SUGGESTED);
+ }
+
+ @Override
+ public void onItemClick(
+ AdapterView<?> parent, View view, int position, long id) {
+ SuggestItem item = mAdapter.getItem(position);
+ onSelect(SuggestionsAdapter.getSuggestionUrl(item), item.type, item.extra);
+ }
+
+ interface UrlInputListener {
+
+ public void onDismiss();
+
+ public void onAction(String text, String extra, String source);
+
+ public void onCopySuggestion(String text);
+
+ }
+
+ public void setIncognitoMode(boolean incognito) {
+ mIncognitoMode = incognito;
+ mAdapter.setIncognitoMode(mIncognitoMode);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent evt) {
+ if (keyCode == KeyEvent.KEYCODE_ESCAPE && !isInTouchMode()) {
+ finishInput(null, null, null);
+ return true;
+ }
+ return super.onKeyDown(keyCode, evt);
+ }
+
+ public SuggestionsAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ /*
+ * no-op to prevent scrolling of webview when embedded titlebar
+ * gets edited
+ */
+ @Override
+ public boolean requestRectangleOnScreen(Rect rect, boolean immediate) {
+ return false;
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ if (StateListener.STATE_HIGHLIGHTED == mState) {
+ changeState(StateListener.STATE_EDITED);
+ }
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) { }
+
+}
diff --git a/src/com/android/browser/UrlSelectionActionMode.java b/src/com/android/browser/UrlSelectionActionMode.java
new file mode 100644
index 0000000..87446ec
--- /dev/null
+++ b/src/com/android/browser/UrlSelectionActionMode.java
@@ -0,0 +1,63 @@
+/*
+ * 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.R;
+
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuItem;
+
+public class UrlSelectionActionMode implements ActionMode.Callback {
+
+ private UiController mUiController;
+
+ public UrlSelectionActionMode(UiController controller) {
+ mUiController = controller;
+ }
+
+ // ActionMode.Callback implementation
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ mode.getMenuInflater().inflate(R.menu.url_selection, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.share:
+ mUiController.shareCurrentPage();
+ mode.finish();
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return true;
+ }
+
+}
diff --git a/src/com/android/browser/UrlUtils.java b/src/com/android/browser/UrlUtils.java
new file mode 100755
index 0000000..ff78647
--- /dev/null
+++ b/src/com/android/browser/UrlUtils.java
@@ -0,0 +1,175 @@
+/*
+ * 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|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 to strip http:// and optionally
+ // the trailing slash
+ 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
+ */
+ public static String stripUrl(String url) {
+ if (url == null) return null;
+ Matcher m = STRIP_URL_PATTERN.matcher(url);
+ if (m.matches()) {
+ return m.group(1);
+ } 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
+ *
+ */
+ public static String smartUrlFilter(String url) {
+ return smartUrlFilter(url, true);
+ }
+
+ /**
+ * Attempts to determine whether user input is a URL or search
+ * terms. Anything with a space is passed to search if canBeSearch is true.
+ *
+ * Converts to lowercase any mistakenly uppercased schema (i.e.,
+ * "Http://" converts to "http://"
+ *
+ * @param canBeSearch If true, will return a search url if it isn't a valid
+ * URL. If false, invalid URLs will return null
+ * @return Original or modified URL
+ *
+ */
+ public static String smartUrlFilter(String url, boolean canBeSearch) {
+ 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 && Patterns.WEB_URL.matcher(inUrl).matches()) {
+ inUrl = inUrl.replace(" ", "%20");
+ }
+ return inUrl;
+ }
+ if (!hasSpace) {
+ if (Patterns.WEB_URL.matcher(inUrl).matches()) {
+ return URLUtil.guessUrl(inUrl);
+ }
+ }
+ if (canBeSearch) {
+ return URLUtil.composeSearchUrl(inUrl,
+ QUICKSEARCH_G, QUERY_PLACE_HOLDER);
+ }
+ return null;
+ }
+
+ public 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;
+ }
+
+ // Returns the filtered URL. Cannot return null, but can return an empty string
+ /* package */ static String filteredUrl(String inUrl) {
+ if (inUrl == null) {
+ return "";
+ }
+ if (inUrl.startsWith("content:")
+ || inUrl.startsWith("browser:")) {
+ return "";
+ }
+ return inUrl;
+ }
+
+}
diff --git a/src/com/android/browser/WallpaperHandler.java b/src/com/android/browser/WallpaperHandler.java
new file mode 100644
index 0000000..5c539b5
--- /dev/null
+++ b/src/com/android/browser/WallpaperHandler.java
@@ -0,0 +1,200 @@
+/*
+ * 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.BitmapFactory;
+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.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import com.android.browser.R;
+
+/**
+ * Handle setWallpaper requests
+ *
+ */
+public class WallpaperHandler extends Thread
+ implements OnMenuItemClickListener, DialogInterface.OnCancelListener {
+
+ private static final String LOGTAG = "WallpaperHandler";
+ // This should be large enough for BitmapFactory to decode the header so
+ // that we can mark and reset the input stream to avoid duplicate network i/o
+ private static final int BUFFER_SIZE = 128 * 1024;
+
+ private Context mContext;
+ private String mUrl;
+ private ProgressDialog mWallpaperProgress;
+ private boolean mCanceled = false;
+
+ public WallpaperHandler(Context context, String url) {
+ mContext = context;
+ mUrl = url;
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ mCanceled = true;
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ if (mUrl != null && getState() == State.NEW) {
+ // 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() {
+ WallpaperManager wm = WallpaperManager.getInstance(mContext);
+ Drawable oldWallpaper = wm.getDrawable();
+ InputStream inputstream = null;
+ 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 = openStream();
+ if (inputstream != null) {
+ if (!inputstream.markSupported()) {
+ inputstream = new BufferedInputStream(inputstream, BUFFER_SIZE);
+ }
+ inputstream.mark(BUFFER_SIZE);
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ // We give decodeStream a wrapped input stream so it doesn't
+ // mess with our mark (currently it sets a mark of 1024)
+ BitmapFactory.decodeStream(
+ new BufferedInputStream(inputstream), null, options);
+ int maxWidth = wm.getDesiredMinimumWidth();
+ int maxHeight = wm.getDesiredMinimumHeight();
+ // Give maxWidth and maxHeight some leeway
+ maxWidth *= 1.25;
+ maxHeight *= 1.25;
+ int bmWidth = options.outWidth;
+ int bmHeight = options.outHeight;
+
+ int scale = 1;
+ while (bmWidth > maxWidth || bmHeight > maxHeight) {
+ scale <<= 1;
+ bmWidth >>= 1;
+ bmHeight >>= 1;
+ }
+ options.inJustDecodeBounds = false;
+ options.inSampleSize = scale;
+ try {
+ inputstream.reset();
+ } catch (IOException e) {
+ // BitmapFactory read more than we could buffer
+ // Re-open the stream
+ inputstream.close();
+ inputstream = openStream();
+ }
+ Bitmap scaledWallpaper = BitmapFactory.decodeStream(inputstream,
+ null, options);
+ if (scaledWallpaper != null) {
+ wm.setBitmap(scaledWallpaper);
+ } else {
+ Log.e(LOGTAG, "Unable to set new wallpaper, " +
+ "decodeStream returned null.");
+ }
+ }
+ } 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;
+ } finally {
+ if (inputstream != null) {
+ try {
+ inputstream.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+ }
+
+ 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);
+ canvas.setBitmap(null);
+ try {
+ wm.setBitmap(bm);
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Unable to restore old wallpaper.");
+ }
+ mCanceled = false;
+ }
+
+ if (mWallpaperProgress.isShowing()) {
+ mWallpaperProgress.dismiss();
+ }
+ }
+
+ /**
+ * Opens the input stream for the URL that the class should
+ * use to set the wallpaper. Abstracts the difference between
+ * standard URLs and data URLs.
+ * @return An open InputStream for the data at the URL
+ * @throws IOException if there is an error opening the URL stream
+ * @throws MalformedURLException if the URL is malformed
+ */
+ private InputStream openStream() throws IOException, MalformedURLException {
+ InputStream inputStream = null;
+ if (DataUri.isDataUri(mUrl)) {
+ DataUri dataUri = new DataUri(mUrl);
+ inputStream = new ByteArrayInputStream(dataUri.getData());
+ } else {
+ URL url = new URL(mUrl);
+ inputStream = url.openStream();
+ }
+ return inputStream;
+ }
+}
diff --git a/src/com/android/browser/WebStorageSizeManager.java b/src/com/android/browser/WebStorageSizeManager.java
new file mode 100644
index 0000000..0a6a514
--- /dev/null
+++ b/src/com/android/browser/WebStorageSizeManager.java
@@ -0,0 +1,423 @@
+/*
+ * 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 com.android.browser.R;
+import com.android.browser.preferences.WebsiteSettingsFragment;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.StatFs;
+import android.preference.PreferenceActivity;
+import android.util.Log;
+import android.webkit.WebStorage;
+
+import java.io.File;
+
+
+/**
+ * Package level class for managing the disk size consumed by the WebDatabase
+ * and ApplicationCaches APIs (henceforth called Web storage).
+ *
+ * Currently, the situation on the WebKit side is as follows:
+ * - WebDatabase enforces a quota for each origin.
+ * - Session/LocalStorage do not enforce any disk limits.
+ * - ApplicationCaches enforces a maximum size for all origins.
+ *
+ * The WebStorageSizeManager maintains a global limit for the disk space
+ * consumed by the WebDatabase and ApplicationCaches. As soon as WebKit will
+ * have a limit for Session/LocalStorage, this class will manage the space used
+ * by those APIs as well.
+ *
+ * The global limit is computed as a function of the size of the partition where
+ * these APIs store their data (they must store it on the same partition for
+ * this to work) and the size of the available space on that partition.
+ * The global limit is not subject to user configuration but we do provide
+ * a debug-only setting.
+ * TODO(andreip): implement the debug setting.
+ *
+ * The size of the disk space used for Web storage is initially divided between
+ * WebDatabase and ApplicationCaches as follows:
+ *
+ * 75% for WebDatabase
+ * 25% for ApplicationCaches
+ *
+ * When an origin's database usage reaches its current quota, WebKit invokes
+ * the following callback function:
+ * - exceededDatabaseQuota(Frame* frame, const String& database_name);
+ * Note that the default quota for a new origin is 0, so we will receive the
+ * 'exceededDatabaseQuota' callback before a new origin gets the chance to
+ * create its first database.
+ *
+ * When the total ApplicationCaches usage reaches its current quota, WebKit
+ * invokes the following callback function:
+ * - void reachedMaxAppCacheSize(int64_t spaceNeeded);
+ *
+ * The WebStorageSizeManager's main job is to respond to the above two callbacks
+ * by inspecting the amount of unused Web storage quota (i.e. global limit -
+ * sum of all other origins' quota) and deciding if a quota increase for the
+ * out-of-space origin is allowed or not.
+ *
+ * The default quota for an origin is its estimated size. If we cannot satisfy
+ * the estimated size, then WebCore will not create the database.
+ * Quota increases are done in steps, where the increase step is
+ * min(QUOTA_INCREASE_STEP, unused_quota).
+ *
+ * When all the Web storage space is used, the WebStorageSizeManager creates
+ * a system notification that will guide the user to the WebSettings UI. There,
+ * the user can free some of the Web storage space by deleting all the data used
+ * by an origin.
+ */
+public class WebStorageSizeManager {
+ // Logging flags.
+ 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";
+ // The default quota value for an origin.
+ public final static long ORIGIN_DEFAULT_QUOTA = 3 * 1024 * 1024; // 3MB
+ // The default value for quota increases.
+ public final static long QUOTA_INCREASE_STEP = 1 * 1024 * 1024; // 1MB
+ // Extra padding space for appcache maximum size increases. This is needed
+ // because WebKit sends us an estimate of the amount of space needed
+ // but this estimate may, currently, be slightly less than what is actually
+ // needed. We therefore add some 'padding'.
+ // TODO(andreip): fix this in WebKit.
+ public final static long APPCACHE_MAXSIZE_PADDING = 512 * 1024; // 512KB
+ // The system status bar notification id.
+ private final static int OUT_OF_SPACE_ID = 1;
+ // The time of the last out of space notification
+ private static long mLastOutOfSpaceNotificationTime = -1;
+ // Delay between two notification in ms
+ private final static long NOTIFICATION_INTERVAL = 5 * 60 * 1000;
+ // Delay in ms used when resetting the notification time
+ private final static long RESET_NOTIFICATION_INTERVAL = 3 * 1000;
+ // The application context.
+ private final Context mContext;
+ // The global Web storage limit.
+ private final long mGlobalLimit;
+ // The maximum size of the application cache file.
+ private long mAppCacheMaxSize;
+
+ /**
+ * Interface used by the WebStorageSizeManager to obtain information
+ * about the underlying file system. This functionality is separated
+ * into its own interface mainly for testing purposes.
+ */
+ public interface DiskInfo {
+ /**
+ * @return the size of the free space in the file system.
+ */
+ public long getFreeSpaceSizeBytes();
+
+ /**
+ * @return the total size of the file system.
+ */
+ public long getTotalSizeBytes();
+ };
+
+ private DiskInfo mDiskInfo;
+ // For convenience, we provide a DiskInfo implementation that uses StatFs.
+ public static class StatFsDiskInfo implements DiskInfo {
+ private StatFs mFs;
+
+ public StatFsDiskInfo(String path) {
+ mFs = new StatFs(path);
+ }
+
+ public long getFreeSpaceSizeBytes() {
+ return (long)(mFs.getAvailableBlocks()) * mFs.getBlockSize();
+ }
+
+ public long getTotalSizeBytes() {
+ return (long)(mFs.getBlockCount()) * mFs.getBlockSize();
+ }
+ };
+
+ /**
+ * Interface used by the WebStorageSizeManager to obtain information
+ * about the appcache file. This functionality is separated into its own
+ * interface mainly for testing purposes.
+ */
+ public interface AppCacheInfo {
+ /**
+ * @return the current size of the appcache file.
+ */
+ public long getAppCacheSizeBytes();
+ };
+
+ // For convenience, we provide an AppCacheInfo implementation.
+ public static class WebKitAppCacheInfo implements AppCacheInfo {
+ // The name of the application cache file. Keep in sync with
+ // WebCore/loader/appcache/ApplicationCacheStorage.cpp
+ private final static String APPCACHE_FILE = "ApplicationCache.db";
+ private String mAppCachePath;
+
+ public WebKitAppCacheInfo(String path) {
+ mAppCachePath = path;
+ }
+
+ public long getAppCacheSizeBytes() {
+ File file = new File(mAppCachePath
+ + File.separator
+ + APPCACHE_FILE);
+ return file.length();
+ }
+ };
+
+ /**
+ * Public ctor
+ * @param ctx is the application context
+ * @param diskInfo is the DiskInfo instance used to query the file system.
+ * @param appCacheInfo is the AppCacheInfo used to query info about the
+ * appcache file.
+ */
+ public WebStorageSizeManager(Context ctx, DiskInfo diskInfo,
+ AppCacheInfo appCacheInfo) {
+ mContext = ctx.getApplicationContext();
+ mDiskInfo = diskInfo;
+ mGlobalLimit = getGlobalLimit();
+ // The initial max size of the app cache is either 25% of the global
+ // limit or the current size of the app cache file, whichever is bigger.
+ mAppCacheMaxSize = Math.max(mGlobalLimit / 4,
+ appCacheInfo.getAppCacheSizeBytes());
+ }
+
+ /**
+ * Returns the maximum size of the application cache.
+ */
+ public long getAppCacheMaxSize() {
+ return mAppCacheMaxSize;
+ }
+
+ /**
+ * The origin has exceeded its database quota.
+ * @param url the URL that exceeded the quota
+ * @param databaseIdentifier the identifier of the database on
+ * which the transaction that caused the quota overflow was run
+ * @param currentQuota the current quota for the origin.
+ * @param estimatedSize the estimated size of a new database, or 0 if
+ * this has been invoked in response to an existing database
+ * overflowing its quota.
+ * @param totalUsedQuota is the sum of all origins' quota.
+ * @param quotaUpdater The callback to run when a decision to allow or
+ * deny quota has been made. Don't forget to call this!
+ */
+ public void onExceededDatabaseQuota(String url,
+ String databaseIdentifier, long currentQuota, long estimatedSize,
+ long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) {
+ if(LOGV_ENABLED) {
+ Log.v(LOGTAG,
+ "Received onExceededDatabaseQuota for "
+ + url
+ + ":"
+ + databaseIdentifier
+ + "(current quota: "
+ + currentQuota
+ + ", total used quota: "
+ + totalUsedQuota
+ + ")");
+ }
+ long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize;
+
+ if (totalUnusedQuota <= 0) {
+ // There definitely isn't any more space. Fire notifications
+ // if needed and exit.
+ if (totalUsedQuota > 0) {
+ // We only fire the notification if there are some other websites
+ // using some of the quota. This avoids the degenerate case where
+ // the first ever website to use Web storage tries to use more
+ // data than it is actually available. In such a case, showing
+ // the notification would not help at all since there is nothing
+ // the user can do.
+ scheduleOutOfSpaceNotification();
+ }
+ quotaUpdater.updateQuota(currentQuota);
+ if(LOGV_ENABLED) {
+ Log.v(LOGTAG, "onExceededDatabaseQuota: out of space.");
+ }
+ return;
+ }
+
+ // We have some space inside mGlobalLimit.
+ long newOriginQuota = currentQuota;
+ if (newOriginQuota == 0) {
+ // This is a new origin, give it the size it asked for if possible.
+ // If we cannot satisfy the estimatedSize, we should return 0 as
+ // returning a value less that what the site requested will lead
+ // to webcore not creating the database.
+ if (totalUnusedQuota >= estimatedSize) {
+ newOriginQuota = estimatedSize;
+ } else {
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG,
+ "onExceededDatabaseQuota: Unable to satisfy" +
+ " estimatedSize for the new database " +
+ " (estimatedSize: " + estimatedSize +
+ ", unused quota: " + totalUnusedQuota);
+ }
+ newOriginQuota = 0;
+ }
+ } else {
+ // This is an origin we have seen before. It wants a quota
+ // increase. There are two circumstances: either the origin
+ // is creating a new database or it has overflowed an existing database.
+
+ // Increase the quota. If estimatedSize == 0, then this is a quota overflow
+ // rather than the creation of a new database.
+ long quotaIncrease = estimatedSize == 0 ?
+ Math.min(QUOTA_INCREASE_STEP, totalUnusedQuota) :
+ estimatedSize;
+ newOriginQuota += quotaIncrease;
+
+ if (quotaIncrease > totalUnusedQuota) {
+ // We can't fit, so deny quota.
+ newOriginQuota = currentQuota;
+ }
+ }
+
+ quotaUpdater.updateQuota(newOriginQuota);
+
+ if(LOGV_ENABLED) {
+ Log.v(LOGTAG, "onExceededDatabaseQuota set new quota to "
+ + newOriginQuota);
+ }
+ }
+
+ /**
+ * The Application Cache has exceeded its max size.
+ * @param spaceNeeded is the amount of disk space that would be needed
+ * in order for the last appcache operation to succeed.
+ * @param totalUsedQuota is the sum of all origins' quota.
+ * @param quotaUpdater A callback to inform the WebCore thread that a new
+ * app cache size is available. This callback must always be executed at
+ * some point to ensure that the sleeping WebCore thread is woken up.
+ */
+ public void onReachedMaxAppCacheSize(long spaceNeeded, long totalUsedQuota,
+ WebStorage.QuotaUpdater quotaUpdater) {
+ if(LOGV_ENABLED) {
+ Log.v(LOGTAG, "Received onReachedMaxAppCacheSize with spaceNeeded "
+ + spaceNeeded + " bytes.");
+ }
+
+ long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize;
+
+ if (totalUnusedQuota < spaceNeeded + APPCACHE_MAXSIZE_PADDING) {
+ // There definitely isn't any more space. Fire notifications
+ // if needed and exit.
+ if (totalUsedQuota > 0) {
+ // We only fire the notification if there are some other websites
+ // using some of the quota. This avoids the degenerate case where
+ // the first ever website to use Web storage tries to use more
+ // data than it is actually available. In such a case, showing
+ // the notification would not help at all since there is nothing
+ // the user can do.
+ scheduleOutOfSpaceNotification();
+ }
+ quotaUpdater.updateQuota(0);
+ if(LOGV_ENABLED) {
+ Log.v(LOGTAG, "onReachedMaxAppCacheSize: out of space.");
+ }
+ return;
+ }
+ // There is enough space to accommodate spaceNeeded bytes.
+ mAppCacheMaxSize += spaceNeeded + APPCACHE_MAXSIZE_PADDING;
+ quotaUpdater.updateQuota(mAppCacheMaxSize);
+
+ if(LOGV_ENABLED) {
+ Log.v(LOGTAG, "onReachedMaxAppCacheSize set new max size to "
+ + mAppCacheMaxSize);
+ }
+ }
+
+ // Reset the notification time; we use this iff the user
+ // use clear all; we reset it to some time in the future instead
+ // of just setting it to -1, as the clear all method is asynchronous
+ public static void resetLastOutOfSpaceNotificationTime() {
+ mLastOutOfSpaceNotificationTime = System.currentTimeMillis() -
+ NOTIFICATION_INTERVAL + RESET_NOTIFICATION_INTERVAL;
+ }
+
+ // Computes the global limit as a function of the size of the data
+ // partition and the amount of free space on that partition.
+ private long getGlobalLimit() {
+ long freeSpace = mDiskInfo.getFreeSpaceSizeBytes();
+ long fileSystemSize = mDiskInfo.getTotalSizeBytes();
+ return calculateGlobalLimit(fileSystemSize, freeSpace);
+ }
+
+ /*package*/ static long calculateGlobalLimit(long fileSystemSizeBytes,
+ long freeSpaceBytes) {
+ if (fileSystemSizeBytes <= 0
+ || freeSpaceBytes <= 0
+ || freeSpaceBytes > fileSystemSizeBytes) {
+ return 0;
+ }
+
+ long fileSystemSizeRatio =
+ 2 << ((int) Math.floor(Math.log10(
+ fileSystemSizeBytes / (1024 * 1024))));
+ long maxSizeBytes = (long) Math.min(Math.floor(
+ fileSystemSizeBytes / fileSystemSizeRatio),
+ Math.floor(freeSpaceBytes / 2));
+ // Round maxSizeBytes up to a multiple of 1024KB (but only if
+ // maxSizeBytes > 1MB).
+ long maxSizeStepBytes = 1024 * 1024;
+ if (maxSizeBytes < maxSizeStepBytes) {
+ return 0;
+ }
+ long roundingExtra = maxSizeBytes % maxSizeStepBytes == 0 ? 0 : 1;
+ return (maxSizeStepBytes
+ * ((maxSizeBytes / maxSizeStepBytes) + roundingExtra));
+ }
+
+ // Schedules a system notification that takes the user to the WebSettings
+ // activity when clicked.
+ private void scheduleOutOfSpaceNotification() {
+ if(LOGV_ENABLED) {
+ Log.v(LOGTAG, "scheduleOutOfSpaceNotification called.");
+ }
+ if ((mLastOutOfSpaceNotificationTime == -1) ||
+ (System.currentTimeMillis() - mLastOutOfSpaceNotificationTime > NOTIFICATION_INTERVAL)) {
+ // setup the notification boilerplate.
+ int icon = android.R.drawable.stat_sys_warning;
+ CharSequence title = mContext.getString(
+ R.string.webstorage_outofspace_notification_title);
+ CharSequence text = mContext.getString(
+ R.string.webstorage_outofspace_notification_text);
+ long when = System.currentTimeMillis();
+ Intent intent = new Intent(mContext, BrowserPreferencesPage.class);
+ intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT,
+ WebsiteSettingsFragment.class.getName());
+ PendingIntent contentIntent =
+ PendingIntent.getActivity(mContext, 0, intent, 0);
+ Notification notification = new Notification(icon, title, when);
+ notification.setLatestEventInfo(mContext, title, text, contentIntent);
+ notification.flags |= Notification.FLAG_AUTO_CANCEL;
+ // Fire away.
+ String ns = Context.NOTIFICATION_SERVICE;
+ NotificationManager mgr =
+ (NotificationManager) mContext.getSystemService(ns);
+ if (mgr != null) {
+ mLastOutOfSpaceNotificationTime = System.currentTimeMillis();
+ mgr.notify(OUT_OF_SPACE_ID, notification);
+ }
+ }
+ }
+}
diff --git a/src/com/android/browser/WebViewController.java b/src/com/android/browser/WebViewController.java
new file mode 100644
index 0000000..6864470
--- /dev/null
+++ b/src/com/android/browser/WebViewController.java
@@ -0,0 +1,124 @@
+/*
+ * 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.Context;
+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 org.codeaurora.swe.HttpAuthHandler;
+import org.codeaurora.swe.SslErrorHandler;
+import android.webkit.ValueCallback;
+import android.webkit.WebChromeClient.CustomViewCallback;
+import org.codeaurora.swe.WebChromeClient;
+import org.codeaurora.swe.WebView;
+
+import java.util.List;
+
+/**
+ * WebView aspect of the controller
+ */
+public interface WebViewController {
+
+ Context getContext();
+
+ Activity getActivity();
+
+ TabControl getTabControl();
+
+ WebViewFactory getWebViewFactory();
+
+ void onSetWebView(Tab tab, WebView view);
+
+ void createSubWindow(Tab tab);
+
+ void onPageStarted(Tab tab, WebView view, Bitmap favicon);
+
+ void onPageFinished(Tab tab);
+
+ void onProgressChanged(Tab tab);
+
+ void onReceivedTitle(Tab tab, final String title);
+
+ void onFavicon(Tab tab, WebView view, Bitmap icon);
+
+ boolean shouldOverrideUrlLoading(Tab tab, WebView view, String url);
+
+ boolean shouldOverrideKeyEvent(KeyEvent event);
+
+ boolean onUnhandledKeyEvent(KeyEvent event);
+
+ void doUpdateVisitedHistory(Tab tab, 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, String referer, long contentLength);
+
+ void showCustomView(Tab tab, View view, int requestedOrientation,
+ CustomViewCallback callback);
+
+ void hideCustomView();
+
+ Bitmap getDefaultVideoPoster();
+
+ View getVideoLoadingProgressView();
+
+ void showSslCertificateOnError(WebView view, SslErrorHandler handler,
+ SslError error);
+
+ void onUserCanceledSsl(Tab tab);
+
+ boolean shouldShowErrorConsole();
+
+ void onUpdatedSecurityState(Tab tab);
+
+ void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture);
+
+ void endActionMode();
+
+ void attachSubWindow(Tab tab);
+
+ void dismissSubWindow(Tab tab);
+
+ Tab openTab(String url, boolean incognito, boolean setActive,
+ boolean useCurrent);
+
+ Tab openTab(String url, Tab parent, boolean setActive,
+ boolean useCurrent);
+
+ boolean switchToTab(Tab tab);
+
+ void closeTab(Tab tab);
+
+ void setupAutoFill(Message message);
+
+ void bookmarkedStatusHasChanged(Tab tab);
+
+ void showAutoLogin(Tab tab);
+
+ void hideAutoLogin(Tab tab);
+
+ boolean shouldCaptureThumbnails();
+}
diff --git a/src/com/android/browser/WebViewFactory.java b/src/com/android/browser/WebViewFactory.java
new file mode 100644
index 0000000..3ebd573
--- /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 org.codeaurora.swe.WebView;
+
+/**
+ * Factory for WebViews
+ */
+public interface WebViewFactory {
+
+ public WebView createWebView(boolean privateBrowsing);
+
+ public WebView createSubWebView(boolean privateBrowsing);
+
+}
diff --git a/src/com/android/browser/WebViewProperties.java b/src/com/android/browser/WebViewProperties.java
new file mode 100644
index 0000000..c662957
--- /dev/null
+++ b/src/com/android/browser/WebViewProperties.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2011 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;
+
+public interface WebViewProperties {
+ static final String gfxInvertedScreen = "inverted";
+ static final String gfxInvertedScreenContrast = "inverted_contrast";
+ static final String gfxEnableCpuUploadPath = "enable_cpu_upload_path";
+ static final String gfxUseMinimalMemory = "use_minimal_memory";
+}
diff --git a/src/com/android/browser/WebViewTimersControl.java b/src/com/android/browser/WebViewTimersControl.java
new file mode 100644
index 0000000..ac74fa1
--- /dev/null
+++ b/src/com/android/browser/WebViewTimersControl.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2011 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.os.Looper;
+import android.util.Log;
+import org.codeaurora.swe.WebView;
+
+/**
+ * Centralised point for controlling WebView timers pausing and resuming.
+ *
+ * All methods on this class should only be called from the UI thread.
+ */
+public class WebViewTimersControl {
+
+ private static final boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED;
+ private static final String LOGTAG = "WebViewTimersControl";
+
+ private static WebViewTimersControl sInstance;
+
+ private boolean mBrowserActive;
+ private boolean mPrerenderActive;
+
+ /**
+ * Get the static instance. Must be called from UI thread.
+ */
+ public static WebViewTimersControl getInstance() {
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ throw new IllegalStateException("WebViewTimersControl.get() called on wrong thread");
+ }
+ if (sInstance == null) {
+ sInstance = new WebViewTimersControl();
+ }
+ return sInstance;
+ }
+
+ private WebViewTimersControl() {
+ }
+
+ private void resumeTimers(WebView wv) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "Resuming webview timers, view=" + wv);
+ if (wv != null) {
+ wv.resumeTimers();
+ }
+ }
+
+ private void maybePauseTimers(WebView wv) {
+ if (!mBrowserActive && !mPrerenderActive && wv != null) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "Pausing webview timers, view=" + wv);
+ wv.pauseTimers();
+ }
+ }
+
+ public void onBrowserActivityResume(WebView wv) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "onBrowserActivityResume");
+ mBrowserActive = true;
+ resumeTimers(wv);
+ }
+
+ public void onBrowserActivityPause(WebView wv) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "onBrowserActivityPause");
+ mBrowserActive = false;
+ maybePauseTimers(wv);
+ }
+
+ public void onPrerenderStart(WebView wv) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "onPrerenderStart");
+ mPrerenderActive = true;
+ resumeTimers(wv);
+ }
+
+ public void onPrerenderDone(WebView wv) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "onPrerenderDone");
+ mPrerenderActive = false;
+ maybePauseTimers(wv);
+ }
+
+}
diff --git a/src/com/android/browser/XLargeUi.java b/src/com/android/browser/XLargeUi.java
new file mode 100644
index 0000000..8dd31d8
--- /dev/null
+++ b/src/com/android/browser/XLargeUi.java
@@ -0,0 +1,308 @@
+/*
+ * 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.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.graphics.drawable.PaintDrawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.view.ActionMode;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.R;
+
+import java.util.List;
+
+/**
+ * Ui for xlarge screen sizes
+ */
+public class XLargeUi extends BaseUi {
+
+ private static final String LOGTAG = "XLargeUi";
+
+ private PaintDrawable mFaviconBackground;
+
+ private ActionBar mActionBar;
+ private TabBar mTabBar;
+
+ private NavigationBarTablet mNavBar;
+
+ private Handler mHandler;
+
+ /**
+ * @param browser
+ * @param controller
+ */
+ public XLargeUi(Activity browser, UiController controller) {
+ super(browser, controller);
+ mHandler = new Handler();
+ mNavBar = (NavigationBarTablet) mTitleBar.getNavigationBar();
+ mTabBar = new TabBar(mActivity, mUiController, this);
+ mActionBar = mActivity.getActionBar();
+ setupActionBar();
+ setUseQuickControls(BrowserSettings.getInstance().useQuickControls());
+ }
+
+ private void setupActionBar() {
+ mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
+ mActionBar.setCustomView(mTabBar);
+ }
+
+ public void showComboView(ComboViews startWith, Bundle extras) {
+ super.showComboView(startWith, extras);
+ if (mUseQuickControls) {
+ mActionBar.show();
+ }
+ }
+
+ @Override
+ public void setUseQuickControls(boolean useQuickControls) {
+ super.setUseQuickControls(useQuickControls);
+ checkHideActionBar();
+ if (!useQuickControls) {
+ mActionBar.show();
+ }
+ mTabBar.setUseQuickControls(mUseQuickControls);
+ // We need to update the tabs with this change
+ for (Tab t : mTabControl.getTabs()) {
+ t.updateShouldCaptureThumbnails();
+ }
+ }
+
+ private void checkHideActionBar() {
+ if (mUseQuickControls) {
+ mHandler.post(new Runnable() {
+ public void run() {
+ mActionBar.hide();
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mNavBar.clearCompletions();
+ checkHideActionBar();
+ }
+
+ @Override
+ public void onDestroy() {
+ hideTitleBar();
+ }
+
+ void stopWebViewScrolling() {
+ BrowserWebView web = (BrowserWebView) mUiController.getCurrentWebView();
+ if (web != null) {
+ web.stopScroll();
+ }
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ MenuItem bm = menu.findItem(R.id.bookmarks_menu_id);
+ if (bm != null) {
+ bm.setVisible(false);
+ }
+ return true;
+ }
+
+
+ // WebView callbacks
+
+ @Override
+ public void addTab(Tab tab) {
+ mTabBar.onNewTab(tab);
+ }
+
+ protected void onAddTabCompleted(Tab tab) {
+ checkHideActionBar();
+ }
+
+ @Override
+ public void setActiveTab(final Tab tab) {
+ mTitleBar.cancelTitleBarAnimation(true);
+ mTitleBar.setSkipTitleBarAnimations(true);
+ super.setActiveTab(tab);
+ BrowserWebView view = (BrowserWebView) 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;
+ }
+ mTabBar.onSetActiveTab(tab);
+ updateLockIconToLatest(tab);
+ mTitleBar.setSkipTitleBarAnimations(false);
+ }
+
+ @Override
+ public void updateTabs(List<Tab> tabs) {
+ mTabBar.updateTabs(tabs);
+ checkHideActionBar();
+ }
+
+ @Override
+ public void removeTab(Tab tab) {
+ mTitleBar.cancelTitleBarAnimation(true);
+ mTitleBar.setSkipTitleBarAnimations(true);
+ super.removeTab(tab);
+ mTabBar.onRemoveTab(tab);
+ mTitleBar.setSkipTitleBarAnimations(false);
+ }
+
+ protected void onRemoveTabCompleted(Tab tab) {
+ checkHideActionBar();
+ }
+
+ int getContentWidth() {
+ if (mContentView != null) {
+ return mContentView.getWidth();
+ }
+ return 0;
+ }
+
+ @Override
+ public void editUrl(boolean clearInput, boolean forceIME) {
+ if (mUseQuickControls) {
+ mTitleBar.setShowProgressOnly(false);
+ }
+ super.editUrl(clearInput, forceIME);
+ }
+
+ // action mode callbacks
+
+ @Override
+ public void onActionModeStarted(ActionMode mode) {
+ if (!mTitleBar.isEditingUrl()) {
+ // hide the title bar when CAB is shown
+ hideTitleBar();
+ }
+ }
+
+ @Override
+ public void onActionModeFinished(boolean inLoad) {
+ checkHideActionBar();
+ if (inLoad) {
+ // the titlebar was removed when the CAB was shown
+ // if the page is loading, show it again
+ if (mUseQuickControls) {
+ mTitleBar.setShowProgressOnly(true);
+ }
+ showTitleBar();
+ }
+ }
+
+ @Override
+ protected void updateNavigationState(Tab tab) {
+ mNavBar.updateNavigationState(tab);
+ }
+
+ @Override
+ public void setUrlTitle(Tab tab) {
+ super.setUrlTitle(tab);
+ mTabBar.onUrlAndTitle(tab, tab.getUrl(), tab.getTitle());
+ }
+
+ // Set the favicon in the title bar.
+ @Override
+ public void setFavicon(Tab tab) {
+ super.setFavicon(tab);
+ mTabBar.onFavicon(tab, tab.getFavicon());
+ }
+
+ @Override
+ public void onHideCustomView() {
+ super.onHideCustomView();
+ checkHideActionBar();
+ }
+
+ @Override
+ public boolean dispatchKey(int code, KeyEvent event) {
+ if (mActiveTab != null) {
+ WebView web = mActiveTab.getWebView();
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ switch (code) {
+ case KeyEvent.KEYCODE_TAB:
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if ((web != null) && web.hasFocus() && !mTitleBar.hasFocus()) {
+ editUrl(false, false);
+ return true;
+ }
+ }
+ boolean ctrl = event.hasModifiers(KeyEvent.META_CTRL_ON);
+ if (!ctrl && isTypingKey(event) && !mTitleBar.isEditingUrl()) {
+ editUrl(true, false);
+ return mContentView.dispatchKeyEvent(event);
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean isTypingKey(KeyEvent evt) {
+ return evt.getUnicodeChar() > 0;
+ }
+
+ TabBar getTabBar() {
+ return mTabBar;
+ }
+
+ @Override
+ public boolean shouldCaptureThumbnails() {
+ return mUseQuickControls;
+ }
+
+ private Drawable getFaviconBackground() {
+ if (mFaviconBackground == null) {
+ mFaviconBackground = new PaintDrawable();
+ Resources res = mActivity.getResources();
+ mFaviconBackground.getPaint().setColor(
+ res.getColor(R.color.tabFaviconBackground));
+ mFaviconBackground.setCornerRadius(
+ res.getDimension(R.dimen.tab_favicon_corner_radius));
+ }
+ return mFaviconBackground;
+ }
+
+ @Override
+ public Drawable getFaviconDrawable(Bitmap icon) {
+ Drawable[] array = new Drawable[2];
+ array[0] = getFaviconBackground();
+ if (icon == null) {
+ array[1] = mGenericFavicon;
+ } else {
+ array[1] = new BitmapDrawable(mActivity.getResources(), icon);
+ }
+ LayerDrawable d = new LayerDrawable(array);
+ d.setLayerInset(1, 2, 2, 2, 2);
+ return d;
+ }
+
+}
diff --git a/src/com/android/browser/addbookmark/FolderSpinner.java b/src/com/android/browser/addbookmark/FolderSpinner.java
new file mode 100644
index 0000000..dd85cda
--- /dev/null
+++ b/src/com/android/browser/addbookmark/FolderSpinner.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2011 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.addbookmark;
+
+import android.content.Context;
+import android.view.View;
+import android.util.AttributeSet;
+import android.widget.AdapterView;
+import android.widget.Spinner;
+
+/**
+ * Special Spinner class with its own callback for when the selection is set, which
+ * can be ignored by calling setSelectionIgnoringSelectionChange
+ */
+public class FolderSpinner extends Spinner
+ implements AdapterView.OnItemSelectedListener {
+ private OnSetSelectionListener mOnSetSelectionListener;
+ private boolean mFireSetSelection;
+
+ /**
+ * Callback for knowing when the selection has been manually set. Does not
+ * get called until the selected view has changed.
+ */
+ public interface OnSetSelectionListener {
+ public void onSetSelection(long id);
+ }
+
+ public FolderSpinner(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ super.setOnItemSelectedListener(this);
+ }
+
+ @Override
+ public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener l) {
+ // Disallow setting an OnItemSelectedListener, since it is used by us
+ // to fire onSetSelection.
+ throw new RuntimeException("Cannot set an OnItemSelectedListener on a FolderSpinner");
+ }
+
+ public void setOnSetSelectionListener(OnSetSelectionListener l) {
+ mOnSetSelectionListener = l;
+ }
+
+ /**
+ * Call setSelection, without firing the callback
+ * @param position New position to select.
+ */
+ public void setSelectionIgnoringSelectionChange(int position) {
+ super.setSelection(position);
+ }
+
+ @Override
+ public void setSelection(int position) {
+ mFireSetSelection = true;
+ int oldPosition = getSelectedItemPosition();
+ super.setSelection(position);
+ if (mOnSetSelectionListener != null) {
+ if (oldPosition == position) {
+ long id = getAdapter().getItemId(position);
+ // Normally this is not called because the item did not actually
+ // change, but in this case, we still want it to be called.
+ onItemSelected(this, null, position, id);
+ }
+ }
+ }
+
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ if (mFireSetSelection) {
+ mOnSetSelectionListener.onSetSelection(id);
+ mFireSetSelection = false;
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {}
+}
+
diff --git a/src/com/android/browser/addbookmark/FolderSpinnerAdapter.java b/src/com/android/browser/addbookmark/FolderSpinnerAdapter.java
new file mode 100644
index 0000000..f86c9c6
--- /dev/null
+++ b/src/com/android/browser/addbookmark/FolderSpinnerAdapter.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2011 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.addbookmark;
+
+import com.android.browser.R;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+/**
+ * SpinnerAdapter used in the AddBookmarkPage to select where to save a
+ * bookmark/folder.
+ */
+public class FolderSpinnerAdapter extends BaseAdapter {
+
+ public static final int HOME_SCREEN = 0;
+ public static final int ROOT_FOLDER = 1;
+ public static final int OTHER_FOLDER = 2;
+ public static final int RECENT_FOLDER = 3;
+
+ private boolean mIncludeHomeScreen;
+ private boolean mIncludesRecentFolder;
+ private long mRecentFolderId;
+ private String mRecentFolderName;
+ private LayoutInflater mInflater;
+ private Context mContext;
+ private String mOtherFolderDisplayText;
+
+ public FolderSpinnerAdapter(Context context, boolean includeHomeScreen) {
+ mIncludeHomeScreen = includeHomeScreen;
+ mContext = context;
+ mInflater = LayoutInflater.from(mContext);
+ }
+
+ public void addRecentFolder(long folderId, String folderName) {
+ mIncludesRecentFolder = true;
+ mRecentFolderId = folderId;
+ mRecentFolderName = folderName;
+ }
+
+ public long recentFolderId() { return mRecentFolderId; }
+
+ private void bindView(int position, View view, boolean isDropDown) {
+ int labelResource;
+ int drawableResource;
+ if (!mIncludeHomeScreen) {
+ position++;
+ }
+ switch (position) {
+ case HOME_SCREEN:
+ labelResource = R.string.add_to_homescreen_menu_option;
+ drawableResource = R.drawable.ic_home_holo_dark;
+ break;
+ case ROOT_FOLDER:
+ labelResource = R.string.add_to_bookmarks_menu_option;
+ drawableResource = R.drawable.ic_bookmarks_holo_dark;
+ break;
+ case RECENT_FOLDER:
+ // Fall through and use the same icon resource
+ case OTHER_FOLDER:
+ labelResource = R.string.add_to_other_folder_menu_option;
+ drawableResource = R.drawable.ic_folder_holo_dark;
+ break;
+ default:
+ labelResource = 0;
+ drawableResource = 0;
+ // assert
+ break;
+ }
+ TextView textView = (TextView) view;
+ if (position == RECENT_FOLDER) {
+ textView.setText(mRecentFolderName);
+ } else if (position == OTHER_FOLDER && !isDropDown
+ && mOtherFolderDisplayText != null) {
+ textView.setText(mOtherFolderDisplayText);
+ } else {
+ textView.setText(labelResource);
+ }
+ textView.setGravity(Gravity.CENTER_VERTICAL);
+ Drawable drawable = mContext.getResources().getDrawable(drawableResource);
+ textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null,
+ null, null);
+ }
+
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = mInflater.inflate(
+ android.R.layout.simple_spinner_dropdown_item, parent, false);
+ }
+ bindView(position, convertView, true);
+ return convertView;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = mInflater.inflate(android.R.layout.simple_spinner_item,
+ parent, false);
+ }
+ bindView(position, convertView, false);
+ return convertView;
+ }
+
+ @Override
+ public int getCount() {
+ int count = 2;
+ if (mIncludeHomeScreen) count++;
+ if (mIncludesRecentFolder) count++;
+ return count;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return null;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ long id = position;
+ if (!mIncludeHomeScreen) {
+ id++;
+ }
+ return id;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ public void setOtherFolderDisplayText(String parentTitle) {
+ mOtherFolderDisplayText = parentTitle;
+ notifyDataSetChanged();
+ }
+
+ public void clearRecentFolder() {
+ if (mIncludesRecentFolder) {
+ mIncludesRecentFolder = false;
+ notifyDataSetChanged();
+ }
+ }
+}
diff --git a/src/com/android/browser/homepages/HomeProvider.java b/src/com/android/browser/homepages/HomeProvider.java
new file mode 100644
index 0000000..045cdb8
--- /dev/null
+++ b/src/com/android/browser/homepages/HomeProvider.java
@@ -0,0 +1,124 @@
+
+/*
+ * Copyright (C) 2011 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.homepages;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+import android.webkit.WebResourceResponse;
+
+import com.android.browser.BrowserSettings;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+
+public class HomeProvider extends ContentProvider {
+
+ private static final String TAG = "HomeProvider";
+ public static final String AUTHORITY = "com.android.browser.home";
+ public static final String MOST_VISITED = "content://" + AUTHORITY + "/index";
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return null;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public boolean onCreate() {
+ return false;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ return null;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode) {
+ try {
+ ParcelFileDescriptor[] pipes = ParcelFileDescriptor.createPipe();
+ final ParcelFileDescriptor write = pipes[1];
+ AssetFileDescriptor afd = new AssetFileDescriptor(write, 0, -1);
+ new RequestHandler(getContext(), uri, afd.createOutputStream()).start();
+ return pipes[0];
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to handle request: " + uri, e);
+ return null;
+ }
+ }
+
+ public static WebResourceResponse shouldInterceptRequest(Context context,
+ String url) {
+ try {
+ boolean useMostVisited = BrowserSettings.getInstance().useMostVisitedHomepage();
+ if (useMostVisited && url.startsWith("content://")) {
+ Uri uri = Uri.parse(url);
+ if (AUTHORITY.equals(uri.getAuthority())) {
+ InputStream ins = context.getContentResolver()
+ .openInputStream(uri);
+ return new WebResourceResponse("text/html", "utf-8", ins);
+ }
+ }
+ boolean listFiles = BrowserSettings.getInstance().isDebugEnabled();
+ if (listFiles && interceptFile(url)) {
+ PipedInputStream ins = new PipedInputStream();
+ PipedOutputStream outs = new PipedOutputStream(ins);
+ new RequestHandler(context, Uri.parse(url), outs).start();
+ return new WebResourceResponse("text/html", "utf-8", ins);
+ }
+ } catch (Exception e) {}
+ return null;
+ }
+
+ private static boolean interceptFile(String url) {
+ if (!url.startsWith("file:///")) {
+ return false;
+ }
+ String fpath = url.substring(7);
+ File f = new File(fpath);
+ if (!f.isDirectory()) {
+ return false;
+ }
+ return true;
+ }
+
+}
\ No newline at end of file
diff --git a/src/com/android/browser/homepages/RequestHandler.java b/src/com/android/browser/homepages/RequestHandler.java
new file mode 100644
index 0000000..8dbd0ef
--- /dev/null
+++ b/src/com/android/browser/homepages/RequestHandler.java
@@ -0,0 +1,264 @@
+
+/*
+ * Copyright (C) 2011 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.homepages;
+
+import android.content.Context;
+import android.content.UriMatcher;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.MergeCursor;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Log;
+
+import com.android.browser.R;
+import com.android.browser.homepages.Template.ListEntityIterator;
+import com.android.browser.platformsupport.BrowserContract.Bookmarks;
+import com.android.browser.platformsupport.BrowserContract.History;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.DateFormat;
+import java.text.DecimalFormat;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class RequestHandler extends Thread {
+
+ private static final String TAG = "RequestHandler";
+ private static final int INDEX = 1;
+ private static final int RESOURCE = 2;
+ private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+ Uri mUri;
+ Context mContext;
+ OutputStream mOutput;
+
+ static {
+ sUriMatcher.addURI(HomeProvider.AUTHORITY, "index", INDEX);
+ sUriMatcher.addURI(HomeProvider.AUTHORITY, "res/*/*", RESOURCE);
+ }
+
+ public RequestHandler(Context context, Uri uri, OutputStream out) {
+ mUri = uri;
+ mContext = context.getApplicationContext();
+ mOutput = out;
+ }
+
+ @Override
+ public void run() {
+ super.run();
+ try {
+ doHandleRequest();
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to handle request: " + mUri, e);
+ } finally {
+ cleanup();
+ }
+ }
+
+ void doHandleRequest() throws IOException {
+ if ("file".equals(mUri.getScheme())) {
+ writeFolderIndex();
+ return;
+ }
+ int match = sUriMatcher.match(mUri);
+ switch (match) {
+ case INDEX:
+ writeTemplatedIndex();
+ break;
+ case RESOURCE:
+ writeResource(getUriResourcePath());
+ break;
+ }
+ }
+
+ byte[] htmlEncode(String s) {
+ return TextUtils.htmlEncode(s).getBytes();
+ }
+
+ // We can reuse this for both History and Bookmarks queries because the
+ // columns defined actually belong to the CommonColumn and ImageColumn
+ // interfaces that both History and Bookmarks implement
+ private static final String[] PROJECTION = new String[] {
+ History.URL,
+ History.TITLE,
+ History.THUMBNAIL
+ };
+ private static final String SELECTION = History.URL
+ + " NOT LIKE 'content:%' AND " + History.THUMBNAIL + " IS NOT NULL";
+ void writeTemplatedIndex() throws IOException {
+ Template t = Template.getCachedTemplate(mContext, R.raw.most_visited);
+ Cursor historyResults = mContext.getContentResolver().query(
+ History.CONTENT_URI, PROJECTION, SELECTION,
+ null, History.VISITS + " DESC LIMIT 12");
+ Cursor cursor = historyResults;
+ try {
+ if (cursor.getCount() < 12) {
+ Cursor bookmarkResults = mContext.getContentResolver().query(
+ Bookmarks.CONTENT_URI, PROJECTION, SELECTION,
+ null, Bookmarks.DATE_CREATED + " DESC LIMIT 12");
+ cursor = new MergeCursor(new Cursor[] { historyResults, bookmarkResults }) {
+ @Override
+ public int getCount() {
+ return Math.min(12, super.getCount());
+ }
+ };
+ }
+ t.assignLoop("most_visited", new Template.CursorListEntityWrapper(cursor) {
+ @Override
+ public void writeValue(OutputStream stream, String key) throws IOException {
+ Cursor cursor = getCursor();
+ if (key.equals("url")) {
+ stream.write(htmlEncode(cursor.getString(0)));
+ } else if (key.equals("title")) {
+ stream.write(htmlEncode(cursor.getString(1)));
+ } else if (key.equals("thumbnail")) {
+ stream.write("data:image/png;base64,".getBytes());
+ byte[] thumb = cursor.getBlob(2);
+ stream.write(Base64.encode(thumb, Base64.DEFAULT));
+ }
+ }
+ });
+ t.write(mOutput);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private static final Comparator<File> sFileComparator = new Comparator<File>() {
+ @Override
+ public int compare(File lhs, File rhs) {
+ if (lhs.isDirectory() != rhs.isDirectory()) {
+ return lhs.isDirectory() ? -1 : 1;
+ }
+ return lhs.getName().compareTo(rhs.getName());
+ }
+ };
+
+ void writeFolderIndex() throws IOException {
+ File f = new File(mUri.getPath());
+ final File[] files = f.listFiles();
+ Arrays.sort(files, sFileComparator);
+ Template t = Template.getCachedTemplate(mContext, R.raw.folder_view);
+ t.assign("path", mUri.getPath());
+ t.assign("parent_url", f.getParent() != null ? f.getParent() : f.getPath());
+ t.assignLoop("files", new ListEntityIterator() {
+ int index = -1;
+
+ @Override
+ public void writeValue(OutputStream stream, String key) throws IOException {
+ File f = files[index];
+ if ("name".equals(key)) {
+ stream.write(f.getName().getBytes());
+ }
+ if ("url".equals(key)) {
+ stream.write(("file://" + f.getAbsolutePath()).getBytes());
+ }
+ if ("type".equals(key)) {
+ stream.write((f.isDirectory() ? "dir" : "file").getBytes());
+ }
+ if ("size".equals(key)) {
+ if (f.isFile()) {
+ stream.write(readableFileSize(f.length()).getBytes());
+ }
+ }
+ if ("last_modified".equals(key)) {
+ String date = DateFormat.getDateTimeInstance(
+ DateFormat.SHORT, DateFormat.SHORT)
+ .format(f.lastModified());
+ stream.write(date.getBytes());
+ }
+ if ("alt".equals(key)) {
+ if (index % 2 == 0) {
+ stream.write("alt".getBytes());
+ }
+ }
+ }
+
+ @Override
+ public ListEntityIterator getListIterator(String key) {
+ return null;
+ }
+
+ @Override
+ public void reset() {
+ index = -1;
+ }
+
+ @Override
+ public boolean moveToNext() {
+ return (++index) < files.length;
+ }
+ });
+ t.write(mOutput);
+ }
+
+ static String readableFileSize(long size) {
+ if(size <= 0) return "0";
+ final String[] units = new String[] { "B", "KB", "MB", "GB", "TB" };
+ int digitGroups = (int) (Math.log10(size) / Math.log10(1024));
+ return new DecimalFormat("#,##0.#").format(
+ size / Math.pow(1024, digitGroups)) + " " + units[digitGroups];
+ }
+
+ String getUriResourcePath() {
+ final Pattern pattern = Pattern.compile("/?res/([\\w/]+)");
+ Matcher m = pattern.matcher(mUri.getPath());
+ if (m.matches()) {
+ return m.group(1);
+ } else {
+ return mUri.getPath();
+ }
+ }
+
+ void writeResource(String fileName) throws IOException {
+ Resources res = mContext.getResources();
+ String packageName = R.class.getPackage().getName();
+ int id = res.getIdentifier(fileName, null, packageName);
+ if (id != 0) {
+ InputStream in = res.openRawResource(id);
+ byte[] buf = new byte[4096];
+ int read;
+ while ((read = in.read(buf)) > 0) {
+ mOutput.write(buf, 0, read);
+ }
+ }
+ }
+
+ void writeString(String str) throws IOException {
+ mOutput.write(str.getBytes());
+ }
+
+ void writeString(String str, int offset, int count) throws IOException {
+ mOutput.write(str.getBytes(), offset, count);
+ }
+
+ void cleanup() {
+ try {
+ mOutput.close();
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to close pipe!", e);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/com/android/browser/homepages/Template.java b/src/com/android/browser/homepages/Template.java
new file mode 100644
index 0000000..cf31bbd
--- /dev/null
+++ b/src/com/android/browser/homepages/Template.java
@@ -0,0 +1,281 @@
+
+/*
+ * Copyright (C) 2011 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.homepages;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.util.TypedValue;
+
+import com.android.browser.R;
+
+public class Template {
+
+ private static HashMap<Integer, Template> sCachedTemplates = new HashMap<Integer, Template>();
+
+ public static Template getCachedTemplate(Context context, int id) {
+ synchronized (sCachedTemplates) {
+ Template template = sCachedTemplates.get(id);
+ if (template == null) {
+ template = new Template(context, id);
+ sCachedTemplates.put(id, template);
+ }
+ // Return a copy so that we don't share data
+ return template.copy();
+ }
+ }
+
+ interface Entity {
+ void write(OutputStream stream, EntityData params) throws IOException;
+ }
+
+ interface EntityData {
+ void writeValue(OutputStream stream, String key) throws IOException;
+ ListEntityIterator getListIterator(String key);
+ }
+
+ interface ListEntityIterator extends EntityData {
+ void reset();
+ boolean moveToNext();
+ }
+
+ static class StringEntity implements Entity {
+
+ byte[] mValue;
+
+ public StringEntity(String value) {
+ mValue = value.getBytes();
+ }
+
+ @Override
+ public void write(OutputStream stream, EntityData params) throws IOException {
+ stream.write(mValue);
+ }
+
+ }
+
+ static class SimpleEntity implements Entity {
+
+ String mKey;
+
+ public SimpleEntity(String key) {
+ mKey = key;
+ }
+
+ @Override
+ public void write(OutputStream stream, EntityData params) throws IOException {
+ params.writeValue(stream, mKey);
+ }
+
+ }
+
+ static class ListEntity implements Entity {
+
+ String mKey;
+ Template mSubTemplate;
+
+ public ListEntity(Context context, String key, String subTemplate) {
+ mKey = key;
+ mSubTemplate = new Template(context, subTemplate);
+ }
+
+ @Override
+ public void write(OutputStream stream, EntityData params) throws IOException {
+ ListEntityIterator iter = params.getListIterator(mKey);
+ iter.reset();
+ while (iter.moveToNext()) {
+ mSubTemplate.write(stream, iter);
+ }
+ }
+
+ }
+
+ public abstract static class CursorListEntityWrapper implements ListEntityIterator {
+
+ private Cursor mCursor;
+
+ public CursorListEntityWrapper(Cursor cursor) {
+ mCursor = cursor;
+ }
+
+ @Override
+ public boolean moveToNext() {
+ return mCursor.moveToNext();
+ }
+
+ @Override
+ public void reset() {
+ mCursor.moveToPosition(-1);
+ }
+
+ @Override
+ public ListEntityIterator getListIterator(String key) {
+ return null;
+ }
+
+ public Cursor getCursor() {
+ return mCursor;
+ }
+
+ }
+
+ static class HashMapEntityData implements EntityData {
+
+ HashMap<String, Object> mData;
+
+ public HashMapEntityData(HashMap<String, Object> map) {
+ mData = map;
+ }
+
+ @Override
+ public ListEntityIterator getListIterator(String key) {
+ return (ListEntityIterator) mData.get(key);
+ }
+
+ @Override
+ public void writeValue(OutputStream stream, String key) throws IOException {
+ stream.write((byte[]) mData.get(key));
+ }
+
+ }
+
+ private List<Entity> mTemplate;
+ private HashMap<String, Object> mData = new HashMap<String, Object>();
+ private Template(Context context, int tid) {
+ this(context, readRaw(context, tid));
+ }
+
+ private Template(Context context, String template) {
+ mTemplate = new ArrayList<Entity>();
+ template = replaceConsts(context, template);
+ parseTemplate(context, template);
+ }
+
+ private Template(Template copy) {
+ mTemplate = copy.mTemplate;
+ }
+
+ Template copy() {
+ return new Template(this);
+ }
+
+ void parseTemplate(Context context, String template) {
+ final Pattern pattern = Pattern.compile("<%([=\\{])\\s*(\\w+)\\s*%>");
+ Matcher m = pattern.matcher(template);
+ int start = 0;
+ while (m.find()) {
+ String static_part = template.substring(start, m.start());
+ if (static_part.length() > 0) {
+ mTemplate.add(new StringEntity(static_part));
+ }
+ String type = m.group(1);
+ String name = m.group(2);
+ if (type.equals("=")) {
+ mTemplate.add(new SimpleEntity(name));
+ } else if (type.equals("{")) {
+ Pattern p = Pattern.compile("<%\\}\\s*" + Pattern.quote(name) + "\\s*%>");
+ Matcher end_m = p.matcher(template);
+ if (end_m.find(m.end())) {
+ start = m.end();
+ m.region(end_m.end(), template.length());
+ String subTemplate = template.substring(start, end_m.start());
+ mTemplate.add(new ListEntity(context, name, subTemplate));
+ start = end_m.end();
+ continue;
+ }
+ }
+ start = m.end();
+ }
+ String static_part = template.substring(start, template.length());
+ if (static_part.length() > 0) {
+ mTemplate.add(new StringEntity(static_part));
+ }
+ }
+
+ public void assign(String name, String value) {
+ mData.put(name, value.getBytes());
+ }
+
+ public void assignLoop(String name, ListEntityIterator iter) {
+ mData.put(name, iter);
+ }
+
+ public void write(OutputStream stream) throws IOException {
+ write(stream, new HashMapEntityData(mData));
+ }
+
+ public void write(OutputStream stream, EntityData data) throws IOException {
+ for (Entity ent : mTemplate) {
+ ent.write(stream, data);
+ }
+ }
+
+ private static String replaceConsts(Context context, String template) {
+ final Pattern pattern = Pattern.compile("<%@\\s*(\\w+/\\w+)\\s*%>");
+ final Resources res = context.getResources();
+ final String packageName = R.class.getPackage().getName();
+ Matcher m = pattern.matcher(template);
+ StringBuffer sb = new StringBuffer();
+ while (m.find()) {
+ String name = m.group(1);
+ if (name.startsWith("drawable/")) {
+ m.appendReplacement(sb, "res/" + name);
+ } else {
+ int id = res.getIdentifier(name, null, packageName);
+ if (id != 0) {
+ TypedValue value = new TypedValue();
+ res.getValue(id, value, true);
+ String replacement;
+ if (value.type == TypedValue.TYPE_DIMENSION) {
+ float dimen = res.getDimension(id);
+ int dimeni = (int) dimen;
+ if (dimeni == dimen)
+ replacement = Integer.toString(dimeni);
+ else
+ replacement = Float.toString(dimen);
+ } else {
+ replacement = value.coerceToString().toString();
+ }
+ m.appendReplacement(sb, replacement);
+ }
+ }
+ }
+ m.appendTail(sb);
+ return sb.toString();
+ }
+
+ private static String readRaw(Context context, int id) {
+ InputStream ins = context.getResources().openRawResource(id);
+ try {
+ byte[] buf = new byte[ins.available()];
+ ins.read(buf);
+ return new String(buf, "utf-8");
+ } catch (IOException ex) {
+ return "<html><body>Error</body></html>";
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/com/android/browser/mynavigation/AddMyNavigationPage.java b/src/com/android/browser/mynavigation/AddMyNavigationPage.java
new file mode 100755
index 0000000..e750aa2
--- /dev/null
+++ b/src/com/android/browser/mynavigation/AddMyNavigationPage.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of The Linux Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.android.browser.mynavigation;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.ParseException;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.view.View;
+import android.view.Window;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.util.Log;
+
+import com.android.browser.BrowserUtils;
+import com.android.browser.R;
+import com.android.browser.UrlUtils;
+import com.android.browser.platformsupport.WebAddress;
+
+import java.io.ByteArrayOutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+public class AddMyNavigationPage extends Activity {
+
+ private static final String LOGTAG = "AddMyNavigationPage";
+ private static final int SAVE_SITE_NAVIGATION = 100;
+
+ private EditText mName;
+ private EditText mAddress;
+ private Button mButtonOK;
+ private Button mButtonCancel;
+ private Bundle mMap;
+ private String mItemUrl;
+ private boolean mIsAdding;
+ private TextView mDialogText;
+ private Handler mHandler;
+
+ private View.OnClickListener mOKListener = new View.OnClickListener() {
+ public void onClick(View v) {
+ if (save()) {
+ AddMyNavigationPage.this.setResult(Activity.RESULT_OK,
+ (new Intent()).putExtra("need_refresh", true));
+ finish();
+ }
+ }
+ };
+
+ private View.OnClickListener mCancelListener = new View.OnClickListener() {
+ public void onClick(View v) {
+ finish();
+ }
+ };
+
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ setContentView(R.layout.my_navigation_add_page);
+ String name = null;
+ String url = null;
+ mMap = getIntent().getExtras();
+ if (mMap != null) {
+ Bundle b = mMap.getBundle("websites");
+ if (b != null) {
+ mMap = b;
+ }
+ name = mMap.getString("name");
+ url = mMap.getString("url");
+ mIsAdding = mMap.getBoolean("isAdding");
+ }
+
+ // The original url
+ mItemUrl = url;
+ mName = (EditText) findViewById(R.id.title);
+ mAddress = (EditText) findViewById(R.id.address);
+
+ BrowserUtils.maxLengthFilter(AddMyNavigationPage.this, mName,
+ BrowserUtils.FILENAME_MAX_LENGTH);
+ BrowserUtils.maxLengthFilter(AddMyNavigationPage.this, mAddress,
+ BrowserUtils.ADDRESS_MAX_LENGTH);
+
+ if (url.startsWith("ae://") && url.endsWith("add-fav")) {
+ mName.setText("");
+ mAddress.setText("");
+ } else {
+ mName.setText(name);
+ mAddress.setText(url);
+ }
+ mDialogText = (TextView) findViewById(R.id.dialog_title);
+ if (mIsAdding) {
+ mDialogText.setText(R.string.my_navigation_add_label);
+ }
+
+ mButtonOK = (Button) findViewById(R.id.OK);
+ mButtonOK.setOnClickListener(mOKListener);
+ mButtonCancel = (Button) findViewById(R.id.cancel);
+ mButtonCancel.setOnClickListener(mCancelListener);
+
+ if (!getWindow().getDecorView().isInTouchMode()) {
+ mButtonOK.requestFocus();
+ }
+ }
+
+ /**
+ * Runnable to save a website
+ */
+ private class SaveMyNavigationRunnable implements Runnable {
+ private Message mMessage;
+
+ public SaveMyNavigationRunnable(Message msg) {
+ mMessage = msg;
+ }
+
+ public void run() {
+ Bundle bundle = mMessage.getData();
+ String title = bundle.getString("title");
+ String url = bundle.getString("url");
+ String itemUrl = bundle.getString("itemUrl");
+ Boolean toDefaultThumbnail = bundle.getBoolean("toDefaultThumbnail");
+ ContentResolver cr = AddMyNavigationPage.this.getContentResolver();
+ Cursor cursor = null;
+ try {
+ cursor = cr.query(MyNavigationUtil.MY_NAVIGATION_URI,
+ new String[] {
+ MyNavigationUtil.ID
+ }, "url = ?", new String[] {
+ itemUrl
+ }, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ ContentValues values = new ContentValues();
+ values.put(MyNavigationUtil.TITLE, title);
+ values.put(MyNavigationUtil.URL, url);
+ values.put(MyNavigationUtil.WEBSITE, 1 + "");
+ if (toDefaultThumbnail) {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ Bitmap bm = BitmapFactory.decodeResource(
+ AddMyNavigationPage.this.getResources(),
+ R.raw.my_navigation_thumbnail_default);
+ bm.compress(Bitmap.CompressFormat.PNG, 100, os);
+ values.put(MyNavigationUtil.THUMBNAIL, os.toByteArray());
+ }
+ Uri uri = ContentUris.withAppendedId(MyNavigationUtil.MY_NAVIGATION_URI,
+ cursor.getLong(0));
+ cr.update(uri, values, null, null);
+ } else {
+ Log.e(LOGTAG, "this item does not exist!");
+ }
+ } catch (IllegalStateException e) {
+ Log.e(LOGTAG, "SaveMyNavigationRunnable", e);
+ } finally {
+ if (null != cursor) {
+ cursor.close();
+ }
+ }
+ }
+ }
+
+ boolean save() {
+ String name = mName.getText().toString().trim();
+ String unfilteredUrl = UrlUtils.fixUrl(mAddress.getText().toString());
+ boolean emptyTitle = name.length() == 0;
+ boolean emptyUrl = unfilteredUrl.trim().length() == 0;
+ Resources r = getResources();
+ if (emptyTitle || emptyUrl) {
+ if (emptyTitle) {
+ mName.setError(r.getText(R.string.website_needs_title));
+ }
+ if (emptyUrl) {
+ mAddress.setError(r.getText(R.string.website_needs_url));
+ }
+ return false;
+ }
+ String url = unfilteredUrl.trim();
+ try {
+ if (!url.toLowerCase().startsWith("javascript:")) {
+ URI uriObj = new URI(url);
+ String scheme = uriObj.getScheme();
+ if (!MyNavigationUtil.urlHasAcceptableScheme(url)) {
+ if (scheme != null) {
+ mAddress.setError(r.getText(R.string.my_navigation_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();
+ } else {
+ String mark = "://";
+ int iRet = -1;
+ if (null != url) {
+ iRet = url.indexOf(mark);
+ }
+ if (iRet > 0 && url.indexOf("/", iRet + mark.length()) < 0) {
+ url = url + "/";
+ Log.d(LOGTAG, "URL=" + url);
+ }
+ }
+ }
+ } catch (URISyntaxException e) {
+ mAddress.setError(r.getText(R.string.bookmark_url_not_valid));
+ return false;
+ }
+
+ // When it is adding, avoid duplicate url that already existing in the
+ // database
+ if (!mItemUrl.equals(url)) {
+ boolean exist = MyNavigationUtil.isMyNavigationUrl(this, url);
+ if (exist) {
+ mAddress.setError(r.getText(R.string.my_navigation_duplicate_url));
+ return false;
+ }
+ }
+ Bundle bundle = new Bundle();
+ bundle.putString("title", name);
+ bundle.putString("url", url);
+ bundle.putString("itemUrl", mItemUrl);
+ if (!mItemUrl.equals(url)) {
+ bundle.putBoolean("toDefaultThumbnail", true);
+ } else {
+ bundle.putBoolean("toDefaultThumbnail", false);
+ }
+ Message msg = Message.obtain(mHandler, SAVE_SITE_NAVIGATION);
+ msg.setData(bundle);
+ Thread t = new Thread(new SaveMyNavigationRunnable(msg));
+ t.start();
+ return true;
+ }
+}
diff --git a/src/com/android/browser/mynavigation/MyNavigationRequestHandler.java b/src/com/android/browser/mynavigation/MyNavigationRequestHandler.java
new file mode 100755
index 0000000..1de89d4
--- /dev/null
+++ b/src/com/android/browser/mynavigation/MyNavigationRequestHandler.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of The Linux Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.android.browser.mynavigation;
+
+import android.content.Context;
+import android.content.UriMatcher;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Log;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.android.browser.R;
+
+public class MyNavigationRequestHandler extends Thread {
+
+ private static final String LOGTAG = "MyNavigationRequestHandler";
+ private static final int MY_NAVIGATION = 1;
+ private static final int RESOURCE = 2;
+ private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+
+ Uri mUri;
+ Context mContext;
+ OutputStream mOutput;
+
+ static {
+ URI_MATCHER.addURI(MyNavigationUtil.AUTHORITY, "websites/res/*/*", RESOURCE);
+ URI_MATCHER.addURI(MyNavigationUtil.AUTHORITY, "websites", MY_NAVIGATION);
+ }
+
+ public MyNavigationRequestHandler(Context context, Uri uri, OutputStream out) {
+ mUri = uri;
+ mContext = context.getApplicationContext();
+ mOutput = out;
+ }
+
+ @Override
+ public void run() {
+ super.run();
+ try {
+ doHandleRequest();
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Failed to handle request: " + mUri, e);
+ } finally {
+ cleanup();
+ }
+ }
+
+ void doHandleRequest() throws IOException {
+ int match = URI_MATCHER.match(mUri);
+ switch (match) {
+ case MY_NAVIGATION:
+ writeTemplatedIndex();
+ break;
+ case RESOURCE:
+ writeResource(getUriResourcePath());
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void writeTemplatedIndex() throws IOException {
+ MyNavigationTemplate t = MyNavigationTemplate.getCachedTemplate(mContext,
+ R.raw.my_navigation);
+ Cursor cursor = mContext.getContentResolver().query(
+ Uri.parse("content://com.android.browser.mynavigation/websites"),
+ new String[] {
+ "url", "title", "thumbnail"
+ },
+ null, null, null);
+
+ t.assignLoop("my_navigation", new MyNavigationTemplate.CursorListEntityWrapper(cursor) {
+ @Override
+ public void writeValue(OutputStream stream, String key) throws IOException {
+ Cursor cursor = getCursor();
+ if (key.equals("url")) {
+ stream.write(htmlEncode(cursor.getString(0)));
+ } else if (key.equals("title")) {
+ String title = cursor.getString(1);
+ if (title == null || title.length() == 0) {
+ title = mContext.getString(R.string.my_navigation_add);
+ }
+ stream.write(htmlEncode(title));
+ } else if (key.equals("thumbnail")) {
+ stream.write("data:image/png".getBytes());
+ stream.write(htmlEncode(cursor.getString(0)));
+ stream.write(";base64,".getBytes());
+ byte[] thumb = cursor.getBlob(2);
+ stream.write(Base64.encode(thumb, Base64.DEFAULT));
+ }
+ }
+ });
+ t.write(mOutput);
+ cursor.close();
+ }
+
+ byte[] htmlEncode(String s) {
+ return TextUtils.htmlEncode(s).getBytes();
+ }
+
+ String getUriResourcePath() {
+ final Pattern pattern = Pattern.compile("/?res/([\\w/]+)");
+ Matcher m = pattern.matcher(mUri.getPath());
+ if (m.matches()) {
+ return m.group(1);
+ } else {
+ return mUri.getPath();
+ }
+ }
+
+ void writeResource(String fileName) throws IOException {
+ Resources res = mContext.getResources();
+ String packageName = R.class.getPackage().getName();
+ int id = res.getIdentifier(fileName, null, packageName);
+ if (id != 0) {
+ InputStream in = res.openRawResource(id);
+ byte[] buf = new byte[4096];
+ int read;
+ while ((read = in.read(buf)) > 0) {
+ mOutput.write(buf, 0, read);
+ }
+ }
+ }
+
+ void writeString(String str) throws IOException {
+ mOutput.write(str.getBytes());
+ }
+
+ void writeString(String str, int offset, int count) throws IOException {
+ mOutput.write(str.getBytes(), offset, count);
+ }
+
+ void cleanup() {
+ try {
+ mOutput.close();
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Failed to close pipe!", e);
+ }
+ }
+}
diff --git a/src/com/android/browser/mynavigation/MyNavigationTemplate.java b/src/com/android/browser/mynavigation/MyNavigationTemplate.java
new file mode 100755
index 0000000..85d1baf
--- /dev/null
+++ b/src/com/android/browser/mynavigation/MyNavigationTemplate.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of The Linux Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.android.browser.mynavigation;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.util.TypedValue;
+import android.util.Log;
+
+import com.android.browser.R;
+
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class MyNavigationTemplate {
+
+ private static final String LOGTAG = "MyNavigationTemplate";
+ private static HashMap<Integer, MyNavigationTemplate> sCachedTemplates =
+ new HashMap<Integer, MyNavigationTemplate>();
+ private static boolean sCountryChanged = false;
+ private static String sCurrentCountry = "US";
+
+ private List<Entity> mTemplate;
+ private HashMap<String, Object> mData = new HashMap<String, Object>();
+
+ public static MyNavigationTemplate getCachedTemplate(Context context, int id) {
+
+ String changeToCountry = context.getResources().getConfiguration().locale
+ .getDisplayCountry();
+ Log.d(LOGTAG, "MyNavigationTemplate.getCachedTemplate() display country :"
+ + changeToCountry + ", before country :" + sCurrentCountry);
+ if (changeToCountry != null && !changeToCountry.equals(sCurrentCountry)) {
+ sCountryChanged = true;
+ sCurrentCountry = changeToCountry;
+ }
+ synchronized (sCachedTemplates) {
+ MyNavigationTemplate template = sCachedTemplates.get(id);
+ if (template == null || sCountryChanged) {
+ sCountryChanged = false;
+ template = new MyNavigationTemplate(context, id);
+ sCachedTemplates.put(id, template);
+ }
+ return template.copy();
+ }
+ }
+
+ interface Entity {
+ void write(OutputStream stream, EntityData params) throws IOException;
+ }
+
+ interface EntityData {
+ void writeValue(OutputStream stream, String key) throws IOException;
+
+ ListEntityIterator getListIterator(String key);
+ }
+
+ interface ListEntityIterator extends EntityData {
+ void reset();
+
+ boolean moveToNext();
+ }
+
+ static class StringEntity implements Entity {
+ byte[] mValue;
+
+ public StringEntity(String value) {
+ mValue = value.getBytes();
+ }
+
+ @Override
+ public void write(OutputStream stream, EntityData params) throws IOException {
+ stream.write(mValue);
+ }
+ }
+
+ static class SimpleEntity implements Entity {
+ String mKey;
+
+ public SimpleEntity(String key) {
+ mKey = key;
+ }
+
+ @Override
+ public void write(OutputStream stream, EntityData params) throws IOException {
+ params.writeValue(stream, mKey);
+ }
+ }
+
+ static class ListEntity implements Entity {
+ String mKey;
+ MyNavigationTemplate mSubTemplate;
+
+ public ListEntity(Context context, String key, String subTemplate) {
+ mKey = key;
+ mSubTemplate = new MyNavigationTemplate(context, subTemplate);
+ }
+
+ @Override
+ public void write(OutputStream stream, EntityData params) throws IOException {
+ ListEntityIterator iter = params.getListIterator(mKey);
+ if (null == iter) {
+ return;
+ }
+ iter.reset();
+ while (iter.moveToNext()) {
+ mSubTemplate.write(stream, iter);
+ }
+ }
+ }
+
+ public abstract static class CursorListEntityWrapper implements ListEntityIterator {
+ private Cursor mCursor;
+
+ public CursorListEntityWrapper(Cursor cursor) {
+ mCursor = cursor;
+ }
+
+ @Override
+ public boolean moveToNext() {
+ return mCursor.moveToNext();
+ }
+
+ @Override
+ public void reset() {
+ mCursor.moveToPosition(-1);
+ }
+
+ @Override
+ public ListEntityIterator getListIterator(String key) {
+ return null;
+ }
+
+ public Cursor getCursor() {
+ return mCursor;
+ }
+ }
+
+ static class HashMapEntityData implements EntityData {
+ HashMap<String, Object> mData;
+
+ public HashMapEntityData(HashMap<String, Object> map) {
+ mData = map;
+ }
+
+ @Override
+ public ListEntityIterator getListIterator(String key) {
+ return (ListEntityIterator) mData.get(key);
+ }
+
+ @Override
+ public void writeValue(OutputStream stream, String key) throws IOException {
+ stream.write((byte[]) mData.get(key));
+ }
+ }
+
+ private MyNavigationTemplate(Context context, int tid) {
+ this(context, readRaw(context, tid));
+ }
+
+ private MyNavigationTemplate(Context context, String template) {
+ mTemplate = new ArrayList<Entity>();
+ template = replaceConsts(context, template);
+ parseTemplate(context, template);
+ }
+
+ private MyNavigationTemplate(MyNavigationTemplate copy) {
+ mTemplate = copy.mTemplate;
+ }
+
+ MyNavigationTemplate copy() {
+ return new MyNavigationTemplate(this);
+ }
+
+ void parseTemplate(Context context, String template) {
+ final Pattern pattern = Pattern.compile("<%([=\\{])\\s*(\\w+)\\s*%>");
+ Matcher m = pattern.matcher(template);
+ int start = 0;
+ while (m.find()) {
+ String staticPart = template.substring(start, m.start());
+ if (staticPart.length() > 0) {
+ mTemplate.add(new StringEntity(staticPart));
+ }
+ String type = m.group(1);
+ String name = m.group(2);
+ if (type.equals("=")) {
+ mTemplate.add(new SimpleEntity(name));
+ } else if (type.equals("{")) {
+ Pattern p = Pattern.compile("<%\\}\\s*" + Pattern.quote(name) + "\\s*%>");
+ Matcher end = p.matcher(template);
+ if (end.find(m.end())) {
+ start = m.end();
+ m.region(end.end(), template.length());
+ String subTemplate = template.substring(start, end.start());
+ mTemplate.add(new ListEntity(context, name, subTemplate));
+ start = end.end();
+ continue;
+ }
+ }
+ start = m.end();
+ }
+ String staticPart = template.substring(start, template.length());
+ if (staticPart.length() > 0) {
+ mTemplate.add(new StringEntity(staticPart));
+ }
+ }
+
+ public void assign(String name, String value) {
+ mData.put(name, value.getBytes());
+ }
+
+ public void assignLoop(String name, ListEntityIterator iter) {
+ mData.put(name, iter);
+ }
+
+ public void write(OutputStream stream) throws IOException {
+ write(stream, new HashMapEntityData(mData));
+ }
+
+ public void write(OutputStream stream, EntityData data) throws IOException {
+ for (Entity ent : mTemplate) {
+ ent.write(stream, data);
+ }
+ }
+
+ private static String replaceConsts(Context context, String template) {
+ final Pattern pattern = Pattern.compile("<%@\\s*(\\w+/\\w+)\\s*%>");
+ final Resources res = context.getResources();
+ final String packageName = R.class.getPackage().getName();
+ Matcher m = pattern.matcher(template);
+ StringBuffer sb = new StringBuffer();
+ while (m.find()) {
+ String name = m.group(1);
+ if (name.startsWith("drawable/")) {
+ m.appendReplacement(sb, "res/" + name);
+ } else {
+ int id = res.getIdentifier(name, null, packageName);
+ if (id != 0) {
+ TypedValue value = new TypedValue();
+ res.getValue(id, value, true);
+ String replacement;
+ if (value.type == TypedValue.TYPE_DIMENSION) {
+ float dimen = res.getDimension(id);
+ int dimeni = (int) dimen;
+ if (dimeni == dimen) {
+ replacement = Integer.toString(dimeni);
+ } else {
+ replacement = Float.toString(dimen);
+ }
+ } else {
+ replacement = value.coerceToString().toString();
+ }
+ m.appendReplacement(sb, replacement);
+ }
+ }
+ }
+ m.appendTail(sb);
+ return sb.toString();
+ }
+
+ private static String readRaw(Context context, int id) {
+ InputStream ins = context.getResources().openRawResource(id);
+ try {
+ byte[] buf = new byte[ins.available()];
+ ins.read(buf);
+ return new String(buf, "utf-8");
+ } catch (IOException ex) {
+ return "<html><body>Error</body></html>";
+ }
+ }
+}
diff --git a/src/com/android/browser/mynavigation/MyNavigationUtil.java b/src/com/android/browser/mynavigation/MyNavigationUtil.java
new file mode 100755
index 0000000..3b1836d
--- /dev/null
+++ b/src/com/android/browser/mynavigation/MyNavigationUtil.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of The Linux Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.android.browser.mynavigation;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.ContentResolver;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.util.Log;
+
+public class MyNavigationUtil {
+
+ public static final String ID = "_id";
+ public static final String URL = "url";
+ public static final String TITLE = "title";
+ public static final String DATE_CREATED = "created";
+ public static final String WEBSITE = "website";
+ public static final String FAVICON = "favicon";
+ public static final String THUMBNAIL = "thumbnail";
+ public static final int WEBSITE_NUMBER = 12;
+
+ public static final String AUTHORITY = "com.android.browser.mynavigation";
+ public static final String MY_NAVIGATION = "content://" + AUTHORITY + "/" + "websites";
+ public static final Uri MY_NAVIGATION_URI = Uri
+ .parse("content://com.android.browser.mynavigation/websites");
+ public static final String DEFAULT_THUMB = "default_thumb";
+ public static final String LOGTAG = "MyNavigationUtil";
+
+ public static boolean isDefaultMyNavigation(String url) {
+ if (url != null && url.startsWith("ae://") && url.endsWith("add-fav")) {
+ Log.d(LOGTAG, "isDefaultMyNavigation will return true.");
+ return true;
+ }
+ return false;
+ }
+
+ public static String getMyNavigationUrl(String srcUrl) {
+ String srcPrefix = "data:image/png";
+ String srcSuffix = ";base64,";
+ if (srcUrl != null && srcUrl.startsWith(srcPrefix)) {
+ int indexPrefix = srcPrefix.length();
+ int indexSuffix = srcUrl.indexOf(srcSuffix);
+ return srcUrl.substring(indexPrefix, indexSuffix);
+ }
+ return "";
+ }
+
+ public static boolean isMyNavigationUrl(Context context, String itemUrl) {
+ ContentResolver cr = context.getContentResolver();
+ Cursor cursor = null;
+ try {
+ cursor = cr.query(MyNavigationUtil.MY_NAVIGATION_URI,
+ new String[] {
+ MyNavigationUtil.TITLE
+ }, "url = ?", new String[] {
+ itemUrl
+ }, null);
+ if (null != cursor && cursor.moveToFirst()) {
+ Log.d(LOGTAG, "isMyNavigationUrl will return true.");
+ return true;
+ }
+ } catch (IllegalStateException e) {
+ Log.e(LOGTAG, "isMyNavigationUrl", e);
+ } finally {
+ if (null != cursor) {
+ cursor.close();
+ }
+ }
+ return false;
+ }
+
+ private static final String ACCEPTABLE_WEBSITE_SCHEMES[] = {
+ "http:",
+ "https:",
+ "about:",
+ "data:",
+ "javascript:",
+ "file:",
+ "content:"
+ };
+
+ public static boolean urlHasAcceptableScheme(String url) {
+ if (url == null) {
+ return false;
+ }
+
+ for (int i = 0; i < ACCEPTABLE_WEBSITE_SCHEMES.length; i++) {
+ if (url.startsWith(ACCEPTABLE_WEBSITE_SCHEMES[i])) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/browser/platformsupport/BookmarkColumns.java b/src/com/android/browser/platformsupport/BookmarkColumns.java
new file mode 100644
index 0000000..c1d9db8
--- /dev/null
+++ b/src/com/android/browser/platformsupport/BookmarkColumns.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2013 The Linux Foundation. All rights reserved.
+ * Not a contribution.
+ *
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser.platformsupport;
+
+public class BookmarkColumns {
+ /**
+ * The URL of the bookmark or history item.
+ * <p>Type: TEXT (URL)</p>
+ */
+ public static final String URL = "url";
+
+ /**
+ * The number of time the item has been visited.
+ * <p>Type: NUMBER</p>
+ */
+ public static final String VISITS = "visits";
+
+ /**
+ * The date the item was last visited, in milliseconds since the epoch.
+ * <p>Type: NUMBER (date in milliseconds since January 1, 1970)</p>
+ */
+ public static final String DATE = "date";
+
+ /**
+ * Flag indicating that an item is a bookmark. A value of 1 indicates a bookmark, a value
+ * of 0 indicates a history item.
+ * <p>Type: INTEGER (boolean)</p>
+ */
+ public static final String BOOKMARK = "bookmark";
+
+ /**
+ * The user visible title of the bookmark or history item.
+ * <p>Type: TEXT</p>
+ */
+ public static final String TITLE = "title";
+
+ /**
+ * The date the item created, in milliseconds since the epoch.
+ * <p>Type: NUMBER (date in milliseconds since January 1, 1970)</p>
+ */
+ public static final String CREATED = "created";
+
+ /**
+ * The favicon of the bookmark. Must decode via {@link BitmapFactory#decodeByteArray}.
+ * <p>Type: BLOB (image)</p>
+ */
+ public static final String FAVICON = "favicon";
+
+ /**
+ * @hide
+ */
+ public static final String THUMBNAIL = "thumbnail";
+
+ /**
+ * @hide
+ */
+ public static final String TOUCH_ICON = "touch_icon";
+
+ /**
+ * @hide
+ */
+ public static final String USER_ENTERED = "user_entered";
+}
diff --git a/src/com/android/browser/platformsupport/BrowserContract.java b/src/com/android/browser/platformsupport/BrowserContract.java
new file mode 100644
index 0000000..755e6a3
--- /dev/null
+++ b/src/com/android/browser/platformsupport/BrowserContract.java
@@ -0,0 +1,744 @@
+/*
+ * Copyright (c) 2013 The Linux Foundation. All rights reserved.
+ * Not a contribution.
+ *
+ * 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.platformsupport;
+
+import android.accounts.Account;
+import android.content.ContentProviderClient;
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.util.Pair;
+import android.provider.SyncStateContract;
+/**
+ * <p>
+ * The contract between the browser provider and applications. Contains the definition
+ * for the supported URIS and columns.
+ * </p>
+ * <h3>Overview</h3>
+ * <p>
+ * BrowserContract defines an database of browser-related information which are bookmarks,
+ * history, images and the mapping between the image and URL.
+ * </p>
+ * @hide
+ */
+public class BrowserContract {
+ /** The authority for the browser provider */
+ public static final String AUTHORITY = "com.android.browser";
+
+ /** A content:// style uri to the authority for the browser provider */
+ public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
+
+ /**
+ * An optional insert, update or delete URI parameter that allows the caller
+ * to specify that it is a sync adapter. The default value is false. If true
+ * the dirty flag is not automatically set and the "syncToNetwork" parameter
+ * is set to false when calling
+ * {@link ContentResolver#notifyChange(android.net.Uri, android.database.ContentObserver, boolean)}.
+ * @hide
+ */
+ public static final String CALLER_IS_SYNCADAPTER = "caller_is_syncadapter";
+
+ /**
+ * A parameter for use when querying any table that allows specifying a limit on the number
+ * of rows returned.
+ * @hide
+ */
+ public static final String PARAM_LIMIT = "limit";
+
+ /**
+ * Generic columns for use by sync adapters. The specific functions of
+ * these columns are private to the sync adapter. Other clients of the API
+ * should not attempt to either read or write these columns.
+ *
+ * @hide
+ */
+ interface BaseSyncColumns {
+ /** Generic column for use by sync adapters. */
+ public static final String SYNC1 = "sync1";
+ /** Generic column for use by sync adapters. */
+ public static final String SYNC2 = "sync2";
+ /** Generic column for use by sync adapters. */
+ public static final String SYNC3 = "sync3";
+ /** Generic column for use by sync adapters. */
+ public static final String SYNC4 = "sync4";
+ /** Generic column for use by sync adapters. */
+ public static final String SYNC5 = "sync5";
+ }
+
+ /**
+ * Convenience definitions for use in implementing chrome bookmarks sync in the Bookmarks table.
+ * @hide
+ */
+ public static final class ChromeSyncColumns {
+ private ChromeSyncColumns() {}
+
+ /** The server unique ID for an item */
+ public static final String SERVER_UNIQUE = BaseSyncColumns.SYNC3;
+
+ public static final String FOLDER_NAME_ROOT = "google_chrome";
+ public static final String FOLDER_NAME_BOOKMARKS = "google_chrome_bookmarks";
+ public static final String FOLDER_NAME_BOOKMARKS_BAR = "bookmark_bar";
+ public static final String FOLDER_NAME_OTHER_BOOKMARKS = "other_bookmarks";
+
+ /** The client unique ID for an item */
+ public static final String CLIENT_UNIQUE = BaseSyncColumns.SYNC4;
+ }
+
+ /**
+ * Columns that appear when each row of a table belongs to a specific
+ * account, including sync information that an account may need.
+ * @hide
+ */
+ interface SyncColumns extends BaseSyncColumns {
+ /**
+ * The name of the account instance to which this row belongs, which when paired with
+ * {@link #ACCOUNT_TYPE} identifies a specific account.
+ * <P>Type: TEXT</P>
+ */
+ public static final String ACCOUNT_NAME = "account_name";
+
+ /**
+ * The type of account to which this row belongs, which when paired with
+ * {@link #ACCOUNT_NAME} identifies a specific account.
+ * <P>Type: TEXT</P>
+ */
+ public static final String ACCOUNT_TYPE = "account_type";
+
+ /**
+ * String that uniquely identifies this row to its source account.
+ * <P>Type: TEXT</P>
+ */
+ public static final String SOURCE_ID = "sourceid";
+
+ /**
+ * Version number that is updated whenever this row or its related data
+ * changes.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String VERSION = "version";
+
+ /**
+ * Flag indicating that {@link #VERSION} has changed, and this row needs
+ * to be synchronized by its owning account.
+ * <P>Type: INTEGER (boolean)</P>
+ */
+ public static final String DIRTY = "dirty";
+
+ /**
+ * The time that this row was last modified by a client (msecs since the epoch).
+ * <P>Type: INTEGER</P>
+ */
+ public static final String DATE_MODIFIED = "modified";
+ }
+
+ interface CommonColumns {
+ /**
+ * The unique ID for a row.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String _ID = "_id";
+
+ /**
+ * This column is valid when the row is a URL. The history table's URL
+ * can not be updated.
+ * <P>Type: TEXT (URL)</P>
+ */
+ public static final String URL = "url";
+
+ /**
+ * The user visible title.
+ * <P>Type: TEXT</P>
+ */
+ public static final String TITLE = "title";
+
+ /**
+ * The time that this row was created on its originating client (msecs
+ * since the epoch).
+ * <P>Type: INTEGER</P>
+ * @hide
+ */
+ public static final String DATE_CREATED = "created";
+ }
+
+ /**
+ * @hide
+ */
+ interface ImageColumns {
+ /**
+ * The favicon of the bookmark, may be NULL.
+ * Must decode via {@link BitmapFactory#decodeByteArray}.
+ * <p>Type: BLOB (image)</p>
+ */
+ public static final String FAVICON = "favicon";
+
+ /**
+ * A thumbnail of the page,may be NULL.
+ * Must decode via {@link BitmapFactory#decodeByteArray}.
+ * <p>Type: BLOB (image)</p>
+ */
+ public static final String THUMBNAIL = "thumbnail";
+
+ /**
+ * The touch icon for the web page, may be NULL.
+ * Must decode via {@link BitmapFactory#decodeByteArray}.
+ * <p>Type: BLOB (image)</p>
+ */
+ public static final String TOUCH_ICON = "touch_icon";
+ }
+
+ interface HistoryColumns {
+ /**
+ * The date the item was last visited, in milliseconds since the epoch.
+ * <p>Type: INTEGER (date in milliseconds since January 1, 1970)</p>
+ */
+ public static final String DATE_LAST_VISITED = "date";
+
+ /**
+ * The number of times the item has been visited.
+ * <p>Type: INTEGER</p>
+ */
+ public static final String VISITS = "visits";
+
+ /**
+ * @hide
+ */
+ public static final String USER_ENTERED = "user_entered";
+ }
+
+ interface ImageMappingColumns {
+ /**
+ * The ID of the image in Images. One image can map onto the multiple URLs.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String IMAGE_ID = "image_id";
+
+ /**
+ * The URL. The URL can map onto the different type of images.
+ * <P>Type: TEXT (URL)</P>
+ */
+ public static final String URL = "url";
+ }
+
+ /**
+ * The bookmarks table, which holds the user's browser bookmarks.
+ */
+ public static final class Bookmarks implements CommonColumns, ImageColumns, SyncColumns {
+ /**
+ * This utility class cannot be instantiated.
+ */
+ private Bookmarks() {}
+
+ /**
+ * The content:// style URI for this table
+ */
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "bookmarks");
+
+ /**
+ * Used in {@link Bookmarks#TYPE} column and indicats the row is a bookmark.
+ */
+ public static final int BOOKMARK_TYPE_BOOKMARK = 1;
+
+ /**
+ * Used in {@link Bookmarks#TYPE} column and indicats the row is a folder.
+ */
+ public static final int BOOKMARK_TYPE_FOLDER = 2;
+
+ /**
+ * Used in {@link Bookmarks#TYPE} column and indicats the row is the bookmark bar folder.
+ */
+ public static final int BOOKMARK_TYPE_BOOKMARK_BAR_FOLDER = 3;
+
+ /**
+ * Used in {@link Bookmarks#TYPE} column and indicats the row is other folder and
+ */
+ public static final int BOOKMARK_TYPE_OTHER_FOLDER = 4;
+
+ /**
+ * Used in {@link Bookmarks#TYPE} column and indicats the row is other folder, .
+ */
+ public static final int BOOKMARK_TYPE_MOBILE_FOLDER = 5;
+
+ /**
+ * The type of the item.
+ * <P>Type: INTEGER</P>
+ * <p>Allowed values are:</p>
+ * <p>
+ * <ul>
+ * <li>{@link #BOOKMARK_TYPE_BOOKMARK}</li>
+ * <li>{@link #BOOKMARK_TYPE_FOLDER}</li>
+ * <li>{@link #BOOKMARK_TYPE_BOOKMARK_BAR_FOLDER}</li>
+ * <li>{@link #BOOKMARK_TYPE_OTHER_FOLDER}</li>
+ * <li>{@link #BOOKMARK_TYPE_MOBILE_FOLDER}</li>
+ * </ul>
+ * </p>
+ * <p> The TYPE_BOOKMARK_BAR_FOLDER, TYPE_OTHER_FOLDER and TYPE_MOBILE_FOLDER
+ * can not be updated or deleted.</p>
+ */
+ public static final String TYPE = "type";
+
+ /**
+ * The content:// style URI for the default folder
+ * @hide
+ */
+ public static final Uri CONTENT_URI_DEFAULT_FOLDER =
+ Uri.withAppendedPath(CONTENT_URI, "folder");
+
+ /**
+ * Query parameter used to specify an account name
+ * @hide
+ */
+ public static final String PARAM_ACCOUNT_NAME = "acct_name";
+
+ /**
+ * Query parameter used to specify an account type
+ * @hide
+ */
+ public static final String PARAM_ACCOUNT_TYPE = "acct_type";
+
+ /**
+ * Builds a URI that points to a specific folder.
+ * @param folderId the ID of the folder to point to
+ * @hide
+ */
+ public static final Uri buildFolderUri(long folderId) {
+ return ContentUris.withAppendedId(CONTENT_URI_DEFAULT_FOLDER, folderId);
+ }
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of bookmarks.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/bookmark";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} of a single bookmark.
+ */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/bookmark";
+
+ /**
+ * Query parameter to use if you want to see deleted bookmarks that are still
+ * around on the device and haven't been synced yet.
+ * @see #IS_DELETED
+ * @hide
+ */
+ public static final String QUERY_PARAMETER_SHOW_DELETED = "show_deleted";
+
+ /**
+ * Flag indicating if an item is a folder or bookmark. Non-zero values indicate
+ * a folder and zero indicates a bookmark.
+ * <P>Type: INTEGER (boolean)</P>
+ * @hide
+ */
+ public static final String IS_FOLDER = "folder";
+
+ /**
+ * The ID of the parent folder. ID 0 is the root folder.
+ * <P>Type: INTEGER (reference to item in the same table)</P>
+ */
+ public static final String PARENT = "parent";
+
+ /**
+ * The source ID for an item's parent. Read-only.
+ * @see #PARENT
+ * @hide
+ */
+ public static final String PARENT_SOURCE_ID = "parent_source";
+
+ /**
+ * The position of the bookmark in relation to it's siblings that share the same
+ * {@link #PARENT}. May be negative.
+ * <P>Type: INTEGER</P>
+ * @hide
+ */
+ public static final String POSITION = "position";
+
+ /**
+ * The item that the bookmark should be inserted after.
+ * May be negative.
+ * <P>Type: INTEGER</P>
+ * @hide
+ */
+ public static final String INSERT_AFTER = "insert_after";
+
+ /**
+ * The source ID for the item that the bookmark should be inserted after. Read-only.
+ * May be negative.
+ * <P>Type: INTEGER</P>
+ * @see #INSERT_AFTER
+ * @hide
+ */
+ public static final String INSERT_AFTER_SOURCE_ID = "insert_after_source";
+
+ /**
+ * A flag to indicate if an item has been deleted. Queries will not return deleted
+ * entries unless you add the {@link #QUERY_PARAMETER_SHOW_DELETED} query paramter
+ * to the URI when performing your query.
+ * <p>Type: INTEGER (non-zero if the item has been deleted, zero if it hasn't)
+ * @see #QUERY_PARAMETER_SHOW_DELETED
+ * @hide
+ */
+ public static final String IS_DELETED = "deleted";
+ }
+
+ /**
+ * Read-only table that lists all the accounts that are used to provide bookmarks.
+ * @hide
+ */
+ public static final class Accounts {
+ /**
+ * Directory under {@link Bookmarks#CONTENT_URI}
+ */
+ public static final Uri CONTENT_URI =
+ AUTHORITY_URI.buildUpon().appendPath("accounts").build();
+
+ /**
+ * The name of the account instance to which this row belongs, which when paired with
+ * {@link #ACCOUNT_TYPE} identifies a specific account.
+ * <P>Type: TEXT</P>
+ */
+ public static final String ACCOUNT_NAME = "account_name";
+
+ /**
+ * The type of account to which this row belongs, which when paired with
+ * {@link #ACCOUNT_NAME} identifies a specific account.
+ * <P>Type: TEXT</P>
+ */
+ public static final String ACCOUNT_TYPE = "account_type";
+
+ /**
+ * The ID of the account's root folder. This will be the ID of the folder
+ * returned when querying {@link Bookmarks#CONTENT_URI_DEFAULT_FOLDER}.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String ROOT_ID = "root_id";
+ }
+
+ /**
+ * The history table, which holds the browsing history.
+ */
+ public static final class History implements CommonColumns, HistoryColumns, ImageColumns {
+ /**
+ * This utility class cannot be instantiated.
+ */
+ private History() {}
+
+ /**
+ * The content:// style URI for this table
+ */
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "history");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of browser history items.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/browser-history";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} of a single browser history item.
+ */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/browser-history";
+ }
+
+ /**
+ * The search history table.
+ * @hide
+ */
+ public static final class Searches {
+ private Searches() {}
+
+ /**
+ * The content:// style URI for this table
+ */
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "searches");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of browser search items.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/searches";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} of a single browser search item.
+ */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/searches";
+
+ /**
+ * The unique ID for a row.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String _ID = "_id";
+
+ /**
+ * The user entered search term.
+ */
+ public static final String SEARCH = "search";
+
+ /**
+ * The date the search was performed, in milliseconds since the epoch.
+ * <p>Type: NUMBER (date in milliseconds since January 1, 1970)</p>
+ */
+ public static final String DATE = "date";
+ }
+
+ /**
+ * A table provided for sync adapters to use for storing private sync state data.
+ *
+ * @see SyncStateContract
+ * @hide
+ */
+ public static final class SyncState implements SyncStateContract.Columns {
+ /**
+ * This utility class cannot be instantiated
+ */
+ private SyncState() {}
+
+ public static final String CONTENT_DIRECTORY =
+ SyncStateContract.Constants.CONTENT_DIRECTORY;
+
+ /**
+ * The content:// style URI for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.withAppendedPath(AUTHORITY_URI, CONTENT_DIRECTORY);
+
+ /**
+ * @see android.provider.SyncStateContract.Helpers#get
+ */
+ public static byte[] get(ContentProviderClient provider, Account account)
+ throws RemoteException {
+ return SyncStateContract.Helpers.get(provider, CONTENT_URI, account);
+ }
+
+ /**
+ * @see android.provider.SyncStateContract.Helpers#get
+ */
+ public static Pair<Uri, byte[]> getWithUri(ContentProviderClient provider, Account account)
+ throws RemoteException {
+ return SyncStateContract.Helpers.getWithUri(provider, CONTENT_URI, account);
+ }
+
+ /**
+ * @see android.provider.SyncStateContract.Helpers#set
+ */
+ public static void set(ContentProviderClient provider, Account account, byte[] data)
+ throws RemoteException {
+ SyncStateContract.Helpers.set(provider, CONTENT_URI, account, data);
+ }
+
+ /**
+ * @see android.provider.SyncStateContract.Helpers#newSetOperation
+ */
+ public static ContentProviderOperation newSetOperation(Account account, byte[] data) {
+ return SyncStateContract.Helpers.newSetOperation(CONTENT_URI, account, data);
+ }
+ }
+
+ /**
+ * <p>
+ * Stores images for URLs.
+ * </p>
+ * <p>
+ * The rows in this table can not be updated since there might have multiple URLs mapping onto
+ * the same image. If you want to update a URL's image, you need to add the new image in this
+ * table, then update the mapping onto the added image.
+ * </p>
+ * <p>
+ * Every image should be at least associated with one URL, otherwise it will be removed after a
+ * while.
+ * </p>
+ */
+ public static final class Images implements ImageColumns {
+ /**
+ * This utility class cannot be instantiated
+ */
+ private Images() {}
+
+ /**
+ * The content:// style URI for this table
+ */
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "images");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of images.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/images";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} of a single image.
+ */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/images";
+
+ /**
+ * Used in {@link Images#TYPE} column and indicats the row is a favicon.
+ */
+ public static final int IMAGE_TYPE_FAVICON = 1;
+
+ /**
+ * Used in {@link Images#TYPE} column and indicats the row is a precomposed touch icon.
+ */
+ public static final int IMAGE_TYPE_PRECOMPOSED_TOUCH_ICON = 2;
+
+ /**
+ * Used in {@link Images#TYPE} column and indicats the row is a touch icon.
+ */
+ public static final int IMAGE_TYPE_TOUCH_ICON = 4;
+
+ /**
+ * The type of item in the table.
+ * <P>Type: INTEGER</P>
+ * <p>Allowed values are:</p>
+ * <p>
+ * <ul>
+ * <li>{@link #IMAGE_TYPE_FAVICON}</li>
+ * <li>{@link #IMAGE_TYPE_PRECOMPOSED_TOUCH_ICON}</li>
+ * <li>{@link #IMAGE_TYPE_TOUCH_ICON}</li>
+ * </ul>
+ * </p>
+ */
+ public static final String TYPE = "type";
+
+ /**
+ * The image data.
+ * <p>Type: BLOB (image)</p>
+ */
+ public static final String DATA = "data";
+
+ /**
+ * The URL the images came from.
+ * <P>Type: TEXT (URL)</P>
+ * @hide
+ */
+ public static final String URL = "url_key";
+ }
+
+ /**
+ * <p>
+ * A table that stores the mappings between the image and the URL.
+ * </p>
+ * <p>
+ * Deleting or Updating a mapping might also deletes the mapped image if there is no other URL
+ * maps onto it.
+ * </p>
+ */
+ public static final class ImageMappings implements ImageMappingColumns {
+ /**
+ * This utility class cannot be instantiated
+ */
+ private ImageMappings() {}
+
+ /**
+ * The content:// style URI for this table
+ */
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "image_mappings");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of image mappings.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/image_mappings";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} of a single image mapping.
+ */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/image_mappings";
+ }
+
+ /**
+ * A combined view of bookmarks and history. All bookmarks in all folders are included and
+ * no folders are included.
+ * @hide
+ */
+ public static final class Combined implements CommonColumns, HistoryColumns, ImageColumns {
+ /**
+ * This utility class cannot be instantiated
+ */
+ private Combined() {}
+
+ /**
+ * The content:// style URI for this table
+ */
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "combined");
+
+ /**
+ * Flag indicating that an item is a bookmark. A value of 1 indicates a bookmark, a value
+ * of 0 indicates a history item.
+ * <p>Type: INTEGER (boolean)</p>
+ */
+ public static final String IS_BOOKMARK = "bookmark";
+ }
+
+ /**
+ * A table that stores settings specific to the browser. Only support query and insert.
+ * @hide
+ */
+ public static final class Settings {
+ /**
+ * This utility class cannot be instantiated
+ */
+ private Settings() {}
+
+ /**
+ * The content:// style URI for this table
+ */
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "settings");
+
+ /**
+ * Key for a setting value.
+ */
+ public static final String KEY = "key";
+
+ /**
+ * Value for a setting.
+ */
+ public static final String VALUE = "value";
+
+ /**
+ * If set to non-0 the user has opted into bookmark sync.
+ */
+ public static final String KEY_SYNC_ENABLED = "sync_enabled";
+
+ /**
+ * Returns true if bookmark sync is enabled
+ */
+ static public boolean isSyncEnabled(Context context) {
+ Cursor cursor = null;
+ try {
+ cursor = context.getContentResolver().query(CONTENT_URI, new String[] { VALUE },
+ KEY + "=?", new String[] { KEY_SYNC_ENABLED }, null);
+ if (cursor == null || !cursor.moveToFirst()) {
+ return false;
+ }
+ return cursor.getInt(0) != 0;
+ } finally {
+ if (cursor != null) cursor.close();
+ }
+ }
+
+ /**
+ * Sets the bookmark sync enabled setting.
+ */
+ static public void setSyncEnabled(Context context, boolean enabled) {
+ ContentValues values = new ContentValues();
+ values.put(KEY, KEY_SYNC_ENABLED);
+ values.put(VALUE, enabled ? 1 : 0);
+ context.getContentResolver().insert(CONTENT_URI, values);
+ }
+ }
+}
diff --git a/src/com/android/browser/platformsupport/Process.java b/src/com/android/browser/platformsupport/Process.java
new file mode 100644
index 0000000..5731b27
--- /dev/null
+++ b/src/com/android/browser/platformsupport/Process.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of The Linux Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+package com.android.browser.platformsupport;
+
+public class Process {
+
+ public static final int PROC_SPACE_TERM = (int)' ';
+ public static final int PROC_COMBINE = 0x100;
+ public static final int PROC_OUT_LONG = 0x2000;
+
+
+ public static long getElapsedCpuTime() {
+ return 0;
+ }
+
+ public static boolean readProcFile(String s, int[] systemCpuFormat, Object o, long[]
+ sysCpu, Object o1) {
+ return false;
+ }
+}
diff --git a/src/com/android/browser/platformsupport/SeekBarPreference.java b/src/com/android/browser/platformsupport/SeekBarPreference.java
new file mode 100644
index 0000000..41b7915
--- /dev/null
+++ b/src/com/android/browser/platformsupport/SeekBarPreference.java
@@ -0,0 +1,231 @@
+package com.android.browser.platformsupport;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.preference.Preference;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+public class SeekBarPreference extends Preference
+ implements OnSeekBarChangeListener {
+
+ private int mProgress;
+ private int mMax;
+ private boolean mTrackingTouch;
+
+ public SeekBarPreference(
+ Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ //SWE: Unable to attain the internal resources via reflection, instead
+ //attaining the max values from xml directly
+ int max = attrs.getAttributeIntValue(
+ "http://schemas.android.com/apk/res/android", "max", mMax);
+ setMax(max);
+ setLayoutResource(com.android.browser.R.layout.preference_widget_seekbar);
+ }
+
+ public SeekBarPreference(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SeekBarPreference(Context context) {
+ this(context, null);
+ }
+
+ @Override
+ protected void onBindView(View view) {
+ super.onBindView(view);
+ SeekBar seekBar = (SeekBar) view.findViewById(
+ com.android.browser.R.id.seekbar2);
+ seekBar.setOnSeekBarChangeListener(this);
+ seekBar.setMax(mMax);
+ seekBar.setProgress(mProgress);
+ seekBar.setEnabled(isEnabled());
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ return null;
+ }
+
+ @Override
+ protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
+ setProgress(restoreValue ? getPersistedInt(mProgress)
+ : (Integer) defaultValue);
+ }
+
+ @Override
+ protected Object onGetDefaultValue(TypedArray a, int index) {
+ return a.getInt(index, 0);
+ }
+
+ //@Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (event.getAction() != KeyEvent.ACTION_UP) {
+ if (keyCode == KeyEvent.KEYCODE_PLUS
+ || keyCode == KeyEvent.KEYCODE_EQUALS) {
+ setProgress(getProgress() + 1);
+ return true;
+ }
+ if (keyCode == KeyEvent.KEYCODE_MINUS) {
+ setProgress(getProgress() - 1);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void setMax(int max) {
+ if (max != mMax) {
+ mMax = max;
+ notifyChanged();
+ }
+ }
+
+ public void setProgress(int progress) {
+ setProgress(progress, true);
+ }
+
+ private void setProgress(int progress, boolean notifyChanged) {
+ if (progress > mMax) {
+ progress = mMax;
+ }
+ if (progress < 0) {
+ progress = 0;
+ }
+ if (progress != mProgress) {
+ mProgress = progress;
+ persistInt(progress);
+ if (notifyChanged) {
+ notifyChanged();
+ }
+ }
+ }
+
+ public int getProgress() {
+ return mProgress;
+ }
+
+ /**
+ * Persist the seekBar's progress value if callChangeListener
+ * returns true, otherwise set the seekBar's progress to the stored value
+ */
+ void syncProgress(SeekBar seekBar) {
+ int progress = seekBar.getProgress();
+ if (progress != mProgress) {
+ if (callChangeListener(progress)) {
+ setProgress(progress, false);
+ } else {
+ seekBar.setProgress(mProgress);
+ }
+ }
+ }
+
+ @Override
+ public void onProgressChanged(
+ SeekBar seekBar, int progress, boolean fromUser) {
+ if (fromUser && !mTrackingTouch) {
+ syncProgress(seekBar);
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ mTrackingTouch = true;
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ mTrackingTouch = false;
+ if (seekBar.getProgress() != mProgress) {
+ syncProgress(seekBar);
+ }
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ /*
+ * Suppose a client uses this preference type without persisting. We
+ * must save the instance state so it is able to, for example, survive
+ * orientation changes.
+ */
+
+ final Parcelable superState = super.onSaveInstanceState();
+ if (isPersistent()) {
+ // No need to save instance state since it's persistent
+ return superState;
+ }
+
+ // Save the instance state
+ final SavedState myState = new SavedState(superState);
+ myState.progress = mProgress;
+ myState.max = mMax;
+ return myState;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ if (!state.getClass().equals(SavedState.class)) {
+ // Didn't save state for us in onSaveInstanceState
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ // Restore the instance state
+ SavedState myState = (SavedState) state;
+ super.onRestoreInstanceState(myState.getSuperState());
+ mProgress = myState.progress;
+ mMax = myState.max;
+ notifyChanged();
+ }
+
+ /**
+ * SavedState, a subclass of {@link BaseSavedState}, will store the state
+ * of MyPreference, a subclass of Preference.
+ * <p>
+ * It is important to always call through to super methods.
+ */
+ private static class SavedState extends BaseSavedState {
+ int progress;
+ int max;
+
+ public SavedState(Parcel source) {
+ super(source);
+
+ // Restore the click counter
+ progress = source.readInt();
+ max = source.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+
+ // Save the click counter
+ dest.writeInt(progress);
+ dest.writeInt(max);
+ }
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ @SuppressWarnings("unused")
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+}
+
diff --git a/src/com/android/browser/platformsupport/SyncStateContentProviderHelper.java b/src/com/android/browser/platformsupport/SyncStateContentProviderHelper.java
new file mode 100644
index 0000000..d6a40c7
--- /dev/null
+++ b/src/com/android/browser/platformsupport/SyncStateContentProviderHelper.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2013 The Linux Foundation. All rights reserved.
+ * Not a contribution.
+ *
+ * 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.platformsupport;
+
+import android.accounts.Account;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.provider.SyncStateContract;
+
+/**
+ * Extends the schema of a ContentProvider to include the _sync_state table
+ * and implements query/insert/update/delete to access that table using the
+ * authority "syncstate". This can be used to store the sync state for a
+ * set of accounts.
+ */
+public class SyncStateContentProviderHelper {
+ private static final String SELECT_BY_ACCOUNT =
+ SyncStateContract.Columns.ACCOUNT_NAME + "=? AND "
+ + SyncStateContract.Columns.ACCOUNT_TYPE + "=?";
+
+ private static final String SYNC_STATE_TABLE = "_sync_state";
+ private static final String SYNC_STATE_META_TABLE = "_sync_state_metadata";
+ private static final String SYNC_STATE_META_VERSION_COLUMN = "version";
+
+ private static long DB_VERSION = 1;
+
+ private static final String[] ACCOUNT_PROJECTION =
+ new String[]{SyncStateContract.Columns.ACCOUNT_NAME,
+ SyncStateContract.Columns.ACCOUNT_TYPE};
+
+ public static final String PATH = "syncstate";
+
+ private static final String QUERY_COUNT_SYNC_STATE_ROWS =
+ "SELECT count(*)"
+ + " FROM " + SYNC_STATE_TABLE
+ + " WHERE " + SyncStateContract.Columns._ID + "=?";
+
+ public void createDatabase(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS " + SYNC_STATE_TABLE);
+ db.execSQL("CREATE TABLE " + SYNC_STATE_TABLE + " ("
+ + SyncStateContract.Columns._ID + " INTEGER PRIMARY KEY,"
+ + SyncStateContract.Columns.ACCOUNT_NAME + " TEXT NOT NULL,"
+ + SyncStateContract.Columns.ACCOUNT_TYPE + " TEXT NOT NULL,"
+ + SyncStateContract.Columns.DATA + " TEXT,"
+ + "UNIQUE(" + SyncStateContract.Columns.ACCOUNT_NAME + ", "
+ + SyncStateContract.Columns.ACCOUNT_TYPE + "));");
+
+ db.execSQL("DROP TABLE IF EXISTS " + SYNC_STATE_META_TABLE);
+ db.execSQL("CREATE TABLE " + SYNC_STATE_META_TABLE + " ("
+ + SYNC_STATE_META_VERSION_COLUMN + " INTEGER);");
+ ContentValues values = new ContentValues();
+ values.put(SYNC_STATE_META_VERSION_COLUMN, DB_VERSION);
+ db.insert(SYNC_STATE_META_TABLE, SYNC_STATE_META_VERSION_COLUMN, values);
+ }
+
+ public void onDatabaseOpened(SQLiteDatabase db) {
+ long version = DatabaseUtils.longForQuery(db,
+ "SELECT " + SYNC_STATE_META_VERSION_COLUMN + " FROM " + SYNC_STATE_META_TABLE,
+ null);
+ if (version != DB_VERSION) {
+ createDatabase(db);
+ }
+ }
+
+ public Cursor query(SQLiteDatabase db, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder) {
+ return db.query(SYNC_STATE_TABLE, projection, selection, selectionArgs,
+ null, null, sortOrder);
+ }
+
+ public long insert(SQLiteDatabase db, ContentValues values) {
+ return db.replace(SYNC_STATE_TABLE, SyncStateContract.Columns.ACCOUNT_NAME, values);
+ }
+
+ public int delete(SQLiteDatabase db, String userWhere, String[] whereArgs) {
+ return db.delete(SYNC_STATE_TABLE, userWhere, whereArgs);
+ }
+
+ public int update(SQLiteDatabase db, ContentValues values,
+ String selection, String[] selectionArgs) {
+ return db.update(SYNC_STATE_TABLE, values, selection, selectionArgs);
+ }
+
+ public int update(SQLiteDatabase db, long rowId, Object data) {
+ if (DatabaseUtils.longForQuery(db, QUERY_COUNT_SYNC_STATE_ROWS,
+ new String[]{Long.toString(rowId)}) < 1) {
+ return 0;
+ }
+ db.execSQL("UPDATE " + SYNC_STATE_TABLE
+ + " SET " + SyncStateContract.Columns.DATA + "=?"
+ + " WHERE " + SyncStateContract.Columns._ID + "=" + rowId,
+ new Object[]{data});
+ // assume a row was modified since we know it exists
+ return 1;
+ }
+
+ public void onAccountsChanged(SQLiteDatabase db, Account[] accounts) {
+ Cursor c = db.query(SYNC_STATE_TABLE, ACCOUNT_PROJECTION, null, null, null, null, null);
+ try {
+ while (c.moveToNext()) {
+ final String accountName = c.getString(0);
+ final String accountType = c.getString(1);
+ Account account = new Account(accountName, accountType);
+ if (!contains(accounts, account)) {
+ db.delete(SYNC_STATE_TABLE, SELECT_BY_ACCOUNT,
+ new String[]{accountName, accountType});
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Checks that value is present as at least one of the elements of the array.
+ * @param array the array to check in
+ * @param value the value to check for
+ * @return true if the value is present in the array
+ */
+ private static <T> boolean contains(T[] array, T value) {
+ for (T element : array) {
+ if (element == null) {
+ if (value == null) return true;
+ } else {
+ if (value != null && element.equals(value)) return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/browser/platformsupport/WebAddress.java b/src/com/android/browser/platformsupport/WebAddress.java
new file mode 100644
index 0000000..10fac15
--- /dev/null
+++ b/src/com/android/browser/platformsupport/WebAddress.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 2013 The Linux Foundation. All rights reserved.
+ * Not a contribution.
+ *
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser.platformsupport;
+
+import static android.util.Patterns.GOOD_IRI_CHAR;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * {@hide}
+ *
+ * Web Address Parser
+ *
+ * This is called WebAddress, rather than URL or URI, because it
+ * attempts to parse the stuff that a user will actually type into a
+ * browser address widget.
+ *
+ * Unlike java.net.uri, this parser will not choke on URIs missing
+ * schemes. It will only throw a ParseException if the input is
+ * really hosed.
+ *
+ * If given an https scheme but no port, fills in port
+ *
+ */
+public class WebAddress {
+
+ private String mScheme;
+ private String mHost;
+ private int mPort;
+ private String mPath;
+ private String mAuthInfo;
+
+ static final int MATCH_GROUP_SCHEME = 1;
+ static final int MATCH_GROUP_AUTHORITY = 2;
+ static final int MATCH_GROUP_HOST = 3;
+ static final int MATCH_GROUP_PORT = 4;
+ static final int MATCH_GROUP_PATH = 5;
+
+ /* ENRICO: imported the ParseExeption here */
+ public static class ParseException extends RuntimeException {
+ public String response;
+
+ ParseException(String response) {
+ this.response = response;
+ }
+ }
+
+ static Pattern sAddressPattern = Pattern.compile(
+ /* scheme */ "(?:(http|https|file)\\:\\/\\/)?" +
+ /* authority */ "(?:([-A-Za-z0-9$_.+!*'(),;?&=]+(?:\\:[-A-Za-z0-9$_.+!*'(),;?&=]+)?)@)?" +
+ /* host */ "([" + GOOD_IRI_CHAR + "%_-][" + GOOD_IRI_CHAR + "%_\\.-]*|\\[[0-9a-fA-F:\\.]+\\])?" +
+ /* port */ "(?:\\:([0-9]*))?" +
+ /* path */ "(\\/?[^#]*)?" +
+ /* anchor */ ".*", Pattern.CASE_INSENSITIVE);
+
+ /** parses given uriString. */
+ public WebAddress(String address) throws ParseException {
+ if (address == null) {
+ throw new NullPointerException();
+ }
+
+ // android.util.Log.d(LOGTAG, "WebAddress: " + address);
+
+ mScheme = "";
+ mHost = "";
+ mPort = -1;
+ mPath = "/";
+ mAuthInfo = "";
+
+ Matcher m = sAddressPattern.matcher(address);
+ String t;
+ if (m.matches()) {
+ t = m.group(MATCH_GROUP_SCHEME);
+ if (t != null) mScheme = t.toLowerCase();
+ t = m.group(MATCH_GROUP_AUTHORITY);
+ if (t != null) mAuthInfo = t;
+ t = m.group(MATCH_GROUP_HOST);
+ if (t != null) mHost = t;
+ t = m.group(MATCH_GROUP_PORT);
+ if (t != null && t.length() > 0) {
+ // The ':' character is not returned by the regex.
+ try {
+ mPort = Integer.parseInt(t);
+ } catch (NumberFormatException ex) {
+ throw new ParseException("Bad port");
+ }
+ }
+ t = m.group(MATCH_GROUP_PATH);
+ if (t != null && t.length() > 0) {
+ /* handle busted myspace frontpage redirect with
+ missing initial "/" */
+ if (t.charAt(0) == '/') {
+ mPath = t;
+ } else {
+ mPath = "/" + t;
+ }
+ }
+
+ } else {
+ // nothing found... outa here
+ throw new ParseException("Bad address");
+ }
+
+ /* Get port from scheme or scheme from port, if necessary and
+ possible */
+ if (mPort == 443 && mScheme.equals("")) {
+ mScheme = "https";
+ } else if (mPort == -1) {
+ if (mScheme.equals("https"))
+ mPort = 443;
+ else
+ mPort = 80; // default
+ }
+ if (mScheme.equals("")) mScheme = "http";
+ }
+
+ @Override
+ public String toString() {
+ String port = "";
+ if ((mPort != 443 && mScheme.equals("https")) ||
+ (mPort != 80 && mScheme.equals("http"))) {
+ port = ":" + Integer.toString(mPort);
+ }
+ String authInfo = "";
+ if (mAuthInfo.length() > 0) {
+ authInfo = mAuthInfo + "@";
+ }
+
+ return mScheme + "://" + authInfo + mHost + port + mPath;
+ }
+
+ public void setScheme(String scheme) {
+ mScheme = scheme;
+ }
+
+ public String getScheme() {
+ return mScheme;
+ }
+
+ public void setHost(String host) {
+ mHost = host;
+ }
+
+ public String getHost() {
+ return mHost;
+ }
+
+ public void setPort(int port) {
+ mPort = port;
+ }
+
+ public int getPort() {
+ return mPort;
+ }
+
+ public void setPath(String path) {
+ mPath = path;
+ }
+
+ public String getPath() {
+ return mPath;
+ }
+
+ public void setAuthInfo(String authInfo) {
+ mAuthInfo = authInfo;
+ }
+
+ public String getAuthInfo() {
+ return mAuthInfo;
+ }
+}
diff --git a/src/com/android/browser/preferences/AboutPreferencesFragment.java b/src/com/android/browser/preferences/AboutPreferencesFragment.java
new file mode 100644
index 0000000..d979333
--- /dev/null
+++ b/src/com/android/browser/preferences/AboutPreferencesFragment.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2014, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of The Linux Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+package com.android.browser.preferences;
+
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceFragment;
+
+import com.android.browser.R;
+
+public class AboutPreferencesFragment extends PreferenceFragment
+ implements OnPreferenceClickListener {
+
+ static final String PREF_ABOUT = "about_preference";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ addPreferencesFromResource(R.xml.about_preferences);
+ Preference aboutPreference = (Preference) findPreference(PREF_ABOUT);
+ String about_text = getString(R.string.about_text);
+ about_text = about_text.substring(about_text.indexOf("Hash"), about_text.length());
+ aboutPreference.setSummary(about_text);
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ return false;
+ }
+}
diff --git a/src/com/android/browser/preferences/AccessibilityPreferencesFragment.java b/src/com/android/browser/preferences/AccessibilityPreferencesFragment.java
new file mode 100644
index 0000000..529e388
--- /dev/null
+++ b/src/com/android/browser/preferences/AccessibilityPreferencesFragment.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2011 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 android.content.Context;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+
+import org.codeaurora.swe.WebView;
+import java.text.NumberFormat;
+
+public class AccessibilityPreferencesFragment extends PreferenceFragment
+ implements Preference.OnPreferenceChangeListener {
+
+ NumberFormat mFormat;
+ // Used to pause/resume timers, which are required for WebViewPreview
+ WebView mControlWebView;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mControlWebView = new WebView(getActivity());
+ addPreferencesFromResource(R.xml.accessibility_preferences);
+ BrowserSettings settings = BrowserSettings.getInstance();
+ mFormat = NumberFormat.getPercentInstance();
+
+ Preference e = findPreference(PreferenceKeys.PREF_MIN_FONT_SIZE);
+ e.setOnPreferenceChangeListener(this);
+ updateMinFontSummary(e, settings.getMinimumFontSize());
+ e = findPreference(PreferenceKeys.PREF_TEXT_ZOOM);
+ e.setOnPreferenceChangeListener(this);
+ updateTextZoomSummary(e, settings.getTextZoom());
+ e = findPreference(PreferenceKeys.PREF_DOUBLE_TAP_ZOOM);
+ e.setOnPreferenceChangeListener(this);
+ updateDoubleTapZoomSummary(e, settings.getDoubleTapZoom());
+ /*
+ * SWE_TODO: Commented out functionality for inverted rendering
+ * (as well as corresponding sections below)
+ e = findPreference(PreferenceKeys.PREF_INVERTED_CONTRAST);
+ e.setOnPreferenceChangeListener(this);
+ updateInvertedContrastSummary(e, (int) (settings.getInvertedContrast() * 100));
+ */
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mControlWebView.resumeTimers();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mControlWebView.pauseTimers();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mControlWebView.destroy();
+ mControlWebView = null;
+ }
+
+ void updateMinFontSummary(Preference pref, int minFontSize) {
+ Context c = getActivity();
+ pref.setSummary(c.getString(R.string.pref_min_font_size_value, minFontSize));
+ }
+
+ void updateTextZoomSummary(Preference pref, int textZoom) {
+ pref.setSummary(mFormat.format(textZoom / 100.0));
+ }
+
+ void updateDoubleTapZoomSummary(Preference pref, int doubleTapZoom) {
+ pref.setSummary(mFormat.format(doubleTapZoom / 100.0));
+ }
+
+ /*
+ void updateInvertedContrastSummary(Preference pref, int contrast) {
+ pref.setSummary(mFormat.format(contrast / 100.0));
+ }
+ */
+
+ @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.
+ return false;
+ }
+
+ if (PreferenceKeys.PREF_MIN_FONT_SIZE.equals(pref.getKey())) {
+ updateMinFontSummary(pref, BrowserSettings
+ .getAdjustedMinimumFontSize((Integer) objValue));
+ }
+ if (PreferenceKeys.PREF_TEXT_ZOOM.equals(pref.getKey())) {
+ BrowserSettings settings = BrowserSettings.getInstance();
+ updateTextZoomSummary(pref, settings
+ .getAdjustedTextZoom((Integer) objValue));
+ }
+ if (PreferenceKeys.PREF_DOUBLE_TAP_ZOOM.equals(pref.getKey())) {
+ BrowserSettings settings = BrowserSettings.getInstance();
+ updateDoubleTapZoomSummary(pref, settings
+ .getAdjustedDoubleTapZoom((Integer) objValue));
+ }
+ /*
+ if (PreferenceKeys.PREF_INVERTED_CONTRAST.equals(pref.getKey())) {
+ updateInvertedContrastSummary(pref,
+ (int) ((10 + (Integer) objValue) * 10));
+ }
+ */
+
+ return true;
+ }
+
+}
diff --git a/src/com/android/browser/preferences/AdvancedPreferencesFragment.java b/src/com/android/browser/preferences/AdvancedPreferencesFragment.java
new file mode 100644
index 0000000..4abd301
--- /dev/null
+++ b/src/com/android/browser/preferences/AdvancedPreferencesFragment.java
@@ -0,0 +1,220 @@
+/*
+ * 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 android.app.Activity;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.content.SharedPreferences.Editor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+import android.util.Log;
+import android.webkit.ValueCallback;
+import android.widget.Toast;
+
+import com.android.browser.BrowserActivity;
+import com.android.browser.BrowserSettings;
+import com.android.browser.DownloadHandler;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+
+import java.util.Map;
+import java.util.Set;
+import org.codeaurora.swe.GeolocationPermissions;
+import org.codeaurora.swe.WebStorage;
+
+public class AdvancedPreferencesFragment extends PreferenceFragment
+ implements Preference.OnPreferenceChangeListener {
+
+ private static final int DOWNLOAD_PATH_RESULT_CODE = 1;
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the XML preferences file
+ addPreferencesFromResource(R.xml.advanced_preferences);
+
+ PreferenceScreen websiteSettings = (PreferenceScreen) findPreference(
+ PreferenceKeys.PREF_WEBSITE_SETTINGS);
+ websiteSettings.setFragment(WebsiteSettingsFragment.class.getName());
+
+ Preference e = findPreference(PreferenceKeys.PREF_DEFAULT_ZOOM);
+ e.setOnPreferenceChangeListener(this);
+ e.setSummary(getVisualDefaultZoomName(
+ getPreferenceScreen().getSharedPreferences()
+ .getString(PreferenceKeys.PREF_DEFAULT_ZOOM, null)) );
+
+ e = findPreference(PreferenceKeys.PREF_DEFAULT_TEXT_ENCODING);
+ e.setOnPreferenceChangeListener(this);
+
+ e = findPreference(PreferenceKeys.PREF_RESET_DEFAULT_PREFERENCES);
+ e.setOnPreferenceChangeListener(this);
+
+ e = findPreference(PreferenceKeys.PREF_SEARCH_ENGINE);
+ e.setOnPreferenceChangeListener(this);
+ updateListPreferenceSummary((ListPreference) e);
+
+ e = findPreference(PreferenceKeys.PREF_PLUGIN_STATE);
+ e.setOnPreferenceChangeListener(this);
+ updateListPreferenceSummary((ListPreference) e);
+ onInitdownloadSettingsPreference();
+ }
+
+ private void onInitdownloadSettingsPreference() {
+ addPreferencesFromResource(R.xml.download_settings_preferences);
+ PreferenceScreen downloadPathPreset =
+ (PreferenceScreen) findPreference(PreferenceKeys.PREF_DOWNLOAD_PATH);
+ downloadPathPreset.setOnPreferenceClickListener(onClickDownloadPathSettings());
+
+ String downloadPath = downloadPathPreset.getSharedPreferences().
+ getString(PreferenceKeys.PREF_DOWNLOAD_PATH,
+ BrowserSettings.getInstance().getDownloadPath());
+ String downloadPathForUser = DownloadHandler.getDownloadPathForUser(this.getActivity(),
+ downloadPath);
+ downloadPathPreset.setSummary(downloadPathForUser);
+ }
+
+ private Preference.OnPreferenceClickListener onClickDownloadPathSettings() {
+ return new Preference.OnPreferenceClickListener() {
+ public boolean onPreferenceClick(Preference preference) {
+ try {
+ Intent i = new Intent("com.android.fileexplorer.action.DIR_SEL");
+ AdvancedPreferencesFragment.this.startActivityForResult(i,
+ DOWNLOAD_PATH_RESULT_CODE);
+ } catch (Exception e) {
+ String err_msg = getResources().getString(R.string.activity_not_found,
+ "com.android.fileexplorer.action.DIR_SEL");
+ Toast.makeText(getActivity(), err_msg, Toast.LENGTH_LONG).show();
+ }
+ return true;
+ }
+ };
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (requestCode == DOWNLOAD_PATH_RESULT_CODE) {
+ if (resultCode == Activity.RESULT_OK && data != null) {
+ String downloadPath = data.getStringExtra("result_dir_sel");
+ if (downloadPath != null) {
+ PreferenceScreen downloadPathPreset =
+ (PreferenceScreen) findPreference(PreferenceKeys.PREF_DOWNLOAD_PATH);
+ Editor editor = downloadPathPreset.getEditor();
+ editor.putString(PreferenceKeys.PREF_DOWNLOAD_PATH, downloadPath);
+ editor.apply();
+ String downloadPathForUser = DownloadHandler.getDownloadPathForUser(
+ this.getActivity(), downloadPath);
+ downloadPathPreset.setSummary(downloadPathForUser);
+ }
+
+ return;
+ }
+ }
+ return;
+ }
+
+ void updateListPreferenceSummary(ListPreference e) {
+ e.setSummary(e.getEntry());
+ }
+
+ /*
+ * 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(
+ PreferenceKeys.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 (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(PreferenceKeys.PREF_DEFAULT_ZOOM)) {
+ pref.setSummary(getVisualDefaultZoomName((String) objValue));
+ return true;
+ } else if (pref.getKey().equals(PreferenceKeys.PREF_DEFAULT_TEXT_ENCODING)) {
+ pref.setSummary((String) objValue);
+ return true;
+ } else if (pref.getKey().equals(PreferenceKeys.PREF_RESET_DEFAULT_PREFERENCES)) {
+ Boolean value = (Boolean) objValue;
+ if (value.booleanValue() == true) {
+ startActivity(new Intent(BrowserActivity.ACTION_RESTART, null,
+ getActivity(), BrowserActivity.class));
+ return true;
+ }
+ } else if (pref.getKey().equals(PreferenceKeys.PREF_PLUGIN_STATE)
+ || pref.getKey().equals(PreferenceKeys.PREF_SEARCH_ENGINE)) {
+ ListPreference lp = (ListPreference) pref;
+ lp.setValue((String) objValue);
+ updateListPreferenceSummary(lp);
+ return false;
+ }
+ return false;
+ }
+
+ 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 "";
+ }
+}
diff --git a/src/com/android/browser/preferences/BandwidthPreferencesFragment.java b/src/com/android/browser/preferences/BandwidthPreferencesFragment.java
new file mode 100644
index 0000000..0cb064a
--- /dev/null
+++ b/src/com/android/browser/preferences/BandwidthPreferencesFragment.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2011 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 android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+
+public class BandwidthPreferencesFragment extends PreferenceFragment {
+
+ static final String TAG = "BandwidthPreferencesFragment";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Load the XML preferences file
+ addPreferencesFromResource(R.xml.bandwidth_preferences);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ PreferenceScreen prefScreen = getPreferenceScreen();
+ SharedPreferences sharedPrefs = prefScreen.getSharedPreferences();
+ if (!sharedPrefs.contains(PreferenceKeys.PREF_DATA_PRELOAD)) {
+ // set default value for preload setting
+ ListPreference preload = (ListPreference) prefScreen.findPreference(
+ PreferenceKeys.PREF_DATA_PRELOAD);
+ if (preload != null) {
+ preload.setValue(BrowserSettings.getInstance().getDefaultPreloadSetting());
+ }
+ }
+ if (!sharedPrefs.contains(PreferenceKeys.PREF_LINK_PREFETCH)) {
+ // set default value for link prefetch setting
+ ListPreference prefetch = (ListPreference) prefScreen.findPreference(
+ PreferenceKeys.PREF_LINK_PREFETCH);
+ if (prefetch != null) {
+ prefetch.setValue(BrowserSettings.getInstance().getDefaultLinkPrefetchSetting());
+ }
+ }
+ }
+
+}
diff --git a/src/com/android/browser/preferences/DebugPreferencesFragment.java b/src/com/android/browser/preferences/DebugPreferencesFragment.java
new file mode 100644
index 0000000..24821d1
--- /dev/null
+++ b/src/com/android/browser/preferences/DebugPreferencesFragment.java
@@ -0,0 +1,52 @@
+/*
+ * 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 android.os.Bundle;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceFragment;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.GoogleAccountLogin;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+
+public class DebugPreferencesFragment extends PreferenceFragment
+ implements OnPreferenceClickListener {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the XML preferences file
+ addPreferencesFromResource(R.xml.debug_preferences);
+
+ Preference e = findPreference(PreferenceKeys.PREF_RESET_PRELOGIN);
+ e.setOnPreferenceClickListener(this);
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ if (PreferenceKeys.PREF_RESET_PRELOGIN.equals(preference.getKey())) {
+ BrowserSettings.getInstance().getPreferences().edit()
+ .remove(GoogleAccountLogin.PREF_AUTOLOGIN_TIME)
+ .apply();
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/browser/preferences/FontSizePreview.java b/src/com/android/browser/preferences/FontSizePreview.java
new file mode 100644
index 0000000..e9d69d5
--- /dev/null
+++ b/src/com/android/browser/preferences/FontSizePreview.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2011 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 android.content.Context;
+import android.content.res.Resources;
+import android.util.AttributeSet;
+import android.view.View;
+import org.codeaurora.swe.WebSettings;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.R;
+
+public class FontSizePreview extends WebViewPreview {
+
+ static final String HTML_FORMAT = "<!DOCTYPE html><html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"><style type=\"text/css\">p { margin: 2px auto;}</style><body><p style=\"font-size: 4pt\">%s</p><p style=\"font-size: 8pt\">%s</p><p style=\"font-size: 10pt\">%s</p><p style=\"font-size: 14pt\">%s</p><p style=\"font-size: 18pt\">%s</p></body></html>";
+
+ String mHtml;
+
+ public FontSizePreview(
+ Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public FontSizePreview(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public FontSizePreview(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void init(Context context) {
+ super.init(context);
+ Resources res = context.getResources();
+ Object[] visualNames = res.getStringArray(R.array.pref_text_size_choices);
+ mHtml = String.format(HTML_FORMAT, visualNames);
+ }
+
+ @Override
+ protected void updatePreview(boolean forceReload) {
+ if (mWebView == null) return;
+
+ WebSettings ws = mWebView.getSettings();
+ BrowserSettings bs = BrowserSettings.getInstance();
+ ws.setMinimumFontSize(bs.getMinimumFontSize());
+ ws.setTextZoom(bs.getTextZoom());
+ mWebView.loadDataWithBaseURL(null, mHtml, "text/html", "utf-8", null);
+ }
+
+ @Override
+ protected void setupWebView(WebView view) {
+ super.setupWebView(view);
+ view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+ }
+
+}
diff --git a/src/com/android/browser/preferences/GeneralPreferencesFragment.java b/src/com/android/browser/preferences/GeneralPreferencesFragment.java
new file mode 100644
index 0000000..2453f46
--- /dev/null
+++ b/src/com/android/browser/preferences/GeneralPreferencesFragment.java
@@ -0,0 +1,245 @@
+/*
+ * 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 android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+import android.text.InputType;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+
+import com.android.browser.BrowserPreferencesPage;
+import com.android.browser.BrowserSettings;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+import com.android.browser.UrlUtils;
+import com.android.browser.homepages.HomeProvider;
+
+public class GeneralPreferencesFragment extends PreferenceFragment
+ implements Preference.OnPreferenceChangeListener {
+
+ static final String TAG = "PersonalPreferencesFragment";
+
+ static final String BLANK_URL = "about:blank";
+ static final String CURRENT = "current";
+ static final String BLANK = "blank";
+ static final String DEFAULT = "default";
+ static final String MOST_VISITED = "most_visited";
+ static final String OTHER = "other";
+
+ static final String PREF_HOMEPAGE_PICKER = "homepage_picker";
+
+ String[] mChoices, mValues;
+ String mCurrentPage;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Resources res = getActivity().getResources();
+ mChoices = res.getStringArray(R.array.pref_homepage_choices);
+ mValues = res.getStringArray(R.array.pref_homepage_values);
+ mCurrentPage = getActivity().getIntent()
+ .getStringExtra(BrowserPreferencesPage.CURRENT_PAGE);
+
+ // Load the XML preferences file
+ addPreferencesFromResource(R.xml.general_preferences);
+
+ ListPreference pref = (ListPreference) findPreference(PREF_HOMEPAGE_PICKER);
+ pref.setSummary(getHomepageSummary());
+ pref.setPersistent(false);
+ pref.setValue(getHomepageValue());
+ pref.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(PREF_HOMEPAGE_PICKER)) {
+ BrowserSettings settings = BrowserSettings.getInstance();
+ if (CURRENT.equals(objValue)) {
+ settings.setHomePage(mCurrentPage);
+ }
+ if (BLANK.equals(objValue)) {
+ settings.setHomePage(BLANK_URL);
+ }
+ if (DEFAULT.equals(objValue)) {
+ settings.setHomePage(BrowserSettings.getFactoryResetHomeUrl(
+ getActivity()));
+ }
+ if (MOST_VISITED.equals(objValue)) {
+ settings.setHomePage(HomeProvider.MOST_VISITED);
+ }
+ if (OTHER.equals(objValue)) {
+ promptForHomepage();
+ return false;
+ }
+ pref.setSummary(getHomepageSummary());
+ ((ListPreference)pref).setValue(getHomepageValue());
+ return false;
+ }
+
+ return true;
+ }
+
+ void promptForHomepage() {
+ MyAlertDialogFragment fragment = MyAlertDialogFragment.newInstance();
+ fragment.setTargetFragment(this, -1);
+ fragment.show(getActivity().getFragmentManager(), "setHomepage dialog");
+ }
+
+ String getHomepageValue() {
+ BrowserSettings settings = BrowserSettings.getInstance();
+ String homepage = settings.getHomePage();
+ if (TextUtils.isEmpty(homepage) || BLANK_URL.endsWith(homepage)) {
+ return BLANK;
+ }
+ if (HomeProvider.MOST_VISITED.equals(homepage)) {
+ return MOST_VISITED;
+ }
+ String defaultHomepage = BrowserSettings.getFactoryResetHomeUrl(
+ getActivity());
+ if (TextUtils.equals(defaultHomepage, homepage)) {
+ return DEFAULT;
+ }
+ if (TextUtils.equals(mCurrentPage, homepage)) {
+ return CURRENT;
+ }
+ return OTHER;
+ }
+
+ String getHomepageSummary() {
+ BrowserSettings settings = BrowserSettings.getInstance();
+ if (settings.useMostVisitedHomepage()) {
+ return getHomepageLabel(MOST_VISITED);
+ }
+ String homepage = settings.getHomePage();
+ if (TextUtils.isEmpty(homepage) || BLANK_URL.equals(homepage)) {
+ return getHomepageLabel(BLANK);
+ }
+ return homepage;
+ }
+
+ String getHomepageLabel(String value) {
+ for (int i = 0; i < mValues.length; i++) {
+ if (value.equals(mValues[i])) {
+ return mChoices[i];
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ refreshUi();
+ }
+
+ void refreshUi() {
+ PreferenceScreen autoFillSettings =
+ (PreferenceScreen)findPreference(PreferenceKeys.PREF_AUTOFILL_PROFILE);
+ autoFillSettings.setDependency(PreferenceKeys.PREF_AUTOFILL_ENABLED);
+ }
+
+ /*
+ Add this class to manage AlertDialog lifecycle.
+ */
+ public static class MyAlertDialogFragment extends DialogFragment {
+ public static MyAlertDialogFragment newInstance() {
+ MyAlertDialogFragment frag = new MyAlertDialogFragment();
+ return frag;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final BrowserSettings settings = BrowserSettings.getInstance();
+ final EditText editText = new EditText(getActivity());
+ editText.setInputType(InputType.TYPE_CLASS_TEXT
+ | InputType.TYPE_TEXT_VARIATION_URI);
+ editText.setText(settings.getHomePage());
+ editText.setSelectAllOnFocus(true);
+ editText.setSingleLine(true);
+ editText.setImeActionLabel(null, EditorInfo.IME_ACTION_DONE);
+ final AlertDialog dialog = new AlertDialog.Builder(getActivity())
+ .setView(editText)
+ .setPositiveButton(android.R.string.ok, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ String homepage = editText.getText().toString().trim();
+ homepage = UrlUtils.smartUrlFilter(homepage);
+ settings.setHomePage(homepage);
+ Fragment frag = getTargetFragment();
+ if (frag == null || !(frag instanceof GeneralPreferencesFragment)) {
+ Log.e("MyAlertDialogFragment", "get target fragment error!");
+ return;
+ }
+ GeneralPreferencesFragment target = (GeneralPreferencesFragment)frag;
+ ListPreference pref = (ListPreference) target.
+ findPreference(PREF_HOMEPAGE_PICKER);
+ pref.setValue(target.getHomepageValue());
+ pref.setSummary(target.getHomepageSummary());
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ })
+ .setTitle(R.string.pref_set_homepage_to)
+ .create();
+
+ editText.setOnEditorActionListener(new OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick();
+ return true;
+ }
+ return false;
+ }
+ });
+
+ dialog.getWindow().setSoftInputMode(
+ WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+
+ return dialog;
+ }
+ }
+}
diff --git a/src/com/android/browser/preferences/InvertedContrastPreview.java b/src/com/android/browser/preferences/InvertedContrastPreview.java
new file mode 100644
index 0000000..8064c30
--- /dev/null
+++ b/src/com/android/browser/preferences/InvertedContrastPreview.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2011 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 android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import org.codeaurora.swe.WebSettings;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.BrowserWebView;
+import com.android.browser.WebViewProperties;
+
+public class InvertedContrastPreview extends WebViewPreview {
+
+ static final String IMG_ROOT = "content://com.android.browser.home/res/raw/";
+ static final String[] THUMBS = new String[] {
+ "thumb_google",
+ "thumb_amazon",
+ "thumb_cnn",
+ "thumb_espn",
+ "", // break
+ "thumb_bbc",
+ "thumb_nytimes",
+ "thumb_weatherchannel",
+ "thumb_picasa",
+ };
+
+ String mHtml;
+
+ public InvertedContrastPreview(
+ Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public InvertedContrastPreview(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public InvertedContrastPreview(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void init(Context context) {
+ super.init(context);
+ StringBuilder builder = new StringBuilder("<html><body style=\"width: 1000px\">");
+ for (String thumb : THUMBS) {
+ if (TextUtils.isEmpty(thumb)) {
+ builder.append("<br />");
+ continue;
+ }
+ builder.append("<img src=\"");
+ builder.append(IMG_ROOT);
+ builder.append(thumb);
+ builder.append("\" /> ");
+ }
+ builder.append("</body></html>");
+ mHtml = builder.toString();
+ }
+
+ @Override
+ protected void updatePreview(boolean forceReload) {
+ if (mWebView == null) return;
+
+ WebSettings ws = mWebView.getSettings();
+ BrowserSettings bs = BrowserSettings.getInstance();
+ ws.setProperty(WebViewProperties.gfxInvertedScreen,
+ bs.useInvertedRendering() ? "true" : "false");
+ ws.setProperty(WebViewProperties.gfxInvertedScreenContrast,
+ Float.toString(bs.getInvertedContrast()));
+ if (forceReload) {
+ mWebView.loadData(mHtml, "text/html", null);
+ }
+ }
+
+}
diff --git a/src/com/android/browser/preferences/LabPreferencesFragment.java b/src/com/android/browser/preferences/LabPreferencesFragment.java
new file mode 100644
index 0000000..222b5fa
--- /dev/null
+++ b/src/com/android/browser/preferences/LabPreferencesFragment.java
@@ -0,0 +1,36 @@
+/*
+ * 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 android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+import com.android.browser.search.SearchEngine;
+
+public class LabPreferencesFragment extends PreferenceFragment {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Load the XML preferences file
+ addPreferencesFromResource(R.xml.lab_preferences);
+ }
+}
diff --git a/src/com/android/browser/preferences/NonformattingListPreference.java b/src/com/android/browser/preferences/NonformattingListPreference.java
new file mode 100644
index 0000000..51b3231
--- /dev/null
+++ b/src/com/android/browser/preferences/NonformattingListPreference.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2011 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 android.content.Context;
+import android.preference.ListPreference;
+import android.util.AttributeSet;
+
+public class NonformattingListPreference extends ListPreference {
+
+ private CharSequence mSummary;
+
+ public NonformattingListPreference(Context context) {
+ super(context);
+ }
+
+ public NonformattingListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void setSummary(CharSequence summary) {
+ mSummary = summary;
+ super.setSummary(summary);
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ if (mSummary != null) {
+ return mSummary;
+ }
+ return super.getSummary();
+ }
+
+}
diff --git a/src/com/android/browser/preferences/PrivacySecurityPreferencesFragment.java b/src/com/android/browser/preferences/PrivacySecurityPreferencesFragment.java
new file mode 100644
index 0000000..35e6e43
--- /dev/null
+++ b/src/com/android/browser/preferences/PrivacySecurityPreferencesFragment.java
@@ -0,0 +1,61 @@
+/*
+ * 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.PreferenceKeys;
+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 PrivacySecurityPreferencesFragment 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_security_preferences);
+
+ Preference e = findPreference(PreferenceKeys.PREF_PRIVACY_CLEAR_HISTORY);
+ e.setOnPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference pref, Object objValue) {
+ if (pref.getKey().equals(PreferenceKeys.PREF_PRIVACY_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/SeekBarSummaryPreference.java b/src/com/android/browser/preferences/SeekBarSummaryPreference.java
new file mode 100644
index 0000000..5cb8ae6
--- /dev/null
+++ b/src/com/android/browser/preferences/SeekBarSummaryPreference.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2011 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 android.content.Context;
+
+import com.android.browser.platformsupport.SeekBarPreference;
+
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+
+public class SeekBarSummaryPreference extends SeekBarPreference {
+
+ CharSequence mSummary;
+ TextView mSummaryView;
+
+ public SeekBarSummaryPreference(
+ Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ public SeekBarSummaryPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public SeekBarSummaryPreference(Context context) {
+ super(context);
+ init();
+ }
+
+ void init() {
+ setWidgetLayoutResource(com.android.browser.R.layout.font_size_widget);
+ }
+
+ @Override
+ public void setSummary(CharSequence summary) {
+ mSummary = summary;
+ if (mSummaryView != null) {
+ mSummaryView.setText(mSummary);
+ }
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ return null;
+ }
+
+ @Override
+ protected void onBindView(View view) {
+ super.onBindView(view);
+ mSummaryView = (TextView) view.findViewById(com.android.browser.R.id.text);
+ if (TextUtils.isEmpty(mSummary)) {
+ mSummaryView.setVisibility(View.GONE);
+ } else {
+ mSummaryView.setVisibility(View.VISIBLE);
+ mSummaryView.setText(mSummary);
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ // Intentionally blank - prevent super.onStartTrackingTouch from running
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ // Intentionally blank - prevent onStopTrackingTouch from running
+ }
+
+}
diff --git a/src/com/android/browser/preferences/WebViewPreview.java b/src/com/android/browser/preferences/WebViewPreview.java
new file mode 100644
index 0000000..ce24ac3
--- /dev/null
+++ b/src/com/android/browser/preferences/WebViewPreview.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2011 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 android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.preference.Preference;
+import android.preference.PreferenceManager;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.R;
+
+public abstract class WebViewPreview extends Preference
+ implements OnSharedPreferenceChangeListener {
+
+ protected WebView mWebView;
+
+ public WebViewPreview(
+ Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(context);
+ }
+
+ public WebViewPreview(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public WebViewPreview(Context context) {
+ super(context);
+ init(context);
+ }
+
+ protected void init(Context context) {
+ setLayoutResource(R.layout.webview_preview);
+ }
+
+ protected abstract void updatePreview(boolean forceReload);
+
+ protected void setupWebView(WebView view) {}
+
+ @Override
+ protected View onCreateView(ViewGroup parent) {
+ View root = super.onCreateView(parent);
+ WebView webView = (WebView) root.findViewById(R.id.webview);
+ // Tell WebView to really, truly ignore all touch events. No, seriously,
+ // ignore them all. And don't show scrollbars.
+ webView.setFocusable(false);
+ webView.setFocusableInTouchMode(false);
+ webView.setClickable(false);
+ webView.setLongClickable(false);
+ webView.setHorizontalScrollBarEnabled(false);
+ webView.setVerticalScrollBarEnabled(false);
+ setupWebView(webView);
+ return root;
+ }
+
+ @Override
+ protected void onBindView(View view) {
+ super.onBindView(view);
+ mWebView = (WebView) view.findViewById(R.id.webview);
+ updatePreview(true);
+ }
+
+ @Override
+ protected void onAttachedToHierarchy(PreferenceManager preferenceManager) {
+ super.onAttachedToHierarchy(preferenceManager);
+ getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ protected void onPrepareForRemoval() {
+ getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
+ super.onPrepareForRemoval();
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(
+ SharedPreferences sharedPreferences, String key) {
+ updatePreview(false);
+ }
+
+}
diff --git a/src/com/android/browser/preferences/WebsiteSettingsFragment.java b/src/com/android/browser/preferences/WebsiteSettingsFragment.java
new file mode 100644
index 0000000..a621dec
--- /dev/null
+++ b/src/com/android/browser/preferences/WebsiteSettingsFragment.java
@@ -0,0 +1,704 @@
+/*
+ * 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.preferences;
+
+import android.app.AlertDialog;
+import android.app.ListFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+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.os.Parcel;
+import android.os.Parcelable;
+import android.preference.PreferenceActivity;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.webkit.ValueCallback;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.browser.R;
+import com.android.browser.WebStorageSizeManager;
+import com.android.browser.platformsupport.BrowserContract.Bookmarks;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import org.codeaurora.swe.GeolocationPermissions;
+import org.codeaurora.swe.WebStorage;
+
+/**
+ * Manage the settings for an origin.
+ * We use it to keep track of the 'HTML5' settings, i.e. database (webstorage)
+ * and Geolocation.
+ */
+public class WebsiteSettingsFragment extends ListFragment implements OnClickListener {
+
+ private static final String EXTRA_SITE = "site";
+ private String LOGTAG = "WebsiteSettingsActivity";
+ private static String sMBStored = null;
+ private SiteAdapter mAdapter = null;
+ private Site mSite = null;
+
+ static class Site implements Parcelable {
+ private String mOrigin;
+ private String mTitle;
+ private Bitmap mIcon;
+ private int mFeatures;
+
+ // These constants provide the set of features that a site may support
+ // They must be consecutive. To add a new feature, add a new FEATURE_XXX
+ // variable with value equal to the current value of FEATURE_COUNT, then
+ // increment FEATURE_COUNT.
+ final static int FEATURE_WEB_STORAGE = 0;
+ final static int FEATURE_GEOLOCATION = 1;
+ // The number of features available.
+ final static int FEATURE_COUNT = 2;
+
+ public Site(String origin) {
+ mOrigin = origin;
+ mTitle = null;
+ mIcon = null;
+ mFeatures = 0;
+ }
+
+ public void addFeature(int feature) {
+ mFeatures |= (1 << feature);
+ }
+
+ public void removeFeature(int feature) {
+ mFeatures &= ~(1 << feature);
+ }
+
+ public boolean hasFeature(int feature) {
+ return (mFeatures & (1 << feature)) != 0;
+ }
+
+ /**
+ * Gets the number of features supported by this site.
+ */
+ public int getFeatureCount() {
+ int count = 0;
+ for (int i = 0; i < FEATURE_COUNT; ++i) {
+ count += hasFeature(i) ? 1 : 0;
+ }
+ return count;
+ }
+
+ /**
+ * Gets the ID of the nth (zero-based) feature supported by this site.
+ * The return value is a feature ID - one of the FEATURE_XXX values.
+ * This is required to determine which feature is displayed at a given
+ * position in the list of features for this site. This is used both
+ * when populating the view and when responding to clicks on the list.
+ */
+ public int getFeatureByIndex(int n) {
+ int j = -1;
+ for (int i = 0; i < FEATURE_COUNT; ++i) {
+ j += hasFeature(i) ? 1 : 0;
+ if (j == n) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public String getOrigin() {
+ return mOrigin;
+ }
+
+ public void setTitle(String title) {
+ mTitle = title;
+ }
+
+ public void setIcon(Bitmap icon) {
+ mIcon = icon;
+ }
+
+ public Bitmap getIcon() {
+ return mIcon;
+ }
+
+ public String getPrettyOrigin() {
+ return mTitle == null ? null : hideHttp(mOrigin);
+ }
+
+ public String getPrettyTitle() {
+ return mTitle == null ? hideHttp(mOrigin) : mTitle;
+ }
+
+ private String hideHttp(String str) {
+ Uri uri = Uri.parse(str);
+ return "http".equals(uri.getScheme()) ? str.substring(7) : str;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mOrigin);
+ dest.writeString(mTitle);
+ dest.writeInt(mFeatures);
+ dest.writeParcelable(mIcon, flags);
+ }
+
+ private Site(Parcel in) {
+ mOrigin = in.readString();
+ mTitle = in.readString();
+ mFeatures = in.readInt();
+ mIcon = in.readParcelable(null);
+ }
+
+ public static final Parcelable.Creator<Site> CREATOR
+ = new Parcelable.Creator<Site>() {
+ public Site createFromParcel(Parcel in) {
+ return new Site(in);
+ }
+
+ public Site[] newArray(int size) {
+ return new Site[size];
+ }
+ };
+
+ }
+
+ class SiteAdapter extends ArrayAdapter<Site>
+ implements AdapterView.OnItemClickListener {
+ private int mResource;
+ private LayoutInflater mInflater;
+ private Bitmap mDefaultIcon;
+ private Bitmap mUsageEmptyIcon;
+ private Bitmap mUsageLowIcon;
+ private Bitmap mUsageHighIcon;
+ private Bitmap mLocationAllowedIcon;
+ private Bitmap mLocationDisallowedIcon;
+ private Site mCurrentSite;
+
+ public SiteAdapter(Context context, int rsc) {
+ this(context, rsc, null);
+ }
+
+ public SiteAdapter(Context context, int rsc, Site site) {
+ super(context, rsc);
+ mResource = rsc;
+ mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mDefaultIcon = BitmapFactory.decodeResource(getResources(),
+ R.drawable.app_web_browser_sm);
+ mUsageEmptyIcon = BitmapFactory.decodeResource(getResources(),
+ R.drawable.ic_list_data_off);
+ mUsageLowIcon = BitmapFactory.decodeResource(getResources(),
+ R.drawable.ic_list_data_small);
+ mUsageHighIcon = BitmapFactory.decodeResource(getResources(),
+ R.drawable.ic_list_data_large);
+ mLocationAllowedIcon = BitmapFactory.decodeResource(getResources(),
+ R.drawable.ic_gps_on_holo_dark);
+ mLocationDisallowedIcon = BitmapFactory.decodeResource(getResources(),
+ R.drawable.ic_gps_denied_holo_dark);
+ mCurrentSite = site;
+ if (mCurrentSite == null) {
+ askForOrigins();
+ }
+ }
+
+ /**
+ * Adds the specified feature to the site corresponding to supplied
+ * origin in the map. Creates the site if it does not already exist.
+ */
+ private void addFeatureToSite(Map<String, Site> sites, String origin, int feature) {
+ Site site = null;
+ if (sites.containsKey(origin)) {
+ site = (Site) sites.get(origin);
+ } else {
+ site = new Site(origin);
+ sites.put(origin, site);
+ }
+ site.addFeature(feature);
+ }
+
+ public void askForOrigins() {
+ // Get the list of origins we want to display.
+ // All 'HTML 5 modules' (Database, Geolocation etc) form these
+ // origin strings using WebCore::SecurityOrigin::toString(), so it's
+ // safe to group origins here. Note that WebCore::SecurityOrigin
+ // uses 0 (which is not printed) for the port if the port is the
+ // default for the protocol. Eg http://www.google.com and
+ // http://www.google.com:80 both record a port of 0 and hence
+ // toString() == 'http://www.google.com' for both.
+
+ WebStorage.getInstance().getOrigins(new ValueCallback<Map>() {
+ public void onReceiveValue(Map origins) {
+ Map<String, Site> sites = new HashMap<String, Site>();
+ if (origins != null) {
+ Iterator<String> iter = origins.keySet().iterator();
+ while (iter.hasNext()) {
+ addFeatureToSite(sites, iter.next(), Site.FEATURE_WEB_STORAGE);
+ }
+ }
+ askForGeolocation(sites);
+ }
+ });
+ }
+
+ public void askForGeolocation(final Map<String, Site> sites) {
+ GeolocationPermissions.getInstance().getOrigins(new ValueCallback<Set<String> >() {
+ public void onReceiveValue(Set<String> origins) {
+ if (origins != null) {
+ Iterator<String> iter = origins.iterator();
+ while (iter.hasNext()) {
+ addFeatureToSite(sites, iter.next(), Site.FEATURE_GEOLOCATION);
+ }
+ }
+ populateIcons(sites);
+ populateOrigins(sites);
+ }
+ });
+ }
+
+ 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. 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.getApplicationContext();
+ mSites = sites;
+ }
+
+ 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);
+ }
+ 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());
+ }
+ c.close();
+ }
+ return null;
+ }
+
+ protected void onPostExecute(Void unused) {
+ if (mDataSetChanged) {
+ notifyDataSetChanged();
+ }
+ }
+ }
+
+
+ public void populateOrigins(Map<String, Site> sites) {
+ clear();
+
+ // We can now simply populate our array with Site instances
+ Set<Map.Entry<String, Site>> elements = sites.entrySet();
+ Iterator<Map.Entry<String, Site>> entryIterator = elements.iterator();
+ while (entryIterator.hasNext()) {
+ Map.Entry<String, Site> entry = entryIterator.next();
+ Site site = entry.getValue();
+ add(site);
+ }
+
+ notifyDataSetChanged();
+
+ if (getCount() == 0) {
+ finish(); // we close the screen
+ }
+ }
+
+ public int getCount() {
+ if (mCurrentSite == null) {
+ return super.getCount();
+ }
+ return mCurrentSite.getFeatureCount();
+ }
+
+ public String sizeValueToString(long bytes) {
+ // We display the size in MB, to 1dp, rounding up to the next 0.1MB.
+ // bytes should always be greater than zero.
+ if (bytes <= 0) {
+ Log.e(LOGTAG, "sizeValueToString called with non-positive value: " + bytes);
+ return "0";
+ }
+ float megabytes = (float) bytes / (1024.0F * 1024.0F);
+ int truncated = (int) Math.ceil(megabytes * 10.0F);
+ float result = (float) (truncated / 10.0F);
+ return String.valueOf(result);
+ }
+
+ /*
+ * If we receive the back event and are displaying
+ * site's settings, we want to go back to the main
+ * list view. If not, we just do nothing (see
+ * dispatchKeyEvent() below).
+ */
+ public boolean backKeyPressed() {
+ if (mCurrentSite != null) {
+ mCurrentSite = null;
+ askForOrigins();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @hide
+ * Utility function
+ * Set the icon according to the usage
+ */
+ public void setIconForUsage(ImageView usageIcon, long usageInBytes) {
+ float usageInMegabytes = (float) usageInBytes / (1024.0F * 1024.0F);
+ // We set the correct icon:
+ // 0 < empty < 0.1MB
+ // 0.1MB < low < 5MB
+ // 5MB < high
+ if (usageInMegabytes <= 0.1) {
+ usageIcon.setImageBitmap(mUsageEmptyIcon);
+ } else if (usageInMegabytes > 0.1 && usageInMegabytes <= 5) {
+ usageIcon.setImageBitmap(mUsageLowIcon);
+ } else if (usageInMegabytes > 5) {
+ usageIcon.setImageBitmap(mUsageHighIcon);
+ }
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view;
+ final TextView title;
+ final TextView subtitle;
+ final ImageView icon;
+ final ImageView usageIcon;
+ final ImageView locationIcon;
+ final ImageView featureIcon;
+
+ if (convertView == null) {
+ view = mInflater.inflate(mResource, parent, false);
+ } else {
+ view = convertView;
+ }
+
+ title = (TextView) view.findViewById(R.id.title);
+ subtitle = (TextView) view.findViewById(R.id.subtitle);
+ icon = (ImageView) view.findViewById(R.id.icon);
+ featureIcon = (ImageView) view.findViewById(R.id.feature_icon);
+ usageIcon = (ImageView) view.findViewById(R.id.usage_icon);
+ locationIcon = (ImageView) view.findViewById(R.id.location_icon);
+ usageIcon.setVisibility(View.GONE);
+ locationIcon.setVisibility(View.GONE);
+
+ if (mCurrentSite == null) {
+
+ Site site = getItem(position);
+ title.setText(site.getPrettyTitle());
+ String subtitleText = site.getPrettyOrigin();
+ if (subtitleText != null) {
+ title.setMaxLines(1);
+ title.setSingleLine(true);
+ subtitle.setVisibility(View.VISIBLE);
+ subtitle.setText(subtitleText);
+ } else {
+ subtitle.setVisibility(View.GONE);
+ title.setMaxLines(2);
+ title.setSingleLine(false);
+ }
+
+ icon.setVisibility(View.VISIBLE);
+ usageIcon.setVisibility(View.INVISIBLE);
+ locationIcon.setVisibility(View.INVISIBLE);
+ featureIcon.setVisibility(View.GONE);
+ Bitmap bmp = site.getIcon();
+ if (bmp == null) {
+ bmp = mDefaultIcon;
+ }
+ icon.setImageBitmap(bmp);
+ // We set the site as the view's tag,
+ // so that we can get it in onItemClick()
+ view.setTag(site);
+
+ String origin = site.getOrigin();
+ if (site.hasFeature(Site.FEATURE_WEB_STORAGE)) {
+ WebStorage.getInstance().getUsageForOrigin(origin, new ValueCallback<Long>() {
+ public void onReceiveValue(Long value) {
+ if (value != null) {
+ setIconForUsage(usageIcon, value.longValue());
+ usageIcon.setVisibility(View.VISIBLE);
+ }
+ }
+ });
+ }
+
+ if (site.hasFeature(Site.FEATURE_GEOLOCATION)) {
+ locationIcon.setVisibility(View.VISIBLE);
+ GeolocationPermissions.getInstance().getAllowed(origin, new ValueCallback<Boolean>() {
+ public void onReceiveValue(Boolean allowed) {
+ if (allowed != null) {
+ if (allowed.booleanValue()) {
+ locationIcon.setImageBitmap(mLocationAllowedIcon);
+ } else {
+ locationIcon.setImageBitmap(mLocationDisallowedIcon);
+ }
+ }
+ }
+ });
+ }
+ } else {
+ icon.setVisibility(View.GONE);
+ locationIcon.setVisibility(View.GONE);
+ usageIcon.setVisibility(View.GONE);
+ featureIcon.setVisibility(View.VISIBLE);
+ String origin = mCurrentSite.getOrigin();
+ switch (mCurrentSite.getFeatureByIndex(position)) {
+ case Site.FEATURE_WEB_STORAGE:
+ WebStorage.getInstance().getUsageForOrigin(origin, new ValueCallback<Long>() {
+ public void onReceiveValue(Long value) {
+ if (value != null) {
+ String usage = sizeValueToString(value.longValue()) + " " + sMBStored;
+ title.setText(R.string.webstorage_clear_data_title);
+ subtitle.setText(usage);
+ subtitle.setVisibility(View.VISIBLE);
+ setIconForUsage(featureIcon, value.longValue());
+ }
+ }
+ });
+ break;
+ case Site.FEATURE_GEOLOCATION:
+ title.setText(R.string.geolocation_settings_page_title);
+ GeolocationPermissions.getInstance().getAllowed(origin, new ValueCallback<Boolean>() {
+ public void onReceiveValue(Boolean allowed) {
+ if (allowed != null) {
+ if (allowed.booleanValue()) {
+ subtitle.setText(R.string.geolocation_settings_page_summary_allowed);
+ featureIcon.setImageBitmap(mLocationAllowedIcon);
+ } else {
+ subtitle.setText(R.string.geolocation_settings_page_summary_not_allowed);
+ featureIcon.setImageBitmap(mLocationDisallowedIcon);
+ }
+ subtitle.setVisibility(View.VISIBLE);
+ }
+ }
+ });
+ break;
+ }
+ }
+
+ return view;
+ }
+
+ public void onItemClick(AdapterView<?> parent,
+ View view,
+ int position,
+ long id) {
+ if (mCurrentSite != null) {
+ switch (mCurrentSite.getFeatureByIndex(position)) {
+ case Site.FEATURE_WEB_STORAGE:
+ new AlertDialog.Builder(getContext())
+ .setMessage(R.string.webstorage_clear_data_dialog_message)
+ .setPositiveButton(R.string.webstorage_clear_data_dialog_ok_button,
+ new AlertDialog.OnClickListener() {
+ public void onClick(DialogInterface dlg, int which) {
+ WebStorage.getInstance().deleteOrigin(mCurrentSite.getOrigin());
+ // If this site has no more features, then go back to the
+ // origins list.
+ mCurrentSite.removeFeature(Site.FEATURE_WEB_STORAGE);
+ if (mCurrentSite.getFeatureCount() == 0) {
+ finish();
+ }
+ askForOrigins();
+ notifyDataSetChanged();
+ }})
+ .setNegativeButton(R.string.webstorage_clear_data_dialog_cancel_button, null)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .show();
+ break;
+ case Site.FEATURE_GEOLOCATION:
+ new AlertDialog.Builder(getContext())
+ .setMessage(R.string.geolocation_settings_page_dialog_message)
+ .setPositiveButton(R.string.geolocation_settings_page_dialog_ok_button,
+ new AlertDialog.OnClickListener() {
+ public void onClick(DialogInterface dlg, int which) {
+ GeolocationPermissions.getInstance().clear(mCurrentSite.getOrigin());
+ mCurrentSite.removeFeature(Site.FEATURE_GEOLOCATION);
+ if (mCurrentSite.getFeatureCount() == 0) {
+ finish();
+ }
+ askForOrigins();
+ notifyDataSetChanged();
+ }})
+ .setNegativeButton(R.string.geolocation_settings_page_dialog_cancel_button, null)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .show();
+ break;
+ }
+ } else {
+ Site site = (Site) view.getTag();
+ PreferenceActivity activity = (PreferenceActivity) getActivity();
+ if (activity != null) {
+ Bundle args = new Bundle();
+ args.putParcelable(EXTRA_SITE, site);
+ activity.startPreferencePanel(WebsiteSettingsFragment.class.getName(), args, 0,
+ site.getPrettyTitle(), null, 0);
+ }
+ }
+ }
+
+ public Site currentSite() {
+ return mCurrentSite;
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.website_settings, container, false);
+ Bundle args = getArguments();
+ if (args != null) {
+ mSite = (Site) args.getParcelable(EXTRA_SITE);
+ }
+ if (mSite == null) {
+ View clear = view.findViewById(R.id.clear_all_button);
+ clear.setVisibility(View.VISIBLE);
+ clear.setOnClickListener(this);
+ }
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ if (sMBStored == null) {
+ sMBStored = getString(R.string.webstorage_origin_summary_mb_stored);
+ }
+ mAdapter = new SiteAdapter(getActivity(), R.layout.website_settings_row);
+ if (mSite != null) {
+ mAdapter.mCurrentSite = mSite;
+ }
+ getListView().setAdapter(mAdapter);
+ getListView().setOnItemClickListener(mAdapter);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mAdapter.askForOrigins();
+ }
+
+ private void finish() {
+ PreferenceActivity activity = (PreferenceActivity) getActivity();
+ if (activity != null) {
+ activity.finishPreferencePanel(this, 0, null);
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.clear_all_button:
+ // Show the prompt to clear all origins of their data and geolocation permissions.
+ new AlertDialog.Builder(getActivity())
+ .setMessage(R.string.website_settings_clear_all_dialog_message)
+ .setPositiveButton(R.string.website_settings_clear_all_dialog_ok_button,
+ new AlertDialog.OnClickListener() {
+ public void onClick(DialogInterface dlg, int which) {
+ WebStorage.getInstance().deleteAllData();
+ GeolocationPermissions.getInstance().clearAll();
+ WebStorageSizeManager.resetLastOutOfSpaceNotificationTime();
+ mAdapter.askForOrigins();
+ finish();
+ }})
+ .setNegativeButton(R.string.website_settings_clear_all_dialog_cancel_button, null)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .show();
+ break;
+ }
+ }
+}
diff --git a/src/com/android/browser/provider/BrowserProvider.java b/src/com/android/browser/provider/BrowserProvider.java
new file mode 100644
index 0000000..744032c
--- /dev/null
+++ b/src/com/android/browser/provider/BrowserProvider.java
@@ -0,0 +1,1040 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser.provider;
+
+import android.app.SearchManager;
+import android.app.backup.BackupManager;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+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;
+import android.os.Process;
+import android.preference.PreferenceManager;
+import android.provider.Browser;
+import android.provider.Browser.BookmarkColumns;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Patterns;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.R;
+import com.android.browser.search.SearchEngine;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.util.Date;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+public class BrowserProvider extends ContentProvider {
+
+ private SQLiteOpenHelper mOpenHelper;
+ private BackupManager mBackupManager;
+ static final String sDatabaseName = "browser.db";
+ private static final String TAG = "BrowserProvider";
+ private static final String ORDER_BY = "visits DESC, date DESC";
+
+ private static final String PICASA_URL = "http://picasaweb.google.com/m/" +
+ "viewer?source=androidclient";
+
+ static final String[] TABLE_NAMES = new String[] {
+ "bookmarks", "searches"
+ };
+ private static final String[] SUGGEST_PROJECTION = new String[] {
+ "_id", "url", "title", "bookmark", "user_entered"
+ };
+ private static final String SUGGEST_SELECTION =
+ "(url LIKE ? OR url LIKE ? OR url LIKE ? OR url LIKE ?"
+ + " OR title LIKE ?) AND (bookmark = 1 OR user_entered = 1)";
+ private String[] SUGGEST_ARGS = new String[5];
+
+ // shared suggestion array index, make sure to match COLUMNS
+ private static final int SUGGEST_COLUMN_INTENT_ACTION_ID = 1;
+ private static final int SUGGEST_COLUMN_INTENT_DATA_ID = 2;
+ private static final int SUGGEST_COLUMN_TEXT_1_ID = 3;
+ private static final int SUGGEST_COLUMN_TEXT_2_ID = 4;
+ private static final int SUGGEST_COLUMN_TEXT_2_URL_ID = 5;
+ private static final int SUGGEST_COLUMN_ICON_1_ID = 6;
+ private static final int SUGGEST_COLUMN_ICON_2_ID = 7;
+ 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",
+ SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
+ SearchManager.SUGGEST_COLUMN_INTENT_DATA,
+ SearchManager.SUGGEST_COLUMN_TEXT_1,
+ SearchManager.SUGGEST_COLUMN_TEXT_2,
+ SearchManager.SUGGEST_COLUMN_TEXT_2_URL,
+ SearchManager.SUGGEST_COLUMN_ICON_1,
+ SearchManager.SUGGEST_COLUMN_ICON_2,
+ SearchManager.SUGGEST_COLUMN_QUERY,
+ SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA};
+
+
+ // make sure that these match the index of TABLE_NAMES
+ static final int URI_MATCH_BOOKMARKS = 0;
+ private static final int URI_MATCH_SEARCHES = 1;
+ // (id % 10) should match the table name index
+ private static final int URI_MATCH_BOOKMARKS_ID = 10;
+ private static final int URI_MATCH_SEARCHES_ID = 11;
+ //
+ private static final int URI_MATCH_SUGGEST = 20;
+ private static final int URI_MATCH_BOOKMARKS_SUGGEST = 21;
+
+ private static final UriMatcher URI_MATCHER;
+
+ static {
+ URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+ URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_BOOKMARKS],
+ URI_MATCH_BOOKMARKS);
+ URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_BOOKMARKS] + "/#",
+ URI_MATCH_BOOKMARKS_ID);
+ URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_SEARCHES],
+ URI_MATCH_SEARCHES);
+ URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_SEARCHES] + "/#",
+ URI_MATCH_SEARCHES_ID);
+ URI_MATCHER.addURI("browser", SearchManager.SUGGEST_URI_PATH_QUERY,
+ URI_MATCH_SUGGEST);
+ URI_MATCHER.addURI("browser",
+ TABLE_NAMES[URI_MATCH_BOOKMARKS] + "/" + SearchManager.SUGGEST_URI_PATH_QUERY,
+ URI_MATCH_BOOKMARKS_SUGGEST);
+ }
+
+ // 1 -> 2 add cache table
+ // 2 -> 3 update history table
+ // 3 -> 4 add passwords table
+ // 4 -> 5 add settings table
+ // 5 -> 6 ?
+ // 6 -> 7 ?
+ // 7 -> 8 drop proxy table
+ // 8 -> 9 drop settings table
+ // 9 -> 10 add form_urls and form_data
+ // 10 -> 11 add searches table
+ // 11 -> 12 modify cache table
+ // 12 -> 13 modify cache table
+ // 13 -> 14 correspond with Google Bookmarks schema
+ // 14 -> 15 move couple of tables to either browser private database or webview database
+ // 15 -> 17 Set it up for the SearchManager
+ // 17 -> 18 Added favicon in bookmarks table for Home shortcuts
+ // 18 -> 19 Remove labels table
+ // 19 -> 20 Added thumbnail
+ // 20 -> 21 Added touch_icon
+ // 21 -> 22 Remove "clientid"
+ // 22 -> 23 Added user_entered
+ // 23 -> 24 Url not allowed to be null anymore.
+ private static final int DATABASE_VERSION = 24;
+
+ // 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 BrowserSettings mSettings;
+
+ private int mMaxSuggestionShortSize;
+ private int mMaxSuggestionLongSize;
+
+ public BrowserProvider() {
+ }
+
+ // XXX: This is a major hack to remove our dependency on gsf constants and
+ // its content provider. http://b/issue?id=2425179
+ public static String getClientId(ContentResolver cr) {
+ String ret = "android-google";
+ Cursor legacyClientIdCursor = null;
+ Cursor searchClientIdCursor = null;
+
+ // search_client_id includes search prefix, legacy client_id does not include prefix
+ try {
+ searchClientIdCursor = cr.query(Uri.parse("content://com.google.settings/partner"),
+ new String[] { "value" }, "name='search_client_id'", null, null);
+ if (searchClientIdCursor != null && searchClientIdCursor.moveToNext()) {
+ ret = searchClientIdCursor.getString(0);
+ } else {
+ legacyClientIdCursor = cr.query(Uri.parse("content://com.google.settings/partner"),
+ new String[] { "value" }, "name='client_id'", null, null);
+ if (legacyClientIdCursor != null && legacyClientIdCursor.moveToNext()) {
+ ret = "ms-" + legacyClientIdCursor.getString(0);
+ }
+ }
+ } catch (RuntimeException ex) {
+ // fall through to return the default
+ } finally {
+ if (legacyClientIdCursor != null) {
+ legacyClientIdCursor.close();
+ }
+ if (searchClientIdCursor != null) {
+ searchClientIdCursor.close();
+ }
+ }
+ return ret;
+ }
+
+ private static 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;
+ }
+
+ static class DatabaseHelper extends SQLiteOpenHelper {
+ private Context mContext;
+
+ public DatabaseHelper(Context context) {
+ super(context, sDatabaseName, null, DATABASE_VERSION);
+ mContext = context;
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE bookmarks (" +
+ "_id INTEGER PRIMARY KEY," +
+ "title TEXT," +
+ "url TEXT NOT NULL," +
+ "visits INTEGER," +
+ "date LONG," +
+ "created LONG," +
+ "description TEXT," +
+ "bookmark INTEGER," +
+ "favicon BLOB DEFAULT NULL," +
+ "thumbnail BLOB DEFAULT NULL," +
+ "touch_icon BLOB DEFAULT NULL," +
+ "user_entered INTEGER" +
+ ");");
+
+ final CharSequence[] bookmarks = mContext.getResources()
+ .getTextArray(R.array.bookmarks);
+ int size = bookmarks.length;
+ try {
+ for (int i = 0; i < size; i = i + 2) {
+ CharSequence bookmarkDestination = replaceSystemPropertyInString(mContext, bookmarks[i + 1]);
+ db.execSQL("INSERT INTO bookmarks (title, url, visits, " +
+ "date, created, bookmark)" + " VALUES('" +
+ bookmarks[i] + "', '" + bookmarkDestination +
+ "', 0, 0, 0, 1);");
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ }
+
+ db.execSQL("CREATE TABLE searches (" +
+ "_id INTEGER PRIMARY KEY," +
+ "search TEXT," +
+ "date LONG" +
+ ");");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+ + newVersion);
+ if (oldVersion == 18) {
+ db.execSQL("DROP TABLE IF EXISTS labels");
+ }
+ if (oldVersion <= 19) {
+ db.execSQL("ALTER TABLE bookmarks ADD COLUMN thumbnail BLOB DEFAULT NULL;");
+ }
+ if (oldVersion < 21) {
+ db.execSQL("ALTER TABLE bookmarks ADD COLUMN touch_icon BLOB DEFAULT NULL;");
+ }
+ if (oldVersion < 22) {
+ db.execSQL("DELETE FROM bookmarks WHERE (bookmark = 0 AND url LIKE \"%.google.%client=ms-%\")");
+ removeGears();
+ }
+ if (oldVersion < 23) {
+ db.execSQL("ALTER TABLE bookmarks ADD COLUMN user_entered INTEGER;");
+ }
+ if (oldVersion < 24) {
+ /* SQLite does not support ALTER COLUMN, hence the lengthy code. */
+ db.execSQL("DELETE FROM bookmarks WHERE url IS NULL;");
+ db.execSQL("ALTER TABLE bookmarks RENAME TO bookmarks_temp;");
+ db.execSQL("CREATE TABLE bookmarks (" +
+ "_id INTEGER PRIMARY KEY," +
+ "title TEXT," +
+ "url TEXT NOT NULL," +
+ "visits INTEGER," +
+ "date LONG," +
+ "created LONG," +
+ "description TEXT," +
+ "bookmark INTEGER," +
+ "favicon BLOB DEFAULT NULL," +
+ "thumbnail BLOB DEFAULT NULL," +
+ "touch_icon BLOB DEFAULT NULL," +
+ "user_entered INTEGER" +
+ ");");
+ db.execSQL("INSERT INTO bookmarks SELECT * FROM bookmarks_temp;");
+ db.execSQL("DROP TABLE bookmarks_temp;");
+ } else {
+ db.execSQL("DROP TABLE IF EXISTS bookmarks");
+ db.execSQL("DROP TABLE IF EXISTS searches");
+ onCreate(db);
+ }
+ }
+
+ private void removeGears() {
+ new Thread() {
+ @Override
+ public void run() {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+ String browserDataDirString = mContext.getApplicationInfo().dataDir;
+ final String appPluginsDirString = "app_plugins";
+ final String gearsPrefix = "gears";
+ File appPluginsDir = new File(browserDataDirString + File.separator
+ + appPluginsDirString);
+ if (!appPluginsDir.exists()) {
+ return;
+ }
+ // Delete the Gears plugin files
+ File[] gearsFiles = appPluginsDir.listFiles(new FilenameFilter() {
+ public boolean accept(File dir, String filename) {
+ return filename.startsWith(gearsPrefix);
+ }
+ });
+ for (int i = 0; i < gearsFiles.length; ++i) {
+ if (gearsFiles[i].isDirectory()) {
+ deleteDirectory(gearsFiles[i]);
+ } else {
+ gearsFiles[i].delete();
+ }
+ }
+ // Delete the Gears data files
+ File gearsDataDir = new File(browserDataDirString + File.separator
+ + gearsPrefix);
+ if (!gearsDataDir.exists()) {
+ return;
+ }
+ deleteDirectory(gearsDataDir);
+ }
+
+ private void deleteDirectory(File currentDir) {
+ File[] files = currentDir.listFiles();
+ for (int i = 0; i < files.length; ++i) {
+ if (files[i].isDirectory()) {
+ deleteDirectory(files[i]);
+ }
+ files[i].delete();
+ }
+ currentDir.delete();
+ }
+ }.start();
+ }
+ }
+
+ @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.
+ // To avoid erasing the bookmark table, we added it explicitly for
+ // version 18 and 19 as in the other cases, we will erase the table.
+ if (DATABASE_VERSION == 18 || DATABASE_VERSION == 19) {
+ SharedPreferences p = PreferenceManager
+ .getDefaultSharedPreferences(context);
+ boolean fix = p.getBoolean("fix_picasa", true);
+ if (fix) {
+ fixPicasaBookmark();
+ Editor ed = p.edit();
+ ed.putBoolean("fix_picasa", false);
+ ed.apply();
+ }
+ }
+ mSettings = BrowserSettings.getInstance();
+ return true;
+ }
+
+ private void fixPicasaBookmark() {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ Cursor cursor = db.rawQuery("SELECT _id FROM bookmarks WHERE " +
+ "bookmark = 1 AND url = ?", new String[] { PICASA_URL });
+ try {
+ if (!cursor.moveToFirst()) {
+ // set "created" so that it will be on the top of the list
+ db.execSQL("INSERT INTO bookmarks (title, url, visits, " +
+ "date, created, bookmark)" + " VALUES('" +
+ getContext().getString(R.string.picasa) + "', '"
+ + PICASA_URL + "', 0, 0, " + new Date().getTime()
+ + ", 1);");
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ /*
+ * Subclass AbstractCursor so we can combine multiple Cursors and add
+ * "Search the web".
+ * Here are the rules.
+ * 1. We only have MAX_SUGGESTION_LONG_ENTRIES in the list plus
+ * "Search the web";
+ * 2. If bookmark/history entries has a match, "Search the web" shows up at
+ * the second place. Otherwise, "Search the web" shows up at the first
+ * place.
+ */
+ private class MySuggestionCursor extends AbstractCursor {
+ private Cursor mHistoryCursor;
+ private Cursor mSuggestCursor;
+ private int mHistoryCount;
+ private int mSuggestionCount;
+ private boolean mIncludeWebSearch;
+ private String mString;
+ private int mSuggestText1Id;
+ private int mSuggestText2Id;
+ private int mSuggestText2UrlId;
+ private int mSuggestQueryId;
+ private int mSuggestIntentExtraDataId;
+
+ public MySuggestionCursor(Cursor hc, Cursor sc, String string) {
+ mHistoryCursor = hc;
+ mSuggestCursor = sc;
+ mHistoryCount = hc != null ? hc.getCount() : 0;
+ mSuggestionCount = sc != null ? sc.getCount() : 0;
+ if (mSuggestionCount > (mMaxSuggestionLongSize - mHistoryCount)) {
+ mSuggestionCount = mMaxSuggestionLongSize - mHistoryCount;
+ }
+ mString = string;
+ mIncludeWebSearch = string.length() > 0;
+
+ // Some web suggest providers only give suggestions and have no description string for
+ // items. The order of the result columns may be different as well. So retrieve the
+ // column indices for the fields we need now and check before using below.
+ if (mSuggestCursor == null) {
+ mSuggestText1Id = -1;
+ mSuggestText2Id = -1;
+ mSuggestText2UrlId = -1;
+ mSuggestQueryId = -1;
+ mSuggestIntentExtraDataId = -1;
+ } else {
+ mSuggestText1Id = mSuggestCursor.getColumnIndex(
+ SearchManager.SUGGEST_COLUMN_TEXT_1);
+ mSuggestText2Id = mSuggestCursor.getColumnIndex(
+ SearchManager.SUGGEST_COLUMN_TEXT_2);
+ mSuggestText2UrlId = mSuggestCursor.getColumnIndex(
+ SearchManager.SUGGEST_COLUMN_TEXT_2_URL);
+ mSuggestQueryId = mSuggestCursor.getColumnIndex(
+ SearchManager.SUGGEST_COLUMN_QUERY);
+ mSuggestIntentExtraDataId = mSuggestCursor.getColumnIndex(
+ SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
+ }
+ }
+
+ @Override
+ public boolean onMove(int oldPosition, int newPosition) {
+ if (mHistoryCursor == null) {
+ return false;
+ }
+ if (mIncludeWebSearch) {
+ if (mHistoryCount == 0 && newPosition == 0) {
+ return true;
+ } else if (mHistoryCount > 0) {
+ if (newPosition == 0) {
+ mHistoryCursor.moveToPosition(0);
+ return true;
+ } else if (newPosition == 1) {
+ return true;
+ }
+ }
+ newPosition--;
+ }
+ if (mHistoryCount > newPosition) {
+ mHistoryCursor.moveToPosition(newPosition);
+ } else {
+ mSuggestCursor.moveToPosition(newPosition - mHistoryCount);
+ }
+ return true;
+ }
+
+ @Override
+ public int getCount() {
+ if (mIncludeWebSearch) {
+ return mHistoryCount + mSuggestionCount + 1;
+ } else {
+ return mHistoryCount + mSuggestionCount;
+ }
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ return COLUMNS;
+ }
+
+ @Override
+ public String getString(int columnIndex) {
+ if ((mPos != -1 && mHistoryCursor != null)) {
+ int type = -1; // 0: web search; 1: history; 2: suggestion
+ if (mIncludeWebSearch) {
+ if (mHistoryCount == 0 && mPos == 0) {
+ type = 0;
+ } else if (mHistoryCount > 0) {
+ if (mPos == 0) {
+ type = 1;
+ } else if (mPos == 1) {
+ type = 0;
+ }
+ }
+ if (type == -1) type = (mPos - 1) < mHistoryCount ? 1 : 2;
+ } else {
+ type = mPos < mHistoryCount ? 1 : 2;
+ }
+
+ switch(columnIndex) {
+ case SUGGEST_COLUMN_INTENT_ACTION_ID:
+ if (type == 1) {
+ return Intent.ACTION_VIEW;
+ } else {
+ return Intent.ACTION_SEARCH;
+ }
+
+ case SUGGEST_COLUMN_INTENT_DATA_ID:
+ if (type == 1) {
+ return mHistoryCursor.getString(1);
+ } else {
+ return null;
+ }
+
+ case SUGGEST_COLUMN_TEXT_1_ID:
+ if (type == 0) {
+ return mString;
+ } else if (type == 1) {
+ return getHistoryTitle();
+ } else {
+ if (mSuggestText1Id == -1) return null;
+ return mSuggestCursor.getString(mSuggestText1Id);
+ }
+
+ case SUGGEST_COLUMN_TEXT_2_ID:
+ if (type == 0) {
+ return getContext().getString(R.string.search_the_web);
+ } else if (type == 1) {
+ return null; // Use TEXT_2_URL instead
+ } else {
+ if (mSuggestText2Id == -1) return null;
+ return mSuggestCursor.getString(mSuggestText2Id);
+ }
+
+ case SUGGEST_COLUMN_TEXT_2_URL_ID:
+ if (type == 0) {
+ return null;
+ } else if (type == 1) {
+ return getHistoryUrl();
+ } else {
+ if (mSuggestText2UrlId == -1) return null;
+ return mSuggestCursor.getString(mSuggestText2UrlId);
+ }
+
+ case SUGGEST_COLUMN_ICON_1_ID:
+ if (type == 1) {
+ if (mHistoryCursor.getInt(3) == 1) {
+ return Integer.valueOf(
+ R.drawable.ic_search_category_bookmark)
+ .toString();
+ } else {
+ return Integer.valueOf(
+ R.drawable.ic_search_category_history)
+ .toString();
+ }
+ } else {
+ return Integer.valueOf(
+ R.drawable.ic_search_category_suggest)
+ .toString();
+ }
+
+ case SUGGEST_COLUMN_ICON_2_ID:
+ return "0";
+
+ case SUGGEST_COLUMN_QUERY_ID:
+ if (type == 0) {
+ return mString;
+ } else if (type == 1) {
+ // Return the url in the intent query column. This is ignored
+ // within the browser because our searchable is set to
+ // android:searchMode="queryRewriteFromData", but it is used by
+ // global search for query rewriting.
+ return mHistoryCursor.getString(1);
+ } else {
+ if (mSuggestQueryId == -1) return null;
+ return mSuggestCursor.getString(mSuggestQueryId);
+ }
+
+ case SUGGEST_COLUMN_INTENT_EXTRA_DATA:
+ if (type == 0) {
+ return null;
+ } else if (type == 1) {
+ return null;
+ } else {
+ if (mSuggestIntentExtraDataId == -1) return null;
+ return mSuggestCursor.getString(mSuggestIntentExtraDataId);
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public double getDouble(int column) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public float getFloat(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 short getShort(int column) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isNull(int column) {
+ throw new UnsupportedOperationException();
+ }
+
+ // TODO Temporary change, finalize after jq's changes go in
+ @Override
+ public void deactivate() {
+ if (mHistoryCursor != null) {
+ mHistoryCursor.deactivate();
+ }
+ if (mSuggestCursor != null) {
+ mSuggestCursor.deactivate();
+ }
+ 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) {
+ mHistoryCursor.close();
+ mHistoryCursor = null;
+ }
+ if (mSuggestCursor != null) {
+ mSuggestCursor.close();
+ mSuggestCursor = null;
+ }
+ }
+
+ /**
+ * 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 getHistoryTitle() {
+ String title = mHistoryCursor.getString(2 /* webpage title */);
+ if (TextUtils.isEmpty(title) || TextUtils.getTrimmedLength(title) == 0) {
+ title = stripUrl(mHistoryCursor.getString(1 /* 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 getHistoryUrl() {
+ String title = mHistoryCursor.getString(2 /* webpage title */);
+ if (TextUtils.isEmpty(title) || TextUtils.getTrimmedLength(title) == 0) {
+ return null;
+ } else {
+ return stripUrl(mHistoryCursor.getString(1 /* url */));
+ }
+ }
+
+ }
+
+ @Override
+ public Cursor query(Uri url, String[] projectionIn, String selection,
+ String[] selectionArgs, String sortOrder)
+ throws IllegalStateException {
+ int match = URI_MATCHER.match(url);
+ if (match == -1) {
+ throw new IllegalArgumentException("Unknown URL");
+ }
+
+ if (match == URI_MATCH_SUGGEST || match == URI_MATCH_BOOKMARKS_SUGGEST) {
+ // Handle suggestions
+ return doSuggestQuery(selection, selectionArgs, match == URI_MATCH_BOOKMARKS_SUGGEST);
+ }
+
+ String[] projection = null;
+ if (projectionIn != null && projectionIn.length > 0) {
+ projection = new String[projectionIn.length + 1];
+ System.arraycopy(projectionIn, 0, projection, 0, projectionIn.length);
+ projection[projectionIn.length] = "_id AS _id";
+ }
+
+ String whereClause = null;
+ if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) {
+ whereClause = "_id = " + url.getPathSegments().get(1);
+ }
+
+ 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));
+
+ 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]);
+ }
+ }
+
+ @Override
+ public String getType(Uri url) {
+ int match = URI_MATCHER.match(url);
+ switch (match) {
+ case URI_MATCH_BOOKMARKS:
+ return "vnd.android.cursor.dir/bookmark";
+
+ case URI_MATCH_BOOKMARKS_ID:
+ return "vnd.android.cursor.item/bookmark";
+
+ case URI_MATCH_SEARCHES:
+ return "vnd.android.cursor.dir/searches";
+
+ case URI_MATCH_SEARCHES_ID:
+ return "vnd.android.cursor.item/searches";
+
+ case URI_MATCH_SUGGEST:
+ return SearchManager.SUGGEST_MIME_TYPE;
+
+ default:
+ throw new IllegalArgumentException("Unknown URL");
+ }
+ }
+
+ @Override
+ public Uri insert(Uri url, ContentValues initialValues) {
+ boolean isBookmarkTable = false;
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+ int match = URI_MATCHER.match(url);
+ Uri uri = null;
+ switch (match) {
+ case URI_MATCH_BOOKMARKS: {
+ // Insert into the bookmarks table
+ long rowID = db.insert(TABLE_NAMES[URI_MATCH_BOOKMARKS], "url",
+ initialValues);
+ if (rowID > 0) {
+ uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI,
+ rowID);
+ }
+ isBookmarkTable = true;
+ break;
+ }
+
+ case URI_MATCH_SEARCHES: {
+ // Insert into the searches table
+ long rowID = db.insert(TABLE_NAMES[URI_MATCH_SEARCHES], "url",
+ initialValues);
+ if (rowID > 0) {
+ uri = ContentUris.withAppendedId(Browser.SEARCHES_URI,
+ rowID);
+ }
+ break;
+ }
+
+ default:
+ throw new IllegalArgumentException("Unknown URL");
+ }
+
+ if (uri == null) {
+ throw new IllegalArgumentException("Unknown URL");
+ }
+ getContext().getContentResolver().notifyChange(uri, null);
+
+ // Back up the new bookmark set if we just inserted one.
+ // A row created when bookmarks are added from scratch will have
+ // bookmark=1 in the initial value set.
+ if (isBookmarkTable
+ && initialValues.containsKey(BookmarkColumns.BOOKMARK)
+ && initialValues.getAsInteger(BookmarkColumns.BOOKMARK) != 0) {
+ mBackupManager.dataChanged();
+ }
+ return uri;
+ }
+
+ @Override
+ public int delete(Uri url, String where, String[] whereArgs) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+ int match = URI_MATCHER.match(url);
+ if (match == -1 || match == URI_MATCH_SUGGEST) {
+ throw new IllegalArgumentException("Unknown URL");
+ }
+
+ // need to know whether it's the bookmarks table for a couple of reasons
+ boolean isBookmarkTable = (match == URI_MATCH_BOOKMARKS_ID);
+ String id = null;
+
+ if (isBookmarkTable || match == URI_MATCH_SEARCHES_ID) {
+ StringBuilder sb = new StringBuilder();
+ if (where != null && where.length() > 0) {
+ sb.append("( ");
+ sb.append(where);
+ sb.append(" ) AND ");
+ }
+ id = url.getPathSegments().get(1);
+ sb.append("_id = ");
+ sb.append(id);
+ where = sb.toString();
+ }
+
+ ContentResolver cr = getContext().getContentResolver();
+
+ // we'lll need to back up the bookmark set if we are about to delete one
+ if (isBookmarkTable) {
+ Cursor cursor = cr.query(Browser.BOOKMARKS_URI,
+ new String[] { BookmarkColumns.BOOKMARK },
+ "_id = " + id, null, null);
+ if (cursor.moveToNext()) {
+ if (cursor.getInt(0) != 0) {
+ // yep, this record is a bookmark
+ mBackupManager.dataChanged();
+ }
+ }
+ cursor.close();
+ }
+
+ int count = db.delete(TABLE_NAMES[match % 10], where, whereArgs);
+ cr.notifyChange(url, null);
+ return count;
+ }
+
+ @Override
+ public int update(Uri url, ContentValues values, String where,
+ String[] whereArgs) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+ int match = URI_MATCHER.match(url);
+ if (match == -1 || match == URI_MATCH_SUGGEST) {
+ throw new IllegalArgumentException("Unknown URL");
+ }
+
+ if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) {
+ StringBuilder sb = new StringBuilder();
+ if (where != null && where.length() > 0) {
+ sb.append("( ");
+ sb.append(where);
+ sb.append(" ) AND ");
+ }
+ String id = url.getPathSegments().get(1);
+ sb.append("_id = ");
+ sb.append(id);
+ where = sb.toString();
+ }
+
+ ContentResolver cr = getContext().getContentResolver();
+
+ // Not all bookmark-table updates should be backed up. Look to see
+ // whether we changed the title, url, or "is a bookmark" state, and
+ // request a backup if so.
+ if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_BOOKMARKS) {
+ boolean changingBookmarks = false;
+ // Alterations to the bookmark field inherently change the bookmark
+ // set, so we don't need to query the record; we know a priori that
+ // we will need to back up this change.
+ if (values.containsKey(BookmarkColumns.BOOKMARK)) {
+ changingBookmarks = true;
+ } else if ((values.containsKey(BookmarkColumns.TITLE)
+ || values.containsKey(BookmarkColumns.URL))
+ && values.containsKey(BookmarkColumns._ID)) {
+ // If a title or URL has been changed, check to see if it is to
+ // a bookmark. The ID should have been included in the update,
+ // so use it.
+ Cursor cursor = cr.query(Browser.BOOKMARKS_URI,
+ new String[] { BookmarkColumns.BOOKMARK },
+ BookmarkColumns._ID + " = "
+ + values.getAsString(BookmarkColumns._ID), null, null);
+ if (cursor.moveToNext()) {
+ changingBookmarks = (cursor.getInt(0) != 0);
+ }
+ cursor.close();
+ }
+
+ // if this *is* a bookmark row we're altering, we need to back it up.
+ if (changingBookmarks) {
+ mBackupManager.dataChanged();
+ }
+ }
+
+ int ret = db.update(TABLE_NAMES[match % 10], values, where, whereArgs);
+ cr.notifyChange(url, null);
+ return ret;
+ }
+
+ /**
+ * 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
+ */
+ private 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;
+ }
+ }
+
+ 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/provider/BrowserProvider2.java b/src/com/android/browser/provider/BrowserProvider2.java
new file mode 100644
index 0000000..cbb39b6
--- /dev/null
+++ b/src/com/android/browser/provider/BrowserProvider2.java
@@ -0,0 +1,2366 @@
+/*
+ * 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 android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.SearchManager;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.UriMatcher;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.database.AbstractCursor;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.MatrixCursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.provider.Browser;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.SyncStateContract;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.R;
+import com.android.browser.UrlUtils;
+import com.android.browser.platformsupport.BookmarkColumns;
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.SyncStateContentProviderHelper;
+import com.android.browser.platformsupport.BrowserContract.Accounts;
+import com.android.browser.platformsupport.BrowserContract.Bookmarks;
+import com.android.browser.platformsupport.BrowserContract.ChromeSyncColumns;
+import com.android.browser.platformsupport.BrowserContract.Combined;
+import com.android.browser.platformsupport.BrowserContract.History;
+import com.android.browser.platformsupport.BrowserContract.Images;
+import com.android.browser.platformsupport.BrowserContract.Searches;
+import com.android.browser.platformsupport.BrowserContract.Settings;
+import com.android.browser.platformsupport.BrowserContract.SyncState;
+import com.android.browser.reflect.ReflectHelper;
+import com.android.browser.widget.BookmarkThumbnailWidgetProvider;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.HashMap;
+
+public class BrowserProvider2 extends SQLiteContentProvider {
+
+ private static final String TAG = "BrowserProvider2";
+
+ public static final String PARAM_GROUP_BY = "groupBy";
+ public static final String PARAM_ALLOW_EMPTY_ACCOUNTS = "allowEmptyAccounts";
+
+ public static final String LEGACY_AUTHORITY = "browser";
+ static final Uri LEGACY_AUTHORITY_URI = new Uri.Builder()
+ .authority(LEGACY_AUTHORITY).scheme("content").build();
+
+ public static interface Thumbnails {
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(
+ BrowserContract.AUTHORITY_URI, "thumbnails");
+ public static final String _ID = "_id";
+ public static final String THUMBNAIL = "thumbnail";
+ }
+
+ public static interface OmniboxSuggestions {
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(
+ BrowserContract.AUTHORITY_URI, "omnibox_suggestions");
+ public static final String _ID = "_id";
+ public static final String URL = "url";
+ public static final String TITLE = "title";
+ public static final String IS_BOOKMARK = "bookmark";
+ }
+
+ 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 TABLE_SNAPSHOTS = "snapshots";
+ static final String TABLE_THUMBNAILS = "thumbnails";
+
+ 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 VIEW_ACCOUNTS = "v_accounts";
+ static final String VIEW_SNAPSHOTS_COMBINED = "v_snapshots_combined";
+ static final String VIEW_OMNIBOX_SUGGESTIONS = "v_omnibox_suggestions";
+
+ static final String FORMAT_COMBINED_JOIN_SUBQUERY_JOIN_IMAGES =
+ "history LEFT OUTER JOIN (%s) bookmarks " +
+ "ON history.url = bookmarks.url LEFT OUTER JOIN images " +
+ "ON history.url = images.url_key";
+
+ static final String DEFAULT_SORT_HISTORY = History.DATE_LAST_VISITED + " DESC";
+ static final String DEFAULT_SORT_ACCOUNTS =
+ Accounts.ACCOUNT_NAME + " IS NOT NULL DESC, "
+ + Accounts.ACCOUNT_NAME + " ASC";
+
+ private static final String TABLE_BOOKMARKS_JOIN_HISTORY =
+ "history LEFT OUTER JOIN bookmarks ON history.url = bookmarks.url";
+
+ private static final String[] SUGGEST_PROJECTION = new String[] {
+ qualifyColumn(TABLE_HISTORY, History._ID),
+ qualifyColumn(TABLE_HISTORY, History.URL),
+ bookmarkOrHistoryColumn(Combined.TITLE),
+ bookmarkOrHistoryLiteral(Combined.URL,
+ Integer.toString(R.drawable.ic_bookmark_off_holo_dark),
+ Integer.toString(R.drawable.ic_history_holo_dark)),
+ qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED)};
+
+ private static final String SUGGEST_SELECTION =
+ "history.url LIKE ? OR history.url LIKE ? OR history.url LIKE ? OR history.url LIKE ?"
+ + " OR history.title LIKE ? OR bookmarks.title LIKE ?";
+
+ private static final String ZERO_QUERY_SUGGEST_SELECTION =
+ TABLE_HISTORY + "." + History.DATE_LAST_VISITED + " != 0";
+
+ private static final String IMAGE_PRUNE =
+ "url_key NOT IN (SELECT url FROM bookmarks " +
+ "WHERE url IS NOT NULL AND deleted == 0) AND url_key NOT IN " +
+ "(SELECT url FROM history WHERE url IS NOT NULL)";
+
+ static final int THUMBNAILS = 10;
+ static final int THUMBNAILS_ID = 11;
+ static final int OMNIBOX_SUGGESTIONS = 20;
+ static final int HOMEPAGE = 60;
+
+ 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 BOOKMARKS_SUGGESTIONS = 1004;
+ static final int BOOKMARKS_DEFAULT_FOLDER_ID = 1005;
+
+ 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;
+
+ static final int LEGACY = 9000;
+ static final int LEGACY_ID = 9001;
+
+ 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_HISTORY_PROJECTION_MAP = new HashMap<String, String>();
+ static final HashMap<String, String> COMBINED_BOOKMARK_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, "bookmarks/folder/id", BOOKMARKS_DEFAULT_FOLDER_ID);
+ matcher.addURI(authority,
+ SearchManager.SUGGEST_URI_PATH_QUERY,
+ BOOKMARKS_SUGGESTIONS);
+ matcher.addURI(authority,
+ "bookmarks/" + SearchManager.SUGGEST_URI_PATH_QUERY,
+ BOOKMARKS_SUGGESTIONS);
+ 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);
+ matcher.addURI(authority, "thumbnails", THUMBNAILS);
+ matcher.addURI(authority, "thumbnails/#", THUMBNAILS_ID);
+ matcher.addURI(authority, "omnibox_suggestions", OMNIBOX_SUGGESTIONS);
+ matcher.addURI(authority, "homepage", HOMEPAGE);
+
+ // Legacy
+ matcher.addURI(LEGACY_AUTHORITY, "searches", SEARCHES);
+ matcher.addURI(LEGACY_AUTHORITY, "searches/#", SEARCHES_ID);
+ matcher.addURI(LEGACY_AUTHORITY, "bookmarks", LEGACY);
+ matcher.addURI(LEGACY_AUTHORITY, "bookmarks/#", LEGACY_ID);
+ matcher.addURI(LEGACY_AUTHORITY,
+ SearchManager.SUGGEST_URI_PATH_QUERY,
+ BOOKMARKS_SUGGESTIONS);
+ matcher.addURI(LEGACY_AUTHORITY,
+ "bookmarks/" + SearchManager.SUGGEST_URI_PATH_QUERY,
+ BOOKMARKS_SUGGESTIONS);
+
+ // 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);
+ map.put(Accounts.ROOT_ID, Accounts.ROOT_ID);
+
+ // 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);
+ map.put(Bookmarks.TYPE, "CASE "
+ + " WHEN " + Bookmarks.IS_FOLDER + "=0 THEN "
+ + Bookmarks.BOOKMARK_TYPE_BOOKMARK
+ + " WHEN " + ChromeSyncColumns.SERVER_UNIQUE + "='"
+ + ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR + "' THEN "
+ + Bookmarks.BOOKMARK_TYPE_BOOKMARK_BAR_FOLDER
+ + " WHEN " + ChromeSyncColumns.SERVER_UNIQUE + "='"
+ + ChromeSyncColumns.FOLDER_NAME_OTHER_BOOKMARKS + "' THEN "
+ + Bookmarks.BOOKMARK_TYPE_OTHER_FOLDER
+ + " ELSE " + Bookmarks.BOOKMARK_TYPE_FOLDER
+ + " END AS " + Bookmarks.TYPE);
+
+ // 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_HISTORY_PROJECTION_MAP;
+ map.put(Combined._ID, bookmarkOrHistoryColumn(Combined._ID));
+ map.put(Combined.TITLE, bookmarkOrHistoryColumn(Combined.TITLE));
+ map.put(Combined.URL, qualifyColumn(TABLE_HISTORY, Combined.URL));
+ map.put(Combined.DATE_CREATED, qualifyColumn(TABLE_HISTORY, Combined.DATE_CREATED));
+ map.put(Combined.DATE_LAST_VISITED, Combined.DATE_LAST_VISITED);
+ map.put(Combined.IS_BOOKMARK, "CASE WHEN " +
+ TABLE_BOOKMARKS + "." + Bookmarks._ID +
+ " IS NOT NULL THEN 1 ELSE 0 END AS " + 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, "NULL AS " + Combined.USER_ENTERED);
+
+ // Combined bookmark half
+ map = COMBINED_BOOKMARK_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, "NULL AS " + Combined.DATE_LAST_VISITED);
+ map.put(Combined.IS_BOOKMARK, "1 AS " + Combined.IS_BOOKMARK);
+ map.put(Combined.VISITS, "0 AS " + 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, "NULL AS " + 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 bookmarkOrHistoryLiteral(String column, String bookmarkValue,
+ String historyValue) {
+ return "CASE WHEN bookmarks." + column + " IS NOT NULL THEN \"" + bookmarkValue +
+ "\" ELSE \"" + historyValue + "\" END";
+ }
+
+ static final String qualifyColumn(String table, String column) {
+ return table + "." + column + " AS " + column;
+ }
+
+ DatabaseHelper mOpenHelper;
+ SyncStateContentProviderHelper mSyncHelper = new SyncStateContentProviderHelper();
+ // This is so provider tests can intercept widget updating
+ ContentObserver mWidgetObserver = null;
+ boolean mUpdateWidgets = false;
+ boolean mSyncToNetwork = true;
+
+ final class DatabaseHelper extends SQLiteOpenHelper {
+ static final String DATABASE_NAME = "browser2.db";
+ static final int DATABASE_VERSION = 32;
+ public DatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ setWriteAheadLoggingEnabled(true);
+ }
+
+ @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" +
+ ");");
+
+ createAccountsView(db);
+ createThumbnails(db);
+
+ mSyncHelper.createDatabase(db);
+
+ if (!importFromBrowserProvider(db)) {
+ createDefaultBookmarks(db);
+ }
+
+ enableSync(db);
+ createOmniboxSuggestions(db);
+ }
+
+ void createOmniboxSuggestions(SQLiteDatabase db) {
+ db.execSQL(SQL_CREATE_VIEW_OMNIBOX_SUGGESTIONS);
+ }
+
+ void createThumbnails(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_THUMBNAILS + " (" +
+ Thumbnails._ID + " INTEGER PRIMARY KEY," +
+ Thumbnails.THUMBNAIL + " BLOB NOT NULL" +
+ ");");
+ }
+
+ void enableSync(SQLiteDatabase db) {
+ ContentValues values = new ContentValues();
+ values.put(Settings.KEY, Settings.KEY_SYNC_ENABLED);
+ values.put(Settings.VALUE, 1);
+ insertSettingsInTransaction(db, values);
+ // Enable bookmark sync on all accounts
+ AccountManager am = (AccountManager) getContext().getSystemService(
+ Context.ACCOUNT_SERVICE);
+ if (am == null) {
+ return;
+ }
+ Account[] accounts = am.getAccountsByType("com.google");
+ if (accounts == null || accounts.length == 0) {
+ return;
+ }
+ for (Account account : accounts) {
+ if (ContentResolver.getIsSyncable(
+ account, BrowserContract.AUTHORITY) == 0) {
+ // Account wasn't syncable, enable it
+ ContentResolver.setIsSyncable(
+ account, BrowserContract.AUTHORITY, 1);
+ ContentResolver.setSyncAutomatically(
+ account, BrowserContract.AUTHORITY, true);
+ }
+ }
+ }
+
+ boolean importFromBrowserProvider(SQLiteDatabase db) {
+ Context context = getContext();
+ File oldDbFile = context.getDatabasePath(BrowserProvider.sDatabaseName);
+ if (oldDbFile.exists()) {
+ BrowserProvider.DatabaseHelper helper =
+ new BrowserProvider.DatabaseHelper(context);
+ SQLiteDatabase oldDb = helper.getWritableDatabase();
+ Cursor c = null;
+ try {
+ String table = BrowserProvider.TABLE_NAMES[BrowserProvider.URI_MATCH_BOOKMARKS];
+ // Import bookmarks
+ c = oldDb.query(table,
+ new String[] {
+ BookmarkColumns.URL, // 0
+ BookmarkColumns.TITLE, // 1
+ BookmarkColumns.FAVICON, // 2
+ BookmarkColumns.TOUCH_ICON, // 3
+ BookmarkColumns.CREATED, // 4
+ }, BookmarkColumns.BOOKMARK + "!=0", null,
+ null, null, null);
+ if (c != null) {
+ while (c.moveToNext()) {
+ String url = c.getString(0);
+ if (TextUtils.isEmpty(url))
+ continue; // We require a valid URL
+ ContentValues values = new ContentValues();
+ values.put(Bookmarks.URL, url);
+ values.put(Bookmarks.TITLE, c.getString(1));
+ values.put(Bookmarks.DATE_CREATED, c.getInt(4));
+ values.put(Bookmarks.POSITION, 0);
+ values.put(Bookmarks.PARENT, FIXED_ID_ROOT);
+ ContentValues imageValues = new ContentValues();
+ imageValues.put(Images.URL, url);
+ imageValues.put(Images.FAVICON, c.getBlob(2));
+ imageValues.put(Images.TOUCH_ICON, c.getBlob(3));
+ db.insert(TABLE_IMAGES, Images.THUMBNAIL, imageValues);
+ db.insert(TABLE_BOOKMARKS, Bookmarks.DIRTY, values);
+ }
+ c.close();
+ }
+ // Import history
+ c = oldDb.query(table,
+ new String[] {
+ BookmarkColumns.URL, // 0
+ BookmarkColumns.TITLE, // 1
+ BookmarkColumns.VISITS, // 2
+ BookmarkColumns.DATE, // 3
+ BookmarkColumns.CREATED, // 4
+ }, BookmarkColumns.VISITS + " > 0 OR "
+ + BookmarkColumns.BOOKMARK + " = 0",
+ null, null, null, null);
+ if (c != null) {
+ while (c.moveToNext()) {
+ ContentValues values = new ContentValues();
+ String url = c.getString(0);
+ if (TextUtils.isEmpty(url))
+ continue; // We require a valid URL
+ values.put(History.URL, url);
+ values.put(History.TITLE, c.getString(1));
+ values.put(History.VISITS, c.getInt(2));
+ values.put(History.DATE_LAST_VISITED, c.getLong(3));
+ values.put(History.DATE_CREATED, c.getLong(4));
+ db.insert(TABLE_HISTORY, History.FAVICON, values);
+ }
+ c.close();
+ }
+ // Wipe the old DB, in case the delete fails.
+ oldDb.delete(table, null, null);
+ } finally {
+ if (c != null) c.close();
+ oldDb.close();
+ helper.close();
+ }
+ if (!oldDbFile.delete()) {
+ oldDbFile.deleteOnExit();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ void createAccountsView(SQLiteDatabase db) {
+ db.execSQL("CREATE VIEW IF NOT EXISTS v_accounts AS "
+ + "SELECT NULL AS " + Accounts.ACCOUNT_NAME
+ + ", NULL AS " + Accounts.ACCOUNT_TYPE
+ + ", " + FIXED_ID_ROOT + " AS " + Accounts.ROOT_ID
+ + " UNION ALL SELECT " + Accounts.ACCOUNT_NAME
+ + ", " + Accounts.ACCOUNT_TYPE + ", "
+ + Bookmarks._ID + " AS " + Accounts.ROOT_ID
+ + " FROM " + TABLE_BOOKMARKS + " WHERE "
+ + ChromeSyncColumns.SERVER_UNIQUE + " = \""
+ + ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR + "\" AND "
+ + Bookmarks.IS_DELETED + " = 0");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if (oldVersion < 32) {
+ createOmniboxSuggestions(db);
+ }
+ if (oldVersion < 31) {
+ createThumbnails(db);
+ }
+ if (oldVersion < 30) {
+ db.execSQL("DROP VIEW IF EXISTS " + VIEW_SNAPSHOTS_COMBINED);
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_SNAPSHOTS);
+ }
+ if (oldVersion < 28) {
+ enableSync(db);
+ }
+ if (oldVersion < 27) {
+ createAccountsView(db);
+ }
+ if (oldVersion < 26) {
+ db.execSQL("DROP VIEW IF EXISTS combined");
+ }
+ if (oldVersion < 25) {
+ 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);
+ mSyncHelper.onAccountsChanged(db, new Account[] {}); // remove all sync info
+ onCreate(db);
+ }
+ }
+
+ 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);
+
+ // add for carrier bookmark feature
+ Object[] params = { new String("persist.env.c.browser.resource"),
+ new String("default")};
+ Class[] type = new Class[] {String.class, String.class};
+ String browserRes = (String)ReflectHelper.invokeStaticMethod(
+ "android.os.SystemProperties", "get",
+ type, params);
+
+ //don't add default bookmarks for cmcc
+ if (!"cmcc".equals(browserRes)) {
+ addDefaultBookmarks(db, FIXED_ID_ROOT);
+ }
+ if ("ct".equals(browserRes) || "cmcc".equals(browserRes)) {
+ addDefaultCarrierBookmarks(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) {
+ } finally {
+ preloads.recycle();
+ }
+ }
+
+ // add for carrier bookmark feature
+ private void addDefaultCarrierBookmarks(SQLiteDatabase db, long parentId) {
+ Context mResPackageCtx = null;
+ try {
+ mResPackageCtx = getContext().createPackageContext(
+ "com.android.browser.res",
+ Context.CONTEXT_IGNORE_SECURITY);
+ } catch (Exception e) {
+ Log.e(TAG, "Create Res Apk Failed");
+ }
+ if (mResPackageCtx == null)
+ return;
+
+ CharSequence[] bookmarks = null;
+ TypedArray preloads = null;
+ Resources res = mResPackageCtx.getResources();
+ int resBookmarksID = res.getIdentifier("bookmarks",
+ "array",
+ "com.android.browser.res");
+ int resPreloadsID = res.getIdentifier("bookmark_preloads", "array",
+ "com.android.browser.res");
+ if (resBookmarksID != 0 && resPreloadsID != 0) {
+ bookmarks = res.getTextArray(resBookmarksID);
+ preloads = res.obtainTypedArray(resPreloadsID);
+ } else {
+ return;
+ }
+
+ // The Default Carrier bookmarks size
+ int size = bookmarks.length;
+
+ // googleSize the Default Google bookmarks size.
+ // The Default Carrier Bookmarks original position need move to googleSize index.
+ final CharSequence[] googleBookmarks = getContext().getResources().getTextArray(
+ R.array.bookmarks);
+ int googleSize = googleBookmarks.length;
+ 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(googleSize + 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) {
+ } finally {
+ preloads.recycle();
+ }
+ }
+
+ private byte[] readRaw(Resources res, int id) throws IOException {
+ if (id == 0) {
+ return null;
+ }
+ 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);
+ }
+
+ @VisibleForTesting
+ public void setWidgetObserver(ContentObserver obs) {
+ mWidgetObserver = obs;
+ }
+
+ void refreshWidgets() {
+ mUpdateWidgets = true;
+ }
+
+ @Override
+ protected void onEndTransaction(boolean callerIsSyncAdapter) {
+ super.onEndTransaction(callerIsSyncAdapter);
+ if (mUpdateWidgets) {
+ if (mWidgetObserver == null) {
+ BookmarkThumbnailWidgetProvider.refreshWidgets(getContext());
+ } else {
+ mWidgetObserver.dispatchChange(false);
+ }
+ mUpdateWidgets = false;
+ }
+ mSyncToNetwork = true;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ final int match = URI_MATCHER.match(uri);
+ switch (match) {
+ case LEGACY:
+ case BOOKMARKS:
+ return Bookmarks.CONTENT_TYPE;
+ case LEGACY_ID:
+ 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;
+ }
+ return null;
+ }
+
+ boolean isNullAccount(String account) {
+ if (account == null) return true;
+ account = account.trim();
+ return account.length() == 0 || account.equals("null");
+ }
+
+ Object[] getSelectionWithAccounts(Uri uri, String selection, String[] selectionArgs) {
+ // Look for account info
+ String accountType = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE);
+ String accountName = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME);
+ boolean hasAccounts = false;
+ if (accountType != null && accountName != null) {
+ if (!isNullAccount(accountType) && !isNullAccount(accountName)) {
+ selection = DatabaseUtils.concatenateWhere(selection,
+ Bookmarks.ACCOUNT_TYPE + "=? AND " + Bookmarks.ACCOUNT_NAME + "=? ");
+ selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+ new String[] { accountType, accountName });
+ hasAccounts = true;
+ } else {
+ selection = DatabaseUtils.concatenateWhere(selection,
+ Bookmarks.ACCOUNT_NAME + " IS NULL AND " +
+ Bookmarks.ACCOUNT_TYPE + " IS NULL");
+ }
+ }
+ return new Object[] { selection, selectionArgs, hasAccounts };
+ }
+
+ @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);
+ String groupBy = uri.getQueryParameter(PARAM_GROUP_BY);
+ switch (match) {
+ case ACCOUNTS: {
+ qb.setTables(VIEW_ACCOUNTS);
+ qb.setProjectionMap(ACCOUNTS_PROJECTION_MAP);
+ String allowEmpty = uri.getQueryParameter(PARAM_ALLOW_EMPTY_ACCOUNTS);
+ if ("false".equals(allowEmpty)) {
+ selection = DatabaseUtils.concatenateWhere(selection,
+ SQL_WHERE_ACCOUNT_HAS_BOOKMARKS);
+ }
+ if (sortOrder == null) {
+ sortOrder = DEFAULT_SORT_ACCOUNTS;
+ }
+ 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)) });
+ }
+
+ Object[] withAccount = getSelectionWithAccounts(uri, selection, selectionArgs);
+ selection = (String) withAccount[0];
+ selectionArgs = (String[]) withAccount[1];
+ boolean hasAccounts = (Boolean) withAccount[2];
+
+ // Set a default sort order if one isn't specified
+ if (TextUtils.isEmpty(sortOrder)) {
+ if (hasAccounts) {
+ 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: {
+ // Look for an account
+ boolean useAccount = false;
+ String accountType = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE);
+ String accountName = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME);
+ if (!isNullAccount(accountType) && !isNullAccount(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);
+ String where = Bookmarks.PARENT + "=? AND " + Bookmarks.IS_DELETED + "=0";
+ where = DatabaseUtils.concatenateWhere(where, selection);
+ args = new String[] { Long.toString(FIXED_ID_ROOT) };
+ if (selectionArgs != null) {
+ args = DatabaseUtils.appendSelectionArgs(args, selectionArgs);
+ }
+ query = qb.buildQuery(projection, where, null, null, sortOrder, null);
+ } else {
+ qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
+ String where = 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";
+ where = DatabaseUtils.concatenateWhere(where, selection);
+ String bookmarksBarQuery = qb.buildQuery(projection,
+ where, null, null, null, null);
+ args = new String[] {accountType, accountName,
+ accountType, accountName};
+ if (selectionArgs != null) {
+ args = DatabaseUtils.appendSelectionArgs(args, selectionArgs);
+ }
+
+ where = Bookmarks.ACCOUNT_TYPE + "=? AND " + Bookmarks.ACCOUNT_NAME + "=?" +
+ " AND " + ChromeSyncColumns.SERVER_UNIQUE + "=?";
+ where = DatabaseUtils.concatenateWhere(where, selection);
+ qb.setProjectionMap(OTHER_BOOKMARKS_PROJECTION_MAP);
+ String otherBookmarksQuery = qb.buildQuery(projection,
+ where, null, null, null, null);
+
+ query = qb.buildUnionQuery(
+ new String[] { bookmarksBarQuery, otherBookmarksQuery },
+ sortOrder, limit);
+
+ args = DatabaseUtils.appendSelectionArgs(args, new String[] {
+ accountType, accountName, ChromeSyncColumns.FOLDER_NAME_OTHER_BOOKMARKS,
+ });
+ if (selectionArgs != null) {
+ args = DatabaseUtils.appendSelectionArgs(args, selectionArgs);
+ }
+ }
+
+ Cursor cursor = db.rawQuery(query, args);
+ if (cursor != null) {
+ cursor.setNotificationUri(getContext().getContentResolver(),
+ BrowserContract.AUTHORITY_URI);
+ }
+ return cursor;
+ }
+
+ case BOOKMARKS_DEFAULT_FOLDER_ID: {
+ String accountName = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME);
+ String accountType = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE);
+ long id = queryDefaultFolderId(accountName, accountType);
+ MatrixCursor c = new MatrixCursor(new String[] {Bookmarks._ID});
+ c.newRow().add(id);
+ return c;
+ }
+
+ case BOOKMARKS_SUGGESTIONS: {
+ return doSuggestQuery(selection, selectionArgs, limit);
+ }
+
+ 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: {
+ filterSearchClient(selectionArgs);
+ 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: {
+ 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 LEGACY_ID:
+ case COMBINED_ID: {
+ selection = DatabaseUtils.concatenateWhere(
+ selection, Combined._ID + " = CAST(? AS INTEGER)");
+ selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+ new String[] { Long.toString(ContentUris.parseId(uri)) });
+ // fall through
+ }
+ case LEGACY:
+ case COMBINED: {
+ if ((match == LEGACY || match == LEGACY_ID)
+ && projection == null) {
+ projection = Browser.HISTORY_PROJECTION;
+ }
+ String[] args = createCombinedQuery(uri, projection, qb);
+ if (selectionArgs == null) {
+ selectionArgs = args;
+ } else {
+ selectionArgs = DatabaseUtils.appendSelectionArgs(args, selectionArgs);
+ }
+ break;
+ }
+
+ case SETTINGS: {
+ qb.setTables(TABLE_SETTINGS);
+ qb.setProjectionMap(SETTINGS_PROJECTION_MAP);
+ break;
+ }
+
+ case THUMBNAILS_ID: {
+ selection = DatabaseUtils.concatenateWhere(
+ selection, Thumbnails._ID + " = ?");
+ selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+ new String[] { Long.toString(ContentUris.parseId(uri)) });
+ // fall through
+ }
+ case THUMBNAILS: {
+ qb.setTables(TABLE_THUMBNAILS);
+ break;
+ }
+
+ case OMNIBOX_SUGGESTIONS: {
+ qb.setTables(VIEW_OMNIBOX_SUGGESTIONS);
+ break;
+ }
+
+ case HOMEPAGE: {
+ String homepage = BrowserSettings.getInstance().getHomePage();
+ Log.d(TAG,"get home page for DM");
+ if (null == homepage) {
+ return null;
+ }
+ String arrColumns[] = {"homepage"};
+ String arrHomepage[] = {homepage};
+ MatrixCursor matrixCursor = new MatrixCursor(arrColumns, 1);
+ matrixCursor.addRow(arrHomepage);
+ return matrixCursor;
+ }
+
+ default: {
+ throw new UnsupportedOperationException("Unknown URL " + uri.toString());
+ }
+ }
+
+ Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy,
+ null, sortOrder, limit);
+ cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.AUTHORITY_URI);
+ return cursor;
+ }
+
+ private Cursor doSuggestQuery(String selection, String[] selectionArgs, String limit) {
+ if (TextUtils.isEmpty(selectionArgs[0])) {
+ selection = ZERO_QUERY_SUGGEST_SELECTION;
+ selectionArgs = null;
+ } else {
+ String like = selectionArgs[0] + "%";
+ if (selectionArgs[0].startsWith("http")
+ || selectionArgs[0].startsWith("file")) {
+ selectionArgs[0] = like;
+ } else {
+ selectionArgs = new String[6];
+ selectionArgs[0] = "http://" + like;
+ selectionArgs[1] = "http://www." + like;
+ selectionArgs[2] = "https://" + like;
+ selectionArgs[3] = "https://www." + like;
+ // To match against titles.
+ selectionArgs[4] = like;
+ selectionArgs[5] = like;
+ selection = SUGGEST_SELECTION;
+ }
+ selection = DatabaseUtils.concatenateWhere(selection,
+ Bookmarks.IS_DELETED + "=0 AND " + Bookmarks.IS_FOLDER + "=0");
+
+ }
+ Cursor c = mOpenHelper.getReadableDatabase().query(TABLE_BOOKMARKS_JOIN_HISTORY,
+ SUGGEST_PROJECTION, selection, selectionArgs, null, null,
+ null, null);
+
+ return new SuggestionsCursor(c);
+ }
+
+ private String[] createCombinedQuery(
+ Uri uri, String[] projection, SQLiteQueryBuilder qb) {
+ String[] args = null;
+ StringBuilder whereBuilder = new StringBuilder(128);
+ whereBuilder.append(Bookmarks.IS_DELETED);
+ whereBuilder.append(" = 0");
+ // Look for account info
+ Object[] withAccount = getSelectionWithAccounts(uri, null, null);
+ String selection = (String) withAccount[0];
+ String[] selectionArgs = (String[]) withAccount[1];
+ if (selection != null) {
+ whereBuilder.append(" AND " + selection);
+ if (selectionArgs != null) {
+ // We use the selection twice, hence we need to duplicate the args
+ args = new String[selectionArgs.length * 2];
+ System.arraycopy(selectionArgs, 0, args, 0, selectionArgs.length);
+ System.arraycopy(selectionArgs, 0, args, selectionArgs.length,
+ selectionArgs.length);
+ }
+ }
+ String where = whereBuilder.toString();
+ // Build the bookmark subquery for history union subquery
+ qb.setTables(TABLE_BOOKMARKS);
+ String subQuery = qb.buildQuery(null, where, null, null, null, null);
+ // Build the history union subquery
+ qb.setTables(String.format(FORMAT_COMBINED_JOIN_SUBQUERY_JOIN_IMAGES, subQuery));
+ qb.setProjectionMap(COMBINED_HISTORY_PROJECTION_MAP);
+ String historySubQuery = qb.buildQuery(null,
+ null, null, null, null, null);
+ // Build the bookmark union subquery
+ qb.setTables(TABLE_BOOKMARKS_JOIN_IMAGES);
+ qb.setProjectionMap(COMBINED_BOOKMARK_PROJECTION_MAP);
+ where += String.format(" AND %s NOT IN (SELECT %s FROM %s)",
+ Combined.URL, History.URL, TABLE_HISTORY);
+ String bookmarksSubQuery = qb.buildQuery(null, where,
+ null, null, null, null);
+ // Put it all together
+ String query = qb.buildUnionQuery(
+ new String[] {historySubQuery, bookmarksSubQuery},
+ null, null);
+ qb.setTables("(" + query + ")");
+ qb.setProjectionMap(null);
+ return args;
+ }
+
+ int deleteBookmarks(String selection, String[] selectionArgs,
+ boolean callerIsSyncAdapter) {
+ //TODO cascade deletes down from folders
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ if (callerIsSyncAdapter) {
+ return db.delete(TABLE_BOOKMARKS, selection, selectionArgs);
+ }
+ ContentValues values = new ContentValues();
+ values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
+ values.put(Bookmarks.IS_DELETED, 1);
+ return updateBookmarksInTransaction(values, selection, selectionArgs,
+ callerIsSyncAdapter);
+ }
+
+ @Override
+ public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs,
+ boolean callerIsSyncAdapter) {
+ final int match = URI_MATCHER.match(uri);
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ int deleted = 0;
+ 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: {
+ // Look for account info
+ Object[] withAccount = getSelectionWithAccounts(uri, selection, selectionArgs);
+ selection = (String) withAccount[0];
+ selectionArgs = (String[]) withAccount[1];
+ deleted = deleteBookmarks(selection, selectionArgs, callerIsSyncAdapter);
+ pruneImages();
+ if (deleted > 0) {
+ refreshWidgets();
+ }
+ break;
+ }
+
+ 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: {
+ filterSearchClient(selectionArgs);
+ deleted = db.delete(TABLE_HISTORY, selection, selectionArgs);
+ pruneImages();
+ 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: {
+ deleted = db.delete(TABLE_SEARCHES, selection, selectionArgs);
+ break;
+ }
+
+ case SYNCSTATE: {
+ deleted = mSyncHelper.delete(db, selection, selectionArgs);
+ break;
+ }
+ case SYNCSTATE_ID: {
+ String selectionWithId =
+ (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
+ + (selection == null ? "" : " AND (" + selection + ")");
+ deleted = mSyncHelper.delete(db, selectionWithId, selectionArgs);
+ break;
+ }
+ case LEGACY_ID: {
+ selection = DatabaseUtils.concatenateWhere(
+ selection, Combined._ID + " = CAST(? AS INTEGER)");
+ selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+ new String[] { Long.toString(ContentUris.parseId(uri)) });
+ // fall through
+ }
+ case LEGACY: {
+ String[] projection = new String[] { Combined._ID,
+ Combined.IS_BOOKMARK, Combined.URL };
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ String[] args = createCombinedQuery(uri, projection, qb);
+ if (selectionArgs == null) {
+ selectionArgs = args;
+ } else {
+ selectionArgs = DatabaseUtils.appendSelectionArgs(
+ args, selectionArgs);
+ }
+ Cursor c = qb.query(db, projection, selection, selectionArgs,
+ null, null, null);
+ while (c.moveToNext()) {
+ long id = c.getLong(0);
+ boolean isBookmark = c.getInt(1) != 0;
+ String url = c.getString(2);
+ if (isBookmark) {
+ deleted += deleteBookmarks(Bookmarks._ID + "=?",
+ new String[] { Long.toString(id) },
+ callerIsSyncAdapter);
+ db.delete(TABLE_HISTORY, History.URL + "=?",
+ new String[] { url });
+ } else {
+ deleted += db.delete(TABLE_HISTORY,
+ Bookmarks._ID + "=?",
+ new String[] { Long.toString(id) });
+ }
+ }
+ c.close();
+ break;
+ }
+ case THUMBNAILS_ID: {
+ selection = DatabaseUtils.concatenateWhere(
+ selection, Thumbnails._ID + " = ?");
+ selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+ new String[] { Long.toString(ContentUris.parseId(uri)) });
+ // fall through
+ }
+ case THUMBNAILS: {
+ deleted = db.delete(TABLE_THUMBNAILS, selection, selectionArgs);
+ break;
+ }
+ default: {
+ throw new UnsupportedOperationException("Unknown delete URI " + uri);
+ }
+ }
+ if (deleted > 0) {
+ postNotifyUri(uri);
+ if (shouldNotifyLegacy(uri)) {
+ postNotifyUri(LEGACY_AUTHORITY_URI);
+ }
+ }
+ return deleted;
+ }
+
+ long queryDefaultFolderId(String accountName, String accountType) {
+ if (!isNullAccount(accountName) && !isNullAccount(accountType)) {
+ final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ Cursor c = db.query(TABLE_BOOKMARKS, new String[] { Bookmarks._ID },
+ ChromeSyncColumns.SERVER_UNIQUE + " = ?" +
+ " AND account_type = ? AND account_name = ?",
+ new String[] { ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR,
+ accountType, accountName }, null, null, null);
+ try {
+ if (c.moveToFirst()) {
+ return c.getLong(0);
+ }
+ } finally {
+ c.close();
+ }
+ }
+ return FIXED_ID_ROOT;
+ }
+
+ @Override
+ public Uri insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
+ int match = URI_MATCHER.match(uri);
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ long id = -1;
+ if (match == LEGACY) {
+ // Intercept and route to the correct table
+ Integer bookmark = values.getAsInteger(BookmarkColumns.BOOKMARK);
+ values.remove(BookmarkColumns.BOOKMARK);
+ if (bookmark == null || bookmark == 0) {
+ match = HISTORY;
+ } else {
+ match = BOOKMARKS;
+ values.remove(BookmarkColumns.DATE);
+ values.remove(BookmarkColumns.VISITS);
+ values.remove(BookmarkColumns.USER_ENTERED);
+ values.put(Bookmarks.IS_FOLDER, 0);
+ }
+ }
+ 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);
+
+ boolean hasAccounts = values.containsKey(Bookmarks.ACCOUNT_TYPE)
+ || values.containsKey(Bookmarks.ACCOUNT_NAME);
+ String accountType = values
+ .getAsString(Bookmarks.ACCOUNT_TYPE);
+ String accountName = values
+ .getAsString(Bookmarks.ACCOUNT_NAME);
+ boolean hasParent = values.containsKey(Bookmarks.PARENT);
+ if (hasParent && hasAccounts) {
+ // Let's make sure it's valid
+ long parentId = values.getAsLong(Bookmarks.PARENT);
+ hasParent = isValidParent(
+ accountType, accountName, parentId);
+ } else if (hasParent && !hasAccounts) {
+ long parentId = values.getAsLong(Bookmarks.PARENT);
+ hasParent = setParentValues(parentId, values);
+ }
+
+ // If no parent is set default to the "Bookmarks Bar" folder
+ if (!hasParent) {
+ values.put(Bookmarks.PARENT,
+ queryDefaultFolderId(accountName, accountType));
+ }
+ }
+
+ // 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);
+ refreshWidgets();
+ 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());
+ }
+ String url = values.getAsString(History.URL);
+ url = filterSearchClient(url);
+ values.put(History.URL, url);
+
+ // 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;
+ }
+
+ case THUMBNAILS: {
+ id = db.replaceOrThrow(TABLE_THUMBNAILS, null, values);
+ break;
+ }
+
+ default: {
+ throw new UnsupportedOperationException("Unknown insert URI " + uri);
+ }
+ }
+
+ if (id >= 0) {
+ postNotifyUri(uri);
+ if (shouldNotifyLegacy(uri)) {
+ postNotifyUri(LEGACY_AUTHORITY_URI);
+ }
+ return ContentUris.withAppendedId(uri, id);
+ } else {
+ return null;
+ }
+ }
+
+ private String[] getAccountNameAndType(long id) {
+ if (id <= 0) {
+ return null;
+ }
+ Uri uri = ContentUris.withAppendedId(Bookmarks.CONTENT_URI, id);
+ Cursor c = query(uri,
+ new String[] { Bookmarks.ACCOUNT_NAME, Bookmarks.ACCOUNT_TYPE },
+ null, null, null);
+ try {
+ if (c.moveToFirst()) {
+ String parentName = c.getString(0);
+ String parentType = c.getString(1);
+ return new String[] { parentName, parentType };
+ }
+ return null;
+ } finally {
+ c.close();
+ }
+ }
+
+ private boolean setParentValues(long parentId, ContentValues values) {
+ String[] parent = getAccountNameAndType(parentId);
+ if (parent == null) {
+ return false;
+ }
+ values.put(Bookmarks.ACCOUNT_NAME, parent[0]);
+ values.put(Bookmarks.ACCOUNT_TYPE, parent[1]);
+ return true;
+ }
+
+ private boolean isValidParent(String accountType, String accountName,
+ long parentId) {
+ String[] parent = getAccountNameAndType(parentId);
+ if (parent != null
+ && TextUtils.equals(accountName, parent[0])
+ && TextUtils.equals(accountType, parent[1])) {
+ return true;
+ }
+ return false;
+ }
+
+ private void filterSearchClient(String[] selectionArgs) {
+ if (selectionArgs != null) {
+ for (int i = 0; i < selectionArgs.length; i++) {
+ selectionArgs[i] = filterSearchClient(selectionArgs[i]);
+ }
+ }
+ }
+
+ // Filters out the client= param for search urls
+ private String filterSearchClient(String url) {
+ // remove "client" before updating it to the history so that it wont
+ // show up in the auto-complete list.
+ int index = url.indexOf("client=");
+ 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);
+ }
+ }
+ return url;
+ }
+
+ /**
+ * 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) {
+ int match = URI_MATCHER.match(uri);
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ if (match == LEGACY || match == LEGACY_ID) {
+ // Intercept and route to the correct table
+ Integer bookmark = values.getAsInteger(BookmarkColumns.BOOKMARK);
+ values.remove(BookmarkColumns.BOOKMARK);
+ if (bookmark == null || bookmark == 0) {
+ if (match == LEGACY) {
+ match = HISTORY;
+ } else {
+ match = HISTORY_ID;
+ }
+ } else {
+ if (match == LEGACY) {
+ match = BOOKMARKS;
+ } else {
+ match = BOOKMARKS_ID;
+ }
+ values.remove(BookmarkColumns.DATE);
+ values.remove(BookmarkColumns.VISITS);
+ values.remove(BookmarkColumns.USER_ENTERED);
+ }
+ }
+ int modified = 0;
+ 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: {
+ Object[] withAccount = getSelectionWithAccounts(uri, selection, selectionArgs);
+ selection = (String) withAccount[0];
+ selectionArgs = (String[]) withAccount[1];
+ modified = updateBookmarksInTransaction(values, selection, selectionArgs,
+ callerIsSyncAdapter);
+ if (modified > 0) {
+ refreshWidgets();
+ }
+ break;
+ }
+
+ 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: {
+ modified = updateHistoryInTransaction(values, selection, selectionArgs);
+ break;
+ }
+
+ case SYNCSTATE: {
+ modified = mSyncHelper.update(mDb, values,
+ appendAccountToSelection(uri, selection), selectionArgs);
+ break;
+ }
+
+ case SYNCSTATE_ID: {
+ selection = appendAccountToSelection(uri, selection);
+ String selectionWithId =
+ (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
+ + (selection == null ? "" : " AND (" + selection + ")");
+ modified = mSyncHelper.update(mDb, values,
+ selectionWithId, selectionArgs);
+ break;
+ }
+
+ case IMAGES: {
+ String url = values.getAsString(Images.URL);
+ if (TextUtils.isEmpty(url)) {
+ throw new IllegalArgumentException("Images.URL is required");
+ }
+ if (!shouldUpdateImages(db, url, values)) {
+ return 0;
+ }
+ int count = db.update(TABLE_IMAGES, values, Images.URL + "=?",
+ new String[] { url });
+ if (count == 0) {
+ db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, values);
+ count = 1;
+ }
+ // Only favicon is exposed in the public API. If we updated
+ // the thumbnail or touch icon don't bother notifying the
+ // legacy authority since it can't read it anyway.
+ boolean updatedLegacy = false;
+ if (getUrlCount(db, TABLE_BOOKMARKS, url) > 0) {
+ postNotifyUri(Bookmarks.CONTENT_URI);
+ updatedLegacy = values.containsKey(Images.FAVICON);
+ refreshWidgets();
+ }
+ if (getUrlCount(db, TABLE_HISTORY, url) > 0) {
+ postNotifyUri(History.CONTENT_URI);
+ updatedLegacy = values.containsKey(Images.FAVICON);
+ }
+ if (pruneImages() > 0 || updatedLegacy) {
+ postNotifyUri(LEGACY_AUTHORITY_URI);
+ }
+ // Even though we may be calling notifyUri on Bookmarks, don't
+ // sync to network as images aren't synced. Otherwise this
+ // unnecessarily triggers a bookmark sync.
+ mSyncToNetwork = false;
+ return count;
+ }
+
+ case SEARCHES: {
+ modified = db.update(TABLE_SEARCHES, values, selection, selectionArgs);
+ break;
+ }
+
+ case ACCOUNTS: {
+ Account[] accounts = AccountManager.get(getContext()).getAccounts();
+ mSyncHelper.onAccountsChanged(mDb, accounts);
+ break;
+ }
+
+ case THUMBNAILS: {
+ modified = db.update(TABLE_THUMBNAILS, values,
+ selection, selectionArgs);
+ break;
+ }
+
+ case HOMEPAGE: {
+ if (null != values) {
+ String homepage = values.getAsString("homepage");
+ if (null != homepage) {
+ if (BrowserSettings.getInstance() == null) {
+ BrowserSettings.initialize(getContext());
+ }
+ BrowserSettings.getInstance().setHomePage(homepage);
+ Log.d(TAG,"set home page for DM");
+ return 1;
+ }
+ }
+ return 0;
+ }
+
+ default: {
+ throw new UnsupportedOperationException("Unknown update URI " + uri);
+ }
+ }
+ pruneImages();
+ if (modified > 0) {
+ postNotifyUri(uri);
+ if (shouldNotifyLegacy(uri)) {
+ postNotifyUri(LEGACY_AUTHORITY_URI);
+ }
+ }
+ return modified;
+ }
+
+ // We want to avoid sending out more URI notifications than we have to
+ // Thus, we check to see if the images we are about to store are already there
+ // This is used because things like a site's favion or touch icon is rarely
+ // changed, but the browser tries to update it every time the page loads.
+ // Without this, we will always send out 3 URI notifications per page load.
+ // With this, that drops to 0 or 1, depending on if the thumbnail changed.
+ private boolean shouldUpdateImages(
+ SQLiteDatabase db, String url, ContentValues values) {
+ final String[] projection = new String[] {
+ Images.FAVICON,
+ Images.THUMBNAIL,
+ Images.TOUCH_ICON,
+ };
+ Cursor cursor = db.query(TABLE_IMAGES, projection, Images.URL + "=?",
+ new String[] { url }, null, null, null);
+ byte[] nfavicon = values.getAsByteArray(Images.FAVICON);
+ byte[] nthumb = values.getAsByteArray(Images.THUMBNAIL);
+ byte[] ntouch = values.getAsByteArray(Images.TOUCH_ICON);
+ byte[] cfavicon = null;
+ byte[] cthumb = null;
+ byte[] ctouch = null;
+ try {
+ if (cursor.getCount() <= 0) {
+ return nfavicon != null || nthumb != null || ntouch != null;
+ }
+ while (cursor.moveToNext()) {
+ if (nfavicon != null) {
+ cfavicon = cursor.getBlob(0);
+ if (!Arrays.equals(nfavicon, cfavicon)) {
+ return true;
+ }
+ }
+ if (nthumb != null) {
+ cthumb = cursor.getBlob(1);
+ if (!Arrays.equals(nthumb, cthumb)) {
+ return true;
+ }
+ }
+ if (ntouch != null) {
+ ctouch = cursor.getBlob(2);
+ if (!Arrays.equals(ntouch, ctouch)) {
+ return true;
+ }
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ return false;
+ }
+
+ int getUrlCount(SQLiteDatabase db, String table, String url) {
+ Cursor c = db.query(table, new String[] { "COUNT(*)" },
+ "url = ?", new String[] { url }, null, null, null);
+ try {
+ int count = 0;
+ if (c.moveToFirst()) {
+ count = c.getInt(0);
+ }
+ return count;
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * 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();
+ final String[] bookmarksProjection = new String[] {
+ Bookmarks._ID, // 0
+ Bookmarks.VERSION, // 1
+ Bookmarks.URL, // 2
+ Bookmarks.TITLE, // 3
+ Bookmarks.IS_FOLDER, // 4
+ Bookmarks.ACCOUNT_NAME, // 5
+ Bookmarks.ACCOUNT_TYPE, // 6
+ };
+ Cursor cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection,
+ selection, selectionArgs, null, null, null);
+ boolean updatingParent = values.containsKey(Bookmarks.PARENT);
+ String parentAccountName = null;
+ String parentAccountType = null;
+ if (updatingParent) {
+ long parent = values.getAsLong(Bookmarks.PARENT);
+ Cursor c = db.query(TABLE_BOOKMARKS, new String[] {
+ Bookmarks.ACCOUNT_NAME, Bookmarks.ACCOUNT_TYPE},
+ "_id = ?", new String[] { Long.toString(parent) },
+ null, null, null);
+ if (c.moveToFirst()) {
+ parentAccountName = c.getString(0);
+ parentAccountType = c.getString(1);
+ }
+ c.close();
+ } else if (values.containsKey(Bookmarks.ACCOUNT_NAME)
+ || values.containsKey(Bookmarks.ACCOUNT_TYPE)) {
+ // TODO: Implement if needed (no one needs this yet)
+ }
+ 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()) {
+ long id = cursor.getLong(0);
+ args[0] = Long.toString(id);
+ String accountName = cursor.getString(5);
+ String accountType = cursor.getString(6);
+ // If we are updating the parent and either the account name or
+ // type do not match that of the new parent
+ if (updatingParent
+ && (!TextUtils.equals(accountName, parentAccountName)
+ || !TextUtils.equals(accountType, parentAccountType))) {
+ // Parent is a different account
+ // First, insert a new bookmark/folder with the new account
+ // Then, if this is a folder, reparent all it's children
+ // Finally, delete the old bookmark/folder
+ ContentValues newValues = valuesFromCursor(cursor);
+ newValues.putAll(values);
+ newValues.remove(Bookmarks._ID);
+ newValues.remove(Bookmarks.VERSION);
+ newValues.put(Bookmarks.ACCOUNT_NAME, parentAccountName);
+ newValues.put(Bookmarks.ACCOUNT_TYPE, parentAccountType);
+ Uri insertUri = insertInTransaction(Bookmarks.CONTENT_URI,
+ newValues, callerIsSyncAdapter);
+ long newId = ContentUris.parseId(insertUri);
+ if (cursor.getInt(4) != 0) {
+ // This is a folder, reparent
+ ContentValues updateChildren = new ContentValues(1);
+ updateChildren.put(Bookmarks.PARENT, newId);
+ count += updateBookmarksInTransaction(updateChildren,
+ Bookmarks.PARENT + "=?", new String[] {
+ Long.toString(id)}, callerIsSyncAdapter);
+ }
+ // Now, delete the old one
+ Uri uri = ContentUris.withAppendedId(Bookmarks.CONTENT_URI, id);
+ deleteInTransaction(uri, null, null, callerIsSyncAdapter);
+ count += 1;
+ } else {
+ 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;
+ }
+
+ ContentValues valuesFromCursor(Cursor c) {
+ int count = c.getColumnCount();
+ ContentValues values = new ContentValues(count);
+ String[] colNames = c.getColumnNames();
+ for (int i = 0; i < count; i++) {
+ switch (c.getType(i)) {
+ case Cursor.FIELD_TYPE_BLOB:
+ values.put(colNames[i], c.getBlob(i));
+ break;
+ case Cursor.FIELD_TYPE_FLOAT:
+ values.put(colNames[i], c.getFloat(i));
+ break;
+ case Cursor.FIELD_TYPE_INTEGER:
+ values.put(colNames[i], c.getLong(i));
+ break;
+ case Cursor.FIELD_TYPE_STRING:
+ values.put(colNames[i], c.getString(i));
+ break;
+ }
+ }
+ return values;
+ }
+
+ /**
+ * 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();
+ filterSearchClient(selectionArgs);
+ 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 = filterSearchClient(values.getAsString(History.URL));
+ values.put(History.URL, 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;
+ }
+
+ int pruneImages() {
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ return db.delete(TABLE_IMAGES, IMAGE_PRUNE, null);
+ }
+
+ boolean shouldNotifyLegacy(Uri uri) {
+ if (uri.getPathSegments().contains("history")
+ || uri.getPathSegments().contains("bookmarks")
+ || uri.getPathSegments().contains("searches")) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected boolean syncToNetwork(Uri uri) {
+ if (BrowserContract.AUTHORITY.equals(uri.getAuthority())
+ && uri.getPathSegments().contains("bookmarks")) {
+ return mSyncToNetwork;
+ }
+ if (LEGACY_AUTHORITY.equals(uri.getAuthority())) {
+ // Allow for 3rd party sync adapters
+ return true;
+ }
+ return false;
+ }
+
+ static class SuggestionsCursor extends AbstractCursor {
+ private static final int ID_INDEX = 0;
+ private static final int URL_INDEX = 1;
+ private static final int TITLE_INDEX = 2;
+ private static final int ICON_INDEX = 3;
+ private static final int LAST_ACCESS_TIME_INDEX = 4;
+ // shared suggestion array index, make sure to match COLUMNS
+ private static final int SUGGEST_COLUMN_INTENT_ACTION_ID = 1;
+ private static final int SUGGEST_COLUMN_INTENT_DATA_ID = 2;
+ private static final int SUGGEST_COLUMN_TEXT_1_ID = 3;
+ private static final int SUGGEST_COLUMN_TEXT_2_TEXT_ID = 4;
+ private static final int SUGGEST_COLUMN_TEXT_2_URL_ID = 5;
+ private static final int SUGGEST_COLUMN_ICON_1_ID = 6;
+ private static final int SUGGEST_COLUMN_LAST_ACCESS_HINT_ID = 7;
+
+ // shared suggestion columns
+ private static final String[] COLUMNS = new String[] {
+ BaseColumns._ID,
+ SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
+ SearchManager.SUGGEST_COLUMN_INTENT_DATA,
+ SearchManager.SUGGEST_COLUMN_TEXT_1,
+ SearchManager.SUGGEST_COLUMN_TEXT_2,
+ SearchManager.SUGGEST_COLUMN_TEXT_2_URL,
+ SearchManager.SUGGEST_COLUMN_ICON_1,
+ SearchManager.SUGGEST_COLUMN_LAST_ACCESS_HINT};
+
+ private final Cursor mSource;
+
+ public SuggestionsCursor(Cursor cursor) {
+ mSource = cursor;
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ return COLUMNS;
+ }
+
+ @Override
+ public String getString(int columnIndex) {
+ switch (columnIndex) {
+ case ID_INDEX:
+ return mSource.getString(columnIndex);
+ case SUGGEST_COLUMN_INTENT_ACTION_ID:
+ return Intent.ACTION_VIEW;
+ case SUGGEST_COLUMN_INTENT_DATA_ID:
+ return mSource.getString(URL_INDEX);
+ case SUGGEST_COLUMN_TEXT_2_TEXT_ID:
+ case SUGGEST_COLUMN_TEXT_2_URL_ID:
+ return UrlUtils.stripUrl(mSource.getString(URL_INDEX));
+ case SUGGEST_COLUMN_TEXT_1_ID:
+ return mSource.getString(TITLE_INDEX);
+ case SUGGEST_COLUMN_ICON_1_ID:
+ return mSource.getString(ICON_INDEX);
+ case SUGGEST_COLUMN_LAST_ACCESS_HINT_ID:
+ return mSource.getString(LAST_ACCESS_TIME_INDEX);
+ }
+ return null;
+ }
+
+ @Override
+ public int getCount() {
+ return mSource.getCount();
+ }
+
+ @Override
+ public double getDouble(int column) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public float getFloat(int column) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getInt(int column) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getLong(int column) {
+ switch (column) {
+ case ID_INDEX:
+ return mSource.getLong(ID_INDEX);
+ case SUGGEST_COLUMN_LAST_ACCESS_HINT_ID:
+ return mSource.getLong(LAST_ACCESS_TIME_INDEX);
+ }
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public short getShort(int column) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isNull(int column) {
+ return mSource.isNull(column);
+ }
+
+ @Override
+ public boolean onMove(int oldPosition, int newPosition) {
+ return mSource.moveToPosition(newPosition);
+ }
+ }
+
+ // ---------------------------------------------------
+ // SQL below, be warned
+ // ---------------------------------------------------
+
+ private static final String SQL_CREATE_VIEW_OMNIBOX_SUGGESTIONS =
+ "CREATE VIEW IF NOT EXISTS v_omnibox_suggestions "
+ + " AS "
+ + " SELECT _id, url, title, 1 AS bookmark, 0 AS visits, 0 AS date"
+ + " FROM bookmarks "
+ + " WHERE deleted = 0 AND folder = 0 "
+ + " UNION ALL "
+ + " SELECT _id, url, title, 0 AS bookmark, visits, date "
+ + " FROM history "
+ + " WHERE url NOT IN (SELECT url FROM bookmarks"
+ + " WHERE deleted = 0 AND folder = 0) "
+ + " ORDER BY bookmark DESC, visits DESC, date DESC ";
+
+ private static final String SQL_WHERE_ACCOUNT_HAS_BOOKMARKS =
+ "0 < ( "
+ + "SELECT count(*) "
+ + "FROM bookmarks "
+ + "WHERE deleted = 0 AND folder = 0 "
+ + " AND ( "
+ + " v_accounts.account_name = bookmarks.account_name "
+ + " OR (v_accounts.account_name IS NULL AND bookmarks.account_name IS NULL) "
+ + " ) "
+ + " AND ( "
+ + " v_accounts.account_type = bookmarks.account_type "
+ + " OR (v_accounts.account_type IS NULL AND bookmarks.account_type IS NULL) "
+ + " ) "
+ + ")";
+}
diff --git a/src/com/android/browser/provider/MyNavigationProvider.java b/src/com/android/browser/provider/MyNavigationProvider.java
new file mode 100755
index 0000000..4cd3391
--- /dev/null
+++ b/src/com/android/browser/provider/MyNavigationProvider.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of The Linux Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.android.browser.provider;
+
+import android.content.Context;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.text.TextUtils;
+import android.util.Log;
+import android.webkit.WebResourceResponse;
+
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.R;
+import com.android.browser.homepages.RequestHandler;
+import com.android.browser.mynavigation.MyNavigationRequestHandler;
+import com.android.browser.mynavigation.MyNavigationUtil;
+import com.android.browser.provider.BrowserProvider2;
+
+public class MyNavigationProvider extends ContentProvider {
+
+ private static final String LOGTAG = "MyNavigationProvider";
+ private static final String TABLE_WEB_SITES = "websites";
+ private static final int WEB_SITES_ALL = 0;
+ private static final int WEB_SITES_ID = 1;
+ private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+ static {
+ URI_MATCHER.addURI(MyNavigationUtil.AUTHORITY, "websites", WEB_SITES_ALL);
+ URI_MATCHER.addURI(MyNavigationUtil.AUTHORITY, "websites/#", WEB_SITES_ID);
+ }
+ private static final Uri NOTIFICATION_URI = MyNavigationUtil.MY_NAVIGATION_URI;
+
+ private SiteNavigationDatabaseHelper mOpenHelper;
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ // Current not used, just return 0
+ return 0;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ // Current not used, just return null
+ return null;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ // Current not used, just return null
+ return null;
+ }
+
+ @Override
+ public boolean onCreate() {
+ mOpenHelper = new SiteNavigationDatabaseHelper(this.getContext());
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(TABLE_WEB_SITES);
+ switch (URI_MATCHER.match(uri)) {
+ case WEB_SITES_ALL:
+ break;
+ case WEB_SITES_ID:
+ qb.appendWhere(MyNavigationUtil.ID + "=" + uri.getPathSegments().get(0));
+ break;
+ default:
+ Log.e(LOGTAG, "query Unknown URI: " + uri);
+ return null;
+ }
+
+ String orderBy;
+ if (TextUtils.isEmpty(sortOrder)) {
+ orderBy = null;
+ } else {
+ orderBy = sortOrder;
+ }
+ SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy);
+ if (c != null) {
+ c.setNotificationUri(getContext().getContentResolver(), NOTIFICATION_URI);
+ }
+ return c;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ int count = 0;
+
+ switch (URI_MATCHER.match(uri)) {
+ case WEB_SITES_ALL:
+ count = db.update(TABLE_WEB_SITES, values, selection, selectionArgs);
+ break;
+ case WEB_SITES_ID:
+ String newIdSelection = MyNavigationUtil.ID + "=" + uri.getLastPathSegment()
+ + (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");
+ count = db.update(TABLE_WEB_SITES, values, newIdSelection, selectionArgs);
+ break;
+ default:
+ Log.e(LOGTAG, "update Unknown URI: " + uri);
+ return count;
+ }
+
+ if (count > 0) {
+ ContentResolver cr = getContext().getContentResolver();
+ cr.notifyChange(uri, null);
+ }
+ return count;
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode) {
+ try {
+ ParcelFileDescriptor[] pipes = ParcelFileDescriptor.createPipe();
+ final ParcelFileDescriptor write = pipes[1];
+ AssetFileDescriptor afd = new AssetFileDescriptor(write, 0, -1);
+ new MyNavigationRequestHandler(getContext(), uri, afd.createOutputStream())
+ .start();
+ return pipes[0];
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Failed to handle request: " + uri, e);
+ return null;
+ }
+ }
+
+ public static WebResourceResponse shouldInterceptRequest(Context context,
+ String url) {
+ try {
+ if (MyNavigationUtil.MY_NAVIGATION.equals(url)) {
+ Uri uri = Uri.parse(url);
+ if (MyNavigationUtil.AUTHORITY.equals(uri.getAuthority())) {
+ InputStream ins = context.getContentResolver()
+ .openInputStream(uri);
+ return new WebResourceResponse("text/html", "utf-8", ins);
+ }
+ }
+ boolean listFiles = BrowserSettings.getInstance().isDebugEnabled();
+ if (listFiles && interceptFile(url)) {
+ PipedInputStream ins = new PipedInputStream();
+ PipedOutputStream outs = new PipedOutputStream(ins);
+ new RequestHandler(context, Uri.parse(url), outs).start();
+ return new WebResourceResponse("text/html", "utf-8", ins);
+ }
+ } catch (Exception e) {}
+ return null;
+ }
+
+ private static boolean interceptFile(String url) {
+ if (!url.startsWith("file:///")) {
+ return false;
+ }
+ String fpath = url.substring(7);
+ File f = new File(fpath);
+ if (!f.isDirectory()) {
+ return false;
+ }
+ return true;
+ }
+
+ private class SiteNavigationDatabaseHelper extends SQLiteOpenHelper {
+ private Context mContext;
+ static final String DATABASE_NAME = "mynavigation.db";
+
+ public SiteNavigationDatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, 1); // "1" is the db version here
+ // TODO Auto-generated constructor stub
+ mContext = context;
+ }
+
+ public SiteNavigationDatabaseHelper(Context context, String name,
+ CursorFactory factory, int version) {
+ super(context, name, factory, version);
+ // TODO Auto-generated constructor stub
+ mContext = context;
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ // TODO Auto-generated method stub
+ createWebsitesTable(db);
+ initWebsitesTable(db);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase arg0, int arg1, int arg2) {
+ // TODO Auto-generated method stub
+ }
+
+ private void createWebsitesTable(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE websites (" +
+ MyNavigationUtil.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ MyNavigationUtil.URL + " TEXT," +
+ MyNavigationUtil.TITLE + " TEXT," +
+ MyNavigationUtil.DATE_CREATED + " LONG," +
+ MyNavigationUtil.WEBSITE + " INTEGER," +
+ MyNavigationUtil.THUMBNAIL + " BLOB DEFAULT NULL," +
+ MyNavigationUtil.FAVICON + " BLOB DEFAULT NULL," +
+ MyNavigationUtil.DEFAULT_THUMB + " TEXT" +
+ ");");
+ }
+
+ // initial table , insert websites to table websites
+ private void initWebsitesTable(SQLiteDatabase db) {
+ int WebsiteNumber = MyNavigationUtil.WEBSITE_NUMBER;
+ for (int i = 0; i < WebsiteNumber; i++) {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ Bitmap bm = BitmapFactory.decodeResource(mContext.getResources(),
+ R.raw.my_navigation_add);
+ bm.compress(Bitmap.CompressFormat.PNG, 100, os);
+ ContentValues values = new ContentValues();
+ values.put(MyNavigationUtil.URL, "ae://" + (i + 1) + "add-fav");
+ values.put(MyNavigationUtil.TITLE, mContext.getString(R.string.my_navigation_add));
+ values.put(MyNavigationUtil.DATE_CREATED, 0 + "");
+ values.put(MyNavigationUtil.WEBSITE, 1 + "");
+ values.put(MyNavigationUtil.THUMBNAIL, os.toByteArray());
+ db.insertOrThrow(TABLE_WEB_SITES, MyNavigationUtil.URL, values);
+ }
+ }
+ }
+}
diff --git a/src/com/android/browser/provider/SQLiteContentProvider.java b/src/com/android/browser/provider/SQLiteContentProvider.java
new file mode 100644
index 0000000..75e298e
--- /dev/null
+++ b/src/com/android/browser/provider/SQLiteContentProvider.java
@@ -0,0 +1,250 @@
+/*
+ * 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.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * General purpose {@link ContentProvider} base class that uses SQLiteDatabase for storage.
+ */
+public abstract class SQLiteContentProvider extends ContentProvider {
+
+ private static final String TAG = "SQLiteContentProvider";
+
+ private SQLiteOpenHelper mOpenHelper;
+ private Set<Uri> mChangedUris;
+ 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);
+ mChangedUris = new HashSet<Uri>();
+ 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);
+
+ /**
+ * Call this to add a URI to the list of URIs to be notified when the transaction
+ * is committed.
+ */
+ protected void postNotifyUri(Uri uri) {
+ synchronized (mChangedUris) {
+ mChangedUris.add(uri);
+ }
+ }
+
+ 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.beginTransaction();
+ try {
+ result = insertInTransaction(uri, values, callerIsSyncAdapter);
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+
+ onEndTransaction(callerIsSyncAdapter);
+ } else {
+ result = insertInTransaction(uri, values, callerIsSyncAdapter);
+ }
+ return result;
+ }
+
+ @Override
+ public int bulkInsert(Uri uri, ContentValues[] values) {
+ int numValues = values.length;
+ boolean callerIsSyncAdapter = isCallerSyncAdapter(uri);
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransaction();
+ try {
+ for (int i = 0; i < numValues; i++) {
+ Uri result = insertInTransaction(uri, values[i], callerIsSyncAdapter);
+ 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.beginTransaction();
+ try {
+ count = updateInTransaction(uri, values, selection, selectionArgs,
+ callerIsSyncAdapter);
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+
+ onEndTransaction(callerIsSyncAdapter);
+ } else {
+ count = updateInTransaction(uri, values, selection, selectionArgs, callerIsSyncAdapter);
+ }
+
+ 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.beginTransaction();
+ try {
+ count = deleteInTransaction(uri, selection, selectionArgs, callerIsSyncAdapter);
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+
+ onEndTransaction(callerIsSyncAdapter);
+ } else {
+ count = deleteInTransaction(uri, selection, selectionArgs, callerIsSyncAdapter);
+ }
+ return count;
+ }
+
+ @Override
+ public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
+ throws OperationApplicationException {
+ int ypCount = 0;
+ int opCount = 0;
+ boolean callerIsSyncAdapter = false;
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransaction();
+ 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);
+ }
+ }
+
+ protected void onEndTransaction(boolean callerIsSyncAdapter) {
+ Set<Uri> changed;
+ synchronized (mChangedUris) {
+ changed = new HashSet<Uri>(mChangedUris);
+ mChangedUris.clear();
+ }
+ ContentResolver resolver = getContext().getContentResolver();
+ for (Uri uri : changed) {
+ boolean syncToNetwork = !callerIsSyncAdapter && syncToNetwork(uri);
+ resolver.notifyChange(uri, null, syncToNetwork);
+ }
+ }
+
+ protected boolean syncToNetwork(Uri uri) {
+ return false;
+ }
+}
diff --git a/src/com/android/browser/provider/SnapshotProvider.java b/src/com/android/browser/provider/SnapshotProvider.java
new file mode 100644
index 0000000..3226c11
--- /dev/null
+++ b/src/com/android/browser/provider/SnapshotProvider.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2011 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.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+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 com.android.browser.platformsupport.BrowserContract;
+
+import android.text.TextUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.FileOutputStream;
+import java.io.FileInputStream;
+
+public class SnapshotProvider extends ContentProvider {
+
+ public static interface Snapshots {
+
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(
+ SnapshotProvider.AUTHORITY_URI, "snapshots");
+ public static final String _ID = "_id";
+ @Deprecated
+ public static final String VIEWSTATE = "view_state";
+ public static final String BACKGROUND = "background";
+ public static final String TITLE = "title";
+ public static final String URL = "url";
+ public static final String FAVICON = "favicon";
+ public static final String THUMBNAIL = "thumbnail";
+ public static final String DATE_CREATED = "date_created";
+ public static final String VIEWSTATE_PATH = "viewstate_path";
+ public static final String VIEWSTATE_SIZE = "viewstate_size";
+ }
+
+ public static final String AUTHORITY = "com.android.browser.snapshots";
+ public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
+
+ static final String TABLE_SNAPSHOTS = "snapshots";
+ static final int SNAPSHOTS = 10;
+ static final int SNAPSHOTS_ID = 11;
+ static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+ // Workaround that we can't remove the "NOT NULL" constraint on VIEWSTATE
+ static final byte[] NULL_BLOB_HACK = new byte[0];
+
+ SnapshotDatabaseHelper mOpenHelper;
+
+ static {
+ URI_MATCHER.addURI(AUTHORITY, "snapshots", SNAPSHOTS);
+ URI_MATCHER.addURI(AUTHORITY, "snapshots/#", SNAPSHOTS_ID);
+ }
+
+ final static class SnapshotDatabaseHelper extends SQLiteOpenHelper {
+
+ static final String DATABASE_NAME = "snapshots.db";
+ static final int DATABASE_VERSION = 3;
+
+ public SnapshotDatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + TABLE_SNAPSHOTS + "(" +
+ Snapshots._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ Snapshots.TITLE + " TEXT," +
+ Snapshots.URL + " TEXT NOT NULL," +
+ Snapshots.DATE_CREATED + " INTEGER," +
+ Snapshots.FAVICON + " BLOB," +
+ Snapshots.THUMBNAIL + " BLOB," +
+ Snapshots.BACKGROUND + " INTEGER," +
+ Snapshots.VIEWSTATE + " BLOB NOT NULL," +
+ Snapshots.VIEWSTATE_PATH + " TEXT," +
+ Snapshots.VIEWSTATE_SIZE + " INTEGER" +
+ ");");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if (oldVersion < 2) {
+ db.execSQL("DROP TABLE " + TABLE_SNAPSHOTS);
+ onCreate(db);
+ }
+ if (oldVersion < 3) {
+ db.execSQL("ALTER TABLE " + TABLE_SNAPSHOTS + " ADD COLUMN "
+ + Snapshots.VIEWSTATE_PATH + " TEXT");
+ db.execSQL("ALTER TABLE " + TABLE_SNAPSHOTS + " ADD COLUMN "
+ + Snapshots.VIEWSTATE_SIZE + " INTEGER");
+ db.execSQL("UPDATE " + TABLE_SNAPSHOTS + " SET "
+ + Snapshots.VIEWSTATE_SIZE + " = length("
+ + Snapshots.VIEWSTATE + ")");
+ }
+ }
+
+ }
+
+ static File getOldDatabasePath(Context context) {
+ File dir = context.getExternalFilesDir(null);
+ return new File(dir, SnapshotDatabaseHelper.DATABASE_NAME);
+ }
+
+ private static boolean copyFile(File srcFile, File destFile) {
+ try {
+ if (destFile.exists()) {
+ destFile.delete();
+ }
+
+ FileInputStream in = new FileInputStream(srcFile);
+ FileOutputStream out = new FileOutputStream(destFile);
+
+ try {
+ byte[] buffer = new byte[4096];
+ int bytesRead;
+ while ((bytesRead = in.read(buffer)) >= 0) {
+ out.write(buffer, 0, bytesRead);
+ }
+ } finally {
+ out.flush();
+ try {
+ out.getFD().sync();
+ } catch (IOException e) {
+ }
+ in.close();
+ out.close();
+ }
+ return true;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ private void migrateToDataFolder() {
+ File dbPath = getContext().getDatabasePath(SnapshotDatabaseHelper.DATABASE_NAME);
+ if (dbPath.exists()) return;
+ File oldPath = getOldDatabasePath(getContext());
+ if (oldPath.exists()) {
+ // Try to move
+ if (!oldPath.renameTo(dbPath)) {
+ // Failed, do a copy
+ copyFile(oldPath, dbPath);
+ }
+ // Cleanup
+ oldPath.delete();
+ }
+ }
+
+ @Override
+ public boolean onCreate() {
+ migrateToDataFolder();
+ mOpenHelper = new SnapshotDatabaseHelper(getContext());
+ return true;
+ }
+
+ SQLiteDatabase getWritableDatabase() {
+ return mOpenHelper.getWritableDatabase();
+ }
+
+ SQLiteDatabase getReadableDatabase() {
+ return mOpenHelper.getReadableDatabase();
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ SQLiteDatabase db = getReadableDatabase();
+ if (db == null) {
+ return null;
+ }
+ final int match = URI_MATCHER.match(uri);
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
+ switch (match) {
+ case SNAPSHOTS_ID:
+ selection = DatabaseUtils.concatenateWhere(selection, "_id=?");
+ selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+ new String[] { Long.toString(ContentUris.parseId(uri)) });
+ // fall through
+ case SNAPSHOTS:
+ qb.setTables(TABLE_SNAPSHOTS);
+ 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(),
+ AUTHORITY_URI);
+ return cursor;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return null;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ SQLiteDatabase db = getWritableDatabase();
+ if (db == null) {
+ return null;
+ }
+ int match = URI_MATCHER.match(uri);
+ long id = -1;
+ switch (match) {
+ case SNAPSHOTS:
+ if (!values.containsKey(Snapshots.VIEWSTATE)) {
+ values.put(Snapshots.VIEWSTATE, NULL_BLOB_HACK);
+ }
+ id = db.insert(TABLE_SNAPSHOTS, Snapshots.TITLE, values);
+ break;
+ default:
+ throw new UnsupportedOperationException("Unknown insert URI " + uri);
+ }
+ if (id < 0) {
+ return null;
+ }
+ Uri inserted = ContentUris.withAppendedId(uri, id);
+ getContext().getContentResolver().notifyChange(inserted, null, false);
+ return inserted;
+ }
+
+ static final String[] DELETE_PROJECTION = new String[] {
+ Snapshots.VIEWSTATE_PATH,
+ };
+ private void deleteDataFiles(SQLiteDatabase db, String selection,
+ String[] selectionArgs) {
+ Cursor c = db.query(TABLE_SNAPSHOTS, DELETE_PROJECTION, selection,
+ selectionArgs, null, null, null);
+ final Context context = getContext();
+ while (c.moveToNext()) {
+ String filename = c.getString(0);
+ if (TextUtils.isEmpty(filename)) {
+ continue;
+ }
+ File f = context.getFileStreamPath(filename);
+ if (f.exists()) {
+ if (!f.delete()) {
+ f.deleteOnExit();
+ }
+ }
+ }
+ c.close();
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ SQLiteDatabase db = getWritableDatabase();
+ if (db == null) {
+ return 0;
+ }
+ int match = URI_MATCHER.match(uri);
+ int deleted = 0;
+ switch (match) {
+ case SNAPSHOTS_ID: {
+ selection = DatabaseUtils.concatenateWhere(selection, TABLE_SNAPSHOTS + "._id=?");
+ selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+ new String[] { Long.toString(ContentUris.parseId(uri)) });
+ // fall through
+ }
+ case SNAPSHOTS:
+ deleteDataFiles(db, selection, selectionArgs);
+ deleted = db.delete(TABLE_SNAPSHOTS, selection, selectionArgs);
+ break;
+ default:
+ throw new UnsupportedOperationException("Unknown delete URI " + uri);
+ }
+ if (deleted > 0) {
+ getContext().getContentResolver().notifyChange(uri, null, false);
+ }
+ return deleted;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs) {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+}
diff --git a/src/com/android/browser/reflect/ReflectHelper.java b/src/com/android/browser/reflect/ReflectHelper.java
new file mode 100644
index 0000000..5a5f2ae
--- /dev/null
+++ b/src/com/android/browser/reflect/ReflectHelper.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (c) 2014, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of The Linux Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+package com.android.browser.reflect;
+
+import android.util.Log;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Field;
+
+public class ReflectHelper {
+
+ private final static String LOGTAG = "ReflectHelper";
+
+ public static Object newObject(String className) {
+ Object obj = null;
+ try {
+ Class clazz = Class.forName(className);
+ obj = clazz.newInstance();
+ } catch (Exception e) {
+ Log.e(LOGTAG, "An exception occured : " + e.getMessage());
+ }
+ return obj;
+ }
+
+ public static Object newObject(String className, Class[] argTypes, Object[] args) {
+ if (args == null || args.length == 0) {
+ return newObject(className);
+ }
+ Object obj = null;
+ try {
+ Class clazz = Class.forName(className);
+ Constructor ctor = clazz.getDeclaredConstructor(argTypes);
+ obj = ctor.newInstance(args);
+ } catch (Exception e) {
+ Log.e(LOGTAG, "An exception occured : " + e.getMessage() );
+ }
+ return obj;
+ }
+
+ public static Object invokeMethod(Object obj, String method, Class[] argTypes, Object[] args) {
+ Object result = null;
+ boolean modifiedAccessibility = false;
+ if (obj == null || method == null) {
+ throw new IllegalArgumentException("Object and Method must be supplied.");
+ }
+ try {
+ Method m = obj.getClass().getDeclaredMethod(method, argTypes);
+ if(m != null) {
+ // make it visible
+ if (!m.isAccessible()) {
+ modifiedAccessibility = true;
+ m.setAccessible(true);
+ }
+ result = m.invoke(obj, args);
+ if (modifiedAccessibility)
+ m.setAccessible(false);
+ }
+ } catch (Exception e) {
+ Log.e(LOGTAG, "An exception occured : " + e.getMessage() );
+ }
+ return result;
+ }
+
+ public static Object invokeStaticMethod(String className, String method,
+ Class[] argTypes, Object[] args) {
+ Object result = null;
+ boolean modifiedAccessibility = false;
+ if (className == null || method == null) {
+ throw new IllegalArgumentException("Object and Method must be supplied.");
+ }
+ try {
+ Class clazz = Class.forName(className);
+ Method m = clazz.getDeclaredMethod(method, argTypes);
+ if(m != null) {
+ // make it visible
+ if (!m.isAccessible()) {
+ modifiedAccessibility = true;
+ m.setAccessible(true);
+ }
+ result = m.invoke(null, args);
+ if (modifiedAccessibility)
+ m.setAccessible(false);
+ }
+ } catch (Exception e) {
+ Log.e(LOGTAG, "An exception occured : " + e.getMessage() );
+ }
+ return result;
+ }
+
+ public static Object getStaticVariable(String className, String fieldName) {
+ Object result = null;
+ boolean modifiedAccessibility = false;
+ try {
+ Class clazz = Class.forName(className);
+ Field f = clazz.getDeclaredField(fieldName);
+ if(f != null) {
+ if (!f.isAccessible()) {
+ modifiedAccessibility = true;
+ f.setAccessible(true);
+ }
+ f.setAccessible(true);
+ result = f.get(null);
+ if (modifiedAccessibility)
+ f.setAccessible(false);
+ }
+ } catch (Exception e) {
+ Log.e(LOGTAG, "An exception occured : " + e.getMessage() );
+ }
+ return result;
+ }
+
+ public static Object getVariable(Object obj, String fieldName) {
+ Object result = null;
+ boolean modifiedAccessibility = false;
+ try {
+ Class clazz = obj.getClass();
+ Field f = clazz.getDeclaredField(fieldName);
+ if(f != null) {
+ if (!f.isAccessible()) {
+ modifiedAccessibility = true;
+ f.setAccessible(true);
+ }
+ f.setAccessible(true);
+ result = f.get(obj);
+ if (modifiedAccessibility)
+ f.setAccessible(false);
+ }
+ } catch (Exception e) {
+ Log.e(LOGTAG, "An exception occured : " + e.getMessage() );
+ }
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/browser/search/DefaultSearchEngine.java b/src/com/android/browser/search/DefaultSearchEngine.java
new file mode 100644
index 0000000..7613377
--- /dev/null
+++ b/src/com/android/browser/search/DefaultSearchEngine.java
@@ -0,0 +1,141 @@
+/*
+ * 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.search;
+
+import android.app.PendingIntent;
+import android.app.SearchManager;
+import android.app.SearchableInfo;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.provider.Browser;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.browser.reflect.ReflectHelper;
+
+public class DefaultSearchEngine implements SearchEngine {
+
+ private static final String TAG = "DefaultSearchEngine";
+
+ private final SearchableInfo mSearchable;
+
+ private final CharSequence mLabel;
+
+ private DefaultSearchEngine(Context context, SearchableInfo searchable) {
+ mSearchable = searchable;
+ mLabel = loadLabel(context, mSearchable.getSearchActivity());
+ }
+
+ public static DefaultSearchEngine create(Context context) {
+ SearchManager searchManager =
+ (SearchManager) context.getSystemService(Context.SEARCH_SERVICE);
+ ComponentName name = (ComponentName) ReflectHelper.invokeMethod(
+ searchManager, "getWebSearchActivity", null, null);
+
+ if (name == null) return null;
+ SearchableInfo searchable = searchManager.getSearchableInfo(name);
+ if (searchable == null) return null;
+ return new DefaultSearchEngine(context, searchable);
+ }
+
+ private CharSequence loadLabel(Context context, ComponentName activityName) {
+ PackageManager pm = context.getPackageManager();
+ try {
+ ActivityInfo ai = pm.getActivityInfo(activityName, 0);
+ return ai.loadLabel(pm);
+ } catch (PackageManager.NameNotFoundException ex) {
+ Log.e(TAG, "Web search activity not found: " + activityName);
+ return null;
+ }
+ }
+
+ public String getName() {
+ String packageName = mSearchable.getSearchActivity().getPackageName();
+ // Use "google" as name to avoid showing Google twice (app + OpenSearch)
+ if ("com.google.android.googlequicksearchbox".equals(packageName)) {
+ return SearchEngine.GOOGLE;
+ } else if ("com.android.quicksearchbox".equals(packageName)) {
+ return SearchEngine.GOOGLE;
+ } else {
+ return packageName;
+ }
+ }
+
+ public CharSequence getLabel() {
+ return mLabel;
+ }
+
+ public void startSearch(Context context, String query,
+ Bundle appData, String extraData) {
+ try {
+ Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
+ intent.setComponent(mSearchable.getSearchActivity());
+ intent.addCategory(Intent.CATEGORY_DEFAULT);
+ intent.putExtra(SearchManager.QUERY, query);
+ if (appData != null) {
+ intent.putExtra(SearchManager.APP_DATA, appData);
+ }
+ if (extraData != null) {
+ intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
+ }
+ intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
+ Intent viewIntent = new Intent(Intent.ACTION_VIEW);
+ viewIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ viewIntent.setPackage(context.getPackageName());
+ PendingIntent pending = PendingIntent.getActivity(context, 0, viewIntent,
+ PendingIntent.FLAG_ONE_SHOT);
+ intent.putExtra(SearchManager.EXTRA_WEB_SEARCH_PENDINGINTENT, pending);
+ context.startActivity(intent);
+ } catch (ActivityNotFoundException ex) {
+ Log.e(TAG, "Web search activity not found: " +
+ mSearchable.getSearchActivity());
+ }
+ }
+
+ public Cursor getSuggestions(Context context, String query) {
+ SearchManager searchManager =
+ (SearchManager) context.getSystemService(Context.SEARCH_SERVICE);
+ Object[] params = {mSearchable, query};
+ Class[] type = new Class[] {SearchableInfo.class, String.class};
+ Cursor cursor = (Cursor) ReflectHelper.invokeMethod(
+ searchManager, "getSuggestions", type, params);
+ return cursor;
+ }
+
+ public boolean supportsSuggestions() {
+ return !TextUtils.isEmpty(mSearchable.getSuggestAuthority());
+ }
+
+ public void close() {
+ }
+
+ @Override
+ public String toString() {
+ return "ActivitySearchEngine{" + mSearchable + "}";
+ }
+
+ @Override
+ public boolean wantsEmptyQuery() {
+ return false;
+ }
+
+}
diff --git a/src/com/android/browser/search/OpenSearchSearchEngine.java b/src/com/android/browser/search/OpenSearchSearchEngine.java
new file mode 100644
index 0000000..e600aa9
--- /dev/null
+++ b/src/com/android/browser/search/OpenSearchSearchEngine.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.search;
+
+import com.android.browser.R;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.params.HttpParams;
+import org.apache.http.util.EntityUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import android.app.SearchManager;
+import android.content.Context;
+import android.content.Intent;
+import android.database.AbstractCursor;
+import android.database.Cursor;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.net.http.AndroidHttpClient;
+import android.os.Bundle;
+import android.provider.Browser;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Provides search suggestions, if any, for a given web search provider.
+ */
+public class OpenSearchSearchEngine implements SearchEngine {
+
+ private static final String TAG = "OpenSearchSearchEngine";
+
+ private static final String USER_AGENT = "Android/1.0";
+ private static final int HTTP_TIMEOUT_MS = 1000;
+
+ // TODO: this should be defined somewhere
+ private static final String HTTP_TIMEOUT = "http.connection-manager.timeout";
+
+ // Indices of the columns in the below arrays.
+ private static final int COLUMN_INDEX_ID = 0;
+ private static final int COLUMN_INDEX_QUERY = 1;
+ private static final int COLUMN_INDEX_ICON = 2;
+ private static final int COLUMN_INDEX_TEXT_1 = 3;
+ private static final int COLUMN_INDEX_TEXT_2 = 4;
+
+ // The suggestion columns used. If you are adding a new entry to these arrays make sure to
+ // update the list of indices declared above.
+ private static final String[] COLUMNS = new String[] {
+ "_id",
+ SearchManager.SUGGEST_COLUMN_QUERY,
+ SearchManager.SUGGEST_COLUMN_ICON_1,
+ SearchManager.SUGGEST_COLUMN_TEXT_1,
+ SearchManager.SUGGEST_COLUMN_TEXT_2,
+ };
+
+ private static final String[] COLUMNS_WITHOUT_DESCRIPTION = new String[] {
+ "_id",
+ SearchManager.SUGGEST_COLUMN_QUERY,
+ SearchManager.SUGGEST_COLUMN_ICON_1,
+ SearchManager.SUGGEST_COLUMN_TEXT_1,
+ };
+
+ private final SearchEngineInfo mSearchEngineInfo;
+
+ private final AndroidHttpClient mHttpClient;
+
+ public OpenSearchSearchEngine(Context context, SearchEngineInfo searchEngineInfo) {
+ mSearchEngineInfo = searchEngineInfo;
+ mHttpClient = AndroidHttpClient.newInstance(USER_AGENT);
+ HttpParams params = mHttpClient.getParams();
+ params.setLongParameter(HTTP_TIMEOUT, HTTP_TIMEOUT_MS);
+ }
+
+ public String getName() {
+ return mSearchEngineInfo.getName();
+ }
+
+ public CharSequence getLabel() {
+ return mSearchEngineInfo.getLabel();
+ }
+
+ public void startSearch(Context context, String query, Bundle appData, String extraData) {
+ String uri = mSearchEngineInfo.getSearchUriForQuery(query);
+ if (uri == null) {
+ Log.e(TAG, "Unable to get search URI for " + mSearchEngineInfo);
+ } else {
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
+ // Make sure the intent goes to the Browser itself
+ intent.setPackage(context.getPackageName());
+ intent.addCategory(Intent.CATEGORY_DEFAULT);
+ intent.putExtra(SearchManager.QUERY, query);
+ if (appData != null) {
+ intent.putExtra(SearchManager.APP_DATA, appData);
+ }
+ if (extraData != null) {
+ intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
+ }
+ intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
+ context.startActivity(intent);
+ }
+ }
+
+ /**
+ * Queries for a given search term and returns a cursor containing
+ * suggestions ordered by best match.
+ */
+ public Cursor getSuggestions(Context context, String query) {
+ if (TextUtils.isEmpty(query)) {
+ return null;
+ }
+ if (!isNetworkConnected(context)) {
+ Log.i(TAG, "Not connected to network.");
+ return null;
+ }
+
+ String suggestUri = mSearchEngineInfo.getSuggestUriForQuery(query);
+ if (TextUtils.isEmpty(suggestUri)) {
+ // No suggest URI available for this engine
+ return null;
+ }
+
+ try {
+ String content = readUrl(suggestUri);
+ if (content == null) return null;
+ /* The data format is a JSON array with items being regular strings or JSON arrays
+ * themselves. We are interested in the second and third elements, both of which
+ * should be JSON arrays. The second element/array contains the suggestions and the
+ * third element contains the descriptions. Some search engines don't support
+ * suggestion descriptions so the third element is optional.
+ */
+ JSONArray results = new JSONArray(content);
+ JSONArray suggestions = results.getJSONArray(1);
+ JSONArray descriptions = null;
+ if (results.length() > 2) {
+ descriptions = results.getJSONArray(2);
+ // Some search engines given an empty array "[]" for descriptions instead of
+ // not including it in the response.
+ if (descriptions.length() == 0) {
+ descriptions = null;
+ }
+ }
+ return new SuggestionsCursor(suggestions, descriptions);
+ } catch (JSONException e) {
+ Log.w(TAG, "Error", e);
+ }
+ return null;
+ }
+
+ /**
+ * Executes a GET request and returns the response content.
+ *
+ * @param url Request URI.
+ * @return The response content. This is the empty string if the response
+ * contained no content.
+ */
+ public String readUrl(String url) {
+ try {
+ HttpGet method = new HttpGet(url);
+ HttpResponse response = mHttpClient.execute(method);
+ if (response.getStatusLine().getStatusCode() == 200) {
+ return EntityUtils.toString(response.getEntity());
+ } else {
+ Log.i(TAG, "Suggestion request failed");
+ return null;
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Error", e);
+ return null;
+ }
+ }
+
+ public boolean supportsSuggestions() {
+ return mSearchEngineInfo.supportsSuggestions();
+ }
+
+ public void close() {
+ mHttpClient.close();
+ }
+
+ private boolean isNetworkConnected(Context context) {
+ NetworkInfo networkInfo = getActiveNetworkInfo(context);
+ return networkInfo != null && networkInfo.isConnected();
+ }
+
+ private NetworkInfo getActiveNetworkInfo(Context context) {
+ ConnectivityManager connectivity =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (connectivity == null) {
+ return null;
+ }
+ return connectivity.getActiveNetworkInfo();
+ }
+
+ private static class SuggestionsCursor extends AbstractCursor {
+
+ private final JSONArray mSuggestions;
+
+ private final JSONArray mDescriptions;
+
+ public SuggestionsCursor(JSONArray suggestions, JSONArray descriptions) {
+ mSuggestions = suggestions;
+ mDescriptions = descriptions;
+ }
+
+ @Override
+ public int getCount() {
+ return mSuggestions.length();
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ return (mDescriptions != null ? COLUMNS : COLUMNS_WITHOUT_DESCRIPTION);
+ }
+
+ @Override
+ public String getString(int column) {
+ if (mPos != -1) {
+ if ((column == COLUMN_INDEX_QUERY) || (column == COLUMN_INDEX_TEXT_1)) {
+ try {
+ return mSuggestions.getString(mPos);
+ } catch (JSONException e) {
+ Log.w(TAG, "Error", e);
+ }
+ } else if (column == COLUMN_INDEX_TEXT_2) {
+ try {
+ return mDescriptions.getString(mPos);
+ } catch (JSONException e) {
+ Log.w(TAG, "Error", e);
+ }
+ } else if (column == COLUMN_INDEX_ICON) {
+ return String.valueOf(R.drawable.magnifying_glass);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public double getDouble(int column) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public float getFloat(int column) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getInt(int column) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getLong(int column) {
+ if (column == COLUMN_INDEX_ID) {
+ return mPos; // use row# as the _Id
+ }
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public short getShort(int column) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isNull(int column) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "OpenSearchSearchEngine{" + mSearchEngineInfo + "}";
+ }
+
+ @Override
+ public boolean wantsEmptyQuery() {
+ return false;
+ }
+
+}
diff --git a/src/com/android/browser/search/SearchEngine.java b/src/com/android/browser/search/SearchEngine.java
new file mode 100644
index 0000000..8f2d58d
--- /dev/null
+++ b/src/com/android/browser/search/SearchEngine.java
@@ -0,0 +1,64 @@
+/*
+ * 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.search;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Bundle;
+
+/**
+ * Interface for search engines.
+ */
+public interface SearchEngine {
+
+ // Used if the search engine is Google
+ static final String GOOGLE = "google";
+
+ /**
+ * Gets the unique name of this search engine.
+ */
+ public String getName();
+
+ /**
+ * Gets the human-readable name of this search engine.
+ */
+ public CharSequence getLabel();
+
+ /**
+ * Starts a search.
+ */
+ public void startSearch(Context context, String query, Bundle appData, String extraData);
+
+ /**
+ * Gets search suggestions.
+ */
+ public Cursor getSuggestions(Context context, String query);
+
+ /**
+ * Checks whether this search engine supports search suggestions.
+ */
+ public boolean supportsSuggestions();
+
+ /**
+ * Closes this search engine.
+ */
+ public void close();
+
+ /**
+ * Checks whether this search engine should be sent zero char query.
+ */
+ public boolean wantsEmptyQuery();
+}
diff --git a/src/com/android/browser/search/SearchEngineInfo.java b/src/com/android/browser/search/SearchEngineInfo.java
new file mode 100644
index 0000000..ec304f6
--- /dev/null
+++ b/src/com/android/browser/search/SearchEngineInfo.java
@@ -0,0 +1,176 @@
+/*
+ * 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.search;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.browser.R;
+
+import java.net.URLEncoder;
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * Loads and holds data for a given web search engine.
+ */
+public class SearchEngineInfo {
+
+ private static String TAG = "SearchEngineInfo";
+
+ // The fields of a search engine data array, defined in the same order as they appear in the
+ // all_search_engines.xml file.
+ // If you are adding/removing to this list, remember to update NUM_FIELDS below.
+ private static final int FIELD_LABEL = 0;
+ private static final int FIELD_KEYWORD = 1;
+ private static final int FIELD_FAVICON_URI = 2;
+ private static final int FIELD_SEARCH_URI = 3;
+ private static final int FIELD_ENCODING = 4;
+ private static final int FIELD_SUGGEST_URI = 5;
+ private static final int NUM_FIELDS = 6;
+
+ // The OpenSearch URI template parameters that we support.
+ private static final String PARAMETER_LANGUAGE = "{language}";
+ private static final String PARAMETER_SEARCH_TERMS = "{searchTerms}";
+ private static final String PARAMETER_INPUT_ENCODING = "{inputEncoding}";
+
+ private final String mName;
+
+ // The array of strings defining this search engine. The array values are in the same order as
+ // the above enumeration definition.
+ private final String[] mSearchEngineData;
+
+ /**
+ * @throws IllegalArgumentException If the name does not refer to a valid search engine
+ */
+ public SearchEngineInfo(Context context, String name) throws IllegalArgumentException {
+ mName = name;
+ Resources res = context.getResources();
+
+ String packageName = R.class.getPackage().getName();
+ int id_data = res.getIdentifier(name, "array", packageName);
+ if (id_data == 0) {
+ throw new IllegalArgumentException("No resources found for " + name);
+ }
+ mSearchEngineData = res.getStringArray(id_data);
+
+ if (mSearchEngineData == null) {
+ throw new IllegalArgumentException("No data found for " + name);
+ }
+ if (mSearchEngineData.length != NUM_FIELDS) {
+ throw new IllegalArgumentException(
+ name + " has invalid number of fields - " + mSearchEngineData.length);
+ }
+ if (TextUtils.isEmpty(mSearchEngineData[FIELD_SEARCH_URI])) {
+ throw new IllegalArgumentException(name + " has an empty search URI");
+ }
+
+ // Add the current language/country information to the URIs.
+ Locale locale = context.getResources().getConfiguration().locale;
+ StringBuilder language = new StringBuilder(locale.getLanguage());
+ if (!TextUtils.isEmpty(locale.getCountry())) {
+ language.append('-');
+ language.append(locale.getCountry());
+ }
+
+ String language_str = language.toString();
+ mSearchEngineData[FIELD_SEARCH_URI] =
+ mSearchEngineData[FIELD_SEARCH_URI].replace(PARAMETER_LANGUAGE, language_str);
+ mSearchEngineData[FIELD_SUGGEST_URI] =
+ mSearchEngineData[FIELD_SUGGEST_URI].replace(PARAMETER_LANGUAGE, language_str);
+
+ // Default to UTF-8 if not specified.
+ String enc = mSearchEngineData[FIELD_ENCODING];
+ if (TextUtils.isEmpty(enc)) {
+ enc = "UTF-8";
+ mSearchEngineData[FIELD_ENCODING] = enc;
+ }
+
+ // Add the input encoding method to the URI.
+ mSearchEngineData[FIELD_SEARCH_URI] =
+ mSearchEngineData[FIELD_SEARCH_URI].replace(PARAMETER_INPUT_ENCODING, enc);
+ mSearchEngineData[FIELD_SUGGEST_URI] =
+ mSearchEngineData[FIELD_SUGGEST_URI].replace(PARAMETER_INPUT_ENCODING, enc);
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public String getLabel() {
+ return mSearchEngineData[FIELD_LABEL];
+ }
+
+ /**
+ * Returns the URI for launching a web search with the given query (or null if there was no
+ * data available for this search engine).
+ */
+ public String getSearchUriForQuery(String query) {
+ return getFormattedUri(searchUri(), query);
+ }
+
+ /**
+ * Returns the URI for retrieving web search suggestions for the given query (or null if there
+ * was no data available for this search engine).
+ */
+ public String getSuggestUriForQuery(String query) {
+ return getFormattedUri(suggestUri(), query);
+ }
+
+ public boolean supportsSuggestions() {
+ return !TextUtils.isEmpty(suggestUri());
+ }
+
+ public String faviconUri() {
+ return mSearchEngineData[FIELD_FAVICON_URI];
+ }
+
+ private String suggestUri() {
+ return mSearchEngineData[FIELD_SUGGEST_URI];
+ }
+
+ private String searchUri() {
+ return mSearchEngineData[FIELD_SEARCH_URI];
+ }
+
+ /**
+ * Formats a launchable uri out of the template uri by replacing the template parameters with
+ * actual values.
+ */
+ private String getFormattedUri(String templateUri, String query) {
+ if (TextUtils.isEmpty(templateUri)) {
+ return null;
+ }
+
+ // Encode the query terms in the requested encoding (and fallback to UTF-8 if not).
+ String enc = mSearchEngineData[FIELD_ENCODING];
+ try {
+ return templateUri.replace(PARAMETER_SEARCH_TERMS, URLEncoder.encode(query, enc));
+ } catch (java.io.UnsupportedEncodingException e) {
+ Log.e(TAG, "Exception occured when encoding query " + query + " to " + enc);
+ return null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "SearchEngineInfo{" + Arrays.toString(mSearchEngineData) + "}";
+ }
+
+}
diff --git a/src/com/android/browser/search/SearchEnginePreference.java b/src/com/android/browser/search/SearchEnginePreference.java
new file mode 100644
index 0000000..62ce97b
--- /dev/null
+++ b/src/com/android/browser/search/SearchEnginePreference.java
@@ -0,0 +1,70 @@
+/*
+ * 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.search;
+
+import com.android.browser.R;
+
+import android.app.SearchManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.preference.ListPreference;
+import android.util.AttributeSet;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+class SearchEnginePreference extends ListPreference {
+
+ private static final String TAG = "SearchEnginePreference";
+
+ public SearchEnginePreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ ArrayList<CharSequence> entryValues = new ArrayList<CharSequence>();
+ ArrayList<CharSequence> entries = new ArrayList<CharSequence>();
+
+ SearchEngine defaultSearchEngine = SearchEngines.getDefaultSearchEngine(context);
+ String defaultSearchEngineName = null;
+ if (defaultSearchEngine != null) {
+ defaultSearchEngineName = defaultSearchEngine.getName();
+ entryValues.add(defaultSearchEngineName);
+ entries.add(defaultSearchEngine.getLabel());
+ }
+ for (SearchEngineInfo searchEngineInfo : SearchEngines.getSearchEngineInfos(context)) {
+ String name = searchEngineInfo.getName();
+ // Skip entry with same name as default provider
+ if (!name.equals(defaultSearchEngineName)) {
+ entryValues.add(name);
+ entries.add(searchEngineInfo.getLabel());
+ }
+ }
+
+ setEntryValues(entryValues.toArray(new CharSequence[entryValues.size()]));
+ setEntries(entries.toArray(new CharSequence[entries.size()]));
+
+ //for other language the default search engine is google,but for English and
+ //Chinese the default search engine should be Baidu.
+ String language = context.getResources().getConfiguration().locale.toString();
+ if (language.equals("zh_CN")) {
+ setDefaultValue("baidu_cn");
+ } else if (language.equals("en_US")) {
+ setDefaultValue("baidu");
+ }
+ }
+}
diff --git a/src/com/android/browser/search/SearchEngines.java b/src/com/android/browser/search/SearchEngines.java
new file mode 100644
index 0000000..dff5f62
--- /dev/null
+++ b/src/com/android/browser/search/SearchEngines.java
@@ -0,0 +1,81 @@
+/*
+ * 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.search;
+
+import com.android.browser.R;
+import com.android.browser.reflect.ReflectHelper;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.TextUtils;
+import android.util.Log;
+
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SearchEngines {
+
+ private static final String TAG = "SearchEngines";
+
+ public static SearchEngine getDefaultSearchEngine(Context context) {
+ return DefaultSearchEngine.create(context);
+ }
+
+ public static List<SearchEngineInfo> getSearchEngineInfos(Context context) {
+ ArrayList<SearchEngineInfo> searchEngineInfos = new ArrayList<SearchEngineInfo>();
+ Resources res = context.getResources();
+ String[] searchEngines = res.getStringArray(R.array.search_engines);
+ Object[] params = { new String("persist.env.c.browser.resource"),
+ new String("default")};
+ Class[] type = new Class[] {String.class, String.class};
+ String browserRes = (String)ReflectHelper.invokeStaticMethod(
+ "android.os.SystemProperties","get", type, params);
+ for (int i = 0; i < searchEngines.length; i++) {
+ String name = searchEngines[i];
+ if ("cmcc".equals(browserRes)) {
+ SearchEngineInfo info = new SearchEngineInfo(context, name);
+ searchEngineInfos.add(info);
+ } else if (!name.startsWith("cmcc")) {
+ SearchEngineInfo info = new SearchEngineInfo(context, name);
+ searchEngineInfos.add(info);
+ }
+ }
+ return searchEngineInfos;
+ }
+
+ public static SearchEngine get(Context context, String name) {
+ // TODO: cache
+ SearchEngine defaultSearchEngine = getDefaultSearchEngine(context);
+ if (TextUtils.isEmpty(name)
+ || (defaultSearchEngine != null && name.equals(defaultSearchEngine.getName()))) {
+ return defaultSearchEngine;
+ }
+ SearchEngineInfo searchEngineInfo = getSearchEngineInfo(context, name);
+ if (searchEngineInfo == null) return defaultSearchEngine;
+ return new OpenSearchSearchEngine(context, searchEngineInfo);
+ }
+
+ public static SearchEngineInfo getSearchEngineInfo(Context context, String name) {
+ try {
+ return new SearchEngineInfo(context, name);
+ } catch (IllegalArgumentException exception) {
+ Log.e(TAG, "Cannot load search engine " + name, exception);
+ return null;
+ }
+ }
+
+}
diff --git a/src/com/android/browser/stub/NullController.java b/src/com/android/browser/stub/NullController.java
new file mode 100644
index 0000000..149fe4e
--- /dev/null
+++ b/src/com/android/browser/stub/NullController.java
@@ -0,0 +1,152 @@
+package com.android.browser.stub;
+
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.view.ActionMode;
+import android.view.ContextMenu;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ContextMenu.ContextMenuInfo;
+
+import com.android.browser.ActivityController;
+
+
+public class NullController implements ActivityController {
+
+ public static NullController INSTANCE = new NullController();
+
+ private NullController() {}
+
+ @Override
+ public void start(Intent intent) {
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ }
+
+ @Override
+ public void handleNewIntent(Intent intent) {
+ }
+
+ @Override
+ public void onResume() {
+ }
+
+ @Override
+ public boolean onMenuOpened(int featureId, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public void onOptionsMenuClosed(Menu menu) {
+ }
+
+ @Override
+ public void onContextMenuClosed(Menu menu) {
+ }
+
+ @Override
+ public void onPause() {
+ }
+
+ @Override
+ public void onDestroy() {
+ }
+
+ @Override
+ public void onConfgurationChanged(Configuration newConfig) {
+ }
+
+ @Override
+ public void onLowMemory() {
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ return false;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v,
+ ContextMenuInfo menuInfo) {
+
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ return false;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public void onActionModeStarted(ActionMode mode) {
+ }
+
+ @Override
+ public void onActionModeFinished(ActionMode mode) {
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ }
+
+ @Override
+ public boolean onSearchRequested() {
+ return false;
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean dispatchKeyShortcutEvent(KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ return false;
+ }
+
+ @Override
+ public boolean dispatchTrackballEvent(MotionEvent ev) {
+ return false;
+ }
+
+ @Override
+ public boolean dispatchGenericMotionEvent(MotionEvent ev) {
+ return false;
+ }
+
+}
diff --git a/src/com/android/browser/util/ThreadedCursorAdapter.java b/src/com/android/browser/util/ThreadedCursorAdapter.java
new file mode 100644
index 0000000..f07a375
--- /dev/null
+++ b/src/com/android/browser/util/ThreadedCursorAdapter.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2011 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.util;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.Process;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Adapter;
+import android.widget.BaseAdapter;
+import android.widget.CursorAdapter;
+
+import com.android.browser.R;
+
+import java.lang.ref.WeakReference;
+
+public abstract class ThreadedCursorAdapter<T> extends BaseAdapter {
+
+ private static final String LOGTAG = "BookmarksThreadedAdapter";
+ private static final boolean DEBUG = false;
+
+ private Context mContext;
+ private Object mCursorLock = new Object();
+ private CursorAdapter mCursorAdapter;
+ private T mLoadingObject;
+ private Handler mLoadHandler;
+ private Handler mHandler;
+ private int mSize;
+ private boolean mHasCursor;
+ private long mGeneration;
+
+ private class LoadContainer {
+ WeakReference<View> view;
+ int position;
+ T bind_object;
+ Adapter owner;
+ boolean loaded;
+ long generation;
+ }
+
+ public ThreadedCursorAdapter(Context context, Cursor c) {
+ mContext = context;
+ mHasCursor = (c != null);
+ mCursorAdapter = new CursorAdapter(context, c, 0) {
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ throw new IllegalStateException("not supported");
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ throw new IllegalStateException("not supported");
+ }
+
+ @Override
+ public void notifyDataSetChanged() {
+ super.notifyDataSetChanged();
+ mSize = getCount();
+ mGeneration++;
+ ThreadedCursorAdapter.this.notifyDataSetChanged();
+ }
+
+ @Override
+ public void notifyDataSetInvalidated() {
+ super.notifyDataSetInvalidated();
+ mSize = getCount();
+ mGeneration++;
+ ThreadedCursorAdapter.this.notifyDataSetInvalidated();
+ }
+
+ };
+ mSize = mCursorAdapter.getCount();
+ HandlerThread thread = new HandlerThread("threaded_adapter_" + this,
+ Process.THREAD_PRIORITY_BACKGROUND);
+ thread.start();
+ mLoadHandler = new Handler(thread.getLooper()) {
+ @SuppressWarnings("unchecked")
+ @Override
+ public void handleMessage(Message msg) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "loading: " + msg.what);
+ }
+ loadRowObject(msg.what, (LoadContainer) msg.obj);
+ }
+ };
+ mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ @SuppressWarnings("unchecked")
+ LoadContainer container = (LoadContainer) msg.obj;
+ if (container == null) {
+ return;
+ }
+ View view = container.view.get();
+ if (view == null
+ || container.owner != ThreadedCursorAdapter.this
+ || container.position != msg.what
+ || view.getWindowToken() == null
+ || container.generation != mGeneration) {
+ return;
+ }
+ container.loaded = true;
+ bindView(view, container.bind_object);
+ }
+ };
+ }
+
+ @Override
+ public int getCount() {
+ return mSize;
+ }
+
+ @Override
+ public Cursor getItem(int position) {
+ return (Cursor) mCursorAdapter.getItem(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ synchronized (mCursorLock) {
+ return getItemId(getItem(position));
+ }
+ }
+
+ private void loadRowObject(int position, LoadContainer container) {
+ if (container == null
+ || container.position != position
+ || container.owner != ThreadedCursorAdapter.this
+ || container.view.get() == null) {
+ return;
+ }
+ synchronized (mCursorLock) {
+ Cursor c = (Cursor) mCursorAdapter.getItem(position);
+ if (c == null || c.isClosed()) {
+ return;
+ }
+ container.bind_object = getRowObject(c, container.bind_object);
+ }
+ mHandler.obtainMessage(position, container).sendToTarget();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = newView(mContext, parent);
+ }
+ @SuppressWarnings("unchecked")
+ LoadContainer container = (LoadContainer) convertView.getTag(R.id.load_object);
+ if (container == null) {
+ container = new LoadContainer();
+ container.view = new WeakReference<View>(convertView);
+ convertView.setTag(R.id.load_object, container);
+ }
+ if (container.position == position
+ && container.owner == this
+ && container.loaded
+ && container.generation == mGeneration) {
+ bindView(convertView, container.bind_object);
+ } else {
+ bindView(convertView, cachedLoadObject());
+ if (mHasCursor) {
+ container.position = position;
+ container.loaded = false;
+ container.owner = this;
+ container.generation = mGeneration;
+ mLoadHandler.obtainMessage(position, container).sendToTarget();
+ }
+ }
+ return convertView;
+ }
+
+ private T cachedLoadObject() {
+ if (mLoadingObject == null) {
+ mLoadingObject = getLoadingObject();
+ }
+ return mLoadingObject;
+ }
+
+ public void changeCursor(Cursor cursor) {
+ mLoadHandler.removeCallbacksAndMessages(null);
+ mHandler.removeCallbacksAndMessages(null);
+ synchronized (mCursorLock) {
+ mHasCursor = (cursor != null);
+ mCursorAdapter.changeCursor(cursor);
+ }
+ }
+
+ public abstract View newView(Context context, ViewGroup parent);
+ public abstract void bindView(View view, T object);
+ public abstract T getRowObject(Cursor c, T recycleObject);
+ public abstract T getLoadingObject();
+ protected abstract long getItemId(Cursor c);
+}
\ No newline at end of file
diff --git a/src/com/android/browser/view/BasePieView.java b/src/com/android/browser/view/BasePieView.java
new file mode 100644
index 0000000..b9178be
--- /dev/null
+++ b/src/com/android/browser/view/BasePieView.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2011 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.view;
+
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.Adapter;
+
+import java.util.ArrayList;
+
+/**
+ * common code for pie views
+ */
+public abstract class BasePieView implements PieMenu.PieView {
+
+ protected Adapter mAdapter;
+ private DataSetObserver mObserver;
+ protected ArrayList<View> mViews;
+
+ protected OnLayoutListener mListener;
+
+ protected int mCurrent;
+ protected int mChildWidth;
+ protected int mChildHeight;
+ protected int mWidth;
+ protected int mHeight;
+ protected int mLeft;
+ protected int mTop;
+
+ public BasePieView() {
+ }
+
+ public void setLayoutListener(OnLayoutListener l) {
+ mListener = l;
+ }
+
+ public void setAdapter(Adapter adapter) {
+ mAdapter = adapter;
+ if (adapter == null) {
+ if (mAdapter != null) {
+ mAdapter.unregisterDataSetObserver(mObserver);
+ }
+ mViews = null;
+ mCurrent = -1;
+ } else {
+ mObserver = new DataSetObserver() {
+ @Override
+ public void onChanged() {
+ buildViews();
+ }
+
+ @Override
+ public void onInvalidated() {
+ mViews.clear();
+ }
+ };
+ mAdapter.registerDataSetObserver(mObserver);
+ setCurrent(0);
+ }
+ }
+
+ public void setCurrent(int ix) {
+ mCurrent = ix;
+ }
+
+ public Adapter getAdapter() {
+ return mAdapter;
+ }
+
+ protected void buildViews() {
+ if (mAdapter != null) {
+ final int n = mAdapter.getCount();
+ if (mViews == null) {
+ mViews = new ArrayList<View>(n);
+ } else {
+ mViews.clear();
+ }
+ mChildWidth = 0;
+ mChildHeight = 0;
+ for (int i = 0; i < n; i++) {
+ View view = mAdapter.getView(i, null, null);
+ view.measure(View.MeasureSpec.UNSPECIFIED,
+ View.MeasureSpec.UNSPECIFIED);
+ mChildWidth = Math.max(mChildWidth, view.getMeasuredWidth());
+ mChildHeight = Math.max(mChildHeight, view.getMeasuredHeight());
+ mViews.add(view);
+ }
+ }
+ }
+
+ /**
+ * this will be called before the first draw call
+ * needs to set top, left, width, height
+ */
+ @Override
+ public void layout(int anchorX, int anchorY, boolean left, float angle,
+ int parentHeight) {
+ if (mListener != null) {
+ mListener.onLayout(anchorX, anchorY, left);
+ }
+ }
+
+
+ @Override
+ public abstract void draw(Canvas canvas);
+
+ protected void drawView(View view, Canvas canvas) {
+ final int state = canvas.save();
+ canvas.translate(view.getLeft(), view.getTop());
+ view.draw(canvas);
+ canvas.restoreToCount(state);
+ }
+
+ protected abstract int findChildAt(int y);
+
+ @Override
+ public boolean onTouchEvent(MotionEvent evt) {
+ int action = evt.getActionMasked();
+ int evtx = (int) evt.getX();
+ int evty = (int) evt.getY();
+ if ((evtx < mLeft) || (evtx >= mLeft + mWidth)
+ || (evty < mTop) || (evty >= mTop + mHeight)) {
+ return false;
+ }
+ switch (action) {
+ case MotionEvent.ACTION_MOVE:
+ View v = mViews.get(mCurrent);
+ setCurrent(Math.max(0, Math.min(mViews.size() -1,
+ findChildAt(evty))));
+ View v1 = mViews.get(mCurrent);
+ if (v != v1) {
+ v.setPressed(false);
+ v1.setPressed(true);
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ mViews.get(mCurrent).performClick();
+ mViews.get(mCurrent).setPressed(false);
+ break;
+ default:
+ break;
+ }
+ return true;
+ }
+
+}
diff --git a/src/com/android/browser/view/BookmarkContainer.java b/src/com/android/browser/view/BookmarkContainer.java
new file mode 100644
index 0000000..5175589
--- /dev/null
+++ b/src/com/android/browser/view/BookmarkContainer.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2011 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.view;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.StateListDrawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewConfiguration;
+import android.widget.RelativeLayout;
+
+public class BookmarkContainer extends RelativeLayout implements OnClickListener {
+
+ private OnClickListener mClickListener;
+ private boolean mIgnoreRequestLayout = false;
+
+ public BookmarkContainer(Context context) {
+ super(context);
+ init();
+ }
+
+ public BookmarkContainer(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public BookmarkContainer(
+ Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ void init() {
+ setFocusable(true);
+ super.setOnClickListener(this);
+ }
+
+ @Override
+ public void setBackgroundDrawable(Drawable d) {
+ super.setBackgroundDrawable(d);
+ }
+
+ @Override
+ public void setOnClickListener(OnClickListener l) {
+ mClickListener = l;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ updateTransitionDrawable(isPressed());
+ }
+
+ void updateTransitionDrawable(boolean pressed) {
+ final int longPressTimeout = ViewConfiguration.getLongPressTimeout();
+ Drawable selector = getBackground();
+ if (selector != null && selector instanceof StateListDrawable) {
+ Drawable d = ((StateListDrawable)selector).getCurrent();
+ if (d != null && d instanceof TransitionDrawable) {
+ if (pressed && isLongClickable()) {
+ ((TransitionDrawable) d).startTransition(longPressTimeout);
+ } else {
+ ((TransitionDrawable) d).resetTransition();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onClick(View view) {
+ updateTransitionDrawable(false);
+ if (mClickListener != null) {
+ mClickListener.onClick(view);
+ }
+ }
+
+ public void setIgnoreRequestLayout(boolean ignore) {
+ mIgnoreRequestLayout = ignore;
+ }
+
+ @Override
+ public void requestLayout() {
+ if (!mIgnoreRequestLayout) {
+ super.requestLayout();
+ }
+ }
+}
diff --git a/src/com/android/browser/view/BookmarkExpandableView.java b/src/com/android/browser/view/BookmarkExpandableView.java
new file mode 100644
index 0000000..763efa7
--- /dev/null
+++ b/src/com/android/browser/view/BookmarkExpandableView.java
@@ -0,0 +1,473 @@
+/*
+ * Copyright (C) 2011 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.view;
+
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.util.AttributeSet;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.ExpandableListAdapter;
+import android.widget.ExpandableListView;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.browser.BreadCrumbView;
+import com.android.browser.BrowserBookmarksAdapter;
+import com.android.browser.R;
+import com.android.browser.platformsupport.BrowserContract;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class BookmarkExpandableView extends ExpandableListView
+ implements BreadCrumbView.Controller {
+
+ public static final String LOCAL_ACCOUNT_NAME = "local";
+
+ private BookmarkAccountAdapter mAdapter;
+ private int mColumnWidth;
+ private Context mContext;
+ private OnChildClickListener mOnChildClickListener;
+ private ContextMenuInfo mContextMenuInfo = null;
+ private OnCreateContextMenuListener mOnCreateContextMenuListener;
+ private boolean mLongClickable;
+ private BreadCrumbView.Controller mBreadcrumbController;
+ private int mMaxColumnCount;
+
+ public BookmarkExpandableView(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public BookmarkExpandableView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public BookmarkExpandableView(
+ Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(context);
+ }
+
+ void init(Context context) {
+ mContext = context;
+ setItemsCanFocus(true);
+ setLongClickable(false);
+ mMaxColumnCount = mContext.getResources()
+ .getInteger(R.integer.max_bookmark_columns);
+ setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY);
+ mAdapter = new BookmarkAccountAdapter(mContext);
+ super.setAdapter(mAdapter);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ if (width > 0) {
+ mAdapter.measureChildren(width);
+ setPadding(mAdapter.mRowPadding, 0, mAdapter.mRowPadding, 0);
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, widthMode);
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ if (width != getMeasuredWidth()) {
+ mAdapter.measureChildren(getMeasuredWidth());
+ }
+ }
+
+ @Override
+ public void setAdapter(ExpandableListAdapter adapter) {
+ throw new RuntimeException("Not supported");
+ }
+
+ public void setColumnWidthFromLayout(int layout) {
+ LayoutInflater infalter = LayoutInflater.from(mContext);
+ View v = infalter.inflate(layout, this, false);
+ v.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ mColumnWidth = v.getMeasuredWidth();
+ }
+
+ public void clearAccounts() {
+ mAdapter.clear();
+ }
+
+ public void addAccount(String accountName, BrowserBookmarksAdapter adapter,
+ boolean expandGroup) {
+ // First, check if it already exists
+ int indexOf = mAdapter.mGroups.indexOf(accountName);
+ if (indexOf >= 0) {
+ BrowserBookmarksAdapter existing = mAdapter.mChildren.get(indexOf);
+ if (existing != adapter) {
+ existing.unregisterDataSetObserver(mAdapter.mObserver);
+ // Replace the existing one
+ mAdapter.mChildren.remove(indexOf);
+ mAdapter.mChildren.add(indexOf, adapter);
+ adapter.registerDataSetObserver(mAdapter.mObserver);
+ }
+ } else {
+ mAdapter.mGroups.add(accountName);
+ mAdapter.mChildren.add(adapter);
+ adapter.registerDataSetObserver(mAdapter.mObserver);
+ }
+ mAdapter.notifyDataSetChanged();
+ if (expandGroup) {
+ expandGroup(mAdapter.getGroupCount() - 1);
+ }
+ }
+
+ @Override
+ public void setOnChildClickListener(OnChildClickListener onChildClickListener) {
+ mOnChildClickListener = onChildClickListener;
+ }
+
+ @Override
+ public void setOnCreateContextMenuListener(OnCreateContextMenuListener l) {
+ mOnCreateContextMenuListener = l;
+ if (!mLongClickable) {
+ mLongClickable = true;
+ if (mAdapter != null) {
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+ }
+
+ // SWE: com.android.internal.view.menu.MenuBuilder is a hidden class in SDK.
+ // Since the 'menu' object is of type MenuBuilder, java reflection method
+ // is the only way to access MenuBuilder.setCurrentMenuInfo().
+ static void setCurrentMenuInfo(ContextMenu menu, ContextMenuInfo menuInfo) {
+ try {
+ Class<?> proxyClass = Class.forName("com.android.internal.view.menu.MenuBuilder");
+ Class<?> argTypes[] = new Class[1];
+ argTypes[0] = ContextMenuInfo.class;
+ Method m = proxyClass.getDeclaredMethod("setCurrentMenuInfo", argTypes);
+ m.setAccessible(true);
+
+ Object args[] = new Object[1];
+ args[0] = menuInfo;
+ m.invoke(menu, args);
+ } catch (Exception e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void createContextMenu(ContextMenu menu) {
+ // The below is copied from View - we want to bypass the override
+ // in AbsListView
+
+ ContextMenuInfo menuInfo = getContextMenuInfo();
+
+ // Sets the current menu info so all items added to menu will have
+ // my extra info set.
+ setCurrentMenuInfo(menu, menuInfo);
+
+ onCreateContextMenu(menu);
+ if (mOnCreateContextMenuListener != null) {
+ mOnCreateContextMenuListener.onCreateContextMenu(menu, this, menuInfo);
+ }
+
+ // Clear the extra information so subsequent items that aren't mine don't
+ // have my extra info.
+ setCurrentMenuInfo(menu, null);
+
+ if (getParent() != null) {
+ getParent().createContextMenu(menu);
+ }
+ }
+
+ @Override
+ public boolean showContextMenuForChild(View originalView) {
+ int groupPosition = (Integer) originalView.getTag(R.id.group_position);
+ int childPosition = (Integer) originalView.getTag(R.id.child_position);
+
+ mContextMenuInfo = new BookmarkContextMenuInfo(childPosition,
+ groupPosition);
+ if (getParent() != null) {
+ getParent().showContextMenuForChild(this);
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onTop(BreadCrumbView view, int level, Object data) {
+ if (mBreadcrumbController != null) {
+ mBreadcrumbController.onTop(view, level, data);
+ }
+ }
+
+ public void setBreadcrumbController(BreadCrumbView.Controller controller) {
+ mBreadcrumbController = controller;
+ }
+
+ @Override
+ protected ContextMenuInfo getContextMenuInfo() {
+ return mContextMenuInfo;
+ }
+
+ public BrowserBookmarksAdapter getChildAdapter(int groupPosition) {
+ return mAdapter.mChildren.get(groupPosition);
+ }
+
+ private OnClickListener mChildClickListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (v.getVisibility() != View.VISIBLE) {
+ return;
+ }
+ int groupPosition = (Integer) v.getTag(R.id.group_position);
+ int childPosition = (Integer) v.getTag(R.id.child_position);
+ if (mAdapter.getGroupCount() <= groupPosition
+ || mAdapter.mChildren.get(groupPosition).getCount() <= childPosition) {
+ return;
+ }
+ long id = mAdapter.mChildren.get(groupPosition).getItemId(childPosition);
+ if (mOnChildClickListener != null) {
+ mOnChildClickListener.onChildClick(BookmarkExpandableView.this,
+ v, groupPosition, childPosition, id);
+ }
+ }
+ };
+
+ private OnClickListener mGroupOnClickListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ int groupPosition = (Integer) v.getTag(R.id.group_position);
+ if (isGroupExpanded(groupPosition)) {
+ collapseGroup(groupPosition);
+ } else {
+ expandGroup(groupPosition, true);
+ }
+ }
+ };
+
+ public BreadCrumbView getBreadCrumbs(int groupPosition) {
+ return mAdapter.getBreadCrumbView(groupPosition);
+ }
+
+ public JSONObject saveGroupState() throws JSONException {
+ JSONObject obj = new JSONObject();
+ int count = mAdapter.getGroupCount();
+ for (int i = 0; i < count; i++) {
+ String acctName = mAdapter.mGroups.get(i);
+ if (!isGroupExpanded(i)) {
+ obj.put(acctName != null ? acctName : LOCAL_ACCOUNT_NAME, false);
+ }
+ }
+ return obj;
+ }
+
+ class BookmarkAccountAdapter extends BaseExpandableListAdapter {
+ ArrayList<BrowserBookmarksAdapter> mChildren;
+ ArrayList<String> mGroups;
+ HashMap<Integer, BreadCrumbView> mBreadcrumbs =
+ new HashMap<Integer, BreadCrumbView>();
+ LayoutInflater mInflater;
+ int mRowCount = 1; // assume at least 1 child fits in a row
+ int mLastViewWidth = -1;
+ int mRowPadding = -1;
+ DataSetObserver mObserver = new DataSetObserver() {
+ @Override
+ public void onChanged() {
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ notifyDataSetInvalidated();
+ }
+ };
+
+ public BookmarkAccountAdapter(Context context) {
+ mContext = context;
+ mInflater = LayoutInflater.from(mContext);
+ mChildren = new ArrayList<BrowserBookmarksAdapter>();
+ mGroups = new ArrayList<String>();
+ }
+
+ public void clear() {
+ mGroups.clear();
+ mChildren.clear();
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public Object getChild(int groupPosition, int childPosition) {
+ return mChildren.get(groupPosition).getItem(childPosition);
+ }
+
+ @Override
+ public long getChildId(int groupPosition, int childPosition) {
+ return childPosition;
+ }
+
+ @Override
+ public View getChildView(int groupPosition, int childPosition,
+ boolean isLastChild, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.bookmark_grid_row, parent, false);
+ }
+ BrowserBookmarksAdapter childAdapter = mChildren.get(groupPosition);
+ int rowCount = mRowCount;
+ LinearLayout row = (LinearLayout) convertView;
+ if (row.getChildCount() > rowCount) {
+ row.removeViews(rowCount, row.getChildCount() - rowCount);
+ }
+ for (int i = 0; i < rowCount; i++) {
+ View cv = null;
+ if (row.getChildCount() > i) {
+ cv = row.getChildAt(i);
+ }
+ int realChildPosition = (childPosition * rowCount) + i;
+ if (realChildPosition < childAdapter.getCount()) {
+ View v = childAdapter.getView(realChildPosition, cv, row);
+ v.setTag(R.id.group_position, groupPosition);
+ v.setTag(R.id.child_position, realChildPosition);
+ v.setOnClickListener(mChildClickListener);
+ v.setLongClickable(mLongClickable);
+ if (cv == null) {
+ row.addView(v);
+ } else if (cv != v) {
+ row.removeViewAt(i);
+ row.addView(v, i);
+ } else {
+ cv.setVisibility(View.VISIBLE);
+ }
+ } else if (cv != null) {
+ cv.setVisibility(View.GONE);
+ }
+ }
+ return row;
+ }
+
+ @Override
+ public int getChildrenCount(int groupPosition) {
+ BrowserBookmarksAdapter adapter = mChildren.get(groupPosition);
+ return (int) Math.ceil(adapter.getCount() / (float)mRowCount);
+ }
+
+ @Override
+ public Object getGroup(int groupPosition) {
+ return mChildren.get(groupPosition);
+ }
+
+ @Override
+ public int getGroupCount() {
+ return mGroups.size();
+ }
+
+ public void measureChildren(int viewWidth) {
+ if (mLastViewWidth == viewWidth) return;
+
+ int rowCount = viewWidth / mColumnWidth;
+ if (mMaxColumnCount > 0) {
+ rowCount = Math.min(rowCount, mMaxColumnCount);
+ }
+ int rowPadding = (viewWidth - (rowCount * mColumnWidth)) / 2;
+ boolean notify = rowCount != mRowCount || rowPadding != mRowPadding;
+ mRowCount = rowCount;
+ mRowPadding = rowPadding;
+ mLastViewWidth = viewWidth;
+ if (notify) {
+ notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public long getGroupId(int groupPosition) {
+ return groupPosition;
+ }
+
+ @Override
+ public View getGroupView(int groupPosition, boolean isExpanded,
+ View view, ViewGroup parent) {
+ if (view == null) {
+ view = mInflater.inflate(R.layout.bookmark_group_view, parent, false);
+ view.setOnClickListener(mGroupOnClickListener);
+ }
+ view.setTag(R.id.group_position, groupPosition);
+ FrameLayout crumbHolder = (FrameLayout) view.findViewById(R.id.crumb_holder);
+ crumbHolder.removeAllViews();
+ BreadCrumbView crumbs = getBreadCrumbView(groupPosition);
+ if (crumbs.getParent() != null) {
+ ((ViewGroup)crumbs.getParent()).removeView(crumbs);
+ }
+ crumbHolder.addView(crumbs);
+ TextView name = (TextView) view.findViewById(R.id.group_name);
+ String groupName = mGroups.get(groupPosition);
+ if (groupName == null) {
+ groupName = mContext.getString(R.string.local_bookmarks);
+ }
+ name.setText(groupName);
+ return view;
+ }
+
+ public BreadCrumbView getBreadCrumbView(int groupPosition) {
+ BreadCrumbView crumbs = mBreadcrumbs.get(groupPosition);
+ if (crumbs == null) {
+ crumbs = (BreadCrumbView)
+ mInflater.inflate(R.layout.bookmarks_header, null);
+ crumbs.setController(BookmarkExpandableView.this);
+ crumbs.setUseBackButton(true);
+ crumbs.setMaxVisible(1);
+ String bookmarks = mContext.getString(R.string.bookmarks);
+ crumbs.pushView(bookmarks, false,
+ BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER);
+ crumbs.setTag(R.id.group_position, groupPosition);
+ crumbs.setVisibility(View.GONE);
+ mBreadcrumbs.put(groupPosition, crumbs);
+ }
+ return crumbs;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return true;
+ }
+ }
+
+ public static class BookmarkContextMenuInfo implements ContextMenuInfo {
+
+ private BookmarkContextMenuInfo(int childPosition, int groupPosition) {
+ this.childPosition = childPosition;
+ this.groupPosition = groupPosition;
+ }
+
+ public int childPosition;
+ public int groupPosition;
+ }
+
+}
diff --git a/src/com/android/browser/view/CustomScreenLinearLayout.java b/src/com/android/browser/view/CustomScreenLinearLayout.java
new file mode 100644
index 0000000..f5341e8
--- /dev/null
+++ b/src/com/android/browser/view/CustomScreenLinearLayout.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2012 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.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+
+
+public class CustomScreenLinearLayout extends LinearLayout {
+
+ public CustomScreenLinearLayout(Context context) {
+ super(context);
+ setChildrenDrawingOrderEnabled(true);
+ }
+
+ public CustomScreenLinearLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setChildrenDrawingOrderEnabled(true);
+ }
+
+ public CustomScreenLinearLayout(Context context, AttributeSet attrs,
+ int defStyle) {
+ super(context, attrs, defStyle);
+ setChildrenDrawingOrderEnabled(true);
+ }
+
+ @Override
+ protected int getChildDrawingOrder(int childCount, int i) {
+ return childCount - i - 1;
+ }
+
+}
diff --git a/src/com/android/browser/view/EventRedirectingFrameLayout.java b/src/com/android/browser/view/EventRedirectingFrameLayout.java
new file mode 100644
index 0000000..901b021
--- /dev/null
+++ b/src/com/android/browser/view/EventRedirectingFrameLayout.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2011 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.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+public class EventRedirectingFrameLayout extends FrameLayout {
+
+ private int mTargetChild;
+
+ public EventRedirectingFrameLayout(Context context) {
+ super(context);
+ }
+
+ public EventRedirectingFrameLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public EventRedirectingFrameLayout(
+ Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public void setTargetChild(int index) {
+ if (index >= 0 && index < getChildCount()) {
+ mTargetChild = index;
+ getChildAt(mTargetChild).requestFocus();
+ }
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ View child = getChildAt(mTargetChild);
+ if (child != null)
+ return child.dispatchTouchEvent(ev);
+ return false;
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ View child = getChildAt(mTargetChild);
+ if (child != null)
+ return child.dispatchKeyEvent(event);
+ return false;
+ }
+
+ @Override
+ public boolean dispatchKeyEventPreIme(KeyEvent event) {
+ View child = getChildAt(mTargetChild);
+ if (child != null)
+ return child.dispatchKeyEventPreIme(event);
+ return false;
+ }
+
+}
diff --git a/src/com/android/browser/view/PieItem.java b/src/com/android/browser/view/PieItem.java
new file mode 100644
index 0000000..9e04ecb
--- /dev/null
+++ b/src/com/android/browser/view/PieItem.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2011 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.view;
+
+import android.view.View;
+
+import com.android.browser.view.PieMenu.PieView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Pie menu item
+ */
+public class PieItem {
+
+ private View mView;
+ private PieView mPieView;
+ private int level;
+ private float start;
+ private float sweep;
+ private float animate;
+ private int inner;
+ private int outer;
+ private boolean mSelected;
+ private boolean mEnabled;
+ private List<PieItem> mItems;
+
+ public PieItem(View view, int level) {
+ mView = view;
+ this.level = level;
+ mEnabled = true;
+ setAnimationAngle(getAnimationAngle());
+ setAlpha(getAlpha());
+ }
+
+ public PieItem(View view, int level, PieView sym) {
+ mView = view;
+ this.level = level;
+ mPieView = sym;
+ mEnabled = false;
+ }
+
+ public boolean hasItems() {
+ return mItems != null;
+ }
+
+ public List<PieItem> getItems() {
+ return mItems;
+ }
+
+ public void addItem(PieItem item) {
+ if (mItems == null) {
+ mItems = new ArrayList<PieItem>();
+ }
+ mItems.add(item);
+ }
+
+ public void setAlpha(float alpha) {
+ if (mView != null) {
+ mView.setAlpha(alpha);
+ }
+ }
+
+ public float getAlpha() {
+ if (mView != null) {
+ return mView.getAlpha();
+ }
+ return 1;
+ }
+
+ public void setAnimationAngle(float a) {
+ animate = a;
+ }
+
+ public float getAnimationAngle() {
+ return animate;
+ }
+
+ public void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ }
+
+ public void setSelected(boolean s) {
+ mSelected = s;
+ if (mView != null) {
+ mView.setSelected(s);
+ }
+ }
+
+ public boolean isSelected() {
+ return mSelected;
+ }
+
+ public int getLevel() {
+ return level;
+ }
+
+ public void setGeometry(float st, float sw, int inside, int outside) {
+ start = st;
+ sweep = sw;
+ inner = inside;
+ outer = outside;
+ }
+
+ public float getStart() {
+ return start;
+ }
+
+ public float getStartAngle() {
+ return start + animate;
+ }
+
+ public float getSweep() {
+ return sweep;
+ }
+
+ public int getInnerRadius() {
+ return inner;
+ }
+
+ public int getOuterRadius() {
+ return outer;
+ }
+
+ public boolean isPieView() {
+ return (mPieView != null);
+ }
+
+ public View getView() {
+ return mView;
+ }
+
+ public void setPieView(PieView sym) {
+ mPieView = sym;
+ }
+
+ public PieView getPieView() {
+ if (mEnabled) {
+ return mPieView;
+ }
+ return null;
+ }
+
+}
diff --git a/src/com/android/browser/view/PieListView.java b/src/com/android/browser/view/PieListView.java
new file mode 100644
index 0000000..1043fc7
--- /dev/null
+++ b/src/com/android/browser/view/PieListView.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2011 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.view;
+
+import com.android.browser.R;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.view.View;
+
+/**
+ * shows views in a menu style list
+ */
+public class PieListView extends BasePieView {
+
+ private Paint mBgPaint;
+
+ public PieListView(Context ctx) {
+ mBgPaint = new Paint();
+ mBgPaint.setColor(ctx.getResources().getColor(R.color.qcMenuBackground));
+ }
+
+ /**
+ * this will be called before the first draw call
+ */
+ @Override
+ public void layout(int anchorX, int anchorY, boolean left, float angle,
+ int pHeight) {
+ super.layout(anchorX, anchorY, left, angle, pHeight);
+ buildViews();
+ mWidth = mChildWidth;
+ mHeight = mChildHeight * mAdapter.getCount();
+ mLeft = anchorX + (left ? 0 : - mChildWidth);
+ mTop = Math.max(anchorY - mHeight / 2, 0);
+ if (mTop + mHeight > pHeight) {
+ mTop = pHeight - mHeight;
+ }
+ if (mViews != null) {
+ layoutChildrenLinear();
+ }
+ }
+
+ protected void layoutChildrenLinear() {
+ final int n = mViews.size();
+ int top = mTop;
+ for (View view : mViews) {
+ view.layout(mLeft, top, mLeft + mChildWidth, top + mChildHeight);
+ top += mChildHeight;
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ canvas.drawRect(mLeft, mTop, mLeft + mWidth, mTop + mHeight, mBgPaint);
+ if (mViews != null) {
+ for (View view : mViews) {
+ drawView(view, canvas);
+ }
+ }
+ }
+
+ @Override
+ protected int findChildAt(int y) {
+ final int ix = (y - mTop) * mViews.size() / mHeight;
+ return ix;
+ }
+
+}
diff --git a/src/com/android/browser/view/PieMenu.java b/src/com/android/browser/view/PieMenu.java
new file mode 100644
index 0000000..1699c27
--- /dev/null
+++ b/src/com/android/browser/view/PieMenu.java
@@ -0,0 +1,636 @@
+/*
+ * 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.view;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.android.browser.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PieMenu extends FrameLayout {
+
+ private static final int MAX_LEVELS = 5;
+ private static final long ANIMATION = 80;
+
+ public interface PieController {
+ /**
+ * called before menu opens to customize menu
+ * returns if pie state has been changed
+ */
+ public boolean onOpen();
+ public void stopEditingUrl();
+
+ }
+
+ /**
+ * A view like object that lives off of the pie menu
+ */
+ public interface PieView {
+
+ public interface OnLayoutListener {
+ public void onLayout(int ax, int ay, boolean left);
+ }
+
+ public void setLayoutListener(OnLayoutListener l);
+
+ public void layout(int anchorX, int anchorY, boolean onleft, float angle,
+ int parentHeight);
+
+ public void draw(Canvas c);
+
+ public boolean onTouchEvent(MotionEvent evt);
+
+ }
+
+ private Point mCenter;
+ private int mRadius;
+ private int mRadiusInc;
+ private int mSlop;
+ private int mTouchOffset;
+ private Path mPath;
+
+ private boolean mOpen;
+ private PieController mController;
+
+ private List<PieItem> mItems;
+ private int mLevels;
+ private int[] mCounts;
+ private PieView mPieView = null;
+
+ // sub menus
+ private List<PieItem> mCurrentItems;
+ private PieItem mOpenItem;
+
+ private Drawable mBackground;
+ private Paint mNormalPaint;
+ private Paint mSelectedPaint;
+ private Paint mSubPaint;
+
+ // touch handling
+ private PieItem mCurrentItem;
+
+ private boolean mUseBackground;
+ private boolean mAnimating;
+
+ /**
+ * @param context
+ * @param attrs
+ * @param defStyle
+ */
+ public PieMenu(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(context);
+ }
+
+ /**
+ * @param context
+ * @param attrs
+ */
+ public PieMenu(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ /**
+ * @param context
+ */
+ public PieMenu(Context context) {
+ super(context);
+ init(context);
+ }
+
+ private void init(Context ctx) {
+ mItems = new ArrayList<PieItem>();
+ mLevels = 0;
+ mCounts = new int[MAX_LEVELS];
+ Resources res = ctx.getResources();
+ mRadius = (int) res.getDimension(R.dimen.qc_radius_start);
+ mRadiusInc = (int) res.getDimension(R.dimen.qc_radius_increment);
+ mSlop = (int) res.getDimension(R.dimen.qc_slop);
+ mTouchOffset = (int) res.getDimension(R.dimen.qc_touch_offset);
+ mOpen = false;
+ setWillNotDraw(false);
+ setDrawingCacheEnabled(false);
+ mCenter = new Point(0,0);
+ mBackground = res.getDrawable(R.drawable.qc_background_normal);
+ mNormalPaint = new Paint();
+ mNormalPaint.setColor(res.getColor(R.color.qc_normal));
+ mNormalPaint.setAntiAlias(true);
+ mSelectedPaint = new Paint();
+ mSelectedPaint.setColor(res.getColor(R.color.qc_selected));
+ mSelectedPaint.setAntiAlias(true);
+ mSubPaint = new Paint();
+ mSubPaint.setAntiAlias(true);
+ mSubPaint.setColor(res.getColor(R.color.qc_sub));
+ }
+
+ public void setController(PieController ctl) {
+ mController = ctl;
+ }
+
+ public void setUseBackground(boolean useBackground) {
+ mUseBackground = useBackground;
+ }
+
+ public void addItem(PieItem item) {
+ // add the item to the pie itself
+ mItems.add(item);
+ int l = item.getLevel();
+ mLevels = Math.max(mLevels, l);
+ mCounts[l]++;
+ }
+
+ public void removeItem(PieItem item) {
+ mItems.remove(item);
+ }
+
+ public void clearItems() {
+ mItems.clear();
+ }
+
+ private boolean onTheLeft() {
+ return mCenter.x < mSlop;
+ }
+
+ /**
+ * guaranteed has center set
+ * @param show
+ */
+ private void show(boolean show) {
+ mOpen = show;
+ if (mOpen) {
+ // ensure clean state
+ mAnimating = false;
+ mCurrentItem = null;
+ mOpenItem = null;
+ mPieView = null;
+ mController.stopEditingUrl();
+ mCurrentItems = mItems;
+ for (PieItem item : mCurrentItems) {
+ item.setSelected(false);
+ }
+ if (mController != null) {
+ boolean changed = mController.onOpen();
+ }
+ layoutPie();
+ animateOpen();
+ }
+ invalidate();
+ }
+
+ private void animateOpen() {
+ ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
+ anim.addUpdateListener(new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ for (PieItem item : mCurrentItems) {
+ item.setAnimationAngle((1 - animation.getAnimatedFraction()) * (- item.getStart()));
+ }
+ invalidate();
+ }
+
+ });
+ anim.setDuration(2*ANIMATION);
+ anim.start();
+ }
+
+ private void setCenter(int x, int y) {
+ if (x < mSlop) {
+ mCenter.x = 0;
+ } else {
+ mCenter.x = getWidth();
+ }
+ mCenter.y = y;
+ }
+
+ private void layoutPie() {
+ float emptyangle = (float) Math.PI / 16;
+ int rgap = 2;
+ int inner = mRadius + rgap;
+ int outer = mRadius + mRadiusInc - rgap;
+ int gap = 1;
+ for (int i = 0; i < mLevels; i++) {
+ int level = i + 1;
+ float sweep = (float) (Math.PI - 2 * emptyangle) / mCounts[level];
+ float angle = emptyangle + sweep / 2;
+ mPath = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap, outer, inner, mCenter);
+ for (PieItem item : mCurrentItems) {
+ if (item.getLevel() == level) {
+ View view = item.getView();
+ if (view != null) {
+ view.measure(view.getLayoutParams().width,
+ view.getLayoutParams().height);
+ int w = view.getMeasuredWidth();
+ int h = view.getMeasuredHeight();
+ int r = inner + (outer - inner) * 2 / 3;
+ int x = (int) (r * Math.sin(angle));
+ int y = mCenter.y - (int) (r * Math.cos(angle)) - h / 2;
+ if (onTheLeft()) {
+ x = mCenter.x + x - w / 2;
+ } else {
+ x = mCenter.x - x - w / 2;
+ }
+ view.layout(x, y, x + w, y + h);
+ }
+ float itemstart = angle - sweep / 2;
+ item.setGeometry(itemstart, sweep, inner, outer);
+ angle += sweep;
+ }
+ }
+ inner += mRadiusInc;
+ outer += mRadiusInc;
+ }
+ }
+
+
+ /**
+ * converts a
+ *
+ * @param angle from 0..PI to Android degrees (clockwise starting at 3
+ * o'clock)
+ * @return skia angle
+ */
+ private float getDegrees(double angle) {
+ return (float) (270 - 180 * angle / Math.PI);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (mOpen) {
+ int state;
+ if (mUseBackground) {
+ int w = mBackground.getIntrinsicWidth();
+ int h = mBackground.getIntrinsicHeight();
+ int left = mCenter.x - w;
+ int top = mCenter.y - h / 2;
+ mBackground.setBounds(left, top, left + w, top + h);
+ state = canvas.save();
+ if (onTheLeft()) {
+ canvas.scale(-1, 1);
+ }
+ mBackground.draw(canvas);
+ canvas.restoreToCount(state);
+ }
+ // draw base menu
+ PieItem last = mCurrentItem;
+ if (mOpenItem != null) {
+ last = mOpenItem;
+ }
+ for (PieItem item : mCurrentItems) {
+ if (item != last) {
+ drawItem(canvas, item);
+ }
+ }
+ if (last != null) {
+ drawItem(canvas, last);
+ }
+ if (mPieView != null) {
+ mPieView.draw(canvas);
+ }
+ }
+ }
+
+ private void drawItem(Canvas canvas, PieItem item) {
+ if (item.getView() != null) {
+ Paint p = item.isSelected() ? mSelectedPaint : mNormalPaint;
+ if (!mItems.contains(item)) {
+ p = item.isSelected() ? mSelectedPaint : mSubPaint;
+ }
+ int state = canvas.save();
+ if (onTheLeft()) {
+ canvas.scale(-1, 1);
+ }
+ float r = getDegrees(item.getStartAngle()) - 270; // degrees(0)
+ canvas.rotate(r, mCenter.x, mCenter.y);
+ canvas.drawPath(mPath, p);
+ canvas.restoreToCount(state);
+ // draw the item view
+ View view = item.getView();
+ state = canvas.save();
+ canvas.translate(view.getX(), view.getY());
+ view.draw(canvas);
+ canvas.restoreToCount(state);
+ }
+ }
+
+ private Path makeSlice(float start, float end, int outer, int inner, Point center) {
+ RectF bb =
+ new RectF(center.x - outer, center.y - outer, center.x + outer,
+ center.y + outer);
+ RectF bbi =
+ new RectF(center.x - inner, center.y - inner, center.x + inner,
+ center.y + inner);
+ Path path = new Path();
+ path.arcTo(bb, start, end - start, true);
+ path.arcTo(bbi, end, start - end);
+ path.close();
+ return path;
+ }
+
+ // touch handling for pie
+
+ @Override
+ public boolean onTouchEvent(MotionEvent evt) {
+ float x = evt.getX();
+ float y = evt.getY();
+ int action = evt.getActionMasked();
+ if (MotionEvent.ACTION_DOWN == action) {
+ if ((x > getWidth() - mSlop) || (x < mSlop)) {
+ setCenter((int) x, (int) y);
+ show(true);
+ return true;
+ }
+ } else if (MotionEvent.ACTION_UP == action) {
+ if (mOpen) {
+ boolean handled = false;
+ if (mPieView != null) {
+ handled = mPieView.onTouchEvent(evt);
+ }
+ PieItem item = mCurrentItem;
+ if (!mAnimating) {
+ deselect();
+ }
+ show(false);
+ if (!handled && (item != null) && (item.getView() != null)) {
+ if ((item == mOpenItem) || !mAnimating) {
+ item.getView().performClick();
+ }
+ }
+ return true;
+ }
+ } else if (MotionEvent.ACTION_CANCEL == action) {
+ if (mOpen) {
+ show(false);
+ }
+ if (!mAnimating) {
+ deselect();
+ invalidate();
+ }
+ return false;
+ } else if (MotionEvent.ACTION_MOVE == action) {
+ if (mAnimating) return false;
+ boolean handled = false;
+ PointF polar = getPolar(x, y);
+ int maxr = mRadius + mLevels * mRadiusInc + 50;
+ if (mPieView != null) {
+ handled = mPieView.onTouchEvent(evt);
+ }
+ if (handled) {
+ invalidate();
+ return false;
+ }
+ if (polar.y < mRadius) {
+ if (mOpenItem != null) {
+ closeSub();
+ } else if (!mAnimating) {
+ deselect();
+ invalidate();
+ }
+ return false;
+ }
+ if (polar.y > maxr) {
+ deselect();
+ show(false);
+ evt.setAction(MotionEvent.ACTION_DOWN);
+ if (getParent() != null) {
+ ((ViewGroup) getParent()).dispatchTouchEvent(evt);
+ }
+ return false;
+ }
+ PieItem item = findItem(polar);
+ if (item == null) {
+ } else if (mCurrentItem != item) {
+ onEnter(item);
+ if ((item != null) && item.isPieView() && (item.getView() != null)) {
+ int cx = item.getView().getLeft() + (onTheLeft()
+ ? item.getView().getWidth() : 0);
+ int cy = item.getView().getTop();
+ mPieView = item.getPieView();
+ layoutPieView(mPieView, cx, cy,
+ (item.getStartAngle() + item.getSweep()) / 2);
+ }
+ invalidate();
+ }
+ }
+ // always re-dispatch event
+ return false;
+ }
+
+ private void layoutPieView(PieView pv, int x, int y, float angle) {
+ pv.layout(x, y, onTheLeft(), angle, getHeight());
+ }
+
+ /**
+ * enter a slice for a view
+ * updates model only
+ * @param item
+ */
+ private void onEnter(PieItem item) {
+ // deselect
+ if (mCurrentItem != null) {
+ mCurrentItem.setSelected(false);
+ }
+ if (item != null) {
+ // clear up stack
+ playSoundEffect(SoundEffectConstants.CLICK);
+ item.setSelected(true);
+ mPieView = null;
+ mCurrentItem = item;
+ if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) {
+ openSub(mCurrentItem);
+ mOpenItem = item;
+ }
+ } else {
+ mCurrentItem = null;
+ }
+
+ }
+
+ private void animateOut(final PieItem fixed, AnimatorListener listener) {
+ if ((mCurrentItems == null) || (fixed == null)) return;
+ final float target = fixed.getStartAngle();
+ ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
+ anim.addUpdateListener(new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ for (PieItem item : mCurrentItems) {
+ if (item != fixed) {
+ item.setAnimationAngle(animation.getAnimatedFraction()
+ * (target - item.getStart()));
+ }
+ }
+ invalidate();
+ }
+ });
+ anim.setDuration(ANIMATION);
+ anim.addListener(listener);
+ anim.start();
+ }
+
+ private void animateIn(final PieItem fixed, AnimatorListener listener) {
+ if ((mCurrentItems == null) || (fixed == null)) return;
+ final float target = fixed.getStartAngle();
+ ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
+ anim.addUpdateListener(new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ for (PieItem item : mCurrentItems) {
+ if (item != fixed) {
+ item.setAnimationAngle((1 - animation.getAnimatedFraction())
+ * (target - item.getStart()));
+ }
+ }
+ invalidate();
+
+ }
+
+ });
+ anim.setDuration(ANIMATION);
+ anim.addListener(listener);
+ anim.start();
+ }
+
+ private void openSub(final PieItem item) {
+ mAnimating = true;
+ animateOut(item, new AnimatorListenerAdapter() {
+ public void onAnimationEnd(Animator a) {
+ for (PieItem item : mCurrentItems) {
+ item.setAnimationAngle(0);
+ }
+ mCurrentItems = new ArrayList<PieItem>(mItems.size());
+ int i = 0, j = 0;
+ while (i < mItems.size()) {
+ if (mItems.get(i) == item) {
+ mCurrentItems.add(item);
+ } else {
+ mCurrentItems.add(item.getItems().get(j++));
+ }
+ i++;
+ }
+ layoutPie();
+ animateIn(item, new AnimatorListenerAdapter() {
+ public void onAnimationEnd(Animator a) {
+ for (PieItem item : mCurrentItems) {
+ item.setAnimationAngle(0);
+ }
+ mAnimating = false;
+ }
+ });
+ }
+ });
+ }
+
+ private void closeSub() {
+ mAnimating = true;
+ if (mCurrentItem != null) {
+ mCurrentItem.setSelected(false);
+ }
+ animateOut(mOpenItem, new AnimatorListenerAdapter() {
+ public void onAnimationEnd(Animator a) {
+ for (PieItem item : mCurrentItems) {
+ item.setAnimationAngle(0);
+ }
+ mCurrentItems = mItems;
+ mPieView = null;
+ animateIn(mOpenItem, new AnimatorListenerAdapter() {
+ public void onAnimationEnd(Animator a) {
+ for (PieItem item : mCurrentItems) {
+ item.setAnimationAngle(0);
+ }
+ mAnimating = false;
+ mOpenItem = null;
+ mCurrentItem = null;
+ }
+ });
+ }
+ });
+ }
+
+ private void deselect() {
+ if (mCurrentItem != null) {
+ mCurrentItem.setSelected(false);
+ }
+ if (mOpenItem != null) {
+ mOpenItem = null;
+ mCurrentItems = mItems;
+ }
+ mCurrentItem = null;
+ mPieView = null;
+ }
+
+ private PointF getPolar(float x, float y) {
+ PointF res = new PointF();
+ // get angle and radius from x/y
+ res.x = (float) Math.PI / 2;
+ x = mCenter.x - x;
+ if (mCenter.x < mSlop) {
+ x = -x;
+ }
+ y = mCenter.y - y;
+ res.y = (float) Math.sqrt(x * x + y * y);
+ if (y > 0) {
+ res.x = (float) Math.asin(x / res.y);
+ } else if (y < 0) {
+ res.x = (float) (Math.PI - Math.asin(x / res.y ));
+ }
+ return res;
+ }
+
+ /**
+ *
+ * @param polar x: angle, y: dist
+ * @return the item at angle/dist or null
+ */
+ private PieItem findItem(PointF polar) {
+ // find the matching item:
+ for (PieItem item : mCurrentItems) {
+ if (inside(polar, mTouchOffset, item)) {
+ return item;
+ }
+ }
+ return null;
+ }
+
+ private boolean inside(PointF polar, float offset, PieItem item) {
+ return (item.getInnerRadius() - offset < polar.y)
+ && (item.getOuterRadius() - offset > polar.y)
+ && (item.getStartAngle() < polar.x)
+ && (item.getStartAngle() + item.getSweep() > polar.x);
+ }
+
+}
diff --git a/src/com/android/browser/view/PieStackView.java b/src/com/android/browser/view/PieStackView.java
new file mode 100644
index 0000000..e1f41bd
--- /dev/null
+++ b/src/com/android/browser/view/PieStackView.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2011 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.view;
+
+import com.android.browser.R;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.view.View;
+
+/**
+ * shows views in a stack
+ */
+public class PieStackView extends BasePieView {
+
+ private static final int SLOP = 5;
+
+ private OnCurrentListener mCurrentListener;
+ private int mMinHeight;
+
+ public interface OnCurrentListener {
+ public void onSetCurrent(int index);
+ }
+
+ public PieStackView(Context ctx) {
+ mMinHeight = (int) ctx.getResources()
+ .getDimension(R.dimen.qc_tab_title_height);
+ }
+
+ public void setOnCurrentListener(OnCurrentListener l) {
+ mCurrentListener = l;
+ }
+
+ @Override
+ public void setCurrent(int ix) {
+ super.setCurrent(ix);
+ if (mCurrentListener != null) {
+ mCurrentListener.onSetCurrent(ix);
+ }
+ }
+
+ /**
+ * this will be called before the first draw call
+ */
+ @Override
+ public void layout(int anchorX, int anchorY, boolean left, float angle,
+ int pHeight) {
+ super.layout(anchorX, anchorY, left, angle, pHeight);
+ buildViews();
+ mWidth = mChildWidth;
+ mHeight = mChildHeight + (mViews.size() - 1) * mMinHeight;
+ mLeft = anchorX + (left ? SLOP : -(SLOP + mChildWidth));
+ mTop = anchorY - mHeight / 2;
+ if (mViews != null) {
+ layoutChildrenLinear();
+ }
+ }
+
+ private void layoutChildrenLinear() {
+ final int n = mViews.size();
+ int top = mTop;
+ int dy = (n == 1) ? 0 : (mHeight - mChildHeight) / (n - 1);
+ for (View view : mViews) {
+ int x = mLeft;
+ view.layout(x, top, x + mChildWidth, top + mChildHeight);
+ top += dy;
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if ((mViews != null) && (mCurrent > -1)) {
+ final int n = mViews.size();
+ for (int i = 0; i < mCurrent; i++) {
+ drawView(mViews.get(i), canvas);
+ }
+ for (int i = n - 1; i > mCurrent; i--) {
+ drawView(mViews.get(i), canvas);
+ }
+ drawView(mViews.get(mCurrent), canvas);
+ }
+ }
+
+ @Override
+ protected int findChildAt(int y) {
+ final int ix = (y - mTop) * mViews.size() / mHeight;
+ return ix;
+ }
+
+}
diff --git a/src/com/android/browser/view/ScrollerView.java b/src/com/android/browser/view/ScrollerView.java
new file mode 100644
index 0000000..7e5a4c8
--- /dev/null
+++ b/src/com/android/browser/view/ScrollerView.java
@@ -0,0 +1,1952 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser.view;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.FocusFinder;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.AnimationUtils;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.OverScroller;
+import android.widget.TextView;
+
+import java.util.List;
+
+/**
+ * Layout container for a view hierarchy that can be scrolled by the user,
+ * allowing it to be larger than the physical display. A ScrollView
+ * is a {@link FrameLayout}, meaning you should place one child in it
+ * containing the entire contents to scroll; this child may itself be a layout
+ * manager with a complex hierarchy of objects. A child that is often used
+ * is a {@link LinearLayout} in a vertical orientation, presenting a vertical
+ * array of top-level items that the user can scroll through.
+ *
+ * <p>The {@link TextView} class also
+ * takes care of its own scrolling, so does not require a ScrollView, but
+ * using the two together is possible to achieve the effect of a text view
+ * within a larger container.
+ *
+ * <p>ScrollView only supports vertical scrolling.
+ *
+ * @attr ref android.R.styleable#ScrollView_fillViewport
+ */
+public class ScrollerView extends FrameLayout {
+ static final int ANIMATED_SCROLL_GAP = 250;
+
+ static final float MAX_SCROLL_FACTOR = 0.5f;
+
+ private long mLastScroll;
+
+ private final Rect mTempRect = new Rect();
+ protected OverScroller mScroller;
+
+ /**
+ * Position of the last motion event.
+ */
+ private float mLastMotionY;
+
+ /**
+ * True when the layout has changed but the traversal has not come through yet.
+ * Ideally the view hierarchy would keep track of this for us.
+ */
+ private boolean mIsLayoutDirty = true;
+
+ /**
+ * The child to give focus to in the event that a child has requested focus while the
+ * layout is dirty. This prevents the scroll from being wrong if the child has not been
+ * laid out before requesting focus.
+ */
+ protected View mChildToScrollTo = null;
+
+ /**
+ * True if the user is currently dragging this ScrollView around. This is
+ * not the same as 'is being flinged', which can be checked by
+ * mScroller.isFinished() (flinging begins when the user lifts his finger).
+ */
+ protected boolean mIsBeingDragged = false;
+
+ /**
+ * Determines speed during touch scrolling
+ */
+ private VelocityTracker mVelocityTracker;
+
+ /**
+ * When set to true, the scroll view measure its child to make it fill the currently
+ * visible area.
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ private boolean mFillViewport;
+
+ /**
+ * Whether arrow scrolling is animated.
+ */
+ private boolean mSmoothScrollingEnabled = true;
+
+ private int mTouchSlop;
+ protected int mMinimumVelocity;
+ private int mMaximumVelocity;
+
+ private int mOverscrollDistance;
+ private int mOverflingDistance;
+
+ /**
+ * ID of the active pointer. This is used to retain consistency during
+ * drags/flings if multiple pointers are used.
+ */
+ private int mActivePointerId = INVALID_POINTER;
+
+ private static class ThreadSpanState {
+ public Span mActiveHead; // doubly-linked list.
+ public int mActiveSize;
+ public Span mFreeListHead; // singly-linked list. only changes at head.
+ public int mFreeListSize;
+ }
+
+ public static class Span {
+ private String mName;
+ private long mCreateMillis;
+ private Span mNext;
+ private Span mPrev; // not used when in freeList, only active
+ private final ThreadSpanState mContainerState;
+
+ Span(ThreadSpanState threadState) {
+ mContainerState = threadState;
+ }
+
+ // Empty constructor for the NO_OP_SPAN
+ protected Span() {
+ mContainerState = null;
+ }
+
+ /**
+ * To be called when the critical span is complete (i.e. the
+ * animation is done animating). This can be called on any
+ * thread (even a different one from where the animation was
+ * taking place), but that's only a defensive implementation
+ * measure. It really makes no sense for you to call this on
+ * thread other than that where you created it.
+ *
+ * @hide
+ */
+ public void finish() {
+ ThreadSpanState state = mContainerState;
+ synchronized (state) {
+ if (mName == null) {
+ // Duplicate finish call. Ignore.
+ return;
+ }
+
+ // Remove ourselves from the active list.
+ if (mPrev != null) {
+ mPrev.mNext = mNext;
+ }
+ if (mNext != null) {
+ mNext.mPrev = mPrev;
+ }
+ if (state.mActiveHead == this) {
+ state.mActiveHead = mNext;
+ }
+
+ state.mActiveSize--;
+
+ this.mCreateMillis = -1;
+ this.mName = null;
+ this.mPrev = null;
+ this.mNext = null;
+
+ // Add ourselves to the freeList, if it's not already
+ // too big.
+ if (state.mFreeListSize < 5) {
+ this.mNext = state.mFreeListHead;
+ state.mFreeListHead = this;
+ state.mFreeListSize++;
+ }
+ }
+ }
+ }
+
+ private static final Span NO_OP_SPAN = new Span() {
+ public void finish() {
+ // Do nothing.
+ }
+ };
+
+ /**
+ * The StrictMode "critical time span" objects to catch animation
+ * stutters. Non-null when a time-sensitive animation is
+ * in-flight. Must call finish() on them when done animating.
+ * These are no-ops on user builds.
+ */
+ private Span mScrollStrictSpan = null; // aka "drag"
+ private Span mFlingStrictSpan = null;
+
+ /**
+ * Sentinel value for no current active pointer.
+ * Used by {@link #mActivePointerId}.
+ */
+ private static final int INVALID_POINTER = -1;
+
+ /**
+ * orientation of the scrollview
+ */
+ protected boolean mHorizontal;
+
+ protected boolean mIsOrthoDragged;
+ private float mLastOrthoCoord;
+ private View mDownView;
+ private PointF mDownCoords;
+
+
+ public ScrollerView(Context context) {
+ this(context, null);
+ }
+
+ public ScrollerView(Context context, AttributeSet attrs) {
+ this(context, attrs, android.R.attr.scrollViewStyle);
+ }
+
+ public ScrollerView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initScrollView();
+ // SWE_TODO : Fix me
+ /*
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, R.styleable.ScrollView, defStyle, 0);
+ setFillViewport(a.getBoolean(R.styleable.ScrollView_android_fillViewport, false));
+ a.recycle();*/
+ setFillViewport(false);
+ }
+
+ private void initScrollView() {
+ mScroller = new OverScroller(getContext());
+ setFocusable(true);
+ setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+ setWillNotDraw(false);
+ final ViewConfiguration configuration = ViewConfiguration.get(getContext());
+ mTouchSlop = configuration.getScaledTouchSlop();
+ mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
+ mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+ mOverscrollDistance = configuration.getScaledOverscrollDistance();
+ mOverflingDistance = configuration.getScaledOverflingDistance();
+ mDownCoords = new PointF();
+ }
+
+ public void setOrientation(int orientation) {
+ mHorizontal = (orientation == LinearLayout.HORIZONTAL);
+ requestLayout();
+ }
+
+ @Override
+ public boolean shouldDelayChildPressedState() {
+ return true;
+ }
+
+ @Override
+ protected float getTopFadingEdgeStrength() {
+ if (getChildCount() == 0) {
+ return 0.0f;
+ }
+ if (mHorizontal) {
+ final int length = getHorizontalFadingEdgeLength();
+ if (getScrollX() < length) {
+ return getScrollX() / (float) length;
+ }
+ } else {
+ final int length = getVerticalFadingEdgeLength();
+ if (getScrollY() < length) {
+ return getScrollY() / (float) length;
+ }
+ }
+ return 1.0f;
+ }
+
+ @Override
+ protected float getBottomFadingEdgeStrength() {
+ if (getChildCount() == 0) {
+ return 0.0f;
+ }
+ if (mHorizontal) {
+ final int length = getHorizontalFadingEdgeLength();
+ final int bottomEdge = getWidth() - getPaddingRight();
+ final int span = getChildAt(0).getRight() - getScrollX() - bottomEdge;
+ if (span < length) {
+ return span / (float) length;
+ }
+ } else {
+ final int length = getVerticalFadingEdgeLength();
+ final int bottomEdge = getHeight() - getPaddingBottom();
+ final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge;
+ if (span < length) {
+ return span / (float) length;
+ }
+ }
+ return 1.0f;
+ }
+
+ /**
+ * @return The maximum amount this scroll view will scroll in response to
+ * an arrow event.
+ */
+ public int getMaxScrollAmount() {
+ return (int) (MAX_SCROLL_FACTOR * (mHorizontal
+ ? (getRight() - getLeft()) : (getBottom() - getTop())));
+ }
+
+
+ @Override
+ public void addView(View child) {
+ if (getChildCount() > 0) {
+ throw new IllegalStateException("ScrollView can host only one direct child");
+ }
+
+ super.addView(child);
+ }
+
+ @Override
+ public void addView(View child, int index) {
+ if (getChildCount() > 0) {
+ throw new IllegalStateException("ScrollView can host only one direct child");
+ }
+
+ super.addView(child, index);
+ }
+
+ @Override
+ public void addView(View child, ViewGroup.LayoutParams params) {
+ if (getChildCount() > 0) {
+ throw new IllegalStateException("ScrollView can host only one direct child");
+ }
+
+ super.addView(child, params);
+ }
+
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ if (getChildCount() > 0) {
+ throw new IllegalStateException("ScrollView can host only one direct child");
+ }
+
+ super.addView(child, index, params);
+ }
+
+ /**
+ * @return Returns true this ScrollView can be scrolled
+ */
+ private boolean canScroll() {
+ View child = getChildAt(0);
+ if (child != null) {
+ if (mHorizontal) {
+ return getWidth() < child.getWidth() + getPaddingLeft() + getPaddingRight();
+ } else {
+ return getHeight() < child.getHeight() + getPaddingTop() + getPaddingBottom();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Indicates whether this ScrollView's content is stretched to fill the viewport.
+ *
+ * @return True if the content fills the viewport, false otherwise.
+ *
+ * @attr ref android.R.styleable#ScrollView_fillViewport
+ */
+ public boolean isFillViewport() {
+ return mFillViewport;
+ }
+
+ /**
+ * Indicates this ScrollView whether it should stretch its content height to fill
+ * the viewport or not.
+ *
+ * @param fillViewport True to stretch the content's height to the viewport's
+ * boundaries, false otherwise.
+ *
+ * @attr ref android.R.styleable#ScrollView_fillViewport
+ */
+ public void setFillViewport(boolean fillViewport) {
+ if (fillViewport != mFillViewport) {
+ mFillViewport = fillViewport;
+ requestLayout();
+ }
+ }
+
+ /**
+ * @return Whether arrow scrolling will animate its transition.
+ */
+ public boolean isSmoothScrollingEnabled() {
+ return mSmoothScrollingEnabled;
+ }
+
+ /**
+ * Set whether arrow scrolling will animate its transition.
+ * @param smoothScrollingEnabled whether arrow scrolling will animate its transition
+ */
+ public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
+ mSmoothScrollingEnabled = smoothScrollingEnabled;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ if (!mFillViewport) {
+ return;
+ }
+
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ if (heightMode == MeasureSpec.UNSPECIFIED) {
+ return;
+ }
+
+ if (getChildCount() > 0) {
+ final View child = getChildAt(0);
+ if (mHorizontal) {
+ int width = getMeasuredWidth();
+ if (child.getMeasuredWidth() < width) {
+ final FrameLayout.LayoutParams lp = (LayoutParams) child
+ .getLayoutParams();
+
+ int childHeightMeasureSpec = getChildMeasureSpec(
+ heightMeasureSpec, getPaddingTop() + getPaddingBottom(),
+ lp.height);
+ width -= getPaddingLeft();
+ width -= getPaddingRight();
+ int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ width, MeasureSpec.EXACTLY);
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+ } else {
+ int height = getMeasuredHeight();
+ if (child.getMeasuredHeight() < height) {
+ final FrameLayout.LayoutParams lp = (LayoutParams) child
+ .getLayoutParams();
+
+ int childWidthMeasureSpec = getChildMeasureSpec(
+ widthMeasureSpec, getPaddingLeft() + getPaddingRight(),
+ lp.width);
+ height -= getPaddingTop();
+ height -= getPaddingBottom();
+ int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ height, MeasureSpec.EXACTLY);
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ // Let the focused view and/or our descendants get the key first
+ return super.dispatchKeyEvent(event) || executeKeyEvent(event);
+ }
+
+ /**
+ * You can call this function yourself to have the scroll view perform
+ * scrolling from a key event, just as if the event had been dispatched to
+ * it by the view hierarchy.
+ *
+ * @param event The key event to execute.
+ * @return Return true if the event was handled, else false.
+ */
+ public boolean executeKeyEvent(KeyEvent event) {
+ mTempRect.setEmpty();
+
+ if (!canScroll()) {
+ if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) {
+ View currentFocused = findFocus();
+ if (currentFocused == this) currentFocused = null;
+ View nextFocused = FocusFinder.getInstance().findNextFocus(this,
+ currentFocused, View.FOCUS_DOWN);
+ return nextFocused != null
+ && nextFocused != this
+ && nextFocused.requestFocus(View.FOCUS_DOWN);
+ }
+ return false;
+ }
+
+ boolean handled = false;
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (!event.isAltPressed()) {
+ handled = arrowScroll(View.FOCUS_UP);
+ } else {
+ handled = fullScroll(View.FOCUS_UP);
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if (!event.isAltPressed()) {
+ handled = arrowScroll(View.FOCUS_DOWN);
+ } else {
+ handled = fullScroll(View.FOCUS_DOWN);
+ }
+ break;
+ case KeyEvent.KEYCODE_SPACE:
+ pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN);
+ break;
+ }
+ }
+
+ return handled;
+ }
+
+ private boolean inChild(int x, int y) {
+ if (getChildCount() > 0) {
+ final int scrollY = getScrollY();
+ final View child = getChildAt(0);
+ return !(y < child.getTop() - scrollY
+ || y >= child.getBottom() - scrollY
+ || x < child.getLeft()
+ || x >= child.getRight());
+ }
+ return false;
+ }
+
+ private void initOrResetVelocityTracker() {
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ } else {
+ mVelocityTracker.clear();
+ }
+ }
+
+ private void initVelocityTrackerIfNotExists() {
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ }
+
+ private void recycleVelocityTracker() {
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ @Override
+ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ if (disallowIntercept) {
+ recycleVelocityTracker();
+ }
+ super.requestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ /*
+ * This method JUST determines whether we want to intercept the motion.
+ * If we return true, onMotionEvent will be called and we do the actual
+ * scrolling there.
+ */
+
+ /*
+ * Shortcut the most recurring case: the user is in the dragging state
+ * and he is moving his finger. We want to intercept this motion.
+ */
+ final int action = ev.getAction();
+ if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
+ return true;
+ }
+ if ((action == MotionEvent.ACTION_MOVE) && (mIsOrthoDragged)) {
+ return true;
+ }
+ switch (action & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_MOVE: {
+ /*
+ * mIsBeingDragged == false, otherwise the shortcut would have
+ * caught it. Check whether the user has moved far enough from his
+ * original down touch.
+ */
+
+ /*
+ * Locally do absolute value. mLastMotionY is set to the y value of
+ * the down event.
+ */
+ final int activePointerId = mActivePointerId;
+ if (activePointerId == INVALID_POINTER) {
+ // If we don't have a valid id, the touch down wasn't on
+ // content.
+ break;
+ }
+
+ final int pointerIndex = ev.findPointerIndex(activePointerId);
+ final float y = mHorizontal ? ev.getX(pointerIndex) : ev
+ .getY(pointerIndex);
+ final int yDiff = (int) Math.abs(y - mLastMotionY);
+ if (yDiff > mTouchSlop) {
+ mIsBeingDragged = true;
+ mLastMotionY = y;
+ initVelocityTrackerIfNotExists();
+ mVelocityTracker.addMovement(ev);
+ if (mScrollStrictSpan == null) {
+ /*mScrollStrictSpan = StrictMode
+ .enterCriticalSpan("ScrollView-scroll");*/
+ mScrollStrictSpan = NO_OP_SPAN;
+ }
+ } else {
+ final float ocoord = mHorizontal ? ev.getY(pointerIndex) : ev
+ .getX(pointerIndex);
+ if (Math.abs(ocoord - mLastOrthoCoord) > mTouchSlop) {
+ mIsOrthoDragged = true;
+ mLastOrthoCoord = ocoord;
+ initVelocityTrackerIfNotExists();
+ mVelocityTracker.addMovement(ev);
+ }
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_DOWN: {
+ final float y = mHorizontal ? ev.getX() : ev.getY();
+ mDownCoords.x = ev.getX();
+ mDownCoords.y = ev.getY();
+ if (!inChild((int) ev.getX(), (int) ev.getY())) {
+ mIsBeingDragged = false;
+ recycleVelocityTracker();
+ break;
+ }
+
+ /*
+ * Remember location of down touch. ACTION_DOWN always refers to
+ * pointer index 0.
+ */
+ mLastMotionY = y;
+ mActivePointerId = ev.getPointerId(0);
+
+ initOrResetVelocityTracker();
+ mVelocityTracker.addMovement(ev);
+ /*
+ * If being flinged and user touches the screen, initiate drag;
+ * otherwise don't. mScroller.isFinished should be false when being
+ * flinged.
+ */
+ mIsBeingDragged = !mScroller.isFinished();
+ if (mIsBeingDragged && mScrollStrictSpan == null) {
+ /*mScrollStrictSpan = StrictMode
+ .enterCriticalSpan("ScrollView-scroll");*/
+ mScrollStrictSpan = NO_OP_SPAN;
+ }
+ mIsOrthoDragged = false;
+ final float ocoord = mHorizontal ? ev.getY() : ev.getX();
+ mLastOrthoCoord = ocoord;
+ mDownView = findViewAt((int) ev.getX(), (int) ev.getY());
+ break;
+ }
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ /* Release the drag */
+ mIsBeingDragged = false;
+ mIsOrthoDragged = false;
+ mActivePointerId = INVALID_POINTER;
+ recycleVelocityTracker();
+ if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
+ getScrollRange())) {
+ invalidate();
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ onSecondaryPointerUp(ev);
+ break;
+ }
+
+ /*
+ * The only time we want to intercept motion events is if we are in the
+ * drag mode.
+ */
+ return mIsBeingDragged || mIsOrthoDragged;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ initVelocityTrackerIfNotExists();
+ mVelocityTracker.addMovement(ev);
+
+ final int action = ev.getAction();
+ switch (action & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN: {
+ mIsBeingDragged = getChildCount() != 0;
+ if (!mIsBeingDragged) {
+ return false;
+ }
+
+ /*
+ * If being flinged and user touches, stop the fling. isFinished
+ * will be false if being flinged.
+ */
+ if (!mScroller.isFinished()) {
+ mScroller.abortAnimation();
+ if (mFlingStrictSpan != null) {
+ mFlingStrictSpan.finish();
+ mFlingStrictSpan = null;
+ }
+ }
+
+ // Remember where the motion event started
+ mLastMotionY = mHorizontal ? ev.getX() : ev.getY();
+ mActivePointerId = ev.getPointerId(0);
+ break;
+ }
+ case MotionEvent.ACTION_MOVE:
+ if (mIsOrthoDragged) {
+ // Scroll to follow the motion event
+ final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+ final float x = ev.getX(activePointerIndex);
+ final float y = ev.getY(activePointerIndex);
+ if (isOrthoMove(x - mDownCoords.x, y - mDownCoords.y)) {
+ onOrthoDrag(mDownView, mHorizontal
+ ? y - mDownCoords.y
+ : x - mDownCoords.x);
+ }
+ } else if (mIsBeingDragged) {
+ // Scroll to follow the motion event
+ final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+ final float y = mHorizontal ? ev.getX(activePointerIndex)
+ : ev.getY(activePointerIndex);
+ final int deltaY = (int) (mLastMotionY - y);
+ mLastMotionY = y;
+
+ final int oldX = getScrollX();
+ final int oldY = getScrollY();
+ final int range = getScrollRange();
+ if (mHorizontal) {
+ if (overScrollBy(deltaY, 0, getScrollX(), 0, range, 0,
+ mOverscrollDistance, 0, true)) {
+ // Break our velocity if we hit a scroll barrier.
+ mVelocityTracker.clear();
+ }
+ } else {
+ if (overScrollBy(0, deltaY, 0, getScrollY(), 0, range,
+ 0, mOverscrollDistance, true)) {
+ // Break our velocity if we hit a scroll barrier.
+ mVelocityTracker.clear();
+ }
+ }
+ onScrollChanged(getScrollX(), getScrollY(), oldX, oldY);
+
+ final int overscrollMode = getOverScrollMode();
+ if (overscrollMode == OVER_SCROLL_ALWAYS ||
+ (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0)) {
+ final int pulledToY = mHorizontal ? oldX + deltaY : oldY + deltaY;
+ if (pulledToY < 0) {
+ onPull(pulledToY);
+ } else if (pulledToY > range) {
+ onPull(pulledToY - range);
+ } else {
+ onPull(0);
+ }
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ final VelocityTracker vtracker = mVelocityTracker;
+ vtracker.computeCurrentVelocity(1000, mMaximumVelocity);
+ if (isOrthoMove(vtracker.getXVelocity(mActivePointerId),
+ vtracker.getYVelocity(mActivePointerId))
+ && mMinimumVelocity < Math.abs((mHorizontal ? vtracker.getYVelocity()
+ : vtracker.getXVelocity()))) {
+ onOrthoFling(mDownView, mHorizontal ? vtracker.getYVelocity()
+ : vtracker.getXVelocity());
+ break;
+ }
+ if (mIsOrthoDragged) {
+ onOrthoDragFinished(mDownView);
+ mActivePointerId = INVALID_POINTER;
+ endDrag();
+ } else if (mIsBeingDragged) {
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+ int initialVelocity = mHorizontal
+ ? (int) velocityTracker.getXVelocity(mActivePointerId)
+ : (int) velocityTracker.getYVelocity(mActivePointerId);
+
+ if (getChildCount() > 0) {
+ if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
+ fling(-initialVelocity);
+ } else {
+ final int bottom = getScrollRange();
+ if (mHorizontal) {
+ if (mScroller.springBack(getScrollX(), getScrollY(), 0,
+ bottom, 0, 0)) {
+ invalidate();
+ }
+ } else {
+ if (mScroller.springBack(getScrollX(), getScrollY(), 0,
+ 0, 0, bottom)) {
+ invalidate();
+ }
+ }
+ }
+ onPull(0);
+ }
+
+ mActivePointerId = INVALID_POINTER;
+ endDrag();
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ if (mIsOrthoDragged) {
+ onOrthoDragFinished(mDownView);
+ mActivePointerId = INVALID_POINTER;
+ endDrag();
+ } else if (mIsBeingDragged && getChildCount() > 0) {
+ if (mHorizontal) {
+ if (mScroller.springBack(getScrollX(), getScrollY(), 0,
+ getScrollRange(), 0, 0)) {
+ invalidate();
+ }
+ } else {
+ if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
+ getScrollRange())) {
+ invalidate();
+ }
+ }
+ mActivePointerId = INVALID_POINTER;
+ endDrag();
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ final int index = ev.getActionIndex();
+ final float y = mHorizontal ? ev.getX(index) : ev.getY(index);
+ mLastMotionY = y;
+ mLastOrthoCoord = mHorizontal ? ev.getY(index) : ev.getX(index);
+ mActivePointerId = ev.getPointerId(index);
+ break;
+ }
+ case MotionEvent.ACTION_POINTER_UP:
+ onSecondaryPointerUp(ev);
+ mLastMotionY = mHorizontal
+ ? ev.getX(ev.findPointerIndex(mActivePointerId))
+ : ev.getY(ev.findPointerIndex(mActivePointerId));
+ break;
+ }
+ return true;
+ }
+
+ protected View findViewAt(int x, int y) {
+ // subclass responsibility
+ return null;
+ }
+
+ protected void onPull(int delta) {
+ }
+
+ private void onSecondaryPointerUp(MotionEvent ev) {
+ final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
+ MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+ final int pointerId = ev.getPointerId(pointerIndex);
+ if (pointerId == mActivePointerId) {
+ // This was our active pointer going up. Choose a new
+ // active pointer and adjust accordingly.
+ // TODO: Make this decision more intelligent.
+ final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+ mLastMotionY = mHorizontal ? ev.getX(newPointerIndex) : ev.getY(newPointerIndex);
+ mActivePointerId = ev.getPointerId(newPointerIndex);
+ if (mVelocityTracker != null) {
+ mVelocityTracker.clear();
+ }
+ mLastOrthoCoord = mHorizontal ? ev.getY(newPointerIndex)
+ : ev.getX(newPointerIndex);
+ }
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_SCROLL: {
+ if (!mIsBeingDragged) {
+ if (mHorizontal) {
+ final float hscroll = event
+ .getAxisValue(MotionEvent.AXIS_HSCROLL);
+ if (hscroll != 0) {
+ /* SWE_TODO : - disruptive getHorizontalScrollFactor()*/
+ final int delta = (int) (hscroll * 10);
+ final int range = getScrollRange();
+ int oldScrollX = getScrollX();
+ int newScrollX = oldScrollX - delta;
+ if (newScrollX < 0) {
+ newScrollX = 0;
+ } else if (newScrollX > range) {
+ newScrollX = range;
+ }
+ if (newScrollX != oldScrollX) {
+ super.scrollTo(newScrollX, getScrollY());
+ return true;
+ }
+ }
+ } else {
+ final float vscroll = event
+ .getAxisValue(MotionEvent.AXIS_VSCROLL);
+ if (vscroll != 0) {
+ /* SWE_TODO : - disruptive getVerticalScrollFactor()*/
+ final int delta = (int) (vscroll * 10);
+ final int range = getScrollRange();
+ int oldScrollY = getScrollY();
+ int newScrollY = oldScrollY - delta;
+ if (newScrollY < 0) {
+ newScrollY = 0;
+ } else if (newScrollY > range) {
+ newScrollY = range;
+ }
+ if (newScrollY != oldScrollY) {
+ super.scrollTo(getScrollX(), newScrollY);
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return super.onGenericMotionEvent(event);
+ }
+
+ protected void onOrthoDrag(View draggedView, float distance) {
+ }
+
+ protected void onOrthoDragFinished(View draggedView) {
+ }
+
+ protected void onOrthoFling(View draggedView, float velocity) {
+ }
+
+ @Override
+ protected void onOverScrolled(int scrollX, int scrollY,
+ boolean clampedX, boolean clampedY) {
+ // Treat animating scrolls differently; see #computeScroll() for why.
+ if (!mScroller.isFinished()) {
+ setScrollX(scrollX);
+ setScrollY(scrollY);
+ if (isHardwareAccelerated() && getParent() instanceof View) {
+ ((View) getParent()).invalidate();
+ }
+ if (mHorizontal && clampedX) {
+ mScroller.springBack(getScrollX(), getScrollY(), 0, getScrollRange(), 0, 0);
+ } else if (!mHorizontal && clampedY) {
+ mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange());
+ }
+ } else {
+ super.scrollTo(scrollX, scrollY);
+ }
+ awakenScrollBars();
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setScrollable(true);
+ }
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setScrollable(true);
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ // Do not append text content to scroll events they are fired frequently
+ // and the client has already received another event type with the text.
+ if (event.getEventType() != AccessibilityEvent.TYPE_VIEW_SCROLLED) {
+ super.dispatchPopulateAccessibilityEvent(event);
+ }
+ return false;
+ }
+
+ private int getScrollRange() {
+ int scrollRange = 0;
+ if (getChildCount() > 0) {
+ View child = getChildAt(0);
+ if (mHorizontal) {
+ scrollRange = Math.max(0,
+ child.getWidth() - (getWidth() - getPaddingRight() - getPaddingLeft()));
+ } else {
+ scrollRange = Math.max(0,
+ child.getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop()));
+ }
+ }
+ return scrollRange;
+ }
+
+ /**
+ * <p>
+ * Finds the next focusable component that fits in this View's bounds
+ * (excluding fading edges) pretending that this View's top is located at
+ * the parameter top.
+ * </p>
+ *
+ * @param topFocus look for a candidate at the top of the bounds if topFocus is true,
+ * or at the bottom of the bounds if topFocus is false
+ * @param top the top offset of the bounds in which a focusable must be
+ * found (the fading edge is assumed to start at this position)
+ * @param preferredFocusable the View that has highest priority and will be
+ * returned if it is within my bounds (null is valid)
+ * @return the next focusable component in the bounds or null if none can be found
+ */
+ private View findFocusableViewInMyBounds(final boolean topFocus,
+ final int top, View preferredFocusable) {
+ /*
+ * The fading edge's transparent side should be considered for focus
+ * since it's mostly visible, so we divide the actual fading edge length
+ * by 2.
+ */
+ final int fadingEdgeLength = (mHorizontal
+ ? getHorizontalFadingEdgeLength()
+ : getVerticalFadingEdgeLength()) / 2;
+ final int topWithoutFadingEdge = top + fadingEdgeLength;
+ final int bottomWithoutFadingEdge = top + (mHorizontal ? getWidth() : getHeight()) - fadingEdgeLength;
+
+ if ((preferredFocusable != null)
+ && ((mHorizontal ? preferredFocusable.getLeft() : preferredFocusable.getTop())
+ < bottomWithoutFadingEdge)
+ && ((mHorizontal ? preferredFocusable.getRight() : preferredFocusable.getBottom()) > topWithoutFadingEdge)) {
+ return preferredFocusable;
+ }
+
+ return findFocusableViewInBounds(topFocus, topWithoutFadingEdge,
+ bottomWithoutFadingEdge);
+ }
+
+ /**
+ * <p>
+ * Finds the next focusable component that fits in the specified bounds.
+ * </p>
+ *
+ * @param topFocus look for a candidate is the one at the top of the bounds
+ * if topFocus is true, or at the bottom of the bounds if topFocus is
+ * false
+ * @param top the top offset of the bounds in which a focusable must be
+ * found
+ * @param bottom the bottom offset of the bounds in which a focusable must
+ * be found
+ * @return the next focusable component in the bounds or null if none can
+ * be found
+ */
+ private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) {
+
+ List<View> focusables = getFocusables(View.FOCUS_FORWARD);
+ View focusCandidate = null;
+
+ /*
+ * A fully contained focusable is one where its top is below the bound's
+ * top, and its bottom is above the bound's bottom. A partially
+ * contained focusable is one where some part of it is within the
+ * bounds, but it also has some part that is not within bounds. A fully contained
+ * focusable is preferred to a partially contained focusable.
+ */
+ boolean foundFullyContainedFocusable = false;
+
+ int count = focusables.size();
+ for (int i = 0; i < count; i++) {
+ View view = focusables.get(i);
+ int viewTop = mHorizontal ? view.getLeft() : view.getTop();
+ int viewBottom = mHorizontal ? view.getRight() : view.getBottom();
+
+ if (top < viewBottom && viewTop < bottom) {
+ /*
+ * the focusable is in the target area, it is a candidate for
+ * focusing
+ */
+
+ final boolean viewIsFullyContained = (top < viewTop) &&
+ (viewBottom < bottom);
+
+ if (focusCandidate == null) {
+ /* No candidate, take this one */
+ focusCandidate = view;
+ foundFullyContainedFocusable = viewIsFullyContained;
+ } else {
+ final int ctop = mHorizontal ? focusCandidate.getLeft() : focusCandidate.getTop();
+ final int cbot = mHorizontal ? focusCandidate.getRight() : focusCandidate.getBottom();
+ final boolean viewIsCloserToBoundary =
+ (topFocus && viewTop < ctop) ||
+ (!topFocus && viewBottom > cbot);
+
+ if (foundFullyContainedFocusable) {
+ if (viewIsFullyContained && viewIsCloserToBoundary) {
+ /*
+ * We're dealing with only fully contained views, so
+ * it has to be closer to the boundary to beat our
+ * candidate
+ */
+ focusCandidate = view;
+ }
+ } else {
+ if (viewIsFullyContained) {
+ /* Any fully contained view beats a partially contained view */
+ focusCandidate = view;
+ foundFullyContainedFocusable = true;
+ } else if (viewIsCloserToBoundary) {
+ /*
+ * Partially contained view beats another partially
+ * contained view if it's closer
+ */
+ focusCandidate = view;
+ }
+ }
+ }
+ }
+ }
+
+ return focusCandidate;
+ }
+
+ // i was here
+
+ /**
+ * <p>Handles scrolling in response to a "page up/down" shortcut press. This
+ * method will scroll the view by one page up or down and give the focus
+ * to the topmost/bottommost component in the new visible area. If no
+ * component is a good candidate for focus, this scrollview reclaims the
+ * focus.</p>
+ *
+ * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
+ * to go one page up or
+ * {@link android.view.View#FOCUS_DOWN} to go one page down
+ * @return true if the key event is consumed by this method, false otherwise
+ */
+ public boolean pageScroll(int direction) {
+ boolean down = direction == View.FOCUS_DOWN;
+ int height = getHeight();
+
+ if (down) {
+ mTempRect.top = getScrollY() + height;
+ int count = getChildCount();
+ if (count > 0) {
+ View view = getChildAt(count - 1);
+ if (mTempRect.top + height > view.getBottom()) {
+ mTempRect.top = view.getBottom() - height;
+ }
+ }
+ } else {
+ mTempRect.top = getScrollY() - height;
+ if (mTempRect.top < 0) {
+ mTempRect.top = 0;
+ }
+ }
+ mTempRect.bottom = mTempRect.top + height;
+
+ return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom);
+ }
+
+ /**
+ * <p>Handles scrolling in response to a "home/end" shortcut press. This
+ * method will scroll the view to the top or bottom and give the focus
+ * to the topmost/bottommost component in the new visible area. If no
+ * component is a good candidate for focus, this scrollview reclaims the
+ * focus.</p>
+ *
+ * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
+ * to go the top of the view or
+ * {@link android.view.View#FOCUS_DOWN} to go the bottom
+ * @return true if the key event is consumed by this method, false otherwise
+ */
+ public boolean fullScroll(int direction) {
+ boolean down = direction == View.FOCUS_DOWN;
+ int height = getHeight();
+
+ mTempRect.top = 0;
+ mTempRect.bottom = height;
+
+ if (down) {
+ int count = getChildCount();
+ if (count > 0) {
+ View view = getChildAt(count - 1);
+ mTempRect.bottom = view.getBottom() + getPaddingBottom();
+ mTempRect.top = mTempRect.bottom - height;
+ }
+ }
+
+ return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom);
+ }
+
+ /**
+ * <p>Scrolls the view to make the area defined by <code>top</code> and
+ * <code>bottom</code> visible. This method attempts to give the focus
+ * to a component visible in this area. If no component can be focused in
+ * the new visible area, the focus is reclaimed by this ScrollView.</p>
+ *
+ * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
+ * to go upward, {@link android.view.View#FOCUS_DOWN} to downward
+ * @param top the top offset of the new area to be made visible
+ * @param bottom the bottom offset of the new area to be made visible
+ * @return true if the key event is consumed by this method, false otherwise
+ */
+ private boolean scrollAndFocus(int direction, int top, int bottom) {
+ boolean handled = true;
+
+ int height = getHeight();
+ int containerTop = getScrollY();
+ int containerBottom = containerTop + height;
+ boolean up = direction == View.FOCUS_UP;
+
+ View newFocused = findFocusableViewInBounds(up, top, bottom);
+ if (newFocused == null) {
+ newFocused = this;
+ }
+
+ if (top >= containerTop && bottom <= containerBottom) {
+ handled = false;
+ } else {
+ int delta = up ? (top - containerTop) : (bottom - containerBottom);
+ doScrollY(delta);
+ }
+
+ if (newFocused != findFocus()) newFocused.requestFocus(direction);
+
+ return handled;
+ }
+
+ /**
+ * Handle scrolling in response to an up or down arrow click.
+ *
+ * @param direction The direction corresponding to the arrow key that was
+ * pressed
+ * @return True if we consumed the event, false otherwise
+ */
+ public boolean arrowScroll(int direction) {
+
+ View currentFocused = findFocus();
+ if (currentFocused == this) currentFocused = null;
+
+ View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
+
+ final int maxJump = getMaxScrollAmount();
+
+ if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump, getHeight())) {
+ nextFocused.getDrawingRect(mTempRect);
+ offsetDescendantRectToMyCoords(nextFocused, mTempRect);
+ int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
+ doScrollY(scrollDelta);
+ nextFocused.requestFocus(direction);
+ } else {
+ // no new focus
+ int scrollDelta = maxJump;
+
+ if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) {
+ scrollDelta = getScrollY();
+ } else if (direction == View.FOCUS_DOWN) {
+ if (getChildCount() > 0) {
+ int daBottom = getChildAt(0).getBottom();
+ int screenBottom = getScrollY() + getHeight() - getPaddingBottom();
+ if (daBottom - screenBottom < maxJump) {
+ scrollDelta = daBottom - screenBottom;
+ }
+ }
+ }
+ if (scrollDelta == 0) {
+ return false;
+ }
+ doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
+ }
+
+ if (currentFocused != null && currentFocused.isFocused()
+ && isOffScreen(currentFocused)) {
+ // previously focused item still has focus and is off screen, give
+ // it up (take it back to ourselves)
+ // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are
+ // sure to
+ // get it)
+ final int descendantFocusability = getDescendantFocusability(); // save
+ setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
+ requestFocus();
+ setDescendantFocusability(descendantFocusability); // restore
+ }
+ return true;
+ }
+
+ private boolean isOrthoMove(float moveX, float moveY) {
+ return mHorizontal && Math.abs(moveY) > Math.abs(moveX)
+ || !mHorizontal && Math.abs(moveX) > Math.abs(moveY);
+ }
+
+ /**
+ * @return whether the descendant of this scroll view is scrolled off
+ * screen.
+ */
+ private boolean isOffScreen(View descendant) {
+ if (mHorizontal) {
+ return !isWithinDeltaOfScreen(descendant, getWidth(), 0);
+ } else {
+ return !isWithinDeltaOfScreen(descendant, 0, getHeight());
+ }
+ }
+
+ /**
+ * @return whether the descendant of this scroll view is within delta
+ * pixels of being on the screen.
+ */
+ private boolean isWithinDeltaOfScreen(View descendant, int delta, int height) {
+ descendant.getDrawingRect(mTempRect);
+ offsetDescendantRectToMyCoords(descendant, mTempRect);
+ if (mHorizontal) {
+ return (mTempRect.right + delta) >= getScrollX()
+ && (mTempRect.left - delta) <= (getScrollX() + height);
+ } else {
+ return (mTempRect.bottom + delta) >= getScrollY()
+ && (mTempRect.top - delta) <= (getScrollY() + height);
+ }
+ }
+
+ /**
+ * Smooth scroll by a Y delta
+ *
+ * @param delta the number of pixels to scroll by on the Y axis
+ */
+ private void doScrollY(int delta) {
+ if (delta != 0) {
+ if (mSmoothScrollingEnabled) {
+ if (mHorizontal) {
+ smoothScrollBy(0, delta);
+ } else {
+ smoothScrollBy(delta, 0);
+ }
+ } else {
+ if (mHorizontal) {
+ scrollBy(0, delta);
+ } else {
+ scrollBy(delta, 0);
+ }
+ }
+ }
+ }
+
+ /**
+ * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
+ *
+ * @param dx the number of pixels to scroll by on the X axis
+ * @param dy the number of pixels to scroll by on the Y axis
+ */
+ public final void smoothScrollBy(int dx, int dy) {
+ if (getChildCount() == 0) {
+ // Nothing to do.
+ return;
+ }
+ long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
+ if (duration > ANIMATED_SCROLL_GAP) {
+ if (mHorizontal) {
+ final int width = getWidth() - getPaddingRight() - getPaddingLeft();
+ final int right = getChildAt(0).getWidth();
+ final int maxX = Math.max(0, right - width);
+ final int scrollX = getScrollX();
+ dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX;
+ mScroller.startScroll(scrollX, getScrollY(), dx, 0);
+ } else {
+ final int height = getHeight() - getPaddingBottom() - getPaddingTop();
+ final int bottom = getChildAt(0).getHeight();
+ final int maxY = Math.max(0, bottom - height);
+ final int scrollY = getScrollY();
+ dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY;
+ mScroller.startScroll(getScrollX(), scrollY, 0, dy);
+ }
+ invalidate();
+ } else {
+ if (!mScroller.isFinished()) {
+ mScroller.abortAnimation();
+ if (mFlingStrictSpan != null) {
+ mFlingStrictSpan.finish();
+ mFlingStrictSpan = null;
+ }
+ }
+ scrollBy(dx, dy);
+ }
+ mLastScroll = AnimationUtils.currentAnimationTimeMillis();
+ }
+
+ /**
+ * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
+ *
+ * @param x the position where to scroll on the X axis
+ * @param y the position where to scroll on the Y axis
+ */
+ public final void smoothScrollTo(int x, int y) {
+ smoothScrollBy(x - getScrollX(), y - getScrollY());
+ }
+
+ /**
+ * <p>
+ * The scroll range of a scroll view is the overall height of all of its
+ * children.
+ * </p>
+ */
+ @Override
+ protected int computeVerticalScrollRange() {
+ if (mHorizontal) {
+ return super.computeVerticalScrollRange();
+ }
+ final int count = getChildCount();
+ final int contentHeight = getHeight() - getPaddingBottom() - getPaddingTop();
+ if (count == 0) {
+ return contentHeight;
+ }
+
+ int scrollRange = getChildAt(0).getBottom();
+ final int scrollY = getScrollY();
+ final int overscrollBottom = Math.max(0, scrollRange - contentHeight);
+ if (scrollY < 0) {
+ scrollRange -= scrollY;
+ } else if (scrollY > overscrollBottom) {
+ scrollRange += scrollY - overscrollBottom;
+ }
+
+ return scrollRange;
+ }
+
+ /**
+ * <p>
+ * The scroll range of a scroll view is the overall height of all of its
+ * children.
+ * </p>
+ */
+ @Override
+ protected int computeHorizontalScrollRange() {
+ if (!mHorizontal) {
+ return super.computeHorizontalScrollRange();
+ }
+ final int count = getChildCount();
+ final int contentWidth = getWidth() - getPaddingRight() - getPaddingLeft();
+ if (count == 0) {
+ return contentWidth;
+ }
+
+ int scrollRange = getChildAt(0).getRight();
+ final int scrollX = getScrollX();
+ final int overscrollBottom = Math.max(0, scrollRange - contentWidth);
+ if (scrollX < 0) {
+ scrollRange -= scrollX;
+ } else if (scrollX > overscrollBottom) {
+ scrollRange += scrollX - overscrollBottom;
+ }
+
+ return scrollRange;
+ }
+
+ @Override
+ protected int computeVerticalScrollOffset() {
+ return Math.max(0, super.computeVerticalScrollOffset());
+ }
+
+ @Override
+ protected int computeHorizontalScrollOffset() {
+ return Math.max(0, super.computeHorizontalScrollOffset());
+ }
+
+ @Override
+ protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
+ ViewGroup.LayoutParams lp = child.getLayoutParams();
+
+ int childWidthMeasureSpec;
+ int childHeightMeasureSpec;
+
+ if (mHorizontal) {
+ childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, getPaddingTop()
+ + getPaddingBottom(), lp.height);
+
+ childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ } else {
+ childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft()
+ + getPaddingRight(), lp.width);
+
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ }
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+ @Override
+ protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
+ int parentHeightMeasureSpec, int heightUsed) {
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+ int childWidthMeasureSpec;
+ int childHeightMeasureSpec;
+ if (mHorizontal) {
+ childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
+ getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin
+ + heightUsed, lp.height);
+ childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED);
+ } else {
+ childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
+ getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
+ + widthUsed, lp.width);
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
+ }
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+ @Override
+ public void computeScroll() {
+ if (mScroller.computeScrollOffset()) {
+ // This is called at drawing time by ViewGroup. We don't want to
+ // re-show the scrollbars at this point, which scrollTo will do,
+ // so we replicate most of scrollTo here.
+ //
+ // It's a little odd to call onScrollChanged from inside the drawing.
+ //
+ // It is, except when you remember that computeScroll() is used to
+ // animate scrolling. So unless we want to defer the onScrollChanged()
+ // until the end of the animated scrolling, we don't really have a
+ // choice here.
+ //
+ // I agree. The alternative, which I think would be worse, is to post
+ // something and tell the subclasses later. This is bad because there
+ // will be a window where getScrollX()/Y is different from what the app
+ // thinks it is.
+ //
+ int oldX = getScrollX();
+ int oldY = getScrollY();
+ int x = mScroller.getCurrX();
+ int y = mScroller.getCurrY();
+
+ if (oldX != x || oldY != y) {
+ if (mHorizontal) {
+ overScrollBy(x - oldX, y - oldY, oldX, oldY, getScrollRange(), 0,
+ mOverflingDistance, 0, false);
+ } else {
+ overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, getScrollRange(),
+ 0, mOverflingDistance, false);
+ }
+ onScrollChanged(getScrollX(), getScrollY(), oldX, oldY);
+ }
+ awakenScrollBars();
+
+ // Keep on drawing until the animation has finished.
+ postInvalidate();
+ } else {
+ if (mFlingStrictSpan != null) {
+ mFlingStrictSpan.finish();
+ mFlingStrictSpan = null;
+ }
+ }
+ }
+
+ /**
+ * Scrolls the view to the given child.
+ *
+ * @param child the View to scroll to
+ */
+ private void scrollToChild(View child) {
+ child.getDrawingRect(mTempRect);
+
+ /* Offset from child's local coordinates to ScrollView coordinates */
+ offsetDescendantRectToMyCoords(child, mTempRect);
+ scrollToChildRect(mTempRect, true);
+ }
+
+ /**
+ * If rect is off screen, scroll just enough to get it (or at least the
+ * first screen size chunk of it) on screen.
+ *
+ * @param rect The rectangle.
+ * @param immediate True to scroll immediately without animation
+ * @return true if scrolling was performed
+ */
+ private boolean scrollToChildRect(Rect rect, boolean immediate) {
+ final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
+ final boolean scroll = delta != 0;
+ if (scroll) {
+ if (immediate) {
+ if (mHorizontal) {
+ scrollBy(delta, 0);
+ } else {
+ scrollBy(0, delta);
+ }
+ } else {
+ if (mHorizontal) {
+ smoothScrollBy(delta, 0);
+ } else {
+ smoothScrollBy(0, delta);
+ }
+ }
+ }
+ return scroll;
+ }
+
+ /**
+ * Compute the amount to scroll in the Y direction in order to get
+ * a rectangle completely on the screen (or, if taller than the screen,
+ * at least the first screen size chunk of it).
+ *
+ * @param rect The rect.
+ * @return The scroll delta.
+ */
+ protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
+ if (mHorizontal) {
+ return computeScrollDeltaToGetChildRectOnScreenHorizontal(rect);
+ } else {
+ return computeScrollDeltaToGetChildRectOnScreenVertical(rect);
+ }
+ }
+
+ private int computeScrollDeltaToGetChildRectOnScreenVertical(Rect rect) {
+ if (getChildCount() == 0) return 0;
+
+ int height = getHeight();
+ int screenTop = getScrollY();
+ int screenBottom = screenTop + height;
+
+ int fadingEdge = getVerticalFadingEdgeLength();
+
+ // leave room for top fading edge as long as rect isn't at very top
+ if (rect.top > 0) {
+ screenTop += fadingEdge;
+ }
+
+ // leave room for bottom fading edge as long as rect isn't at very bottom
+ if (rect.bottom < getChildAt(0).getHeight()) {
+ screenBottom -= fadingEdge;
+ }
+
+ int scrollYDelta = 0;
+
+ if (rect.bottom > screenBottom && rect.top > screenTop) {
+ // need to move down to get it in view: move down just enough so
+ // that the entire rectangle is in view (or at least the first
+ // screen size chunk).
+
+ if (rect.height() > height) {
+ // just enough to get screen size chunk on
+ scrollYDelta += (rect.top - screenTop);
+ } else {
+ // get entire rect at bottom of screen
+ scrollYDelta += (rect.bottom - screenBottom);
+ }
+
+ // make sure we aren't scrolling beyond the end of our content
+ int bottom = getChildAt(0).getBottom();
+ int distanceToBottom = bottom - screenBottom;
+ scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
+
+ } else if (rect.top < screenTop && rect.bottom < screenBottom) {
+ // need to move up to get it in view: move up just enough so that
+ // entire rectangle is in view (or at least the first screen
+ // size chunk of it).
+
+ if (rect.height() > height) {
+ // screen size chunk
+ scrollYDelta -= (screenBottom - rect.bottom);
+ } else {
+ // entire rect at top
+ scrollYDelta -= (screenTop - rect.top);
+ }
+
+ // make sure we aren't scrolling any further than the top our content
+ scrollYDelta = Math.max(scrollYDelta, -getScrollY());
+ }
+ return scrollYDelta;
+ }
+
+ private int computeScrollDeltaToGetChildRectOnScreenHorizontal(Rect rect) {
+ if (getChildCount() == 0) return 0;
+
+ int width = getWidth();
+ int screenLeft = getScrollX();
+ int screenRight = screenLeft + width;
+
+ int fadingEdge = getHorizontalFadingEdgeLength();
+
+ // leave room for left fading edge as long as rect isn't at very left
+ if (rect.left > 0) {
+ screenLeft += fadingEdge;
+ }
+
+ // leave room for right fading edge as long as rect isn't at very right
+ if (rect.right < getChildAt(0).getWidth()) {
+ screenRight -= fadingEdge;
+ }
+
+ int scrollXDelta = 0;
+
+ if (rect.right > screenRight && rect.left > screenLeft) {
+ // need to move right to get it in view: move right just enough so
+ // that the entire rectangle is in view (or at least the first
+ // screen size chunk).
+
+ if (rect.width() > width) {
+ // just enough to get screen size chunk on
+ scrollXDelta += (rect.left - screenLeft);
+ } else {
+ // get entire rect at right of screen
+ scrollXDelta += (rect.right - screenRight);
+ }
+
+ // make sure we aren't scrolling beyond the end of our content
+ int right = getChildAt(0).getRight();
+ int distanceToRight = right - screenRight;
+ scrollXDelta = Math.min(scrollXDelta, distanceToRight);
+
+ } else if (rect.left < screenLeft && rect.right < screenRight) {
+ // need to move right to get it in view: move right just enough so that
+ // entire rectangle is in view (or at least the first screen
+ // size chunk of it).
+
+ if (rect.width() > width) {
+ // screen size chunk
+ scrollXDelta -= (screenRight - rect.right);
+ } else {
+ // entire rect at left
+ scrollXDelta -= (screenLeft - rect.left);
+ }
+
+ // make sure we aren't scrolling any further than the left our content
+ scrollXDelta = Math.max(scrollXDelta, -getScrollX());
+ }
+ return scrollXDelta;
+ }
+
+
+ @Override
+ public void requestChildFocus(View child, View focused) {
+ if (!mIsLayoutDirty) {
+ scrollToChild(focused);
+ } else {
+ // The child may not be laid out yet, we can't compute the scroll yet
+ mChildToScrollTo = focused;
+ }
+ super.requestChildFocus(child, focused);
+ }
+
+
+ /**
+ * When looking for focus in children of a scroll view, need to be a little
+ * more careful not to give focus to something that is scrolled off screen.
+ *
+ * This is more expensive than the default {@link android.view.ViewGroup}
+ * implementation, otherwise this behavior might have been made the default.
+ */
+ @Override
+ protected boolean onRequestFocusInDescendants(int direction,
+ Rect previouslyFocusedRect) {
+
+ // convert from forward / backward notation to up / down / left / right
+ // (ugh).
+ if (mHorizontal) {
+ if (direction == View.FOCUS_FORWARD) {
+ direction = View.FOCUS_RIGHT;
+ } else if (direction == View.FOCUS_BACKWARD) {
+ direction = View.FOCUS_LEFT;
+ }
+ } else {
+ if (direction == View.FOCUS_FORWARD) {
+ direction = View.FOCUS_DOWN;
+ } else if (direction == View.FOCUS_BACKWARD) {
+ direction = View.FOCUS_UP;
+ }
+ }
+
+ final View nextFocus = previouslyFocusedRect == null ?
+ FocusFinder.getInstance().findNextFocus(this, null, direction) :
+ FocusFinder.getInstance().findNextFocusFromRect(this,
+ previouslyFocusedRect, direction);
+
+ if (nextFocus == null) {
+ return false;
+ }
+
+ if (isOffScreen(nextFocus)) {
+ return false;
+ }
+
+ return nextFocus.requestFocus(direction, previouslyFocusedRect);
+ }
+
+ @Override
+ public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
+ boolean immediate) {
+ // offset into coordinate space of this scroll view
+ rectangle.offset(child.getLeft() - child.getScrollX(),
+ child.getTop() - child.getScrollY());
+
+ return scrollToChildRect(rectangle, immediate);
+ }
+
+ @Override
+ public void requestLayout() {
+ mIsLayoutDirty = true;
+ super.requestLayout();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ if (mScrollStrictSpan != null) {
+ mScrollStrictSpan.finish();
+ mScrollStrictSpan = null;
+ }
+ if (mFlingStrictSpan != null) {
+ mFlingStrictSpan.finish();
+ mFlingStrictSpan = null;
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+ mIsLayoutDirty = false;
+ // Give a child focus if it needs it
+ if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
+ scrollToChild(mChildToScrollTo);
+ }
+ mChildToScrollTo = null;
+
+ // Calling this with the present values causes it to re-clam them
+ scrollTo(getScrollX(), getScrollY());
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ View currentFocused = findFocus();
+ if (null == currentFocused || this == currentFocused)
+ return;
+
+ // If the currently-focused view was visible on the screen when the
+ // screen was at the old height, then scroll the screen to make that
+ // view visible with the new screen height.
+ if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) {
+ currentFocused.getDrawingRect(mTempRect);
+ offsetDescendantRectToMyCoords(currentFocused, mTempRect);
+ int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
+ doScrollY(scrollDelta);
+ }
+ }
+
+ /**
+ * Return true if child is an descendant of parent, (or equal to the parent).
+ */
+ private boolean isViewDescendantOf(View child, View parent) {
+ if (child == parent) {
+ return true;
+ }
+
+ final ViewParent theParent = child.getParent();
+ return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
+ }
+
+ /**
+ * Fling the scroll view
+ *
+ * @param velocityY The initial velocity in the Y direction. Positive
+ * numbers mean that the finger/cursor is moving down the screen,
+ * which means we want to scroll towards the top.
+ */
+ public void fling(int velocityY) {
+ if (getChildCount() > 0) {
+ if (mHorizontal) {
+ int width = getWidth() - getPaddingRight() - getPaddingLeft();
+ int right = getChildAt(0).getWidth();
+
+ mScroller.fling(getScrollX(), getScrollY(), velocityY, 0,
+ 0, Math.max(0, right - width), 0, 0, width/2, 0);
+ } else {
+ int height = getHeight() - getPaddingBottom() - getPaddingTop();
+ int bottom = getChildAt(0).getHeight();
+
+ mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0,
+ Math.max(0, bottom - height), 0, height/2);
+ }
+ if (mFlingStrictSpan == null) {
+ //mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling");
+ mFlingStrictSpan = NO_OP_SPAN;
+ }
+
+ invalidate();
+ }
+ }
+
+ private void endDrag() {
+ mIsBeingDragged = false;
+ mIsOrthoDragged = false;
+ mDownView = null;
+ recycleVelocityTracker();
+ if (mScrollStrictSpan != null) {
+ mScrollStrictSpan.finish();
+ mScrollStrictSpan = null;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This version also clamps the scrolling to the bounds of our child.
+ */
+ @Override
+ public void scrollTo(int x, int y) {
+ // we rely on the fact the View.scrollBy calls scrollTo.
+ if (getChildCount() > 0) {
+ View child = getChildAt(0);
+ x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth());
+ y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight());
+ if (x != getScrollX() || y != getScrollY()) {
+ super.scrollTo(x, y);
+ }
+ }
+ }
+
+ private int clamp(int n, int my, int child) {
+ if (my >= child || n < 0) {
+ /* my >= child is this case:
+ * |--------------- me ---------------|
+ * |------ child ------|
+ * or
+ * |--------------- me ---------------|
+ * |------ child ------|
+ * or
+ * |--------------- me ---------------|
+ * |------ child ------|
+ *
+ * n < 0 is this case:
+ * |------ me ------|
+ * |-------- child --------|
+ * |-- getScrollX() --|
+ */
+ return 0;
+ }
+ if ((my+n) > child) {
+ /* this case:
+ * |------ me ------|
+ * |------ child ------|
+ * |-- getScrollX() --|
+ */
+ return child-my;
+ }
+ return n;
+ }
+
+}
diff --git a/src/com/android/browser/view/SnapshotGridView.java b/src/com/android/browser/view/SnapshotGridView.java
new file mode 100644
index 0000000..ab12060
--- /dev/null
+++ b/src/com/android/browser/view/SnapshotGridView.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2011 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.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.GridView;
+
+public class SnapshotGridView extends GridView {
+
+ private static final int MAX_COLUMNS = 5;
+
+ private int mColWidth;
+
+ public SnapshotGridView(Context context) {
+ super(context);
+ }
+
+ public SnapshotGridView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public SnapshotGridView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ if (widthSize > 0 && mColWidth > 0) {
+ int numCols = widthSize / mColWidth;
+ widthSize = Math.min(
+ Math.min(numCols, MAX_COLUMNS) * mColWidth,
+ widthSize);
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ public void setColumnWidth(int columnWidth) {
+ mColWidth = columnWidth;
+ super.setColumnWidth(columnWidth);
+ }
+}
diff --git a/src/com/android/browser/view/StopProgressView.java b/src/com/android/browser/view/StopProgressView.java
new file mode 100644
index 0000000..3df099d
--- /dev/null
+++ b/src/com/android/browser/view/StopProgressView.java
@@ -0,0 +1,98 @@
+
+package com.android.browser.view;
+
+import com.android.browser.R;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.ProgressBar;
+
+
+public class StopProgressView extends ProgressBar {
+
+ Drawable mOverlayDrawable;
+ Drawable mProgressDrawable;
+ int mWidth;
+ int mHeight;
+
+ /**
+ * @param context
+ * @param attrs
+ * @param defStyle
+ * @param styleRes
+ */
+ public StopProgressView(Context context, AttributeSet attrs, int defStyle, int styleRes) {
+ super(context, attrs, defStyle);
+ init(attrs);
+ }
+
+ /**
+ * @param context
+ * @param attrs
+ * @param defStyle
+ */
+ public StopProgressView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(attrs);
+ }
+
+ /**
+ * @param context
+ * @param attrs
+ */
+ public StopProgressView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(attrs);
+ }
+
+ /**
+ * @param context
+ */
+ public StopProgressView(Context context) {
+ super(context);
+ init(null);
+ }
+
+ private void init(AttributeSet attrs) {
+ mProgressDrawable = getIndeterminateDrawable();
+ setImageDrawable(getContext().getResources()
+ .getDrawable(R.drawable.ic_stop_holo_dark));
+ }
+
+ public void hideProgress() {
+ setIndeterminateDrawable(null);
+ }
+
+ public void showProgress() {
+ setIndeterminateDrawable(mProgressDrawable);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ mWidth = (right - left) * 2 / 3;
+ mHeight = (bottom - top) * 2 / 3;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mOverlayDrawable != null) {
+ int l = (getWidth() - mWidth) / 2;
+ int t = (getHeight() - mHeight) / 2;
+ mOverlayDrawable.setBounds(l, t, l + mWidth, t + mHeight);
+ mOverlayDrawable.draw(canvas);
+ }
+ }
+
+ public Drawable getDrawable() {
+ return mOverlayDrawable;
+ }
+
+ public void setImageDrawable(Drawable d) {
+ mOverlayDrawable = d;
+ }
+
+}
diff --git a/src/com/android/browser/widget/BookmarkThumbnailWidgetProvider.java b/src/com/android/browser/widget/BookmarkThumbnailWidgetProvider.java
new file mode 100644
index 0000000..f3d2675
--- /dev/null
+++ b/src/com/android/browser/widget/BookmarkThumbnailWidgetProvider.java
@@ -0,0 +1,118 @@
+/*
+ * 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.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.widget.RemoteViews;
+
+import com.android.browser.BrowserActivity;
+import com.android.browser.R;
+
+/**
+ * Widget that shows a preview of the user's bookmarks.
+ */
+public class BookmarkThumbnailWidgetProvider extends AppWidgetProvider {
+ public static final String ACTION_BOOKMARK_APPWIDGET_UPDATE =
+ "com.android.browser.BOOKMARK_APPWIDGET_UPDATE";
+
+ @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 onDeleted(Context context, int[] appWidgetIds) {
+ super.onDeleted(context, appWidgetIds);
+ for (int widgetId : appWidgetIds) {
+ BookmarkThumbnailWidgetService.deleteWidgetState(context, widgetId);
+ }
+ removeOrphanedFiles(context);
+ }
+
+ @Override
+ public void onDisabled(Context context) {
+ super.onDisabled(context);
+ removeOrphanedFiles(context);
+ }
+
+ /**
+ * Checks for any state files that may have not received onDeleted
+ */
+ void removeOrphanedFiles(Context context) {
+ AppWidgetManager wm = AppWidgetManager.getInstance(context);
+ int[] ids = wm.getAppWidgetIds(getComponentName(context));
+ BookmarkThumbnailWidgetService.removeOrphanedStates(context, ids);
+ }
+
+ private void performUpdate(Context context,
+ AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ PendingIntent launchBrowser = PendingIntent.getActivity(context, 0,
+ new Intent(BrowserActivity.ACTION_SHOW_BROWSER, null, context,
+ BrowserActivity.class),
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ for (int appWidgetId : appWidgetIds) {
+ Intent updateIntent = new Intent(context, BookmarkThumbnailWidgetService.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.bookmarkthumbnailwidget);
+ views.setOnClickPendingIntent(R.id.app_shortcut, launchBrowser);
+ views.setRemoteAdapter(R.id.bookmarks_list, updateIntent);
+ appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.bookmarks_list);
+ Intent ic = new Intent(context, BookmarkWidgetProxy.class);
+ views.setPendingIntentTemplate(R.id.bookmarks_list,
+ PendingIntent.getBroadcast(context, 0, ic,
+ PendingIntent.FLAG_UPDATE_CURRENT));
+ appWidgetManager.updateAppWidget(appWidgetId, views);
+ }
+ }
+
+ /**
+ * Build {@link ComponentName} describing this specific
+ * {@link AppWidgetProvider}
+ */
+ static ComponentName getComponentName(Context context) {
+ return new ComponentName(context, BookmarkThumbnailWidgetProvider.class);
+ }
+
+ public static void refreshWidgets(Context context) {
+ context.sendBroadcast(new Intent(
+ BookmarkThumbnailWidgetProvider.ACTION_BOOKMARK_APPWIDGET_UPDATE,
+ null, context, BookmarkThumbnailWidgetProvider.class));
+ }
+
+}
diff --git a/src/com/android/browser/widget/BookmarkThumbnailWidgetService.java b/src/com/android/browser/widget/BookmarkThumbnailWidgetService.java
new file mode 100644
index 0000000..6f5e3b2
--- /dev/null
+++ b/src/com/android/browser/widget/BookmarkThumbnailWidgetService.java
@@ -0,0 +1,378 @@
+/*
+ * 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.appwidget.AppWidgetManager;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.MergeCursor;
+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.text.TextUtils;
+import android.util.Log;
+import android.widget.RemoteViews;
+import android.widget.RemoteViewsService;
+
+import com.android.browser.BrowserActivity;
+import com.android.browser.R;
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.BrowserContract.Bookmarks;
+import com.android.browser.provider.BrowserProvider2;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.util.HashSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class BookmarkThumbnailWidgetService extends RemoteViewsService {
+
+ static final String TAG = "BookmarkThumbnailWidgetService";
+ static final String ACTION_CHANGE_FOLDER = "com.android.browser.widget.CHANGE_FOLDER";
+ static final String STATE_CURRENT_FOLDER = "current_folder";
+ static final String STATE_ROOT_FOLDER = "root_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.POSITION, /* needed for order by */
+ BrowserContract.Bookmarks.THUMBNAIL,
+ BrowserContract.Bookmarks.PARENT};
+ private static final int BOOKMARK_INDEX_ID = 0;
+ private static final int BOOKMARK_INDEX_TITLE = 1;
+ private static final int BOOKMARK_INDEX_URL = 2;
+ private static final int BOOKMARK_INDEX_FAVICON = 3;
+ private static final int BOOKMARK_INDEX_IS_FOLDER = 4;
+ private static final int BOOKMARK_INDEX_THUMBNAIL = 6;
+ private static final int BOOKMARK_INDEX_PARENT_ID = 7;
+
+ @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;
+ }
+ return new BookmarkFactory(getApplicationContext(), widgetId);
+ }
+
+ static SharedPreferences getWidgetState(Context context, int widgetId) {
+ return context.getSharedPreferences(
+ String.format("widgetState-%d", widgetId),
+ Context.MODE_PRIVATE);
+ }
+
+ static private File mPreferencesDir;
+ static File getPreferencesDir(Context context) {
+ if (mPreferencesDir == null) {
+ mPreferencesDir = new File(context.getApplicationInfo().dataDir, "shared_prefs");
+ }
+ return mPreferencesDir;
+ }
+ static File makeFilename(File base, String name) {
+ if (name.indexOf(File.separatorChar) < 0) {
+ return new File(base, name);
+ }
+ throw new IllegalArgumentException(
+ "File " + name + " contains a path separator");
+ }
+ static File getSharedPrefsFile(Context context, String name) {
+ return makeFilename(getPreferencesDir(context), name + ".xml");
+ }
+
+ static void deleteWidgetState(Context context, int widgetId) {
+ File file = getSharedPrefsFile(context,
+ String.format("widgetState-%d", widgetId));
+ if (file.exists()) {
+ if (!file.delete()) {
+ file.deleteOnExit();
+ }
+ }
+ }
+
+ static void changeFolder(Context context, Intent intent) {
+ int wid = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
+ long fid = intent.getLongExtra(Bookmarks._ID, -1);
+ if (wid >= 0 && fid >= 0) {
+ SharedPreferences prefs = getWidgetState(context, wid);
+ prefs.edit().putLong(STATE_CURRENT_FOLDER, fid).commit();
+ AppWidgetManager.getInstance(context)
+ .notifyAppWidgetViewDataChanged(wid, R.id.bookmarks_list);
+ }
+ }
+
+ static void setupWidgetState(Context context, int widgetId, long rootFolder) {
+ SharedPreferences pref = getWidgetState(context, widgetId);
+ pref.edit()
+ .putLong(STATE_CURRENT_FOLDER, rootFolder)
+ .putLong(STATE_ROOT_FOLDER, rootFolder)
+ .apply();
+ }
+
+ /**
+ * Checks for any state files that may have not received onDeleted
+ */
+ static void removeOrphanedStates(Context context, int[] widgetIds) {
+ File prefsDirectory = getSharedPrefsFile(context, "null").getParentFile();
+ File[] widgetStates = prefsDirectory.listFiles(new StateFilter(widgetIds));
+ if (widgetStates != null) {
+ for (File f : widgetStates) {
+ Log.w(TAG, "Found orphaned state: " + f.getName());
+ if (!f.delete()) {
+ f.deleteOnExit();
+ }
+ }
+ }
+ }
+
+ static class StateFilter implements FilenameFilter {
+
+ static final Pattern sStatePattern = Pattern.compile("widgetState-(\\d+)\\.xml");
+ HashSet<Integer> mWidgetIds;
+
+ StateFilter(int[] ids) {
+ mWidgetIds = new HashSet<Integer>();
+ for (int id : ids) {
+ mWidgetIds.add(id);
+ }
+ }
+
+ @Override
+ public boolean accept(File dir, String filename) {
+ Matcher m = sStatePattern.matcher(filename);
+ if (m.matches()) {
+ int id = Integer.parseInt(m.group(1));
+ if (!mWidgetIds.contains(id)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ }
+
+ static class BookmarkFactory implements RemoteViewsService.RemoteViewsFactory {
+ private Cursor mBookmarks;
+ private Context mContext;
+ private int mWidgetId;
+ private long mCurrentFolder = -1;
+ private long mRootFolder = -1;
+ private SharedPreferences mPreferences = null;
+
+ public BookmarkFactory(Context context, int widgetId) {
+ mContext = context.getApplicationContext();
+ mWidgetId = widgetId;
+ }
+
+ void syncState() {
+ if (mPreferences == null) {
+ mPreferences = getWidgetState(mContext, mWidgetId);
+ }
+ long currentFolder = mPreferences.getLong(STATE_CURRENT_FOLDER, -1);
+ mRootFolder = mPreferences.getLong(STATE_ROOT_FOLDER, -1);
+ if (currentFolder != mCurrentFolder) {
+ resetBookmarks();
+ mCurrentFolder = currentFolder;
+ }
+ }
+
+ void saveState() {
+ if (mPreferences == null) {
+ mPreferences = getWidgetState(mContext, mWidgetId);
+ }
+ mPreferences.edit()
+ .putLong(STATE_CURRENT_FOLDER, mCurrentFolder)
+ .putLong(STATE_ROOT_FOLDER, mRootFolder)
+ .commit();
+ }
+
+ @Override
+ public int getCount() {
+ if (mBookmarks == null)
+ return 0;
+ return mBookmarks.getCount();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public RemoteViews getLoadingView() {
+ return new RemoteViews(
+ mContext.getPackageName(), R.layout.bookmarkthumbnailwidget_item);
+ }
+
+ @Override
+ public RemoteViews getViewAt(int position) {
+ if (!mBookmarks.moveToPosition(position)) {
+ return null;
+ }
+
+ long id = mBookmarks.getLong(BOOKMARK_INDEX_ID);
+ String title = mBookmarks.getString(BOOKMARK_INDEX_TITLE);
+ String url = mBookmarks.getString(BOOKMARK_INDEX_URL);
+ boolean isFolder = mBookmarks.getInt(BOOKMARK_INDEX_IS_FOLDER) != 0;
+
+ RemoteViews views;
+ // Two layouts are needed because of b/5387153
+ if (isFolder) {
+ views = new RemoteViews(mContext.getPackageName(),
+ R.layout.bookmarkthumbnailwidget_item_folder);
+ } else {
+ views = new RemoteViews(mContext.getPackageName(),
+ R.layout.bookmarkthumbnailwidget_item);
+ }
+ // Set the title of the bookmark. Use the url as a backup.
+ String displayTitle = title;
+ if (TextUtils.isEmpty(displayTitle)) {
+ // The browser always requires a title for bookmarks, but jic...
+ displayTitle = url;
+ }
+ views.setTextViewText(R.id.label, displayTitle);
+ if (isFolder) {
+ if (id == mCurrentFolder) {
+ id = mBookmarks.getLong(BOOKMARK_INDEX_PARENT_ID);
+ views.setImageViewResource(R.id.thumb,
+ R.drawable.thumb_bookmark_widget_folder_back_holo);
+ } else {
+ views.setImageViewResource(R.id.thumb,
+ R.drawable.thumb_bookmark_widget_folder_holo);
+ }
+ views.setImageViewResource(R.id.favicon,
+ R.drawable.ic_bookmark_widget_bookmark_holo_dark);
+ // SWE_TODO : Fix Me
+ //views.setDrawableParameters(R.id.thumb, true, 0, -1, null, -1);
+ } else {
+ // RemoteViews require a valid bitmap config
+ Options options = new Options();
+ options.inPreferredConfig = Config.ARGB_8888;
+ Bitmap thumbnail = null, favicon = null;
+ byte[] blob = mBookmarks.getBlob(BOOKMARK_INDEX_THUMBNAIL);
+ // SWE_TODO : Fix Me
+ //views.setDrawableParameters(R.id.thumb, true, 255, -1, null, -1);
+ if (blob != null && blob.length > 0) {
+ thumbnail = BitmapFactory.decodeByteArray(
+ blob, 0, blob.length, options);
+ views.setImageViewBitmap(R.id.thumb, thumbnail);
+ } else {
+ views.setImageViewResource(R.id.thumb,
+ R.drawable.browser_thumbnail);
+ }
+ blob = mBookmarks.getBlob(BOOKMARK_INDEX_FAVICON);
+ if (blob != null && blob.length > 0) {
+ favicon = BitmapFactory.decodeByteArray(
+ blob, 0, blob.length, options);
+ views.setImageViewBitmap(R.id.favicon, favicon);
+ } else {
+ views.setImageViewResource(R.id.favicon,
+ R.drawable.app_web_browser_sm);
+ }
+ }
+ Intent fillin;
+ if (isFolder) {
+ fillin = new Intent(ACTION_CHANGE_FOLDER)
+ .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId)
+ .putExtra(Bookmarks._ID, id);
+ } else {
+ if (!TextUtils.isEmpty(url)) {
+ fillin = new Intent(Intent.ACTION_VIEW)
+ .addCategory(Intent.CATEGORY_BROWSABLE)
+ .setData(Uri.parse(url));
+ } else {
+ fillin = new Intent(BrowserActivity.ACTION_SHOW_BROWSER);
+ }
+ }
+ views.setOnClickFillInIntent(R.id.list_item, fillin);
+ return views;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 2;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ @Override
+ public void onCreate() {
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mBookmarks != null) {
+ mBookmarks.close();
+ mBookmarks = null;
+ }
+ deleteWidgetState(mContext, mWidgetId);
+ }
+
+ @Override
+ public void onDataSetChanged() {
+ long token = Binder.clearCallingIdentity();
+ syncState();
+ if (mRootFolder < 0 || mCurrentFolder < 0) {
+ // This shouldn't happen, but JIC default to the local account
+ mRootFolder = BrowserProvider2.FIXED_ID_ROOT;
+ mCurrentFolder = mRootFolder;
+ saveState();
+ }
+ loadBookmarks();
+ Binder.restoreCallingIdentity(token);
+ }
+
+ private void resetBookmarks() {
+ if (mBookmarks != null) {
+ mBookmarks.close();
+ mBookmarks = null;
+ }
+ }
+
+ void loadBookmarks() {
+ resetBookmarks();
+
+ Uri uri = ContentUris.withAppendedId(
+ BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER,
+ mCurrentFolder);
+ mBookmarks = mContext.getContentResolver().query(uri, PROJECTION,
+ null, null, null);
+ if (mCurrentFolder != mRootFolder) {
+ uri = ContentUris.withAppendedId(
+ BrowserContract.Bookmarks.CONTENT_URI,
+ mCurrentFolder);
+ Cursor c = mContext.getContentResolver().query(uri, PROJECTION,
+ null, null, null);
+ mBookmarks = new MergeCursor(new Cursor[] { c, mBookmarks });
+ }
+ }
+ }
+
+}
diff --git a/src/com/android/browser/widget/BookmarkWidgetConfigure.java b/src/com/android/browser/widget/BookmarkWidgetConfigure.java
new file mode 100644
index 0000000..2dee989
--- /dev/null
+++ b/src/com/android/browser/widget/BookmarkWidgetConfigure.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2011 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.ListActivity;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.appwidget.AppWidgetManager;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+import com.android.browser.R;
+import com.android.browser.AddBookmarkPage.BookmarkAccount;
+import com.android.browser.platformsupport.BrowserContract.Accounts;
+import com.android.browser.provider.BrowserProvider2;
+
+public class BookmarkWidgetConfigure extends ListActivity
+ implements OnClickListener, LoaderCallbacks<Cursor> {
+
+ static final int LOADER_ACCOUNTS = 1;
+
+ private ArrayAdapter<BookmarkAccount> mAccountAdapter;
+ private int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setResult(RESULT_CANCELED);
+ setVisible(false);
+ setContentView(R.layout.widget_account_selection);
+ findViewById(R.id.cancel).setOnClickListener(this);
+ mAccountAdapter = new ArrayAdapter<BookmarkAccount>(this,
+ android.R.layout.simple_list_item_1);
+ setListAdapter(mAccountAdapter);
+ Intent intent = getIntent();
+ Bundle extras = intent.getExtras();
+ if (extras != null) {
+ mAppWidgetId = extras.getInt(
+ AppWidgetManager.EXTRA_APPWIDGET_ID,
+ AppWidgetManager.INVALID_APPWIDGET_ID);
+ }
+ if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
+ finish();
+ } else {
+ getLoaderManager().initLoader(LOADER_ACCOUNTS, null, this);
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+
+ @Override
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ BookmarkAccount account = mAccountAdapter.getItem(position);
+ pickAccount(account.rootFolderId);
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ return new AccountsLoader(this);
+ }
+
+ void pickAccount(long rootId) {
+ BookmarkThumbnailWidgetService.setupWidgetState(this, mAppWidgetId, rootId);
+ Intent result = new Intent();
+ result.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
+ setResult(RESULT_OK, result);
+ finish();
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+ if (cursor == null || cursor.getCount() < 1) {
+ // We always have the local account, so fall back to that
+ pickAccount(BrowserProvider2.FIXED_ID_ROOT);
+ } else if (cursor.getCount() == 1) {
+ cursor.moveToFirst();
+ pickAccount(cursor.getLong(AccountsLoader.COLUMN_INDEX_ROOT_ID));
+ } else {
+ mAccountAdapter.clear();
+ while (cursor.moveToNext()) {
+ mAccountAdapter.add(new BookmarkAccount(this, cursor));
+ }
+ setVisible(true);
+ }
+ getLoaderManager().destroyLoader(LOADER_ACCOUNTS);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ // Don't care
+ }
+
+ static class AccountsLoader extends CursorLoader {
+
+ static final String[] PROJECTION = new String[] {
+ Accounts.ACCOUNT_NAME,
+ Accounts.ACCOUNT_TYPE,
+ Accounts.ROOT_ID,
+ };
+
+ static final int COLUMN_INDEX_ACCOUNT_NAME = 0;
+ static final int COLUMN_INDEX_ACCOUNT_TYPE = 1;
+ static final int COLUMN_INDEX_ROOT_ID = 2;
+
+ public AccountsLoader(Context context) {
+ super(context, Accounts.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(BrowserProvider2.PARAM_ALLOW_EMPTY_ACCOUNTS, "false")
+ .build(), PROJECTION, null, null, null);
+ }
+
+ }
+
+}
diff --git a/src/com/android/browser/widget/BookmarkWidgetProxy.java b/src/com/android/browser/widget/BookmarkWidgetProxy.java
new file mode 100644
index 0000000..8ab57fc
--- /dev/null
+++ b/src/com/android/browser/widget/BookmarkWidgetProxy.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2011 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 android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+public class BookmarkWidgetProxy extends BroadcastReceiver {
+
+ private static final String TAG = "BookmarkWidgetProxy";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (BookmarkThumbnailWidgetService.ACTION_CHANGE_FOLDER.equals(intent.getAction())) {
+ BookmarkThumbnailWidgetService.changeFolder(context, intent);
+ } else if (BrowserActivity.ACTION_SHOW_BROWSER.equals(intent.getAction())) {
+ startActivity(context,
+ new Intent(BrowserActivity.ACTION_SHOW_BROWSER,
+ null, context, BrowserActivity.class));
+ } else {
+ Intent view = new Intent(intent);
+ view.setComponent(null);
+ startActivity(context, view);
+ }
+ }
+
+ void startActivity(Context context, Intent intent) {
+ try {
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to start intent activity", e);
+ }
+ }
+}