summaryrefslogtreecommitdiffstats
path: root/src/src/com/android/browser
diff options
context:
space:
mode:
Diffstat (limited to 'src/src/com/android/browser')
-rw-r--r--src/src/com/android/browser/AccountsChangedReceiver.java92
-rw-r--r--src/src/com/android/browser/ActivityController.java79
-rw-r--r--src/src/com/android/browser/AddBookmarkFolder.java950
-rw-r--r--src/src/com/android/browser/AddBookmarkPage.java1358
-rw-r--r--src/src/com/android/browser/AddNewBookmark.java55
-rw-r--r--src/src/com/android/browser/AppAdapter.java86
-rw-r--r--src/src/com/android/browser/AppItem.java42
-rw-r--r--src/src/com/android/browser/AutoFillSettingsFragment.java316
-rw-r--r--src/src/com/android/browser/AutofillHandler.java78
-rw-r--r--src/src/com/android/browser/BackgroundHandler.java44
-rw-r--r--src/src/com/android/browser/BaseUi.java1007
-rw-r--r--src/src/com/android/browser/BookmarkItem.java203
-rw-r--r--src/src/com/android/browser/BookmarkSearch.java44
-rw-r--r--src/src/com/android/browser/BookmarkUtils.java296
-rw-r--r--src/src/com/android/browser/Bookmarks.java267
-rw-r--r--src/src/com/android/browser/BookmarksLoader.java71
-rw-r--r--src/src/com/android/browser/BreadCrumbView.java439
-rw-r--r--src/src/com/android/browser/Browser.java151
-rw-r--r--src/src/com/android/browser/BrowserActivity.java437
-rw-r--r--src/src/com/android/browser/BrowserBackupAgent.java226
-rw-r--r--src/src/com/android/browser/BrowserBookmarksAdapter.java137
-rw-r--r--src/src/com/android/browser/BrowserBookmarksAdapterItem.java27
-rw-r--r--src/src/com/android/browser/BrowserBookmarksPage.java879
-rw-r--r--src/src/com/android/browser/BrowserConfigBase.java139
-rw-r--r--src/src/com/android/browser/BrowserHistoryPage.java691
-rw-r--r--src/src/com/android/browser/BrowserLauncher.java52
-rw-r--r--src/src/com/android/browser/BrowserLocationListPreference.java118
-rw-r--r--src/src/com/android/browser/BrowserLocationSwitchPreference.java118
-rw-r--r--src/src/com/android/browser/BrowserPreferencesPage.java149
-rw-r--r--src/src/com/android/browser/BrowserSettings.java1140
-rw-r--r--src/src/com/android/browser/BrowserSnapshotPage.java328
-rw-r--r--src/src/com/android/browser/BrowserSwitches.java71
-rw-r--r--src/src/com/android/browser/BrowserUtils.java95
-rw-r--r--src/src/com/android/browser/BrowserWebView.java226
-rw-r--r--src/src/com/android/browser/BrowserWebViewFactory.java79
-rw-r--r--src/src/com/android/browser/BrowserYesNoPreference.java187
-rw-r--r--src/src/com/android/browser/CombinedBookmarksCallbacks.java23
-rw-r--r--src/src/com/android/browser/ComboTabsAdapter.java127
-rw-r--r--src/src/com/android/browser/ComboView.java262
-rw-r--r--src/src/com/android/browser/ComboViewActivity.java149
-rw-r--r--src/src/com/android/browser/CommandLineManager.java142
-rw-r--r--src/src/com/android/browser/Controller.java3475
-rw-r--r--src/src/com/android/browser/CrashLogExceptionHandler.java388
-rw-r--r--src/src/com/android/browser/CrashRecoveryHandler.java268
-rw-r--r--src/src/com/android/browser/DataController.java303
-rw-r--r--src/src/com/android/browser/DataUri.java73
-rw-r--r--src/src/com/android/browser/DateSortedExpandableListAdapter.java380
-rw-r--r--src/src/com/android/browser/DownloadHandler.java685
-rw-r--r--src/src/com/android/browser/DownloadSettings.java393
-rw-r--r--src/src/com/android/browser/DownloadTouchIcon.java206
-rw-r--r--src/src/com/android/browser/DraggableFrameLayout.java90
-rw-r--r--src/src/com/android/browser/EdgeSwipeController.java560
-rw-r--r--src/src/com/android/browser/EdgeSwipeModel.java118
-rw-r--r--src/src/com/android/browser/EdgeSwipeSettings.java312
-rw-r--r--src/src/com/android/browser/EdgeSwipeView.java235
-rw-r--r--src/src/com/android/browser/EngineInitializer.java436
-rw-r--r--src/src/com/android/browser/EventLogTags.logtags15
-rw-r--r--src/src/com/android/browser/FetchUrlMimeType.java294
-rw-r--r--src/src/com/android/browser/FolderTileView.java218
-rw-r--r--src/src/com/android/browser/HistoryItem.java109
-rw-r--r--src/src/com/android/browser/HomepageHandler.java94
-rw-r--r--src/src/com/android/browser/HttpAuthenticationDialog.java168
-rw-r--r--src/src/com/android/browser/IntentHandler.java392
-rw-r--r--src/src/com/android/browser/LogTag.java62
-rw-r--r--src/src/com/android/browser/MemoryMonitor.java90
-rw-r--r--src/src/com/android/browser/MessagesReceiver.java60
-rw-r--r--src/src/com/android/browser/NavScreen.java254
-rw-r--r--src/src/com/android/browser/NavTabScroller.java587
-rw-r--r--src/src/com/android/browser/NavTabView.java132
-rw-r--r--src/src/com/android/browser/NavigationBarBase.java822
-rw-r--r--src/src/com/android/browser/NavigationBarPhone.java165
-rw-r--r--src/src/com/android/browser/NavigationBarTablet.java178
-rw-r--r--src/src/com/android/browser/NetworkStateHandler.java127
-rw-r--r--src/src/com/android/browser/NfcHandler.java109
-rw-r--r--src/src/com/android/browser/OpenDownloadReceiver.java94
-rw-r--r--src/src/com/android/browser/OptionsMenuHandler.java27
-rw-r--r--src/src/com/android/browser/PageProgressView.java125
-rw-r--r--src/src/com/android/browser/Performance.java134
-rw-r--r--src/src/com/android/browser/PhoneUi.java638
-rw-r--r--src/src/com/android/browser/PowerConnectionReceiver.java60
-rw-r--r--src/src/com/android/browser/PreferenceKeys.java148
-rw-r--r--src/src/com/android/browser/PreloadController.java255
-rw-r--r--src/src/com/android/browser/PreloadRequestReceiver.java135
-rw-r--r--src/src/com/android/browser/PreloadedTabControl.java84
-rw-r--r--src/src/com/android/browser/Preloader.java177
-rw-r--r--src/src/com/android/browser/ShareDialog.java125
-rw-r--r--src/src/com/android/browser/ShortcutActivity.java74
-rw-r--r--src/src/com/android/browser/SiteTileView.java680
-rw-r--r--src/src/com/android/browser/SnapshotBar.java269
-rw-r--r--src/src/com/android/browser/SnapshotTab.java254
-rw-r--r--src/src/com/android/browser/SuggestionsAdapter.java598
-rw-r--r--src/src/com/android/browser/SystemAllowGeolocationOrigins.java202
-rw-r--r--src/src/com/android/browser/Tab.java2133
-rw-r--r--src/src/com/android/browser/TabBar.java515
-rw-r--r--src/src/com/android/browser/TabControl.java772
-rw-r--r--src/src/com/android/browser/TabScrollView.java263
-rw-r--r--src/src/com/android/browser/TitleBar.java329
-rw-r--r--src/src/com/android/browser/UI.java153
-rw-r--r--src/src/com/android/browser/UiController.java116
-rw-r--r--src/src/com/android/browser/UpdateNotificationService.java294
-rw-r--r--src/src/com/android/browser/UploadDialog.java91
-rw-r--r--src/src/com/android/browser/UploadHandler.java584
-rwxr-xr-xsrc/src/com/android/browser/UrlHandler.java292
-rwxr-xr-xsrc/src/com/android/browser/UrlInputView.java369
-rw-r--r--src/src/com/android/browser/UrlSelectionActionMode.java63
-rwxr-xr-xsrc/src/com/android/browser/UrlUtils.java216
-rw-r--r--src/src/com/android/browser/WallpaperHandler.java200
-rw-r--r--src/src/com/android/browser/WebStorageSizeManager.java423
-rw-r--r--src/src/com/android/browser/WebViewController.java115
-rw-r--r--src/src/com/android/browser/WebViewFactory.java32
-rw-r--r--src/src/com/android/browser/WebViewProperties.java23
-rw-r--r--src/src/com/android/browser/WebViewTimersControl.java91
-rw-r--r--src/src/com/android/browser/XLargeUi.java337
-rw-r--r--src/src/com/android/browser/addbookmark/FolderSpinner.java92
-rw-r--r--src/src/com/android/browser/addbookmark/FolderSpinnerAdapter.java163
-rw-r--r--src/src/com/android/browser/appmenu/AppMenu.java372
-rw-r--r--src/src/com/android/browser/appmenu/AppMenuAdapter.java499
-rw-r--r--src/src/com/android/browser/appmenu/AppMenuButtonHelper.java93
-rw-r--r--src/src/com/android/browser/appmenu/AppMenuDragHelper.java269
-rw-r--r--src/src/com/android/browser/appmenu/AppMenuHandler.java187
-rw-r--r--src/src/com/android/browser/appmenu/AppMenuItemIcon.java46
-rw-r--r--src/src/com/android/browser/appmenu/AppMenuObserver.java16
-rw-r--r--src/src/com/android/browser/appmenu/AppMenuPropertiesDelegate.java29
-rw-r--r--src/src/com/android/browser/appmenu/OWNERS2
-rw-r--r--src/src/com/android/browser/homepages/HomeProvider.java126
-rw-r--r--src/src/com/android/browser/homepages/RequestHandler.java270
-rw-r--r--src/src/com/android/browser/homepages/Template.java284
-rw-r--r--src/src/com/android/browser/mdm/AutoFillRestriction.java113
-rw-r--r--src/src/com/android/browser/mdm/DevToolsRestriction.java80
-rw-r--r--src/src/com/android/browser/mdm/DoNotTrackRestriction.java116
-rw-r--r--src/src/com/android/browser/mdm/DownloadDirRestriction.java94
-rw-r--r--src/src/com/android/browser/mdm/EditBookmarksRestriction.java142
-rw-r--r--src/src/com/android/browser/mdm/IncognitoRestriction.java107
-rw-r--r--src/src/com/android/browser/mdm/ManagedBookmarksRestriction.java441
-rw-r--r--src/src/com/android/browser/mdm/ManagedProfileManager.java272
-rw-r--r--src/src/com/android/browser/mdm/MdmCheckBoxPreference.java129
-rw-r--r--src/src/com/android/browser/mdm/ProxyRestriction.java170
-rw-r--r--src/src/com/android/browser/mdm/Restriction.java74
-rw-r--r--src/src/com/android/browser/mdm/SearchEngineRestriction.java98
-rw-r--r--src/src/com/android/browser/mdm/ThirdPartyCookiesRestriction.java100
-rw-r--r--src/src/com/android/browser/mdm/URLFilterRestriction.java63
-rw-r--r--src/src/com/android/browser/mdm/tests/AutoFillRestrictionsTest.java117
-rw-r--r--src/src/com/android/browser/mdm/tests/DNTRestrictionsTest.java127
-rw-r--r--src/src/com/android/browser/mdm/tests/DevToolsRestrictionsTest.java109
-rw-r--r--src/src/com/android/browser/mdm/tests/DownloadDirRestrictionsTest.java190
-rw-r--r--src/src/com/android/browser/mdm/tests/EditBookmarkRestrictionsTest.java109
-rw-r--r--src/src/com/android/browser/mdm/tests/IncognitoRestrictionsTest.java115
-rw-r--r--src/src/com/android/browser/mdm/tests/ManagedBookmarksRestrictionsTest.java278
-rw-r--r--src/src/com/android/browser/mdm/tests/ProxyRestrictionsTest.java335
-rw-r--r--src/src/com/android/browser/mdm/tests/SearchRestrictionsTest.java208
-rw-r--r--src/src/com/android/browser/mdm/tests/ThirdPartyCookiesRestrictionsTest.java122
-rw-r--r--src/src/com/android/browser/mdm/tests/URLRestrictionsTest.java536
-rwxr-xr-xsrc/src/com/android/browser/mynavigation/AddMyNavigationPage.java272
-rwxr-xr-xsrc/src/com/android/browser/mynavigation/MyNavigationRequestHandler.java177
-rwxr-xr-xsrc/src/com/android/browser/mynavigation/MyNavigationTemplate.java307
-rwxr-xr-xsrc/src/com/android/browser/mynavigation/MyNavigationUtil.java124
-rw-r--r--src/src/com/android/browser/platformsupport/Browser.java660
-rw-r--r--src/src/com/android/browser/platformsupport/BrowserContract.java746
-rw-r--r--src/src/com/android/browser/platformsupport/Process.java48
-rw-r--r--src/src/com/android/browser/platformsupport/SeekBarPreference.java231
-rw-r--r--src/src/com/android/browser/platformsupport/SyncStateContentProviderHelper.java149
-rw-r--r--src/src/com/android/browser/platformsupport/WebAddress.java189
-rw-r--r--src/src/com/android/browser/preferences/AboutPreferencesFragment.java228
-rw-r--r--src/src/com/android/browser/preferences/AccessibilityPreferencesFragment.java128
-rw-r--r--src/src/com/android/browser/preferences/AdvancedPreferencesFragment.java225
-rw-r--r--src/src/com/android/browser/preferences/BandwidthPreferencesFragment.java63
-rw-r--r--src/src/com/android/browser/preferences/ContentPreferencesFragment.java113
-rw-r--r--src/src/com/android/browser/preferences/DebugPreferencesFragment.java74
-rw-r--r--src/src/com/android/browser/preferences/FontSizePreview.java56
-rw-r--r--src/src/com/android/browser/preferences/GeneralPreferencesFragment.java393
-rw-r--r--src/src/com/android/browser/preferences/InvertedContrastPreview.java99
-rw-r--r--src/src/com/android/browser/preferences/LegalPreferencesFragment.java121
-rw-r--r--src/src/com/android/browser/preferences/LegalPreviewActivity.java95
-rw-r--r--src/src/com/android/browser/preferences/LegalPreviewFragment.java83
-rw-r--r--src/src/com/android/browser/preferences/NonformattingListPreference.java48
-rw-r--r--src/src/com/android/browser/preferences/PrivacySecurityPreferencesFragment.java250
-rw-r--r--src/src/com/android/browser/preferences/SWEPreferenceFragment.java114
-rw-r--r--src/src/com/android/browser/preferences/SeekBarSummaryPreference.java90
-rw-r--r--src/src/com/android/browser/preferences/SiteSpecificPreferencesFragment.java816
-rw-r--r--src/src/com/android/browser/preferences/WebViewPreview.java93
-rw-r--r--src/src/com/android/browser/preferences/WebsiteSettingsFragment.java388
-rw-r--r--src/src/com/android/browser/provider/BrowserProvider.java1040
-rw-r--r--src/src/com/android/browser/provider/BrowserProvider2.java2290
-rwxr-xr-xsrc/src/com/android/browser/provider/MyNavigationProvider.java271
-rw-r--r--src/src/com/android/browser/provider/SQLiteContentProvider.java250
-rw-r--r--src/src/com/android/browser/provider/SnapshotProvider.java303
-rw-r--r--src/src/com/android/browser/reflect/ReflectHelper.java168
-rw-r--r--src/src/com/android/browser/search/DefaultSearchEngine.java141
-rw-r--r--src/src/com/android/browser/search/OpenSearchSearchEngine.java299
-rw-r--r--src/src/com/android/browser/search/SearchEngine.java64
-rw-r--r--src/src/com/android/browser/search/SearchEngineInfo.java179
-rw-r--r--src/src/com/android/browser/search/SearchEnginePreference.java89
-rw-r--r--src/src/com/android/browser/search/SearchEngines.java68
-rw-r--r--src/src/com/android/browser/stub/NullController.java162
-rw-r--r--src/src/com/android/browser/util/ThreadedCursorAdapter.java221
-rw-r--r--src/src/com/android/browser/view/BookmarkContainer.java201
-rw-r--r--src/src/com/android/browser/view/BookmarkExpandableView.java478
-rw-r--r--src/src/com/android/browser/view/BookmarkThumbImageView.java78
-rw-r--r--src/src/com/android/browser/view/EventRedirectingFrameLayout.java74
-rw-r--r--src/src/com/android/browser/view/ScrollerView.java1952
-rw-r--r--src/src/com/android/browser/view/SnapshotGridView.java59
-rw-r--r--src/src/com/android/browser/widget/BookmarkThumbnailWidgetProvider.java119
-rw-r--r--src/src/com/android/browser/widget/BookmarkThumbnailWidgetService.java379
-rw-r--r--src/src/com/android/browser/widget/BookmarkWidgetConfigure.java138
-rw-r--r--src/src/com/android/browser/widget/BookmarkWidgetProxy.java53
205 files changed, 56817 insertions, 0 deletions
diff --git a/src/src/com/android/browser/AccountsChangedReceiver.java b/src/src/com/android/browser/AccountsChangedReceiver.java
new file mode 100644
index 00000000..a4d10d75
--- /dev/null
+++ b/src/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/src/com/android/browser/ActivityController.java b/src/src/com/android/browser/ActivityController.java
new file mode 100644
index 00000000..d19eaaca
--- /dev/null
+++ b/src/src/com/android/browser/ActivityController.java
@@ -0,0 +1,79 @@
+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 onStart();
+
+ void onStop();
+
+ 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);
+
+ void invalidateOptionsMenu();
+}
diff --git a/src/src/com/android/browser/AddBookmarkFolder.java b/src/src/com/android/browser/AddBookmarkFolder.java
new file mode 100644
index 00000000..faff0274
--- /dev/null
+++ b/src/src/com/android/browser/AddBookmarkFolder.java
@@ -0,0 +1,950 @@
+/*
+ * 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);
+ 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_deco_folder_normal);
+ 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/src/com/android/browser/AddBookmarkPage.java b/src/src/com/android/browser/AddBookmarkPage.java
new file mode 100644
index 00000000..860e00a2
--- /dev/null
+++ b/src/src/com/android/browser/AddBookmarkPage.java
@@ -0,0 +1,1358 @@
+/*
+ * 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.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.URLEncoder;
+import java.net.URISyntaxException;
+import java.io.UnsupportedEncodingException;
+
+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;
+
+ final static int MAX_TITLE_LENGTH = 80;
+
+ private EditText mTitle;
+ private EditText mAddress;
+ private TextView mButton;
+ private View mCancelButton;
+ private View mDeleteButton;
+ 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);
+ }
+
+ 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;
+ }
+
+ @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 == mDeleteButton || v == mRemoveLink) {
+ onDeleteWithConfirm();
+ } 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);
+ }
+ }
+
+ // 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));
+ }
+
+ if (cursor.getCount() < 2) {
+ View accountView = findViewById(R.id.row_account);
+ if (accountView != null) {
+ accountView.setVisibility(View.GONE);
+ }
+ }
+
+ 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;
+ mTouchIconUrl = null;
+
+ mFakeTitle = (TextView) findViewById(R.id.fake_title);
+
+ mDeleteButton = findViewById(R.id.delete);
+ mDeleteButton.setOnClickListener(this);
+
+ 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);
+
+ // Check if title is not empty to prevent NPE
+ if (!TextUtils.isEmpty(title)) {
+ if (title.length() > MAX_TITLE_LENGTH) {
+ title = title.substring(0, MAX_TITLE_LENGTH);
+ }
+ }
+
+ }
+
+ mTitle = (EditText) findViewById(R.id.title);
+ mTitle.setText(title);
+ BrowserUtils.maxLengthFilter(AddBookmarkPage.this, mTitle, MAX_TITLE_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_deco_folder_normal);
+ 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);
+ mFolder.setSelectionIgnoringSelectionChange(1); // Select Bookmarks by default
+
+ mFakeTitleHolder = findViewById(R.id.title_holder);
+
+ if (!window.getDecorView().isInTouchMode()) {
+ mButton.requestFocus();
+ }
+
+ if (!(mCurrentFolder == -1 || mCurrentFolder == 2)) {
+ mFolder.setSelectionIgnoringSelectionChange(2);
+ mFolderAdapter.setOtherFolderDisplayText(getNameFromId(mCurrentFolder));
+ }
+
+ getLoaderManager().restartLoader(LOADER_ID_ACCOUNTS, null, this);
+ }
+
+ private void showRemoveButton() {
+ mDeleteButton.setVisibility(View.VISIBLE);
+ 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 = (mCurrentFolder == -1) ? mRootFolder : mCurrentFolder;
+ 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.FAVICON);
+ 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(touchIconUrl);
+ }
+ 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 onDeleteWithConfirm() {
+ final String title = mTitle.getText().toString().trim();
+ final String unfilteredUrl = UrlUtils.fixUrl(mAddress.getText().toString());
+ final String url = unfilteredUrl.trim();
+ new AlertDialog.Builder(this)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setMessage(getString(R.string.delete_bookmark_warning, title))
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ ContentResolver cr = getContentResolver();
+ Cursor cursor = cr.query(BrowserContract.Bookmarks.CONTENT_URI,
+ BookmarksLoader.PROJECTION,
+ "title = ? OR url = ?",
+ new String[] {
+ title, url
+ },
+ null);
+
+ if (cursor == null) {
+ finish();
+ return;
+ }
+
+ try {
+ if (cursor.moveToFirst()) {
+ do {
+ long index = cursor.getLong(
+ cursor.getColumnIndex(BrowserContract.Bookmarks._ID));
+ cr.delete(ContentUris.withAppendedId(
+ BrowserContract.Bookmarks.CONTENT_URI, index),
+ null, null);
+ } while (cursor.moveToNext());
+ }
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+ finish();
+ }
+ })
+ .show();
+ }
+
+ 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))
+ .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:")) {
+ String encodedUrl = URLEncoder.encode(url, "UTF-8");
+ URI uriObj = new URI(encodedUrl);
+ 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;
+ } catch (UnsupportedEncodingException 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);
+ if (mTouchIconUrl != null) {
+ 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/src/com/android/browser/AddNewBookmark.java b/src/src/com/android/browser/AddNewBookmark.java
new file mode 100644
index 00000000..5decb655
--- /dev/null
+++ b/src/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/src/com/android/browser/AppAdapter.java b/src/src/com/android/browser/AppAdapter.java
new file mode 100644
index 00000000..429ed900
--- /dev/null
+++ b/src/src/com/android/browser/AppAdapter.java
@@ -0,0 +1,86 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.content.pm.ResolveInfo;
+import android.content.pm.PackageManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+import android.widget.ImageView;
+
+import java.util.List;
+
+public class AppAdapter extends ArrayAdapter<ResolveInfo> {
+ private PackageManager pm = null;
+ private Context context = null;
+ private int layoutResourceId = -1;
+
+
+ public AppAdapter (Context context, PackageManager pm, int layoutResourceId, List<ResolveInfo> apps) {
+ super(context, layoutResourceId, apps);
+ this.context = context;
+ this.pm = pm;
+ this.layoutResourceId = layoutResourceId;
+ }
+
+ /*
+ * Overide this method in order to create your own view
+ */
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = newView(parent);
+ }
+
+ bindView(position, convertView);
+ return(convertView);
+ }
+
+ private View newView(ViewGroup parent) {
+ LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE );
+ return(layoutInflater.inflate(layoutResourceId, parent, false));
+ }
+
+ private void bindView(int position, View row) {
+
+ TextView label = (TextView)row.findViewById(R.id.app_label);
+ label.setText( getItem(position).loadLabel(pm));
+
+ ImageView icon = (ImageView)row.findViewById(R.id.app_icon);
+ icon.setImageDrawable( getItem(position).loadIcon(pm));
+
+ }
+}
diff --git a/src/src/com/android/browser/AppItem.java b/src/src/com/android/browser/AppItem.java
new file mode 100644
index 00000000..331f3e95
--- /dev/null
+++ b/src/src/com/android/browser/AppItem.java
@@ -0,0 +1,42 @@
+/*
+ * 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;
+
+import android.content.pm.ResolveInfo;
+import java.util.List;
+
+class AppItem {
+ public List<ResolveInfo> apps;
+
+ public AppItem(List<ResolveInfo> apps) {
+ this.apps = apps;
+ }
+} \ No newline at end of file
diff --git a/src/src/com/android/browser/AutoFillSettingsFragment.java b/src/src/com/android/browser/AutoFillSettingsFragment.java
new file mode 100644
index 00000000..6bde1a2c
--- /dev/null
+++ b/src/src/com/android/browser/AutoFillSettingsFragment.java
@@ -0,0 +1,316 @@
+/*
+ * 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.ActionBar;
+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 MenuItem mDeleteMenuItem;
+
+ private boolean mInitialised;
+
+ // Used to display toast after DB interactions complete.
+ private Handler mHandler;
+ private BrowserSettings mSettings;
+
+ // 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();
+ updateDeleteMenuItemState();
+ }
+
+ 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();
+ updateDeleteMenuItemState();
+ }
+
+ 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();
+
+ @Override
+ public void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+ setHasOptionsMenu(true);
+ mSettings = BrowserSettings.getInstance();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ ActionBar bar = getActivity().getActionBar();
+ if (bar != null) {
+ bar.setTitle(R.string.pref_general_autofill_title);
+ bar.setDisplayHomeAsUpEnabled(false);
+ bar.setHomeButtonEnabled(false);
+ }
+ }
+
+ @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);
+ mDeleteMenuItem = menu.findItem(R.id.autofill_profile_editor_delete_profile_menu_id);
+ updateSaveMenuItemState();
+ updateDeleteMenuItemState();
+ }
+
+ @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();
+ updateDeleteMenuItemState();
+ Toast.makeText(getActivity(), R.string.autofill_profile_successful_delete,
+ Toast.LENGTH_SHORT).show();
+ 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);
+ Toast.makeText(getActivity(), R.string.autofill_profile_successful_save,
+ Toast.LENGTH_SHORT).show();
+ closeEditor();
+
+ 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();
+ updateDeleteMenuItemState();
+
+ return v;
+ }
+
+ private void updateDeleteMenuItemState() {
+ if (mDeleteMenuItem == null) {
+ return;
+ }
+
+ if (!mInitialised) {
+ mDeleteMenuItem.setEnabled(false);
+ return;
+ }
+
+ boolean currentState = mDeleteMenuItem.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) {
+ mDeleteMenuItem.setEnabled(newState);
+ }
+ }
+
+ 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/src/com/android/browser/AutofillHandler.java b/src/src/com/android/browser/AutofillHandler.java
new file mode 100644
index 00000000..0992fcc5
--- /dev/null
+++ b/src/src/com/android/browser/AutofillHandler.java
@@ -0,0 +1,78 @@
+
+/*
+ * 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.preference.PreferenceManager;
+
+
+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/src/com/android/browser/BackgroundHandler.java b/src/src/com/android/browser/BackgroundHandler.java
new file mode 100644
index 00000000..a0d9243e
--- /dev/null
+++ b/src/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/src/com/android/browser/BaseUi.java b/src/src/com/android/browser/BaseUi.java
new file mode 100644
index 00000000..374bc15b
--- /dev/null
+++ b/src/src/com/android/browser/BaseUi.java
@@ -0,0 +1,1007 @@
+/*
+ * 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.content.res.Configuration;
+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.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.text.TextUtils;
+import android.view.ActionMode;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.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.Toast;
+import android.content.res.TypedArray;
+
+import org.codeaurora.swe.BrowserCommandLine;
+import org.codeaurora.swe.WebView;
+
+import java.util.List;
+
+/**
+ * UI interface definitions
+ */
+public abstract class BaseUi implements UI {
+
+ protected static final boolean ENABLE_BORDER_AROUND_FAVICON = false;
+
+ 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);
+
+ Activity mActivity;
+ UiController mUiController;
+ TabControl mTabControl;
+ protected Tab mActiveTab;
+ private InputMethodManager mInputManager;
+
+ private Drawable mGenericFavicon;
+
+ protected FrameLayout mContentView;
+ protected FrameLayout mCustomViewContainer;
+
+ private View mCustomView;
+ private CustomViewCallback mCustomViewCallback;
+ private int mOriginalOrientation;
+
+ private Toast mStopToast;
+
+ // the default <video> poster
+ private Bitmap mDefaultVideoPoster;
+ // the video progress view
+ private View mVideoProgressView;
+
+ private final View mDecorView;
+
+ private boolean mActivityPaused;
+ protected TitleBar mTitleBar;
+ private NavigationBarBase mNavigationBar;
+ private boolean mBlockFocusAnimations;
+ private boolean mFullscreenModeLocked;
+
+ private EdgeSwipeController mEdgeSwipeController;
+ private EdgeSwipeSettings mEdgeSwipeSettings;
+
+ public BaseUi(Activity browser, UiController controller) {
+ mActivity = browser;
+ mUiController = controller;
+ mTabControl = controller.getTabControl();
+ mInputManager = (InputMethodManager)
+ browser.getSystemService(Activity.INPUT_METHOD_SERVICE);
+ // This assumes that the top-level root of our layout has the 'android.R.id.content' id
+ // it's used in place of setContentView because we're attaching a <merge> here.
+ FrameLayout frameLayout = (FrameLayout) mActivity.getWindow()
+ .getDecorView().findViewById(android.R.id.content);
+ LayoutInflater.from(mActivity)
+ .inflate(R.layout.custom_screen, frameLayout);
+ mContentView = (FrameLayout) frameLayout.findViewById(
+ R.id.main_content);
+ mCustomViewContainer = (FrameLayout) frameLayout.findViewById(
+ R.id.fullscreen_custom_content);
+ setFullscreen(BrowserSettings.getInstance().useFullscreen());
+ mTitleBar = new TitleBar(mActivity, mUiController, this,
+ mContentView);
+ mTitleBar.setProgress(100);
+ mNavigationBar = mTitleBar.getNavigationBar();
+
+ // install system ui visibility listeners
+ mDecorView = mActivity.getWindow().getDecorView();
+ mDecorView.setOnSystemUiVisibilityChangeListener(mSystemUiVisibilityChangeListener);
+ mFullscreenModeLocked = false;
+ }
+
+ private View.OnSystemUiVisibilityChangeListener mSystemUiVisibilityChangeListener =
+ new View.OnSystemUiVisibilityChangeListener() {
+ @Override
+ public void onSystemUiVisibilityChange(int visFlags) {
+ final boolean lostFullscreen = (visFlags & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0;
+ if (lostFullscreen)
+ setFullscreen(BrowserSettings.getInstance().useFullscreen());
+ }
+ };
+
+ private void cancelStopToast() {
+ if (mStopToast != null) {
+ mStopToast.cancel();
+ mStopToast = null;
+ }
+ }
+
+ protected Drawable getGenericFavicon() {
+ if (mGenericFavicon == null) {
+ mGenericFavicon = mActivity.getResources().getDrawable(R.drawable.ic_deco_favicon_normal);
+ }
+ return mGenericFavicon;
+ }
+
+ // lifecycle
+
+ public void onPause() {
+ if (isCustomViewShowing()) {
+ onHideCustomView();
+ }
+ if (mTabControl.getCurrentTab() != null) {
+ mTabControl.getCurrentTab().exitFullscreen();
+ }
+ cancelStopToast();
+ mActivityPaused = true;
+ }
+
+ public void onResume() {
+ mActivityPaused = false;
+ // check if we exited without setting active tab
+ // b: 5188145
+ setFullscreen(BrowserSettings.getInstance().useFullscreen());
+ final Tab ct = mTabControl.getCurrentTab();
+ if (ct != null) {
+ setActiveTab(ct);
+ }
+ mTitleBar.onResume();
+ }
+
+ protected boolean isActivityPaused() {
+ return mActivityPaused;
+ }
+
+ public void onConfigurationChanged(Configuration config) {
+ if (mEdgeSwipeController != null) {
+ mEdgeSwipeController.onConfigurationChanged();
+ }
+ if (mEdgeSwipeSettings != null) {
+ mEdgeSwipeSettings.onConfigurationChanged();
+ }
+ }
+
+ public Activity getActivity() {
+ return mActivity;
+ }
+
+ // key handling
+
+ @Override
+ public boolean onBackKey() {
+ if (mCustomView != null) {
+ mUiController.hideCustomView();
+ return true;
+ } else if ((mTabControl.getCurrentTab() != null) &&
+ (mTabControl.getCurrentTab().exitFullscreen())) {
+ return true;
+ }
+ return false;
+ }
+
+ public boolean isFullScreen() {
+ if (mTabControl.getCurrentTab() != null)
+ return mTabControl.getCurrentTab().isTabFullScreen();
+ return false;
+ }
+
+ @Override
+ public boolean onMenuKey() {
+ return false;
+ }
+
+ // Tab callbacks
+ @Override
+ public void onTabDataChanged(Tab tab) {
+ setUrlTitle(tab);
+ updateTabSecurityState(tab);
+ updateNavigationState(tab);
+ mTitleBar.onTabDataChanged(tab);
+ mNavigationBar.onTabDataChanged(tab);
+ onProgressChanged(tab);
+ }
+
+ @Override
+ public void onProgressChanged(Tab tab) {
+ int progress = tab.getLoadProgress();
+ if (tab.inForeground()) {
+ if (tab.inPageLoad()) {
+ mTitleBar.setProgress(progress);
+ } else {
+ mTitleBar.setProgress(100);
+ }
+ }
+ }
+
+ @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) {
+ }
+
+ public void cancelNavScreenRequest(){
+ }
+
+ public void setActiveTab(final Tab tab) {
+ if (tab == null) return;
+ Tab tabToRemove = null;
+ Tab tabToWaitFor = null;
+
+ // block unnecessary focus change animations during tab switch
+ mBlockFocusAnimations = true;
+ if ((tab != mActiveTab) && (mActiveTab != null)) {
+ tabToRemove = mActiveTab;
+ WebView web = mActiveTab.getWebView();
+ if (web != null) {
+ web.setOnTouchListener(null);
+ }
+ }
+ mActiveTab = tab;
+
+ BrowserWebView web = (BrowserWebView) mActiveTab.getWebView();
+ attachTabToContentView(tab);
+ if (web != null) {
+ // Request focus on the top window.
+ web.setTitleBar(mTitleBar);
+ mTitleBar.onScrollChanged();
+ tabToWaitFor = mActiveTab;
+ }
+ mTitleBar.bringToFront();
+ tab.getTopWindow().requestFocus();
+ onTabDataChanged(tab);
+ setFavicon(tab);
+ onProgressChanged(tab);
+ mNavigationBar.setIncognitoMode(tab.isPrivateBrowsingEnabled());
+ mBlockFocusAnimations = false;
+
+ scheduleRemoveTab(tabToRemove, tabToWaitFor);
+
+ updateTabSecurityState(tab);
+ }
+
+ Tab mTabToRemove = null;
+ Tab mTabToWaitFor = null;
+ int mNumRemoveTries = 0;
+ Runnable mRunnable = null;
+
+ protected void scheduleRemoveTab(Tab tabToRemove, Tab tabToWaitFor) {
+
+ if(tabToWaitFor == mTabToRemove) {
+ if (mRunnable != null) {
+ mTitleBar.removeCallbacks(mRunnable);
+ }
+ mTabToRemove = null;
+ mTabToWaitFor = null;
+ mRunnable = null;
+ return;
+ }
+
+ //remove previously scehduled tab
+ if (mTabToRemove != null) {
+ if (mRunnable != null)
+ mTitleBar.removeCallbacks(mRunnable);
+ removeTabFromContentView(mTabToRemove);
+ mTabToRemove.performPostponedDestroy();
+ mRunnable = null;
+ }
+ mTabToRemove = tabToRemove;
+ mTabToWaitFor = tabToWaitFor;
+ mNumRemoveTries = 0;
+
+ if (mTabToRemove != null) {
+ mTabToRemove.postponeDestroy();
+ tryRemoveTab();
+ }
+ }
+
+ protected void tryRemoveTab() {
+ mNumRemoveTries++;
+ // Ensure the webview is still valid
+ if (mNumRemoveTries < 20 && mTabToWaitFor.getWebView() != null) {
+ if (!mTabToWaitFor.getWebView().isReady()) {
+ if (mRunnable == null) {
+ mRunnable = new Runnable() {
+ public void run() {
+ tryRemoveTab();
+ }
+ };
+ }
+ /*if the new tab is still not ready, wait another 2 frames
+ before trying again. 1 frame for the tab to render the first
+ frame, another 1 frame to make sure the swap is done*/
+ mTitleBar.postDelayed(mRunnable, 33);
+ return;
+ }
+ }
+ if (mTabToRemove != null) {
+ if (mRunnable != null)
+ mTitleBar.removeCallbacks(mRunnable);
+ removeTabFromContentView(mTabToRemove);
+ mTabToRemove.performPostponedDestroy();
+ mRunnable = null;
+ }
+ mTabToRemove = null;
+ mTabToWaitFor = 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 parentView = (ViewGroup)mainView.getView().getParent();
+
+ if (wrapper != parentView) {
+ // clean up old view before attaching new view
+ // this helping in fixing issues such touch event
+ // getting triggered on old view instead of new one
+ if (parentView != null) {
+ parentView.removeView(mainView.getView());
+ }
+ wrapper.addView(mainView.getView());
+ }
+ ViewGroup parent = (ViewGroup) container.getParent();
+ if (parent != mContentView) {
+ if (parent != null) {
+ parent.removeView(container);
+ }
+ mContentView.addView(container, COVER_SCREEN_PARAMS);
+ }
+
+ refreshEdgeSwipeController(container);
+
+ mUiController.attachSubWindow(tab);
+ }
+
+ public void refreshEdgeSwipeController(View container) {
+ if (isUiLowPowerMode()) {
+ return;
+ }
+
+ if (mEdgeSwipeController != null) {
+ mEdgeSwipeController.cleanup();
+ }
+
+ mEdgeSwipeSettings = null;
+
+ String action = BrowserSettings.getInstance().getEdgeSwipeAction();
+
+ if (action.equalsIgnoreCase(
+ mActivity.getResources().getString(R.string.value_temporal_edge_swipe))) {
+ mEdgeSwipeController = new EdgeSwipeController(
+ container,
+ R.id.stationary_navview,
+ R.id.sliding_navview,
+ R.id.sliding_navview_shadow,
+ R.id.navview_opacity,
+ R.id.webview_wrapper,
+ R.id.draggable_mainframe,
+ this);
+ } else if (action.equalsIgnoreCase(
+ mActivity.getResources().getString(R.string.value_unknown_edge_swipe))) {
+ mEdgeSwipeSettings = new EdgeSwipeSettings(
+ container,
+ R.id.stationary_navview,
+ R.id.edge_sliding_settings,
+ R.id.sliding_navview_shadow,
+ R.id.webview_wrapper,
+ R.id.draggable_mainframe,
+ this);
+ } else {
+ DraggableFrameLayout draggableView = (DraggableFrameLayout)
+ container.findViewById(R.id.draggable_mainframe);
+ draggableView.setDragHelper(null);
+ }
+ }
+
+ 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.getView());
+ mContentView.removeView(container);
+ mUiController.endActionMode();
+ mUiController.removeSubWindow(tab);
+ }
+
+ @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() {
+ if (canShowTitleBar()) {
+ mTitleBar.showTopControls(false);
+ }
+ }
+
+ protected void hideTitleBar() {
+ if (mTitleBar.isShowing()) {
+ mTitleBar.enableTopControls(false);
+ }
+ }
+
+ 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 hideComboView() {
+ }
+
+ @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();
+ decor.addView(view, COVER_SCREEN_PARAMS);
+ mCustomView = view;
+ showFullscreen(true);
+ ((BrowserWebView) getWebView()).setVisibility(View.INVISIBLE);
+ mCustomViewCallback = callback;
+ mActivity.setRequestedOrientation(requestedOrientation);
+ }
+
+ @Override
+ public void onHideCustomView() {
+ ((BrowserWebView) getWebView()).setVisibility(View.VISIBLE);
+ if (mCustomView == null)
+ return;
+ showFullscreen(false);
+ FrameLayout decor = (FrameLayout) mActivity.getWindow().getDecorView();
+ decor.removeView(mCustomView);
+ 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 boolean isComboViewShowing() {
+ return false;
+ }
+
+ public static boolean isUiLowPowerMode() {
+ return BrowserCommandLine.hasSwitch("ui-low-power-mode")
+ || BrowserSettings.getInstance().isPowerSaveModeEnabled()
+ || BrowserSettings.getInstance().isDisablePerfFeatures();
+ }
+
+ // -------------------------------------------------------------------------
+
+ protected void updateNavigationState(Tab tab) {
+ }
+
+ /**
+ * Update the lock icon to correspond to our latest state.
+ */
+ private void updateTabSecurityState(Tab t) {
+ if (t != null && t.inForeground()) {
+ mNavigationBar.setSecurityState(t.getSecurityState());
+ setUrlTitle(t);
+ }
+ }
+
+ protected void setUrlTitle(Tab tab) {
+ String url = tab.getUrl();
+ String title = tab.getTitle();
+ if (TextUtils.isEmpty(title)) {
+ title = url;
+ }
+ if (tab.inForeground()) {
+ mNavigationBar.setDisplayTitle(title, url);
+ }
+ }
+
+ // Set the favicon in the title bar.
+ public void setFavicon(Tab tab) {
+ mNavigationBar.showCurrentFavicon(tab);
+ }
+
+ // 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) {
+ }
+
+ // -------------------------------------------------------------------------
+ // 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 forceDisableFullscreenMode(boolean disabled) {
+ mFullscreenModeLocked = false;
+ setFullscreen(!disabled);
+ mFullscreenModeLocked = disabled;
+ }
+
+ public void setFullscreen(boolean enabled) {
+ if (mFullscreenModeLocked)
+ return;
+
+ Window win = mActivity.getWindow();
+ WindowManager.LayoutParams winParams = win.getAttributes();
+ final int bits = WindowManager.LayoutParams.FLAG_FULLSCREEN;
+ final int fullscreenImmersiveSetting =
+ View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_FULLSCREEN |
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+
+ if (mCustomView != null) {
+ mCustomView.setSystemUiVisibility(enabled ?
+ fullscreenImmersiveSetting : View.SYSTEM_UI_FLAG_VISIBLE);
+ } else if (Build.VERSION.SDK_INT >= 19) {
+ mContentView.setSystemUiVisibility(enabled ?
+ fullscreenImmersiveSetting : View.SYSTEM_UI_FLAG_VISIBLE);
+ } else {
+ mContentView.setSystemUiVisibility(enabled ?
+ View.SYSTEM_UI_FLAG_LOW_PROFILE : View.SYSTEM_UI_FLAG_VISIBLE);
+ }
+ if (enabled)
+ winParams.flags |= bits;
+ else
+ winParams.flags &= ~bits;
+
+ win.setAttributes(winParams);
+ }
+
+ //make full screen by showing/hiding topbar and system status bar
+ public void showFullscreen(boolean fullScreen) {
+ //Hide/show system ui bar as needed
+ if (!BrowserSettings.getInstance().useFullscreen())
+ setFullscreen(fullScreen);
+ //Hide/show topbar as needed
+ if (getWebView() != null) {
+ BrowserWebView bwv = (BrowserWebView) getWebView();
+ if (fullScreen) {
+ // hide titlebar
+ mTitleBar.hideTopControls(true);
+ } else {
+ // show titlebar
+ mTitleBar.showTopControls(false);
+ // enable auto hide titlebar
+ if (!mTitleBar.isFixed())
+ mTitleBar.enableTopControls(false);
+ }
+ }
+ }
+
+ public void translateTitleBar(float topControlsOffsetYPix) {
+ if (mTitleBar == null || mTitleBar.isFixed())
+ return;
+ if (!mInActionMode) {
+ if (topControlsOffsetYPix != 0.0) {
+ mTitleBar.setEnabled(false);
+ } else {
+ mTitleBar.setEnabled(true);
+ }
+ float currentY = mTitleBar.getTranslationY();
+ float height = mNavigationBar.getHeight();
+ if ((height + currentY) <= 0 && (height + topControlsOffsetYPix) > 0) {
+ mTitleBar.requestLayout();
+ } else if ((height + topControlsOffsetYPix) <= 0) {
+ topControlsOffsetYPix -= 1;
+ mTitleBar.getParent().requestTransparentRegion(mTitleBar);
+ }
+ // This was done to get HTML5 fullscreen API to work with fixed mode since
+ // topcontrols are used to implement HTML5 fullscreen
+ mTitleBar.setTranslationY(topControlsOffsetYPix);
+ }
+ }
+
+ public Drawable getFaviconDrawable(Bitmap icon) {
+ if (ENABLE_BORDER_AROUND_FAVICON) {
+ 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] = getGenericFavicon();
+ } else {
+ array[2] = new BitmapDrawable(mActivity.getResources(), icon);
+ }
+ LayerDrawable d = new LayerDrawable(array);
+ d.setLayerInset(1, 1, 1, 1, 1);
+ d.setLayerInset(2, 2, 2, 2, 2);
+ return d;
+ }
+ return icon == null ? getGenericFavicon() : new BitmapDrawable(mActivity.getResources(), icon);
+ }
+
+ public boolean isLoading() {
+ return mActiveTab != null ? mActiveTab.inPageLoad() : false;
+ }
+
+ protected void setMenuItemVisibility(Menu menu, int id,
+ boolean visibility) {
+ MenuItem item = menu.findItem(id);
+ if (item != null) {
+ item.setVisible(visibility);
+ }
+ }
+
+ protected Handler mHandler = new Handler() {
+
+ @Override
+ public void handleMessage(Message msg) {
+ BaseUi.this.handleMessage(msg);
+ }
+ };
+
+ protected void handleMessage(Message msg) {}
+
+ @Override
+ public void showWeb(boolean animate) {
+ mUiController.hideCustomView();
+ }
+
+ public void setContentViewMarginTop(int margin) {
+ FrameLayout.LayoutParams params =
+ (FrameLayout.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;
+ }
+
+ boolean mInActionMode = false;
+ private float getActionModeHeight() {
+ TypedArray actionBarSizeTypedArray = mActivity.obtainStyledAttributes(
+ new int[] { android.R.attr.actionBarSize });
+ float size = actionBarSizeTypedArray.getDimension(0, 0f);
+ actionBarSizeTypedArray.recycle();
+ return size;
+ }
+
+
+ @Override
+ public void onActionModeStarted(ActionMode mode) {
+ mInActionMode = true;
+
+ if (mTitleBar.isFixed()) {
+ int fixedTbarHeight = mTitleBar.calculateEmbeddedHeight();
+ setContentViewMarginTop(fixedTbarHeight);
+ } else {
+ mTitleBar.setTranslationY(getActionModeHeight());
+ }
+ }
+
+ @Override
+ public void onActionModeFinished(boolean inLoad) {
+ mInActionMode = false;
+ if (mTitleBar.isFixed()) {
+ setContentViewMarginTop(0);
+ } else {
+ mTitleBar.setTranslationY(0);
+ }
+ }
+
+ @Override
+ public boolean shouldCaptureThumbnails() {
+ return true;
+ }
+}
diff --git a/src/src/com/android/browser/BookmarkItem.java b/src/src/com/android/browser/BookmarkItem.java
new file mode 100644
index 00000000..1f7b7d22
--- /dev/null
+++ b/src/src/com/android/browser/BookmarkItem.java
@@ -0,0 +1,203 @@
+/*
+ * 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.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.ScrollView;
+import android.widget.TextView;
+
+/**
+ * Custom layout for an item representing a bookmark in the browser.
+ */
+class BookmarkItem extends ScrollView {
+
+ final static int MAX_TEXTVIEW_LEN = 80;
+
+ protected TextView mTextView;
+ protected TextView mUrlText;
+ protected SiteTileView mTileView;
+ protected String mUrl;
+ protected String mTitle;
+ protected boolean mEnableScrolling = false;
+
+ protected Bitmap mBitmap;
+ /**
+ * 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);
+ mTileView = (SiteTileView) findViewById(R.id.favicon);
+ View star = findViewById(R.id.star);
+ star.setVisibility(View.GONE);
+ }
+
+ /**
+ * 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) {
+ mTileView.replaceFavicon(b);
+ mBitmap = b;
+ }
+ }
+
+ void setFaviconBackground(Drawable d) {
+ mTileView.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);
+
+ /*
+ * Since there are more than 80 characters
+ * in the URL this is formatting the url
+ * to a vertical Scroll View.
+ */
+ if (url.length() > MAX_TEXTVIEW_LEN) {
+
+ // url cannot exceed max length
+ if (url.length() > UrlInputView.URL_MAX_LENGTH) {
+ url = url.substring(0, UrlInputView.URL_MAX_LENGTH);
+ }
+
+ mUrlText.setHorizontallyScrolling(false);
+ mUrlText.setSingleLine(false);
+ mUrlText.setVerticalScrollBarEnabled(true);
+ /*
+ * Only the first 3 lines of the URL will be visible
+ * Rest of it will be scrollable.
+ */
+ mUrlText.setMaxLines(3);
+ }
+
+ 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/src/com/android/browser/BookmarkSearch.java b/src/src/com/android/browser/BookmarkSearch.java
new file mode 100644
index 00000000..4d3ca0f3
--- /dev/null
+++ b/src/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/src/com/android/browser/BookmarkUtils.java b/src/src/com/android/browser/BookmarkUtils.java
new file mode 100644
index 00000000..537530e4
--- /dev/null
+++ b/src/src/com/android/browser/BookmarkUtils.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.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 com.android.browser.R;
+import com.android.browser.platformsupport.Browser;
+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);
+ shortcutIntent.setPackage(context.getPackageName());
+ 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, boolean is_folder) {
+
+ new AlertDialog.Builder(context)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setMessage(is_folder ?
+ context.getString(R.string.delete_folder_warning, title) :
+ 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/src/com/android/browser/Bookmarks.java b/src/src/com/android/browser/Bookmarks.java
new file mode 100644
index 00000000..afc99c37
--- /dev/null
+++ b/src/src/com/android/browser/Bookmarks.java
@@ -0,0 +1,267 @@
+/*
+ * 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.database.sqlite.SQLiteException;
+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.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
+ 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 + " == ?";
+
+ private static String eatTrailingSlash(String input) {
+ if (TextUtils.isEmpty(input)) {
+ return input;
+ }
+
+ if (input.charAt(input.length() - 1) == '/') {
+ return input.substring(0, input.length() - 1);
+ }
+
+ return input;
+ }
+
+ 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);
+ byte[] image = os.toByteArray();
+
+ // The Images update will insert if it doesn't exist
+ ContentValues values = new ContentValues();
+ values.put(Images.FAVICON, image);
+ values.put(Images.THUMBNAIL, image);
+
+ updateImages(cr, removeQuery(originalUrl), values);
+ updateImages(cr, removeQuery(url), values);
+
+ Cursor cursor = null;
+ try {
+ cursor = queryCombinedForUrl(cr, originalUrl, url);
+ if (cursor != null && cursor.moveToFirst()) {
+ do {
+ updateImages(cr, cursor.getString(0), values);
+ } while (cursor.moveToNext());
+ }
+
+ cursor = queryCombinedForUrl(cr, eatTrailingSlash(originalUrl),
+ eatTrailingSlash(url));
+ if (cursor != null && cursor.moveToFirst()) {
+ do {
+ updateImages(cr, cursor.getString(0), values);
+ } while (cursor.moveToNext());
+ }
+ } catch (IllegalStateException e) {
+ // Ignore
+ } catch (SQLiteException s) {
+ // Ignore
+ } finally {
+ if (cursor != null) cursor.close();
+ }
+
+ return null;
+ }
+
+ private void updateImages(final ContentResolver cr,
+ final String url, ContentValues values) {
+ if (!TextUtils.isEmpty(url)) {
+ values.put(Images.URL, url);
+ cr.update(BrowserContract.Images.CONTENT_URI, values, null, null);
+ }
+ }
+ }.execute();
+ }
+}
diff --git a/src/src/com/android/browser/BookmarksLoader.java b/src/src/com/android/browser/BookmarksLoader.java
new file mode 100644
index 00000000..9d551e30
--- /dev/null
+++ b/src/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/src/com/android/browser/BreadCrumbView.java b/src/src/com/android/browser/BreadCrumbView.java
new file mode 100644
index 00000000..1501d211
--- /dev/null
+++ b/src/src/com/android/browser/BreadCrumbView.java
@@ -0,0 +1,439 @@
+/*
+ * 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;
+ private TextView mOverflowView;
+
+ /**
+ * @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 addOverflowLabel(TextView view) {
+ mOverflowView = view;
+ if (view != null) {
+ view.setTextAppearance(mContext, R.style.BookmarkPathText);
+ view.setPadding(mCrumbPadding, 0, mCrumbPadding, 0);
+ view.setGravity(Gravity.CENTER_VERTICAL);
+ view.setText("... >");
+ }
+ }
+
+ 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 = 0;
+ if (mMaxVisible >= 0) {
+ int invisibleCrumbs = size() - mMaxVisible;
+ if (invisibleCrumbs > 0) {
+ int crumbIndex = 0;
+ if (mOverflowView != null) {
+ mOverflowView.setVisibility(VISIBLE);
+ mOverflowView.setOnClickListener(this);
+ }
+ while (crumbIndex < invisibleCrumbs) {
+ // Set the crumb to GONE.
+ mCrumbLayout.getChildAt(childIndex).setVisibility(View.GONE);
+ childIndex++;
+ // Move to the next crumb.
+ crumbIndex++;
+ }
+ } else {
+ if (mOverflowView != null) {
+ mOverflowView.setVisibility(GONE);
+ }
+ }
+ // 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 if (mOverflowView == v) {
+ int maxVisible = getMaxVisible();
+ while (maxVisible > 0) {
+ pop(false);
+ maxVisible--;
+ }
+ 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, R.style.BookmarkPathText);
+ 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/src/com/android/browser/Browser.java b/src/src/com/android/browser/Browser.java
new file mode 100644
index 00000000..8270cfbc
--- /dev/null
+++ b/src/src/com/android/browser/Browser.java
@@ -0,0 +1,151 @@
+/*
+ * 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.Manifest;
+import android.app.Activity;
+import android.app.Application;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.util.Log;
+import android.os.Process;
+
+import org.chromium.chrome.browser.ChromiumApplication;
+import org.chromium.chrome.browser.PKCS11AuthenticationManager;
+import org.codeaurora.swe.SWEEmptyPKCS11AuthenticationManager;
+
+import org.codeaurora.swe.Engine;
+
+public class Browser extends ChromiumApplication {
+
+ 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;
+
+ private static Context mContext;
+
+ public static Context getContext() {
+ return mContext;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ mContext = this;
+
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "Browser.onCreate: this=" + this);
+ }
+
+ registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
+ @Override
+ public void onActivityCreated(final Activity activity, Bundle savedInstanceState) {
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "Browser.onActivityCreated: activity=" + activity);
+ }
+ if (!(activity instanceof BrowserActivity) && !(activity instanceof BrowserLauncher) ) {
+ EngineInitializer.initializeSync((Context) Browser.this);
+ }
+ }
+
+ @Override
+ public void onActivityDestroyed(Activity activity) {
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "Browser.onActivityDestroyed: activity=" + activity);
+ }
+ }
+
+ @Override
+ public void onActivityPaused(Activity activity) {
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "Browser.onActivityPaused: activity=" + activity);
+ }
+ }
+
+ @Override
+ public void onActivityResumed(Activity activity) {
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "Browser.onActivityResumed: activity=" + activity);
+ }
+ }
+
+ @Override
+ public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "Browser.onActivitySaveInstanceState: activity=" + activity);
+ }
+ }
+
+ @Override
+ public void onActivityStarted(Activity activity) {
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "Browser.onActivityStarted: activity=" + activity);
+ }
+ }
+
+ @Override
+ public void onActivityStopped(Activity activity) {
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "Browser.onActivityStopped: activity=" + activity);
+ }
+ }
+ });
+
+ // Chromium specific initialization.
+ Engine.initializeApplicationParameters();
+
+ final boolean isSandboxContext = checkPermission(Manifest.permission.INTERNET,
+ Process.myPid(), Process.myUid()) != PackageManager.PERMISSION_GRANTED;
+
+ // SWE: Avoid initializing the engine for sandboxed processes.
+ if (!isSandboxContext) {
+ BrowserSettings.initialize((Context) this);
+ Preloader.initialize((Context) this);
+ }
+
+ }
+
+ @Override
+ protected PKCS11AuthenticationManager getPKCS11AuthenticationManager() {
+ return new SWEEmptyPKCS11AuthenticationManager();
+ }
+
+ @Override
+ protected void openProtectedContentSettings() {
+ }
+
+ @Override
+ protected boolean areParentalControlsEnabled() {
+ return false;
+ }
+
+ @Override
+ public String getSettingsActivityName() {
+ return null;
+ }
+
+ @Override
+ public void initCommandLine() {
+ }
+}
+
diff --git a/src/src/com/android/browser/BrowserActivity.java b/src/src/com/android/browser/BrowserActivity.java
new file mode 100644
index 00000000..af2fbfa0
--- /dev/null
+++ b/src/src/com/android/browser/BrowserActivity.java
@@ -0,0 +1,437 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ * Copyright (c) 2015, The Linux Foundation. All rights reserved.
+ *
+ * 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.KeyguardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.os.Process;
+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.ViewGroup;
+import android.view.Window;
+
+import org.chromium.base.VisibleForTesting;
+import com.android.browser.R;
+import com.android.browser.search.DefaultSearchEngine;
+import com.android.browser.search.SearchEngine;
+import com.android.browser.stub.NullController;
+
+import java.util.Locale;
+
+import org.codeaurora.net.NetworkServices;
+import org.codeaurora.swe.CookieManager;
+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 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 final Locale mCurrentLocale = Locale.getDefault();
+ public static boolean killOnExitDialog = false;
+
+
+ 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 Bundle mSavedInstanceState;
+ private EngineInitializer.ActivityScheduler mActivityScheduler;
+ public EngineInitializer.ActivityScheduler getScheduler() {
+ return mActivityScheduler;
+ }
+
+ @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 (!isTablet(this)) {
+ final ActionBar bar = getActionBar();
+ bar.hide();
+ }
+
+ // 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;
+ }
+ */
+
+ mActivityScheduler = EngineInitializer.onActivityCreate(BrowserActivity.this);
+
+ Thread.setDefaultUncaughtExceptionHandler(new CrashLogExceptionHandler(this));
+
+ mSavedInstanceState = icicle;
+ // Create the initial UI views
+ mController = createController();
+
+ // Workaround for the black screen flicker on SurfaceView creation
+ ViewGroup topLayout = (ViewGroup) findViewById(R.id.main_content);
+ topLayout.requestTransparentRegion(topLayout);
+
+ EngineInitializer.onPostActivityCreate(BrowserActivity.this);
+ }
+
+ 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;
+ }
+
+ public void startController() {
+ Intent intent = (mSavedInstanceState == null) ? getIntent() : null;
+ mController.start(intent);
+ }
+
+ @VisibleForTesting
+ //public to facilitate testing
+ public Controller getController() {
+ return (Controller) mController;
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ if (shouldIgnoreIntents()) return;
+ EngineInitializer.onNewIntent(BrowserActivity.this, intent);
+ // Note: Do not add any more application logic in this method.
+ // Move any additional app logic into handleOnNewIntent().
+ }
+
+ protected void handleOnNewIntent(Intent intent) {
+ 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 onStart() {
+ super.onStart();
+ EngineInitializer.onActivityStart(BrowserActivity.this);
+ if (!BrowserSettings.getInstance().isPowerSaveModeEnabled()) {
+ //Notify about anticipated network activity
+ NetworkServices.hintUpcomingUserActivity();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "BrowserActivity.onResume: this=" + this);
+ }
+ EngineInitializer.onActivityResume(BrowserActivity.this);
+ // Note: Do not add any more application logic in this method.
+ // Move any additional app logic into handleOnResume().
+ }
+
+ protected void handleOnResume() {
+ mController.onResume();
+ }
+
+ protected void handleOnStart() {
+ mController.onStart();
+ }
+
+ @Override
+ protected void onStop() {
+ EngineInitializer.onActivityStop(BrowserActivity.this);
+ super.onStop();
+ // Note: Do not add any more application logic in this method.
+ // Move any additional app logic into handleOnStop().
+ }
+
+ protected void handleOnStop() {
+ CookieManager.getInstance().flushCookieStore();
+ mController.onStop();
+ }
+
+ @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() {
+ EngineInitializer.onActivityPause(BrowserActivity.this);
+ super.onPause();
+ // Note: Do not add any more application logic in this method.
+ // Move any additional app logic into handleOnPause().
+ }
+
+ protected void handleOnPause() {
+ mController.onPause();
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "BrowserActivity.onDestroy: this=" + this);
+ }
+ super.onDestroy();
+ EngineInitializer.onActivityDestroy(BrowserActivity.this);
+ mController.onDestroy();
+ mController = NullController.INSTANCE;
+ if (!Locale.getDefault().equals(mCurrentLocale) || killOnExitDialog) {
+ Log.i(LOGTAG,"Force Killing Browser");
+ Process.killProcess(Process.myPid());
+ }
+
+ }
+
+ @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 void invalidateOptionsMenu() {
+ super.invalidateOptionsMenu();
+ mController.invalidateOptionsMenu();
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ return false;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ mUiController.getUi().hideComboView();
+ return true;
+ }
+ if (!mController.onOptionsItemSelected(item)) {
+ 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) {
+ EngineInitializer.onActivityResult(BrowserActivity.this, requestCode, resultCode, intent);
+ }
+
+ protected void handleOnActivityResult (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);
+ }
+
+}
diff --git a/src/src/com/android/browser/BrowserBackupAgent.java b/src/src/com/android/browser/BrowserBackupAgent.java
new file mode 100644
index 00000000..0f5fcd8c
--- /dev/null
+++ b/src/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/src/com/android/browser/BrowserBookmarksAdapter.java b/src/src/com/android/browser/BrowserBookmarksAdapter.java
new file mode 100644
index 00000000..a65753c6
--- /dev/null
+++ b/src/src/com/android/browser/BrowserBookmarksAdapter.java
@@ -0,0 +1,137 @@
+/*
+ * 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.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.browser.mdm.EditBookmarksRestriction;
+import com.android.browser.mdm.ManagedBookmarksRestriction;
+import com.android.browser.platformsupport.BrowserContract.Bookmarks;
+import com.android.browser.util.ThreadedCursorAdapter;
+import com.android.browser.view.BookmarkContainer;
+import com.android.browser.view.BookmarkThumbImageView;
+
+public class BrowserBookmarksAdapter extends
+ ThreadedCursorAdapter<BrowserBookmarksAdapterItem> {
+
+ private static final String TAG = "BrowserBookmarksAdapter";
+ 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);
+ }
+
+ 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);
+ }
+
+ @Override
+ public void bindView(View view, BrowserBookmarksAdapterItem item) {
+ BookmarkContainer c = (BookmarkContainer) view;
+
+ // 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 = mContext.getResources()
+ .getDimensionPixelSize(R.dimen.combo_horizontalSpacing);
+ c.setPadding(padding, c.getPaddingTop(), padding, c.getPaddingBottom());
+
+ // configure the main content of the bookmark icon
+ if (item.is_folder) {
+ c.reConfigureAsFolder(item.title.toString(), "");
+ } else {
+ final Bitmap favicon = (item.thumbnail == null || !item.has_thumbnail) ?
+ null : item.thumbnail.getBitmap();
+ c.reConfigureAsSite(favicon);
+ }
+
+ // configure the label under the bookmark
+ if (item.title != null) {
+ c.setBottomLabelText(item.title.toString());
+ }
+
+ // if the item is managed by mdm or edit bookmark restriction, show a badge
+ if (item.title != null &&
+ (item.is_mdm_managed || EditBookmarksRestriction.getInstance().isEnabled())) {
+ c.setOverlayBadge(item.is_mdm_managed ? R.drawable.img_deco_mdm_badge_bright :
+ R.drawable.ic_deco_secure);
+ } else
+ c.setOverlayBadge(0);
+ }
+
+ @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_TOUCH_ICON, thumbnail);
+ if (thumbnail == 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);
+ item.is_mdm_managed = ManagedBookmarksRestriction.getInstance().mDb.isMdmElement(getItemId(c));
+ return item;
+ }
+
+ @Override
+ public BrowserBookmarksAdapterItem getLoadingObject() {
+ BrowserBookmarksAdapterItem item = new BrowserBookmarksAdapterItem();
+ return item;
+ }
+}
diff --git a/src/src/com/android/browser/BrowserBookmarksAdapterItem.java b/src/src/com/android/browser/BrowserBookmarksAdapterItem.java
new file mode 100644
index 00000000..5fb648e1
--- /dev/null
+++ b/src/src/com/android/browser/BrowserBookmarksAdapterItem.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+ public boolean is_mdm_managed;
+}
diff --git a/src/src/com/android/browser/BrowserBookmarksPage.java b/src/src/com/android/browser/BrowserBookmarksPage.java
new file mode 100644
index 00000000..b627994e
--- /dev/null
+++ b/src/src/com/android/browser/BrowserBookmarksPage.java
@@ -0,0 +1,879 @@
+/*
+ * Copyright (c) 2014, Linux Foundation. All rights reserved.
+ * 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.graphics.Canvas;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.util.Log;
+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.ViewGroup;
+import android.widget.Button;
+import android.widget.ExpandableListView;
+import android.widget.ExpandableListView.OnChildClickListener;
+import android.widget.Toast;
+
+import com.android.browser.mdm.EditBookmarksRestriction;
+import com.android.browser.mdm.ManagedBookmarksRestriction;
+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;
+
+import org.codeaurora.net.NetworkServices;
+
+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 {
+
+ private static final String TAG = "BrowserBookmarksPage";
+
+ 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++;
+ }
+ if (id == LOADER_BOOKMARKS){ // Didn't find any bookmarks
+ Bundle args = new Bundle();
+ args.putString(ACCOUNT_NAME, "null");
+ args.putString(ACCOUNT_TYPE, "null");
+ BrowserBookmarksAdapter adapter = new BrowserBookmarksAdapter(
+ getActivity());
+ mBookmarkAdapters.put(id, adapter);
+ boolean expand = true;
+ try {
+ expand = mState.getBoolean(BookmarkExpandableView.LOCAL_ACCOUNT_NAME);
+ } catch (JSONException e) {} // no state for accountName
+ mGrid.addAccount("null", adapter, expand);
+ lm.restartLoader(id, args, this);
+ }
+ // 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);
+ }
+ }
+
+ @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.folder_edit_context_menu_id:
+ 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.folder_delete_context_menu_id:
+ 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;
+ boolean isMdmElem = ManagedBookmarksRestriction.getInstance().mDb.
+ isMdmElement(cursor.getLong(BookmarksLoader.COLUMN_INDEX_ID));
+ boolean isMdmEditRestricted = EditBookmarksRestriction.getInstance().isEnabled();
+
+ final Activity activity = getActivity();
+ MenuInflater inflater = activity.getMenuInflater();
+ inflater.inflate(R.menu.bookmarkscontext, menu);
+ if (isFolder) {
+ menu.setGroupVisible(R.id.FOLDER_CONTEXT_MENU, true);
+ if(isMdmElem || isMdmEditRestricted) {
+ menu.findItem(R.id.folder_edit_context_menu_id).setEnabled(false);
+ menu.findItem(R.id.folder_delete_context_menu_id).setEnabled(false);
+ }
+ } else {
+ menu.setGroupVisible(R.id.BOOKMARK_CONTEXT_MENU, true);
+ if (mDisableNewWindow) {
+ menu.findItem(R.id.new_window_context_menu_id).setVisible(false);
+ }
+ if(isMdmElem || isMdmEditRestricted) {
+ menu.findItem(R.id.edit_context_menu_id).setEnabled(false);
+ menu.findItem(R.id.delete_context_menu_id).setEnabled(false);
+ }
+ }
+ BookmarkItem header = new BookmarkItem(activity);
+ header.setEnableScrolling(true);
+ populateBookmarkItem(cursor, header, isFolder, isMdmElem);
+ 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;
+ }
+
+ public static Bitmap overlayBookmarkBitmap(Context context, Bitmap origImage, int overlayResId,
+ int containerWidth, int containerHeight,
+ float overlayScale, int overlayOffsetY,
+ int overlayMarginX) {
+ if (origImage == null) {
+ Log.e(TAG, "Orig Image is null!");
+ return origImage;
+ }
+
+ // Get metrics for incoming bitmap
+ int origWidth = origImage.getWidth();
+ int origHeight = origImage.getHeight();
+
+ // Compute final overlay scale factor based on container size
+ float willScale, overlayScaleFactor;
+ if (Math.abs(containerWidth - origWidth) > Math.abs(containerHeight - origHeight)) {
+ willScale = (float) containerWidth / (float) origWidth;
+ }
+ else {
+ willScale = (float) containerHeight / (float) origHeight;
+ }
+ overlayScaleFactor = overlayScale / willScale;
+
+ // Load the bitmap for the badge
+ Bitmap srcOverlay = BitmapFactory.decodeResource(context.getResources(), overlayResId);
+ if (srcOverlay == null) {
+ Log.e(TAG, "Overlay bitmap creation failed");
+ return origImage;
+ }
+
+ // Scale the badge
+ float fx = (float) srcOverlay.getWidth() * overlayScaleFactor;
+ float fy = (float) srcOverlay.getHeight() * overlayScaleFactor;
+ Bitmap scaledOverlay = null;
+ try {
+ scaledOverlay = Bitmap.createScaledBitmap(srcOverlay, (int) fx, (int) fy, true);
+ } catch (IllegalArgumentException exception) {
+ Log.e(TAG, "Scaled bitmap creation failed" + exception.getMessage());
+ }
+
+ if (scaledOverlay == null) {
+ srcOverlay.recycle();
+ Runtime.getRuntime().gc();
+ return origImage;
+ }
+
+ // Create the bitmap we are compositing into
+ Bitmap overlaid = null;
+ try {
+ overlaid = Bitmap.createBitmap(origWidth, origHeight, Bitmap.Config.ARGB_8888);
+ } catch (IllegalArgumentException exception) {
+ Log.e(TAG, "Composite bitmap creation failed" + exception.getMessage());
+ }
+
+ if (overlaid == null) {
+ srcOverlay.recycle();
+ scaledOverlay.recycle();
+ Runtime.getRuntime().gc();
+ return origImage;
+ }
+
+ // Do the overlay
+ Canvas comboImage = new Canvas(overlaid);
+ comboImage.drawBitmap(origImage, 0, 0, null);
+
+ // align overlay to right edge. Vertical alignment
+ // determined by overlayOffsetY
+ comboImage.drawBitmap(scaledOverlay,
+ (origWidth - scaledOverlay.getWidth()) - (overlayMarginX / willScale),
+ (overlayOffsetY / willScale),
+ null);
+
+ // Clean up our bitmaps
+ srcOverlay.recycle();
+ scaledOverlay.recycle();
+ Runtime.getRuntime().gc();
+
+ return overlaid;
+ }
+
+ private void populateBookmarkItem(Cursor cursor, BookmarkItem item,
+ boolean isFolder, boolean isMdmElem) {
+ item.setName(cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE));
+
+ // Fetch appropriate bitmap
+ Bitmap bitmap;
+ if (isFolder) {
+ item.setUrl(null);
+ bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_deco_folder_normal);
+ }
+ else {
+ String url = cursor.getString(BookmarksLoader.COLUMN_INDEX_URL);
+ item.setUrl(url);
+ bitmap = getBitmap(cursor, BookmarksLoader.COLUMN_INDEX_FAVICON);
+ if (null == bitmap) {
+ bitmap = BitmapFactory.decodeResource(getResources(),
+ R.drawable.ic_deco_favicon_normal);
+ }
+ }
+
+ // if mdm element or edit bookmark restriction enforced, overlay an indicator
+ if (isMdmElem || EditBookmarksRestriction.getInstance().isEnabled()) {
+ int containerSize = getResources().
+ getDimensionPixelSize(R.dimen.bookmark_widget_favicon_size); // it's square!
+ if (isMdmElem) {
+ bitmap = overlayBookmarkBitmap(getActivity(), bitmap,
+ R.drawable.img_deco_mdm_badge_bright,
+ containerSize, containerSize, 0.25f, 40, 0);
+ }
+ else if (EditBookmarksRestriction.getInstance().isEnabled()) {
+ bitmap = overlayBookmarkBitmap(getActivity(), bitmap,
+ R.drawable.ic_deco_secure,
+ containerSize, containerSize, 0.75f, 0, 0);
+ }
+ }
+
+ // Set the bitmap
+ item.setFavicon(bitmap);
+ if (isFolder) {
+ new LookupBookmarkCount(getActivity(), item)
+ .execute(cursor.getLong(BookmarksLoader.COLUMN_INDEX_ID));
+ }
+ }
+
+ /**
+ * 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);
+ if (mCallbacks == null && getActivity() instanceof CombinedBookmarksCallbacks) {
+ mCallbacks = new CombinedBookmarksCallbackWrapper(
+ (CombinedBookmarksCallbacks) getActivity());
+ }
+ if (mCallbacks == null) {
+ View cb = getActivity().getWindow().getDecorView().findViewById(R.id.combo_view_container);
+ if (cb != null && cb instanceof CombinedBookmarksCallbacks) {
+ mCallbacks = new CombinedBookmarksCallbackWrapper(
+ (CombinedBookmarksCallbacks) cb);
+ }
+ }
+ }
+
+ @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) {
+ final Activity activity = getActivity();
+ 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);
+
+ Button btn = (Button) mRoot.findViewById(R.id.add_bookmark_button);
+ btn.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ManagedBookmarksRestriction mbr = ManagedBookmarksRestriction.getInstance();
+ if ((mbr.isEnabled() && mbr.mDb.isMdmElement(mCurrentFolderId)) ||
+ EditBookmarksRestriction.getInstance().isEnabled()) {
+ Toast.makeText(getActivity().getApplicationContext(),
+ R.string.mdm_managed_alert, Toast.LENGTH_SHORT).show();
+ }
+ else {
+ 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);
+ }
+ }
+ });
+ EditBookmarksRestriction.getInstance().registerControl(btn);
+
+ btn = (Button) mRoot.findViewById(R.id.new_bmfolder_button);
+ btn.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ManagedBookmarksRestriction mbr = ManagedBookmarksRestriction.getInstance();
+ if ((mbr.isEnabled() && mbr.mDb.isMdmElement(mCurrentFolderId)) ||
+ EditBookmarksRestriction.getInstance().isEnabled()) {
+ Toast.makeText(getActivity().getApplicationContext(),
+ R.string.mdm_managed_alert, Toast.LENGTH_SHORT).show();
+ }
+ else {
+ Intent intent = new Intent(activity, AddBookmarkFolder.class);
+ intent.putExtra(BrowserContract.Bookmarks.PARENT, mCurrentFolderId);
+ activity.startActivity(intent);
+ }
+ }
+ });
+ EditBookmarksRestriction.getInstance().registerControl(btn);
+
+ // Start the loaders
+ LoaderManager lm = getLoaderManager();
+ lm.restartLoader(LOADER_ACCOUNTS, null, this);
+
+ if (!BrowserSettings.getInstance().isPowerSaveModeEnabled()) {
+ //Notify about anticipated network activity
+ NetworkServices.hintUpcomingUserActivity();
+ }
+
+ EditBookmarksRestriction.getInstance().registerView(mGrid);
+
+ 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()) {
+ mBookmarkAdapters.get(id).quitThread();
+ lm.destroyLoader(id);
+ }
+ mBookmarkAdapters.clear();
+
+ EditBookmarksRestriction.getInstance().registerView(null);
+ }
+
+ 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,
+ (cursor.getInt(BookmarksLoader.COLUMN_INDEX_IS_FOLDER) == 1));
+ }
+
+ 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);
+ }
+
+ /**
+ * 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 = null;
+ try {
+ c = mContext.getContentResolver().query(uri,
+ null, BrowserContract.Bookmarks.PARENT + "=?",
+ new String[] {params[0].toString()}, null);
+ return c.getCount();
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ @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/src/com/android/browser/BrowserConfigBase.java b/src/src/com/android/browser/BrowserConfigBase.java
new file mode 100644
index 00000000..e2ef7797
--- /dev/null
+++ b/src/src/com/android/browser/BrowserConfigBase.java
@@ -0,0 +1,139 @@
+/*
+ * 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;
+
+import com.android.browser.R;
+
+import java.util.Locale;
+
+import android.os.Build;
+import android.content.Context;
+import android.text.TextUtils;
+
+import org.codeaurora.swe.BrowserCommandLine;
+
+abstract class BrowserConfigBase {
+
+
+ private Context mContext;
+
+ public BrowserConfigBase(Context context) {
+ mContext = context;
+ }
+
+ public void overrideUserAgent() {
+ // Check if the UA is already present using command line file
+ if (BrowserCommandLine.hasSwitch(BrowserSwitches.OVERRIDE_USER_AGENT)) {
+ return;
+ }
+
+ String ua = mContext.getResources().getString(R.string.def_useragent);
+
+ if (TextUtils.isEmpty(ua))
+ return;
+
+ ua = constructUserAgent(ua);
+
+ if (!TextUtils.isEmpty(ua)){
+ BrowserCommandLine.appendSwitchWithValue(BrowserSwitches.OVERRIDE_USER_AGENT, ua);
+ }
+ }
+
+ public void overrideMediaDownload() {
+ boolean defaultAllowMediaDownloadsValue = mContext.getResources().getBoolean(
+ R.bool.def_allow_media_downloads);
+ if (defaultAllowMediaDownloadsValue)
+ BrowserCommandLine.appendSwitchWithValue(BrowserSwitches.OVERRIDE_MEDIA_DOWNLOAD, "1");
+ }
+
+ public void setExtraHTTPRequestHeaders() {
+ String headers = mContext.getResources().getString(
+ R.string.def_extra_http_headers);
+ if (!TextUtils.isEmpty(headers))
+ BrowserCommandLine.appendSwitchWithValue(BrowserSwitches.HTTP_HEADERS, headers);
+ }
+
+ public void initCommandLineSwitches() {
+ //SWE-hide-title-bar - enable following flags
+ BrowserCommandLine.appendSwitchWithValue(BrowserSwitches.TOP_CONTROLS_SHOW_THRESHOLD, "0.5");
+ BrowserCommandLine.appendSwitchWithValue(BrowserSwitches.TOP_CONTROLS_HIDE_THRESHOLD, "0.5");
+
+ // Allow to override UserAgent
+ overrideUserAgent();
+ overrideMediaDownload();
+ setExtraHTTPRequestHeaders();
+ }
+
+ private String constructUserAgent(String userAgent) {
+ try {
+ userAgent = userAgent.replaceAll("<%build_model>", Build.MODEL);
+ userAgent = userAgent.replaceAll("<%build_version>", Build.VERSION.RELEASE);
+ userAgent = userAgent.replaceAll("<%build_id>", Build.ID);
+ userAgent = userAgent.replaceAll("<%language>", Locale.getDefault().getLanguage());
+ userAgent = userAgent.replaceAll("<%country>", Locale.getDefault().getCountry());
+ return userAgent;
+ } catch (Exception ex) {
+ return null;
+ }
+ }
+
+ public static enum Feature {
+ WAP2ESTORE, /* Launch custom app when URL scheme is 'estore:' */
+ DRM_UPLOADS, /* Prevent uploading files with DRM filename extensions */
+ NETWORK_NOTIFIER, /* Prompt user to select WiFi access point or otherwise enable WLAN */
+ EXIT_DIALOG, /* Add 'Exit' menu item and show 'Minimize or quit' dialog */
+ TITLE_IN_URL_BAR, /* Display page title instead of url in URL bar */
+ CUSTOM_DOWNLOAD_PATH, /* Allow users to provide custom download path */
+ DISABLE_HISTORY /* Allow disabling saving history for non-incognito tabs */
+ }
+
+ public boolean hasFeature(Feature feature) {
+ switch (feature) {
+ case WAP2ESTORE:
+ return mContext.getResources().getBoolean(R.bool.feature_wap2estore);
+ case DRM_UPLOADS:
+ return mContext.getResources().getBoolean(R.bool.feature_drm_uploads);
+ case NETWORK_NOTIFIER:
+ return mContext.getResources().getBoolean(R.bool.feature_network_notifier);
+ case EXIT_DIALOG:
+ return mContext.getResources().getBoolean(R.bool.feature_exit_dialog);
+ case TITLE_IN_URL_BAR:
+ return mContext.getResources().getBoolean(R.bool.feature_title_in_URL_bar);
+ case CUSTOM_DOWNLOAD_PATH:
+ return mContext.getResources().getBoolean(R.bool.feature_custom_download_path);
+ case DISABLE_HISTORY:
+ return mContext.getResources().getBoolean(R.bool.feature_disable_history);
+ default:
+ return false;
+ }
+ }
+}
+
diff --git a/src/src/com/android/browser/BrowserHistoryPage.java b/src/src/com/android/browser/BrowserHistoryPage.java
new file mode 100644
index 00000000..282ad6cf
--- /dev/null
+++ b/src/src/com/android/browser/BrowserHistoryPage.java
@@ -0,0 +1,691 @@
+/*
+ * 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.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+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.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.Button;
+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.Browser;
+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 static Bitmap sDefaultFavicon;
+
+ 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);
+
+ Bundle args = getArguments();
+ mDisableNewWindow = args.getBoolean(BrowserBookmarksPage.EXTRA_DISABLE_WINDOW, false);
+ int mvlimit = getResources().getInteger(R.integer.most_visits_limit);
+ mMostVisitsLimit = Integer.toString(mvlimit);
+ if (mCallback == null && getActivity() instanceof CombinedBookmarksCallbacks) {
+ mCallback = (CombinedBookmarksCallbacks) getActivity();
+ } else {
+ View cb = getActivity().getWindow().getDecorView().findViewById(R.id.combo_view_container);
+ if (cb != null && cb instanceof CombinedBookmarksCallbacks) {
+ mCallback = (CombinedBookmarksCallbacks) cb;
+ }
+ }
+ if (sDefaultFavicon == null) {
+ sDefaultFavicon = BitmapFactory.decodeResource(
+ this.getResources(), R.drawable.ic_deco_favicon_normal);
+ }
+ }
+
+ @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();
+ }
+ Button btn = (Button) mRoot.findViewById(R.id.clear_history_button);
+ btn.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ promptToClearHistory();
+ }
+ });
+
+ // Start the loaders
+ getLoaderManager().restartLoader(LOADER_HISTORY, null, this);
+ getLoaderManager().restartLoader(LOADER_MOST_VISITED, null, this);
+
+ return mRoot;
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ Resources res = getActivity().getResources();
+ int paddingTop = (int) res.getDimension(R.dimen.combo_paddingTop);
+ mRoot.setPadding(0, paddingTop, 0, 0);
+ }
+
+ 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);
+ }
+
+ 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();
+ }
+
+ 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:
+ Browser.sendString(activity, url,
+ activity.getText(R.string.choosertitle_sharevia).toString());
+ return true;
+ case R.id.copy_url_context_menu_id:
+ copy(url);
+ return true;
+ case R.id.delete_context_menu_id:
+ Browser.deleteFromHistory(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);
+ item.setTag(R.id.group_position, groupPosition);
+ item.setTag(R.id.child_position, childPosition);
+ item.setTag(R.id.combo_view_container, mHistoryList);
+ } else {
+ item = (HistoryItem) convertView;
+ item.setTag(R.id.group_position, groupPosition);
+ item.setTag(R.id.child_position, childPosition);
+ item.setTag(R.id.combo_view_container, mHistoryList);
+ }
+
+ // 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));
+ } else {
+ item.setFavicon(sDefaultFavicon);
+ }
+ item.setIsBookmark(cursor.getInt(HistoryQuery.INDEX_IS_BOOKMARK) == 1);
+ return item;
+ }
+ }
+}
diff --git a/src/src/com/android/browser/BrowserLauncher.java b/src/src/com/android/browser/BrowserLauncher.java
new file mode 100644
index 00000000..a50b5196
--- /dev/null
+++ b/src/src/com/android/browser/BrowserLauncher.java
@@ -0,0 +1,52 @@
+/*
+ * 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;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+// This is a NoShow activity for chain-loading BrowserActivity.
+// Avoid doing any heavy operations in this activity.
+public class BrowserLauncher extends Activity {
+
+ @Override
+ public void onCreate(Bundle paramBundle)
+ {
+ super.onCreate(paramBundle);
+ Intent localIntent = new Intent(getIntent());
+ localIntent.setClassName(getApplicationContext().getPackageName(), BrowserActivity.class.getName());
+ localIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(localIntent);
+ finish();
+ }
+}
diff --git a/src/src/com/android/browser/BrowserLocationListPreference.java b/src/src/com/android/browser/BrowserLocationListPreference.java
new file mode 100644
index 00000000..1bff5989
--- /dev/null
+++ b/src/src/com/android/browser/BrowserLocationListPreference.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2015, 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.content.Context;
+import android.content.Intent;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.provider.Settings;
+import android.util.AttributeSet;
+import android.view.View;
+
+import org.codeaurora.swe.PermissionsServiceFactory;
+
+public class BrowserLocationListPreference extends ListPreference {
+
+ View mView;
+ private boolean mSwitchEnabled = true; //internal state tracker
+ private OnPreferenceClickListener onPreferenceClickListener;
+ private OnPreferenceChangeListener oldPreferenceChangeListener;
+
+ public BrowserLocationListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public BrowserLocationListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public BrowserLocationListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public BrowserLocationListPreference(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onBindView(View view) {
+ super.onBindView(view);
+ mView = view;
+ if (oldPreferenceChangeListener == null){ //set it just the first time
+ oldPreferenceChangeListener = getOnPreferenceChangeListener();
+ }
+ if (mView != null && mSwitchEnabled) {
+ mView.setAlpha((float) 1.0);
+ }
+ else if (mView != null){
+ mView.setAlpha((float) 0.5); //Gray out the option
+ }
+ }
+
+ @Override
+ public void onClick(){
+ //This shows the popup
+ if (PermissionsServiceFactory.isSystemLocationEnabled() && mSwitchEnabled) super.onClick();
+ else {
+ Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
+ getContext().startActivity(intent);
+ };
+ }
+
+ @Override
+ public void setEnabled(boolean enable){
+ // setEnabled will not call super.setEnabled(enable) because
+ // we want to avoid the default behavior entirely.
+ if (!mSwitchEnabled && enable) { //Transition from off to on
+ if(mView != null)
+ mView.setAlpha((float) 1.0);
+ setOnPreferenceClickListener(onPreferenceClickListener);
+ setOnPreferenceChangeListener(oldPreferenceChangeListener);
+ }
+ else if(!enable && mSwitchEnabled) { //Transition from on to off
+ if (mView != null) {
+ mView.setAlpha((float) 0.5); //Gray out the option
+ }
+ onPreferenceClickListener = getOnPreferenceClickListener();
+ if (oldPreferenceChangeListener == null) //to protect against calling !enable onresume()
+ oldPreferenceChangeListener = getOnPreferenceChangeListener();
+
+ // Prevent clicks from registering.
+ setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ return false; // Do Not update
+ }
+ });
+ }
+ mSwitchEnabled = enable;
+ }
+}
diff --git a/src/src/com/android/browser/BrowserLocationSwitchPreference.java b/src/src/com/android/browser/BrowserLocationSwitchPreference.java
new file mode 100644
index 00000000..6e57fe25
--- /dev/null
+++ b/src/src/com/android/browser/BrowserLocationSwitchPreference.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2015, 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.content.Context;
+import android.content.Intent;
+import android.preference.Preference;
+import android.preference.SwitchPreference;
+import android.provider.Settings;
+import android.util.AttributeSet;
+import android.view.View;
+
+import org.codeaurora.swe.PermissionsServiceFactory;
+
+public class BrowserLocationSwitchPreference extends SwitchPreference {
+
+ View mView;
+ private boolean mSwitchEnabled = true; //internal state tracker
+ private OnPreferenceClickListener onPreferenceClickListener;
+ private OnPreferenceChangeListener oldPreferenceChangeListener;
+
+ public BrowserLocationSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public BrowserLocationSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public BrowserLocationSwitchPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public BrowserLocationSwitchPreference(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onBindView(View view) {
+ super.onBindView(view);
+ mView = view;
+ if (oldPreferenceChangeListener == null){ //set it just the first time
+ oldPreferenceChangeListener = getOnPreferenceChangeListener();
+ }
+ if (mView != null && mSwitchEnabled) {
+ mView.setAlpha((float) 1.0);
+ }
+ else if (mView != null){
+ mView.setAlpha((float) 0.5); //Gray out the option
+ }
+ }
+
+ @Override
+ public void onClick(){
+ //This toggles teh switch
+ if (PermissionsServiceFactory.isSystemLocationEnabled() && mSwitchEnabled) super.onClick();
+ else {
+ Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
+ getContext().startActivity(intent);
+ };
+ }
+
+ @Override
+ public void setEnabled(boolean enable){
+ // setEnabled will not call super.setEnabled(enable) because
+ // we want to avoid the default behavior entirely.
+ if (!mSwitchEnabled && enable) { //Transition from off to on
+ if(mView != null)
+ mView.setAlpha((float) 1.0);
+ setOnPreferenceClickListener(onPreferenceClickListener);
+ setOnPreferenceChangeListener(oldPreferenceChangeListener);
+ }
+ else if(!enable && mSwitchEnabled) { //Transition from on to off
+ if (mView != null) {
+ mView.setAlpha((float) 0.5); //Gray out the option
+ }
+ onPreferenceClickListener = getOnPreferenceClickListener();
+ if (oldPreferenceChangeListener == null) //to protect against calling !enable onresume()
+ oldPreferenceChangeListener = getOnPreferenceChangeListener();
+
+ // Prevent clicks from registering.
+ setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ return false; // Do Not update
+ }
+ });
+ }
+ mSwitchEnabled = enable;
+ }
+}
diff --git a/src/src/com/android/browser/BrowserPreferencesPage.java b/src/src/com/android/browser/BrowserPreferencesPage.java
new file mode 100644
index 00000000..46fde6a0
--- /dev/null
+++ b/src/src/com/android/browser/BrowserPreferencesPage.java
@@ -0,0 +1,149 @@
+/*
+ * 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.Fragment;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+import android.text.TextUtils;
+import android.view.MenuItem;
+
+import com.android.browser.preferences.AboutPreferencesFragment;
+import com.android.browser.preferences.GeneralPreferencesFragment;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+
+public class BrowserPreferencesPage extends Activity {
+ public static String sResultExtra;
+ private static ArrayList<String> sUpdatedUrls =
+ new ArrayList<String>(); //List of URLS for whom settings were updated
+
+ public static void startPreferencesForResult(Activity callerActivity, String url, int requestCode) {
+ final Intent intent = new Intent(callerActivity, BrowserPreferencesPage.class);
+ intent.putExtra(GeneralPreferencesFragment.EXTRA_CURRENT_PAGE, url);
+ callerActivity.startActivityForResult(intent, requestCode);
+ }
+
+ public static void startPreferenceFragmentForResult(Activity callerActivity, String fragmentName, int requestCode) {
+ final Intent intent = new Intent(callerActivity, BrowserPreferencesPage.class);
+ intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT, fragmentName);
+ callerActivity.startActivityForResult(intent, requestCode);
+ }
+
+ public static void startPreferenceFragmentExtraForResult(Activity callerActivity,
+ String fragmentName,
+ Bundle bundle,
+ int requestCode) {
+ final Intent intent = new Intent(callerActivity, BrowserPreferencesPage.class);
+ intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT, fragmentName);
+ intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle);
+ callerActivity.startActivityForResult(intent, requestCode);
+ }
+
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ if (icicle != null) {
+ return;
+ }
+
+ sResultExtra = "";
+ sUpdatedUrls.clear();
+ Intent intent = getIntent();
+ if (intent != null) {
+ String action = intent.getAction();
+ // check if this page was invoked by 'App Data Usage' on the global data monitor
+ if ("android.intent.action.MANAGE_NETWORK_USAGE".equals(action)) {
+ // TODO: switch to the Network fragment here?
+ }
+
+ Bundle extras = intent.getExtras();
+ if (extras == null)
+ return;
+
+ String fragment = (String) extras.getCharSequence(PreferenceActivity.EXTRA_SHOW_FRAGMENT);
+ if (fragment != null) {
+ try {
+ Class<?> cls = Class.forName(fragment);
+ Constructor<?> ctor = cls.getConstructor();
+ Object obj = ctor.newInstance();
+
+ if (obj instanceof Fragment) {
+ Fragment frag = (Fragment) obj;
+
+ Bundle bundle = extras.getBundle(PreferenceActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS);
+ if (bundle != null) {
+ frag.setArguments(bundle);
+ }
+
+ getFragmentManager().beginTransaction().replace(
+ android.R.id.content,
+ (Fragment) obj).commit();
+ }
+ } catch (ClassNotFoundException e) {
+ } catch (NoSuchMethodException e) {
+ } catch (InvocationTargetException e) {
+ } catch (InstantiationException e) {
+ } catch (IllegalAccessException e) {
+ }
+ return;
+ }
+ }
+
+ getFragmentManager().beginTransaction().replace(android.R.id.content,
+ new GeneralPreferencesFragment()).commit();
+ }
+
+ @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 super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void finish() {
+ if (!TextUtils.isEmpty(sResultExtra)) {
+ Intent intent = this.getIntent();
+ intent.putExtra(Intent.EXTRA_TEXT, sResultExtra);
+ intent.putStringArrayListExtra(Controller.EXTRA_UPDATED_URLS, sUpdatedUrls);
+ this.setResult(RESULT_OK, intent);
+ }
+ super.finish();
+ }
+
+ public static void onUrlNeedsReload(String url) {
+ String host = (Uri.parse(url)).getHost();
+ if (!sUpdatedUrls.contains(host)) {
+ sUpdatedUrls.add(host);
+ }
+ }
+}
diff --git a/src/src/com/android/browser/BrowserSettings.java b/src/src/com/android/browser/BrowserSettings.java
new file mode 100644
index 00000000..fef0b446
--- /dev/null
+++ b/src/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.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Build;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.util.DisplayMetrics;
+import android.webkit.ValueCallback;
+import android.webkit.WebStorage;
+
+import com.android.browser.homepages.HomeProvider;
+import com.android.browser.mdm.AutoFillRestriction;
+import com.android.browser.mdm.DevToolsRestriction;
+import com.android.browser.mdm.DoNotTrackRestriction;
+import com.android.browser.mdm.DownloadDirRestriction;
+import com.android.browser.mdm.EditBookmarksRestriction;
+import com.android.browser.mdm.IncognitoRestriction;
+import com.android.browser.mdm.ManagedBookmarksRestriction;
+import com.android.browser.mdm.ProxyRestriction;
+import com.android.browser.mdm.SearchEngineRestriction;
+import com.android.browser.mdm.ThirdPartyCookiesRestriction;
+import com.android.browser.mdm.URLFilterRestriction;
+import com.android.browser.platformsupport.Browser;
+import com.android.browser.provider.BrowserProvider;
+import com.android.browser.search.SearchEngine;
+import com.android.browser.search.SearchEngines;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import org.codeaurora.swe.AutoFillProfile;
+import org.codeaurora.swe.CookieManager;
+import org.codeaurora.swe.GeolocationPermissions;
+import org.codeaurora.swe.PermissionsServiceFactory;
+import org.codeaurora.swe.WebRefiner;
+import org.codeaurora.swe.WebSettings.LayoutAlgorithm;
+import org.codeaurora.swe.WebSettings.TextSize;
+import org.codeaurora.swe.WebSettings;
+import org.codeaurora.swe.WebView;
+import org.codeaurora.swe.WebViewDatabase;
+
+/**
+ * Class for managing settings
+ */
+public class BrowserSettings implements OnSharedPreferenceChangeListener,
+ PreferenceKeys {
+
+ 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 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;
+
+ private boolean mEngineInitialized = false;
+ private boolean mSyncManagedSettings = false;
+
+ public static synchronized void initialize(final Context context) {
+ if (sInstance == null)
+ sInstance = new BrowserSettings(context);
+ }
+
+ public static BrowserSettings getInstance() {
+ return sInstance;
+ }
+
+ private BrowserSettings(Context context) {
+ mContext = context.getApplicationContext();
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext);
+ mManagedSettings = new LinkedList<WeakReference<WebSettings>>();
+ BackgroundHandler.execute(mSetup);
+ }
+
+ public void setController(Controller controller) {
+ mController = controller;
+ mNeedsSharedSync = true;
+ }
+
+ public void onEngineInitializationComplete() {
+ mEngineInitialized = true;
+
+ // Intialize Web Refiner only once
+ final WebRefiner refiner = WebRefiner.getInstance();
+ if (refiner != null) {
+ mPrefs.edit().putBoolean(PREF_WEB_REFINER, true).apply();
+ refiner.setDefaultPermission(PermissionsServiceFactory.getDefaultPermissions(
+ PermissionsServiceFactory.PermissionType.WEBREFINER));
+
+ PermissionsServiceFactory.getPermissionsService(
+ new ValueCallback<PermissionsServiceFactory.PermissionsService>() {
+ @Override
+ public void onReceiveValue(
+ PermissionsServiceFactory.PermissionsService value) {
+ Set<String> origins = value.getOrigins();
+ ArrayList<String> allowList = new ArrayList<>();
+ ArrayList<String> blockList = new ArrayList<>();
+ for (String origin : origins) {
+ PermissionsServiceFactory.PermissionsService.OriginInfo
+ info = value.getOriginInfo(origin);
+ if (info == null) {
+ continue;
+ }
+ int perm = info.getPermission(
+ PermissionsServiceFactory.PermissionType.WEBREFINER);
+ if (perm == PermissionsServiceFactory.Permission.ALLOW) {
+ allowList.add(origin);
+ } else if (perm == PermissionsServiceFactory.Permission.BLOCK) {
+ blockList.add(origin);
+ }
+ }
+ if (!allowList.isEmpty()) {
+ refiner.setPermissionForOrigins(
+ allowList.toArray(new String[allowList.size()]), true);
+ }
+
+ if (!blockList.isEmpty()) {
+ refiner.setPermissionForOrigins(
+ blockList.toArray(new String[blockList.size()]), false);
+ }
+ }
+ }
+ );
+ } else {
+ mPrefs.edit().putBoolean(PREF_WEB_REFINER, false).apply();
+ }
+
+ mAutofillHandler = new AutofillHandler(mContext);
+ if (mSyncManagedSettings) {
+ syncManagedSettings();
+ }
+ if (mNeedsSharedSync) {
+ syncSharedSettings();
+ }
+
+ // Instantiate all MDM Restriction Singletons.
+ AutoFillRestriction.getInstance();
+ DevToolsRestriction.getInstance();
+ DoNotTrackRestriction.getInstance();
+ DownloadDirRestriction.getInstance();
+ EditBookmarksRestriction.getInstance();
+ IncognitoRestriction.getInstance();
+ ManagedBookmarksRestriction.getInstance();
+ ProxyRestriction.getInstance();
+ SearchEngineRestriction.getInstance();
+ ThirdPartyCookiesRestriction.getInstance();
+ URLFilterRestriction.getInstance();
+ }
+
+ 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;
+ }
+ }
+ }
+
+ 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
+ sFactoryResetUrl = mContext.getResources().getString(R.string.homepage_base);
+
+ if (!mPrefs.contains(PREF_DEFAULT_TEXT_ENCODING)) {
+ mPrefs.edit().putString(PREF_DEFAULT_TEXT_ENCODING, "auto").apply();
+ }
+
+ if (!mPrefs.contains(PREF_EDGE_SWIPE)) {
+ mPrefs.edit().putString(PREF_EDGE_SWIPE,
+ mContext.getResources().getString(
+ R.string.value_unknown_edge_swipe)).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.setMinimumFontSize(getMinimumFontSize());
+ settings.setMinimumLogicalFontSize(getMinimumFontSize());
+ settings.setTextZoom(getTextZoom());
+ settings.setLayoutAlgorithm(getLayoutAlgorithm());
+ settings.setJavaScriptCanOpenWindowsAutomatically(!blockPopupWindows());
+ settings.setLoadsImagesAutomatically(loadImages());
+ settings.setLoadWithOverviewMode(loadPageInOverviewMode());
+ settings.setSavePassword(rememberPasswords());
+ settings.setSaveFormData(saveFormdata());
+ settings.setUseWideViewPort(isWideViewport());
+ settings.setDoNotTrack(doNotTrack());
+ settings.setNightModeEnabled(isNightModeEnabled());
+ settings.setMediaPlaybackRequiresUserGesture(false);
+
+ 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(true);
+
+ // 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);
+ settings.setFullscreenSupported(true);
+
+ //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;
+ CookieManager.getInstance().setAcceptCookie(acceptCookies());
+
+ }
+
+ private void syncManagedSettings() {
+ if (!mEngineInitialized) {
+ mSyncManagedSettings = true;
+ return;
+ }
+ mSyncManagedSettings = false;
+ 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_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.TEXT_AUTOSIZING;
+ }
+ 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() {
+ 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 WebView getTopWebView(){
+ if (mController!= null)
+ return mController.getCurrentTopWebView();
+
+ return null;
+ }
+
+ 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.clearHttpAuthUsernamePassword();
+ }
+
+ public void clearDatabases() {
+ WebStorage.getInstance().deleteAllData();
+ }
+
+ public void clearLocationAccess() {
+ GeolocationPermissions.getInstance().clearAll();
+ if (GeolocationPermissions.isIncognitoCreated()) {
+ GeolocationPermissions.getIncognitoInstance().clearAll();
+ }
+ }
+
+ public void resetDefaultPreferences() {
+ WebRefiner webRefiner = WebRefiner.getInstance();
+ if (webRefiner != null) {
+ List<String> webrefiner_list = PermissionsServiceFactory.getOriginsForPermission(
+ PermissionsServiceFactory.PermissionType.WEBREFINER);
+ if (!webrefiner_list.isEmpty()) {
+ String[] origins = webrefiner_list.toArray(new String[webrefiner_list.size()]);
+ webRefiner.useDefaultPermissionForOrigins(origins);
+ }
+ }
+
+ PermissionsServiceFactory.resetDefaultPermissions();
+ mPrefs.edit().clear().apply();
+
+ resetCachedValues();
+
+ if (webRefiner != null) {
+ mPrefs.edit().putBoolean(PREF_WEB_REFINER, true).apply();
+ } else {
+ mPrefs.edit().putBoolean(PREF_WEB_REFINER, false).apply();
+ }
+ 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 && view.getUseDesktopUserAgent();
+ }
+
+ public void toggleDesktopUseragent(WebView view) {
+ if (view == null) {
+ return;
+ }
+ if (hasDesktopUseragent(view))
+ view.setUseDesktopUserAgent(false, true);
+ else
+ view.setUseDesktopUserAgent(true, true);
+ }
+
+ 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() {
+ // The following is a NOP if the SEARCH_ENGINE restriction has already been created. Otherwise,
+ // it creates the restriction and if enabled it sets the <default_search_engine_value>.
+ SearchEngineRestriction.getInstance();
+
+ 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);
+ }
+
+
+ public boolean loadPageInOverviewMode() {
+ return mPrefs.getBoolean(PREF_LOAD_PAGE, true);
+ }
+
+ public boolean autofitPages() {
+ return mPrefs.getBoolean(PREF_AUTOFIT_PAGES, true);
+ }
+
+ public boolean blockPopupWindows() {
+ return !PermissionsServiceFactory.getDefaultPermissions(
+ PermissionsServiceFactory.PermissionType.POPUP);
+ }
+
+ public boolean loadImages() {
+ return mPrefs.getBoolean(PREF_LOAD_IMAGES, true);
+ }
+
+ public String getDefaultTextEncoding() {
+ return mPrefs.getString(PREF_DEFAULT_TEXT_ENCODING, "auto");
+ }
+
+ public String getEdgeSwipeAction() {
+ return mPrefs.getString(PREF_EDGE_SWIPE,
+ mContext.getResources().getString(R.string.value_unknown_edge_swipe));
+ }
+
+ public void setEdgeSwipeTemporal() {
+ mPrefs.edit().putString(PREF_EDGE_SWIPE,
+ mContext.getResources().getString(R.string.value_temporal_edge_swipe)).apply();
+ }
+
+ public void setEdgeSwipeSpatial() {
+ mPrefs.edit().putString(PREF_EDGE_SWIPE,
+ mContext.getResources().getString(R.string.value_spatial_edge_swipe)).apply();
+ }
+
+ public void setEdgeSwipeDisabled() {
+ mPrefs.edit().putString(PREF_EDGE_SWIPE,
+ mContext.getResources().getString(R.string.value_disable_edge_swipe)).apply();
+ }
+
+ // -----------------------------
+ // 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();
+ }
+
+ public boolean isPowerSaveModeEnabled() {
+ return mPrefs.getBoolean(PREF_POWERSAVE_ENABLED, false);
+ }
+
+ public void setPowerSaveModeEnabled(boolean value) {
+ mPrefs.edit().putBoolean(PREF_POWERSAVE_ENABLED, value).apply();
+ }
+
+ public boolean isNightModeEnabled() {
+ return mPrefs.getBoolean(PREF_NIGHTMODE_ENABLED, false);
+ }
+
+ public void setNightModeEnabled(boolean value) {
+ mPrefs.edit().putBoolean(PREF_NIGHTMODE_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 boolean isDisablePerfFeatures() {
+ // This value is flipped in the prefs.
+ return !mPrefs.getBoolean(PREF_DISABLE_PERF, true);
+ }
+
+ // -----------------------------
+ // 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 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 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 doNotTrack() {
+ boolean dntVal;
+ if (DoNotTrackRestriction.getInstance().isEnabled()) {
+ dntVal = DoNotTrackRestriction.getInstance().getValue();
+ }
+ else {
+ dntVal = mPrefs.getBoolean(PREF_DO_NOT_TRACK, true);
+ }
+ return dntVal;
+ }
+
+ public boolean acceptCookies() {
+ return PermissionsServiceFactory.getDefaultPermissions(
+ PermissionsServiceFactory.PermissionType.COOKIE);
+ }
+
+ 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/src/com/android/browser/BrowserSnapshotPage.java b/src/src/com/android/browser/BrowserSnapshotPage.java
new file mode 100644
index 00000000..a5a35195
--- /dev/null
+++ b/src/src/com/android/browser/BrowserSnapshotPage.java
@@ -0,0 +1,328 @@
+/*
+ * 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.content.res.Configuration;
+import android.content.res.Resources;
+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;
+ private static Bitmap sDefaultFavicon;
+
+ GridView mGrid;
+ View mEmpty;
+ SnapshotAdapter mAdapter;
+ CombinedBookmarksCallbacks mCallback;
+ long mAnimateId;
+
+ View mRoot;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (mCallback == null && getActivity() instanceof CombinedBookmarksCallbacks) {
+ mCallback = (CombinedBookmarksCallbacks) getActivity();
+ } else {
+ View cb = getActivity().getWindow().getDecorView().findViewById(R.id.combo_view_container);
+ if (cb != null && cb instanceof CombinedBookmarksCallbacks) {
+ mCallback = (CombinedBookmarksCallbacks) cb;
+ }
+ }
+ mAnimateId = getArguments().getLong(EXTRA_ANIMATE_ID);
+ if (sDefaultFavicon == null)
+ sDefaultFavicon = BitmapFactory.decodeResource(getResources(),
+ R.drawable.ic_deco_favicon_normal);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.snapshots, container, false);
+ mRoot = view;
+ 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 onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ Resources res = getActivity().getResources();
+ int paddingTop = (int) res.getDimension(R.dimen.combo_paddingTop);
+ mRoot.setPadding(0, paddingTop, 0, 0);
+ }
+
+ @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));
+ Bitmap favicon = getBitmap(cursor, SNAPSHOT_FAVICON);
+ if (favicon != null) {
+ item.setFavicon(favicon);
+ } else {
+ item.setFavicon(sDefaultFavicon);
+ }
+
+ }
+
+ 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));
+ 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/src/com/android/browser/BrowserSwitches.java b/src/src/com/android/browser/BrowserSwitches.java
new file mode 100644
index 00000000..1a9767e4
--- /dev/null
+++ b/src/src/com/android/browser/BrowserSwitches.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2015 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;
+
+// Contains all of the command line switches that are specific to the SWE Browser
+
+public class BrowserSwitches {
+ //Command line flag for strict mode
+ public final static String STRICT_MODE = "enable-strict-mode";
+
+ // Command line flag for single-process mode.
+ // Must match the value of kSingleProcess in content_switches.cc
+ public static final String SINGLE_PROCESS = "single-process";
+
+ //SWE TODO : Add description for each switch.
+
+ public static final String OVERRIDE_USER_AGENT = "user-agent";
+
+ public static final String OVERRIDE_MEDIA_DOWNLOAD = "media-download";
+
+ public static final String HTTP_HEADERS = "http-headers";
+
+ public static final String ENABLE_SWE = "enabled-swe";
+
+ public static final String DISABLE_TOP_CONTROLS = "disable-top-controls";
+
+ public static final String TOP_CONTROLS_HIDE_THRESHOLD = "top-controls-hide-threshold";
+
+ public static final String TOP_CONTROLS_SHOW_THRESHOLD = "top-controls-show-threshold";
+
+ public static final String CRASH_LOG_SERVER_CMD = "crash-log-server";
+
+ public static final String CMD_LINE_SWITCH_FEEDBACK = "mail-feedback-to";
+
+ public static final String CMD_LINE_SWITCH_HELPURL = "help-url";
+
+ public static final String CMD_LINE_SWITCH_EULA_URL = "legal-eula-url";
+
+ public static final String CMD_LINE_SWITCH_PRIVACY_POLICY_URL = "legal-privacy-policy-url";
+
+ public static final String AUTO_UPDATE_SERVER_CMD = "auto-update-server";
+
+}
diff --git a/src/src/com/android/browser/BrowserUtils.java b/src/src/com/android/browser/BrowserUtils.java
new file mode 100644
index 00000000..be16ab17
--- /dev/null
+++ b/src/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/src/com/android/browser/BrowserWebView.java b/src/src/com/android/browser/BrowserWebView.java
new file mode 100644
index 00000000..afcca1a5
--- /dev/null
+++ b/src/src/com/android/browser/BrowserWebView.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.browser;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.content.res.Resources;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+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 {
+ private static final boolean ENABLE_ROOTVIEW_BACKREMOVAL_OPTIMIZATION = true;
+
+ 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, boolean backgroundTab) {
+ super(context, attrs, defStyle, privateBrowsing, backgroundTab);
+ }
+
+ /**
+ * @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;
+ enableTopControls(true);
+ }
+
+ public void enableTopControls(boolean shinkViewport) {
+ Resources res = getContext().getResources();
+ int titlebarHeight = (int) (res.getDimension(R.dimen.toolbar_height)
+ / res.getDisplayMetrics().density);
+ setTopControlsHeight(titlebarHeight, shinkViewport);
+ }
+
+ // 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 enabled, removes the background from the main view (assumes coverage with opaqueness)
+ if (ENABLE_ROOTVIEW_BACKREMOVAL_OPTIMIZATION) {
+ 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 boolean onTouchEvent(MotionEvent event) {
+ // block touch event if title bar is selected
+ if (mTitleBar.isEditingUrl()) {
+ requestFocus();
+ return true;
+ }
+ else
+ return super.onTouchEvent(event);
+ }
+
+ @Override
+ public void onScrollChanged(int l, int t, int oldl, int oldt) {
+ // NOTE: this function seems to not be called when the WebView is scrolled (it may be fine)
+ 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();
+ }
+
+ @Override
+ public Bitmap getFavicon() {
+ Tab currentTab = mTitleBar.getUiController().getCurrentTab();
+ if (currentTab != null){
+ return currentTab.getFavicon();
+ }
+ else return BitmapFactory.decodeResource(
+ this.getResources(), R.drawable.ic_deco_favicon_normal);
+ }
+
+ @Override
+ public boolean dispatchKeyEventPreIme(KeyEvent event) {
+ Tab currentTab = mTitleBar.getUiController().getCurrentTab();
+ if (currentTab != null && currentTab.isKeyboardShowing()){
+ // Try to detect the "back" key that dismisses the keyboard
+ if(event.getAction() == KeyEvent.ACTION_DOWN &&
+ event.getKeyCode() == KeyEvent.KEYCODE_BACK)
+ mWebViewClient.onKeyboardStateChange(false);
+ }
+ return super.dispatchKeyEventPreIme(event);
+ }
+
+}
diff --git a/src/src/com/android/browser/BrowserWebViewFactory.java b/src/src/com/android/browser/BrowserWebViewFactory.java
new file mode 100644
index 00000000..26b69509
--- /dev/null
+++ b/src/src/com/android/browser/BrowserWebViewFactory.java
@@ -0,0 +1,79 @@
+/*
+ * 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, boolean backgroundTab) {
+ return new BrowserWebView(mContext, attrs, defStyle, privateBrowsing, backgroundTab);
+ }
+
+ @Override
+ public WebView createSubWebView(boolean privateBrowsing) {
+ return createWebView(privateBrowsing);
+ }
+
+ @Override
+ public WebView createWebView(boolean privateBrowsing) {
+ return createWebView(privateBrowsing, false);
+ }
+
+ @Override
+ public WebView createWebView(boolean privateBrowsing, boolean backgroundTab) {
+ WebView w = instantiateWebView(null, android.R.attr.webViewStyle, privateBrowsing, backgroundTab);
+ 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 this WebView to the settings observer list and update the
+ // settings
+ final BrowserSettings s = BrowserSettings.getInstance();
+ s.startManagingSettings(w.getSettings());
+
+ }
+
+}
diff --git a/src/src/com/android/browser/BrowserYesNoPreference.java b/src/src/com/android/browser/BrowserYesNoPreference.java
new file mode 100644
index 00000000..d73ea08b
--- /dev/null
+++ b/src/src/com/android/browser/BrowserYesNoPreference.java
@@ -0,0 +1,187 @@
+/*
+ * 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.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.res.TypedArray;
+import android.preference.DialogPreference;
+import android.preference.PreferenceManager;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+public class BrowserYesNoPreference extends DialogPreference {
+ private SharedPreferences mPrefs;
+ private Context mContext;
+ private String mNeutralBtnTxt;
+ private String mPositiveBtnTxt;
+ private String mNegativeBtnTxt;
+ private boolean mNeutralBtnClicked = false;
+
+ public static final int CANCEL_BTN = 0;
+ public static final int OK_BTN = 1;
+ public static final int OTHER_BTN = 2;
+
+ // This is the constructor called by the inflater
+ public BrowserYesNoPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
+ mContext = context;
+ final TypedArray a = mContext.obtainStyledAttributes(attrs,
+ R.styleable.BrowserYesNoPreference, 0, 0);
+ mNeutralBtnTxt = a.getString(R.styleable.BrowserYesNoPreference_neutralButtonText);
+ mPositiveBtnTxt = a.getString(R.styleable.BrowserYesNoPreference_positiveButtonText);
+ mNegativeBtnTxt = a.getString(R.styleable.BrowserYesNoPreference_negativeButtonText);
+ }
+
+ @Override
+ protected View onCreateView(ViewGroup group) {
+ View child = super.onCreateView(group);
+ View titleView = child.findViewById(android.R.id.title);
+ if (titleView instanceof Button) {
+ Button btn = (Button) titleView;
+ final BrowserYesNoPreference pref = this;
+ btn.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ pref.onClick();
+ }
+ }
+ );
+ }
+
+ return child;
+ }
+
+ @Override
+ protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
+ super.onPrepareDialogBuilder(builder);
+ if (mNeutralBtnTxt != null) {
+ builder.setNeutralButton(mNeutralBtnTxt, this);
+ }
+
+ if (mPositiveBtnTxt != null) {
+ builder.setPositiveButton(mPositiveBtnTxt, this);
+ }
+
+ if (mNegativeBtnTxt != null) {
+ builder.setNegativeButton(mNegativeBtnTxt, this);
+ }
+ }
+
+ @Override
+ protected void onClick() {
+ super.onClick();
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ super.onClick(dialog, which);
+ mNeutralBtnClicked = DialogInterface.BUTTON_NEUTRAL == which;
+ }
+
+ @Override
+ protected View onCreateDialogView() {
+ if (PreferenceKeys.PREF_CLEAR_SELECTED_DATA.equals(getKey())) {
+ String dialogMessage = mContext.getString(R.string.pref_privacy_clear_selected_dlg);
+ boolean itemSelected = false;
+
+ if (mPrefs.getBoolean(PreferenceKeys.PREF_PRIVACY_CLEAR_CACHE, false)) {
+ dialogMessage = dialogMessage.concat("\n\t" +
+ mContext.getString(R.string.pref_privacy_clear_cache));
+ itemSelected = true;
+ }
+ if (mPrefs.getBoolean(PreferenceKeys.PREF_PRIVACY_CLEAR_COOKIES, false)) {
+ dialogMessage = dialogMessage.concat("\n\t" +
+ mContext.getString(R.string.pref_privacy_clear_cookies));
+ itemSelected = true;
+ }
+ if (mPrefs.getBoolean(PreferenceKeys.PREF_PRIVACY_CLEAR_HISTORY, false)) {
+ dialogMessage = dialogMessage.concat("\n\t" +
+ mContext.getString(R.string.history));
+ itemSelected = true;
+ }
+ if (mPrefs.getBoolean(PreferenceKeys.PREF_PRIVACY_CLEAR_FORM_DATA, false)) {
+ dialogMessage = dialogMessage.concat("\n\t" +
+ mContext.getString(R.string.pref_privacy_clear_form_data));
+ itemSelected = true;
+ }
+ if (mPrefs.getBoolean(PreferenceKeys.PREF_PRIVACY_CLEAR_PASSWORDS, false)) {
+ dialogMessage = dialogMessage.concat("\n\t" +
+ mContext.getString(R.string.pref_privacy_clear_passwords));
+ itemSelected = true;
+ }
+ if (mPrefs.getBoolean(PreferenceKeys.PREF_PRIVACY_CLEAR_GEOLOCATION_ACCESS,
+ false)) {
+ dialogMessage = dialogMessage.concat("\n\t" +
+ mContext.getString(R.string.pref_privacy_clear_geolocation_access));
+ itemSelected = true;
+ }
+
+ if (!itemSelected) {
+ setDialogMessage(R.string.pref_select_items);
+ } else {
+ setDialogMessage(dialogMessage);
+ }
+ }
+
+ return super.onCreateDialogView();
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ super.onDialogClosed(positiveResult);
+ Integer result = (positiveResult) ? 1 : 0;
+
+ if (mNeutralBtnTxt != null && mNeutralBtnClicked) {
+ result = 2;
+ }
+
+ if (callChangeListener(result)) {
+ setEnabled(false);
+ if (PreferenceKeys.PREF_CLEAR_SELECTED_DATA.equals(getKey())) {
+ BrowserSettings settings = BrowserSettings.getInstance();
+ if (mPrefs.getBoolean(PreferenceKeys.PREF_PRIVACY_CLEAR_CACHE, false)) {
+ settings.clearCache();
+ settings.clearDatabases();
+ }
+ if (mPrefs.getBoolean(PreferenceKeys.PREF_PRIVACY_CLEAR_COOKIES, false)) {
+ settings.clearCookies();
+ }
+ if (mPrefs.getBoolean(PreferenceKeys.PREF_PRIVACY_CLEAR_HISTORY, false)) {
+ settings.clearHistory();
+ }
+ if (mPrefs.getBoolean(PreferenceKeys.PREF_PRIVACY_CLEAR_FORM_DATA, false)) {
+ settings.clearFormData();
+ }
+ if (mPrefs.getBoolean(PreferenceKeys.PREF_PRIVACY_CLEAR_PASSWORDS, false)) {
+ settings.clearPasswords();
+ }
+ if (mPrefs.getBoolean(PreferenceKeys.PREF_PRIVACY_CLEAR_GEOLOCATION_ACCESS,
+ false)) {
+ settings.clearLocationAccess();
+ }
+ }
+ setEnabled(true);
+ }
+ }
+}
diff --git a/src/src/com/android/browser/CombinedBookmarksCallbacks.java b/src/src/com/android/browser/CombinedBookmarksCallbacks.java
new file mode 100644
index 00000000..cdffb6bf
--- /dev/null
+++ b/src/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/src/com/android/browser/ComboTabsAdapter.java b/src/src/com/android/browser/ComboTabsAdapter.java
new file mode 100644
index 00000000..24900476
--- /dev/null
+++ b/src/src/com/android/browser/ComboTabsAdapter.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) 2015, The Linux Foundation. All rights reserved.
+ * 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.os.Bundle;
+import android.support.v13.app.FragmentPagerAdapter;
+import android.support.v4.view.ViewPager;
+
+import java.util.ArrayList;
+
+/**
+ * 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 class ComboTabsAdapter 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 ComboTabsAdapter(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();
+ }
+
+ public void removeAllTabs() {
+ mActionBar.removeAllTabs();
+ 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) {
+ }
+}
+
diff --git a/src/src/com/android/browser/ComboView.java b/src/src/com/android/browser/ComboView.java
new file mode 100644
index 00000000..8d64ac93
--- /dev/null
+++ b/src/src/com/android/browser/ComboView.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (c) 2015, The Linux Foundation. All rights reserved.
+ * 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.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.view.ViewPager;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AnimationSet;
+import android.view.animation.AnimationUtils;
+import android.widget.HorizontalScrollView;
+import android.widget.LinearLayout;
+
+import com.android.browser.UI.ComboViews;
+
+import java.util.Iterator;
+import java.util.Set;
+
+public class ComboView extends LinearLayout
+ implements CombinedBookmarksCallbacks, View.OnLayoutChangeListener {
+
+ private Activity mActivity;
+ private ViewPager mViewPager;
+ private AnimationSet mInAnimation;
+ private AnimationSet mOutAnimation;
+ private int mActionBarContainerId;
+
+ private ComboTabsAdapter mTabsAdapter;
+ private Bundle mExtraArgs;
+
+ public ComboView(Context context) {
+ super(context);
+ }
+
+ public ComboView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ComboView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public void setupViews(Activity activity) {
+ mActivity = activity;
+
+ this.setId(R.id.combo_view_container);
+
+ mViewPager = (ViewPager)this.findViewById(R.id.combo_view_pager);
+ mViewPager.setId(R.id.tab_view); // ???
+
+ mInAnimation = (AnimationSet) AnimationUtils.loadAnimation(mActivity, R.anim.combo_view_enter);
+ mOutAnimation = (AnimationSet) AnimationUtils.loadAnimation(mActivity, R.anim.combo_view_exit);
+
+ final ActionBar bar = activity.getActionBar();
+ bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+ bar.setDisplayOptions(0);
+
+ mActionBarContainerId = getResources().getIdentifier("action_bar_container", "id", "android");
+ ViewGroup actionBarContainer = (ViewGroup) mActivity.getWindow().getDecorView().findViewById(mActionBarContainerId);
+ if (actionBarContainer != null) {
+ actionBarContainer.addOnLayoutChangeListener(this);
+ }
+ }
+
+ private View getScrollingTabContainerView() {
+ ViewGroup actionBarContainer = (ViewGroup) mActivity.getWindow().getDecorView().findViewById(mActionBarContainerId);
+ int count = actionBarContainer.getChildCount();
+ for (int i = 0; i < count; i++) {
+ View child = actionBarContainer.getChildAt(i);
+ if (child instanceof HorizontalScrollView) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ if (mActivity != null) {
+ if (!isShowing()) {
+ View container = getScrollingTabContainerView();
+ if (container != null) {
+ container.setVisibility(View.INVISIBLE);
+ }
+ }
+ }
+
+ }
+
+ private boolean compareArgs(Bundle b1, Bundle b2) {
+
+ if(b1.size() != b2.size()) {
+ return false;
+ }
+
+ Set<String> keys = b1.keySet();
+ Iterator<String> it = keys.iterator();
+ while (it.hasNext()) {
+ String key = it.next();
+ final Object v1 = b1.get(key);
+ final Object v2 = b2.get(key);
+ if (!b2.containsKey(key)) {
+ return false;
+ } else if (v1 == null) {
+ if (v2 != null) {
+ return false;
+ }
+ } else if (!v1.equals(v2)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public void showViews(Activity activity, Bundle extras /*Bundle savedInstanceState*/) {
+ Bundle args = extras.getBundle(ComboViewActivity.EXTRA_COMBO_ARGS);
+ String svStr = extras.getString(ComboViewActivity.EXTRA_INITIAL_VIEW, null);
+ ComboViews startingView = svStr != null ? ComboViews.valueOf(svStr) : ComboViews.Bookmarks;
+
+
+ // Compare the items in args with old args and recreate the fragments if they don't match.
+ if (mExtraArgs != null && !compareArgs(mExtraArgs, args)) {
+ mTabsAdapter.removeAllTabs();
+ mTabsAdapter = null;
+ }
+
+ final ActionBar bar = activity.getActionBar();
+ bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+ if (BrowserActivity.isTablet(activity)) {
+ bar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME
+ | ActionBar.DISPLAY_USE_LOGO);
+ bar.setDisplayHomeAsUpEnabled(true);
+ } else {
+ bar.setDisplayOptions(0);
+ }
+ if (mTabsAdapter == null) {
+ mExtraArgs = args;
+ mTabsAdapter = new ComboTabsAdapter(activity, mViewPager);
+ mTabsAdapter.addTab(bar.newTab().setText(R.string.bookmarks),
+ BrowserBookmarksPage.class, args);
+ mTabsAdapter.addTab(bar.newTab().setText(R.string.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;
+ }
+ }
+
+ if (!bar.isShowing()) {
+ View v = getScrollingTabContainerView();
+ if (v != null) {
+ v.setVisibility(View.VISIBLE);
+ }
+ bar.show();
+ }
+
+ if (!this.isShowing()) {
+ this.setVisibility(View.VISIBLE);
+ this.requestFocus();
+ if (!(BrowserActivity.isTablet(activity))) {
+ this.startAnimation(mInAnimation);
+ }
+ }
+ }
+
+ public boolean isShowing() {
+ return (this.getVisibility() == View.VISIBLE);
+ }
+
+ public void hideViews() {
+ if(!(BrowserActivity.isTablet(mActivity)))
+ this.startAnimation(mOutAnimation);
+ this.setVisibility(View.INVISIBLE);
+ mActionBarContainerId = getResources().getIdentifier("action_bar_container", "id", "android");
+ ViewGroup actionBarContainer = (ViewGroup) mActivity.getWindow().getDecorView().findViewById(mActionBarContainerId);
+ if (actionBarContainer != null) {
+ actionBarContainer.removeOnLayoutChangeListener(this);
+ }
+ ActionBar actionBar = mActivity.getActionBar();
+ actionBar.hide();
+ }
+
+ //TODO: Save the selected tab on BrowserActivity's onSaveInstanceState
+ /*public 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));
+ i.setClassName(getContext().getPackageName(), BrowserActivity.class.getName());
+ i.putExtra(Controller.EXTRA_REQUEST_CODE, Controller.COMBO_VIEW);
+ i.putExtra(Controller.EXTRA_RESULT_CODE, Activity.RESULT_OK);
+ this.getContext().startActivity(i);
+ hideViews();
+ }
+
+ @Override
+ public void openInNewTab(String... urls) {
+ Intent i = new Intent();
+ i.putExtra(ComboViewActivity.EXTRA_OPEN_ALL, urls);
+ i.putExtra(Controller.EXTRA_REQUEST_CODE, Controller.COMBO_VIEW);
+ i.putExtra(Controller.EXTRA_RESULT_CODE, Activity.RESULT_OK);
+ i.setClassName(getContext().getPackageName(), BrowserActivity.class.getName());
+ this.getContext().startActivity(i);
+ hideViews();
+ }
+
+ @Override
+ public void close() {
+ hideViews();
+ }
+
+ @Override
+ public void openSnapshot(long id) {
+ Intent i = new Intent();
+ i.putExtra(ComboViewActivity.EXTRA_OPEN_SNAPSHOT, id);
+ i.putExtra(Controller.EXTRA_REQUEST_CODE, Controller.COMBO_VIEW);
+ i.putExtra(Controller.EXTRA_RESULT_CODE, Activity.RESULT_OK);
+ i.setClassName(getContext().getPackageName(), BrowserActivity.class.getName());
+ this.getContext().startActivity(i);
+ hideViews();
+ }
+}
diff --git a/src/src/com/android/browser/ComboViewActivity.java b/src/src/com/android/browser/ComboViewActivity.java
new file mode 100644
index 00000000..47314519
--- /dev/null
+++ b/src/src/com/android/browser/ComboViewActivity.java
@@ -0,0 +1,149 @@
+/*
+ * 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.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.view.ViewPager;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.android.browser.UI.ComboViews;
+
+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 ComboTabsAdapter 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 ComboTabsAdapter(this, mViewPager);
+ mTabsAdapter.addTab(bar.newTab().setText(R.string.bookmarks),
+ BrowserBookmarksPage.class, args);
+ mTabsAdapter.addTab(bar.newTab().setText(R.string.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);
+ BrowserPreferencesPage.startPreferencesForResult(this, url, Controller.PREFERENCES_PAGE);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private static String makeFragmentName(int viewId, int index) {
+ return "android:switcher:" + viewId + ":" + index;
+ }
+
+}
diff --git a/src/src/com/android/browser/CommandLineManager.java b/src/src/com/android/browser/CommandLineManager.java
new file mode 100644
index 00000000..3ee1238f
--- /dev/null
+++ b/src/src/com/android/browser/CommandLineManager.java
@@ -0,0 +1,142 @@
+/*
+ * 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;
+
+import org.codeaurora.swe.utils.Logger;
+
+import android.content.Context;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileInputStream;
+
+public class CommandLineManager {
+
+ private final static String LOGTAG = "CommandLineManager";
+ public static final String COMMAND_LINE_FILE = "/data/local/tmp/swe-command-line";
+
+ // Read from the InputStream object
+ private static String parseCommandLine (InputStream is) {
+ BufferedReader br = null;
+ StringBuilder sb = new StringBuilder();
+ String line;
+ try {
+ br = new BufferedReader(new InputStreamReader(is));
+ while ((line = br.readLine()) != null) {
+ line = line.trim();
+
+ int commentIndex = line.indexOf('#');
+ if (commentIndex > -1) {
+ // trim out the comments
+ line = line.substring(0,commentIndex);
+ }
+
+ if (line.isEmpty())
+ continue;
+
+ // Consider only the uncommented lines
+ sb.append(line);
+ sb.append(" ");
+ }
+
+ // Strip the browser name from the commandline options (First string) if options exists
+ if (sb.indexOf("--") >= 0) {
+ sb.delete(0, sb.indexOf(" "));
+ }
+ } catch (IOException e) {
+ Logger.e(LOGTAG, "Exception:", e);
+ } finally {
+ try {
+ // close the streams and reader
+ is.close();
+ if (br != null)
+ br.close();
+ } catch (IOException e) {
+ Logger.e(LOGTAG, "Exception:", e);
+ }
+ }
+ return sb.toString();
+ }
+
+ // Set the browser options
+ private static char[] append(InputStream inStreamDefaultCmdLine,
+ InputStream inStreamUsrCmdLine) {
+ // bail out since there no file to command line to deal with
+ if (inStreamDefaultCmdLine == null && inStreamUsrCmdLine == null){
+ return null;
+ }
+
+ String userArgs = "";
+ String defaultArgs = "";
+ if (inStreamDefaultCmdLine != null) {
+ // Reading the default commandline file
+ // Refers to the internal filename residing in "raw" directory
+ defaultArgs = parseCommandLine(inStreamDefaultCmdLine);
+ }
+
+ // Reading the user commandline file
+ if (inStreamUsrCmdLine != null) {
+ userArgs = parseCommandLine(inStreamUsrCmdLine);
+ }
+
+ // Assumption: The user commandline file can be empty or it exists
+ // with atleast one commandline option.
+ // Insert the content of the default commandline file in the beginning of
+ // the user commandline file content. So that, the user settings will get
+ // the presidence
+ userArgs = "Browser "+defaultArgs+" "+userArgs;
+
+
+ Logger.v(LOGTAG, "(Command Line Arguments): " + userArgs);
+
+ char[] buffer = userArgs.toString().toCharArray();
+ return buffer;
+ }
+
+ public static char[] getCommandLineSwitches(Context context) {
+ InputStream sweCmdLineStream =
+ context.getResources().openRawResource(R.raw.swe_command_line);
+ InputStream usrCmdLineStream = null;
+ File file = new File(COMMAND_LINE_FILE);
+ if(file.exists()){
+ try {
+ usrCmdLineStream = new FileInputStream(COMMAND_LINE_FILE);
+ } catch (FileNotFoundException e) {
+ }
+ }
+ // Set the browser options here
+ return append(sweCmdLineStream, usrCmdLineStream);
+ }
+}
diff --git a/src/src/com/android/browser/Controller.java b/src/src/com/android/browser/Controller.java
new file mode 100644
index 00000000..cbc6ba05
--- /dev/null
+++ b/src/src/com/android/browser/Controller.java
@@ -0,0 +1,3475 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Copyright (c) 2015 The Linux Foundation, All rights reserved.
+ *
+ * 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.Context;
+import android.content.ClipboardManager;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
+import android.content.IntentFilter;
+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.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Rect;
+import android.media.AudioManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.net.wifi.WifiManager;
+import android.net.wifi.ScanResult;
+import android.os.AsyncTask;
+import android.os.Build;
+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.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.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.view.WindowManager;
+import android.webkit.MimeTypeMap;
+import android.webkit.ValueCallback;
+import android.webkit.WebChromeClient.CustomViewCallback;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import org.codeaurora.swe.CookieManager;
+import org.codeaurora.swe.CookieSyncManager;
+import org.codeaurora.swe.Engine;
+import org.codeaurora.swe.HttpAuthHandler;
+import org.codeaurora.swe.WebSettings;
+import org.codeaurora.swe.WebView;
+import org.codeaurora.swe.WebBackForwardList;
+import org.codeaurora.swe.WebHistoryItem;
+
+import com.android.browser.IntentHandler.UrlData;
+import com.android.browser.UI.ComboViews;
+import com.android.browser.mdm.DownloadDirRestriction;
+import com.android.browser.mdm.EditBookmarksRestriction;
+import com.android.browser.mdm.IncognitoRestriction;
+import com.android.browser.mdm.URLFilterRestriction;
+import com.android.browser.mynavigation.AddMyNavigationPage;
+import com.android.browser.mynavigation.MyNavigationUtil;
+import com.android.browser.platformsupport.Browser;
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.preferences.AboutPreferencesFragment;
+import com.android.browser.provider.BrowserProvider2.Thumbnails;
+import com.android.browser.provider.SnapshotProvider.Snapshots;
+import com.android.browser.reflect.ReflectHelper;
+import com.android.browser.appmenu.AppMenuHandler;
+import com.android.browser.appmenu.AppMenuPropertiesDelegate;
+
+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.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,
+ AppMenuPropertiesDelegate {
+
+ private static final String LOGTAG = "Controller";
+ private static final String SEND_APP_ID_EXTRA =
+ "android.speech.extras.SEND_APPLICATION_ID_EXTRA";
+ public static final String INCOGNITO_URI = "chrome://incognito";
+ public static final String EXTRA_REQUEST_CODE = "_fake_request_code_";
+ public static final String EXTRA_RESULT_CODE = "_fake_result_code_";
+ public static final String EXTRA_UPDATED_URLS = "updated_urls";
+
+ // Remind switch to data connection if wifi is unavailable
+ private static final int NETWORK_SWITCH_TYPE_OK = 1;
+
+ // 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;
+ private static final int UNKNOWN_TYPE_MSG = 109;
+
+ 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 HomepageHandler mHomepageHandler;
+ protected TabControl mTabControl;
+ private BrowserSettings mSettings;
+ private WebViewFactory mFactory;
+
+ private WakeLock mWakeLock;
+
+ private UrlHandler mUrlHandler;
+ private UploadHandler mUploadHandler;
+ private IntentHandler mIntentHandler;
+ private NetworkStateHandler mNetworkHandler;
+
+ private Message mAutoFillSetupMessage;
+
+ private boolean mNetworkShouldNotify = true;
+
+ // FIXME, temp address onPrepareMenu performance problem.
+ // When we move everything out of view, we should rewrite this.
+ private int mCurrentMenuState = 0;
+ private int mMenuState = EMPTY_MENU;
+ private int mOldMenuState = EMPTY_MENU;
+ private Menu mCachedMenu;
+
+ private boolean mMenuIsDown;
+
+ private boolean mWasInPageLoad = false;
+ private AppMenuHandler mAppMenuHandler;
+
+ // 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 mActivityStopped = 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;
+ private float mLevel = 0.0f;
+ private WebView.HitTestResult mResult;
+ private PowerConnectionReceiver mLowPowerReceiver;
+ private PowerConnectionReceiver mPowerChangeReceiver;
+
+ private boolean mCurrentPageBookmarked;
+
+ 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);
+
+ 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);
+ mHomepageHandler = new HomepageHandler(browser, this);
+ mAppMenuHandler = new AppMenuHandler(browser, this, R.menu.browser);
+ }
+
+ @Override
+ public void start(final Intent intent) {
+ mMenuState = R.id.MAIN_MENU;
+ WebView.setShouldMonitorWebCoreThread();
+ // mCrashRecoverHandler has any previously saved state.
+ mCrashRecoveryHandler.startRecovery(intent);
+ }
+
+ void doStart(final Bundle icicle, final Intent intent) {
+ // 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);
+
+ 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();
+ 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()) {
+ String landingPage = mActivity.getResources().getString(
+ R.string.def_landing_page);
+ if (!landingPage.isEmpty()) {
+ t = openTab(landingPage, false, true, true);
+ } else {
+ t = openTabToHomePage();
+ }
+ } else {
+ t = openTab(urlData);
+ t.setDerivedFromIntent(true);
+ }
+ 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) {
+ //handle restored pages that may require a JS interface
+ if (t.getWebView() != null) {
+ WebBackForwardList backForwardList = t.getWebView().copyBackForwardList();
+ if (backForwardList != null) {
+ for (int i = 0; i < backForwardList.getSize(); i++) {
+ WebHistoryItem item = backForwardList.getItemAtIndex(i);
+ mHomepageHandler.registerJsInterface( t.getWebView(), item.getUrl());
+ }
+ }
+ }
+ restoredTabs.add(t.getId());
+ if (t != mTabControl.getCurrentTab()) {
+ t.pause();
+ }
+ }
+ 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);
+ }
+ mLowPowerReceiver = new PowerConnectionReceiver();
+ mPowerChangeReceiver = new PowerConnectionReceiver();
+
+ //always track the android framework's power save mode
+ IntentFilter filter = new IntentFilter();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ // Power save mode only exists in Lollipop and above
+ filter.addAction("android.os.action.POWER_SAVE_MODE_CHANGED");
+ }
+ filter.addAction(Intent.ACTION_BATTERY_OKAY);
+ mActivity.registerReceiver(mPowerChangeReceiver, filter);
+ }
+
+ 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) {
+ mUi.onSetWebView(tab, view);
+ URLFilterRestriction.getInstance();
+ }
+
+ @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();
+ }
+
+ private void startHandler() {
+ mHandler = new Handler() {
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case OPEN_BOOKMARKS:
+ bookmarksOrHistoryPicker(ComboViews.Bookmarks);
+ break;
+ case UNKNOWN_TYPE_MSG:
+ HashMap unknownTypeMap = (HashMap) msg.obj;
+ WebView viewForUnknownType = (WebView) unknownTypeMap.get("webview");
+ /*
+ * When the context menu is shown to the user
+ * we need to assure that its happening on the current webview
+ * and its the current webview only which had sent the UNKNOWN_TYPE_MSG
+ */
+ if (getCurrentWebView() != viewForUnknownType)
+ break;
+
+ String unknown_type_src = (String)msg.getData().get("src");
+ String unknown_type_url = (String)msg.getData().get("url");
+ WebView.HitTestResult result = new WebView.HitTestResult();
+
+ // Prevent unnecessary calls to context menu
+ // if url and image src are null
+ if (unknown_type_src == null && unknown_type_url == null)
+ break;
+
+ //setting the HitTestResult with new RESULT TYPE
+ if (!TextUtils.isEmpty(unknown_type_src)) {
+ result.setType(WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
+ result.setExtra(unknown_type_src);
+ } else {
+ result.setType(WebView.HitTestResult.SRC_ANCHOR_TYPE);
+ result.setExtra("about:blank");
+ }
+
+ mResult = result;
+ openContextMenu(viewForUnknownType);
+ mResult = null;
+
+ 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;
+ case R.id.save_link_bookmark_context_menu_id:
+ if(title == null || title == "")
+ title = url;
+
+ Intent bookmarkIntent = new Intent(mActivity, AddBookmarkPage.class);
+ //SWE TODO: No thumbnail support for the url obtained via
+ //browser context menu as its not loaded in webview.
+ if (bookmarkIntent != null) {
+ bookmarkIntent.putExtra(BrowserContract.Bookmarks.URL, url);
+ bookmarkIntent.putExtra(BrowserContract.Bookmarks.TITLE, title);
+ mActivity.startActivity(bookmarkIntent);
+ }
+ 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 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 || tab.getWebView() == null)
+ return;
+
+ final Tab mytab = tab;
+ final ValueCallback<Bitmap> onScreenshot = new ValueCallback<Bitmap>() {
+ @Override
+ public void onReceiveValue(Bitmap bitmap) {
+ sharePage(mActivity, mytab.getTitle(), mytab.getUrl(),
+ mytab.getFavicon(), bitmap);
+ }
+ };
+
+ createScreenshotAsync(
+ tab.getWebView(),
+ getDesiredThumbnailWidth(mActivity),
+ getDesiredThumbnailHeight(mActivity),
+ new ValueCallback<Bitmap>() {
+ @Override
+ public void onReceiveValue(Bitmap bitmap) {
+ sharePage(mActivity, mytab.getTitle(), mytab.getUrl(),
+ mytab.getFavicon(), bitmap);
+ }
+ });
+ }
+
+ /**
+ * 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) {
+
+ ShareDialog sDialog = new ShareDialog((Activity)c, title, url, favicon, screenshot);
+ final AppAdapter adapter = new AppAdapter(c, c.getPackageManager(),
+ R.layout.app_row, sDialog.getApps());
+ sDialog.loadView(adapter);
+ }
+
+ 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();
+ mAppMenuHandler.hideAppMenu();
+ if (mOptionsMenuOpen) {
+ mActivity.closeOptionsMenu();
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(OPEN_MENU), 100);
+ }
+ 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;
+
+ }
+
+ @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);
+ return saveState;
+ }
+
+ @Override
+ public void onResume() {
+ if (!mActivityPaused) {
+ Log.e(LOGTAG, "BrowserActivity is already resumed.");
+ return;
+ }
+ mActivityPaused = false;
+ if (mVoiceResult != null) {
+ mUi.onVoiceResult(mVoiceResult);
+ mVoiceResult = null;
+ }
+ }
+
+ private void releaseWakeLock() {
+ if (mWakeLock != null && mWakeLock.isHeld()) {
+ mHandler.removeMessages(RELEASE_WAKELOCK);
+ mWakeLock.release();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ if (mActivityStopped) {
+ Log.e(LOGTAG, "BrowserActivity is already stoped.");
+ return;
+ }
+ mActivityStopped = 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();
+ NfcHandler.unregister(mActivity);
+ mActivity.unregisterReceiver(mLowPowerReceiver);
+ }
+
+ @Override
+ public void onStart() {
+ if (!mActivityStopped) {
+ Log.e(LOGTAG, "BrowserActivity is already started.");
+ return;
+ }
+ mActivityStopped = false;
+ UpdateNotificationService.updateCheck(mActivity);
+ mSettings.setLastRunPaused(false);
+ Tab current = mTabControl.getCurrentTab();
+ if (current != null) {
+ current.resume();
+ resumeWebViewTimers(current);
+ }
+ releaseWakeLock();
+
+ mUi.onResume();
+ mNetworkHandler.onResume();
+ NfcHandler.register(mActivity, this);
+ if (current != null && current.getWebView().isShowingCrashView())
+ current.getWebView().reload();
+ mActivity.registerReceiver(mLowPowerReceiver, new IntentFilter(Intent.ACTION_BATTERY_LOW));
+
+ }
+
+ /**
+ * 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 ((!mActivityStopped && !inLoad) || (mActivityStopped && 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();
+ // Unregister receiver
+ mActivity.unregisterReceiver(mPowerChangeReceiver);
+ }
+
+ protected boolean isActivityPaused() {
+ return mActivityPaused;
+ }
+
+ @Override
+ public void onLowMemory() {
+ mTabControl.freeMemory();
+ }
+
+ @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) {
+ final String reminderType = getContext().getResources().getString(
+ R.string.def_wifi_browser_interaction_remind_type);
+ final String selectionConnnection = getContext().getResources().getString(
+ R.string.def_action_wifi_selection_data_connections);
+ final String wifiSelection = getContext().getResources().getString(
+ R.string.def_intent_pick_network);
+
+ if (reminderType.isEmpty() || selectionConnnection.isEmpty() ||
+ wifiSelection.isEmpty())
+ return;
+
+ ConnectivityManager conMgr = (ConnectivityManager) this.getContext().getSystemService(
+ Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = conMgr.getActiveNetworkInfo();
+ WifiManager wifiMgr = (WifiManager) this.getContext()
+ .getSystemService(Context.WIFI_SERVICE);
+ if (networkInfo == null
+ || (networkInfo != null && (networkInfo.getType() !=
+ ConnectivityManager.TYPE_WIFI))) {
+ int isReminder = Settings.System.getInt(mActivity.getContentResolver(),
+ reminderType, NETWORK_SWITCH_TYPE_OK);
+ List<ScanResult> list = wifiMgr.getScanResults();
+ // Have no AP's for Wifi's fall back to data
+ if (list != null && list.size() == 0 && isReminder == NETWORK_SWITCH_TYPE_OK) {
+ Intent intent = new Intent(selectionConnnection);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ this.getContext().startActivity(intent);
+ } else {
+ // Request to select Wifi AP
+ try {
+ Intent intent = new Intent(wifiSelection);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ this.getContext().startActivity(intent);
+ } catch (Exception e) {
+ String err_msg = this.getContext().getString(
+ R.string.activity_not_found, wifiSelection);
+ Toast.makeText(this.getContext(), err_msg, Toast.LENGTH_LONG).show();
+ }
+ }
+ mNetworkShouldNotify = false;
+ }
+ }
+
+ // WebViewController
+
+ @Override
+ public void onPageStarted(Tab tab, WebView view, Bitmap favicon) {
+
+ // reset sync timer to avoid sync starts during loading a page
+ CookieSyncManager.getInstance().resetSync();
+ WifiManager wifiMgr = (WifiManager) this.getContext()
+ .getSystemService(Context.WIFI_SERVICE);
+ boolean networkNotifier = BrowserConfig.getInstance(getContext())
+ .hasFeature(BrowserConfig.Feature.NETWORK_NOTIFIER);
+ if (networkNotifier && mNetworkShouldNotify && wifiMgr.isWifiEnabled()){
+ handleNetworkNotify(view);
+ } 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 (mActivityStopped) {
+ resumeWebViewTimers(tab);
+ }
+ mLoadStopped = false;
+ endActionMode();
+
+ mUi.onTabDataChanged(tab);
+
+ String url = tab.getUrl();
+ // update the bookmark database for favicon
+ syncBookmarkFavicon(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());
+ }
+
+ tab.onPageFinished();
+ syncBookmarkFavicon(tab, tab.getOriginalUrl(), tab.getUrl(), tab.getFavicon());
+
+ 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()) {
+ mWasInPageLoad = true;
+ updateInLoadMenuItems(mCachedMenu, tab);
+ } else if (mWasInPageLoad) {
+ mWasInPageLoad = false;
+ updateInLoadMenuItems(mCachedMenu, tab);
+ }
+
+ if (mActivityStopped && pauseWebViewTimers(tab)) {
+ // pause the WebView timer and release the wake lock if it is
+ // finished while BrowserActivity is in pause state.
+ releaseWakeLock();
+ }
+ } else {
+ if (!tab.inPageLoad()) {
+ // onPageFinished may have already been called but a subframe is
+ // still loading
+ // updating the progress and
+ // update the menu items.
+ mWasInPageLoad = false;
+ updateInLoadMenuItems(mCachedMenu, tab);
+ } else {
+ mWasInPageLoad = true;
+ }
+ }
+
+ 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) {
+ syncBookmarkFavicon(tab, view.getOriginalUrl(), view.getUrl(), icon);
+ ((BaseUi)mUi).setFavicon(tab);
+ }
+
+ @Override
+ public boolean shouldOverrideUrlLoading(Tab tab, WebView view, String url) {
+ // if tab is snapshot tab we want to prevent navigation from occuring
+ // since snapshot tab opens a new tab with the url
+ return goLive(url) || 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;
+ }
+
+ private void handleMediaKeyEvent(KeyEvent event) {
+
+ int keyCode = event.getKeyCode();
+ // send media key events to audio manager
+ if (Build.VERSION.SDK_INT >= 19) {
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ case KeyEvent.KEYCODE_MEDIA_STOP:
+ case KeyEvent.KEYCODE_MEDIA_NEXT:
+ case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+ case KeyEvent.KEYCODE_MEDIA_REWIND:
+ case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
+ case KeyEvent.KEYCODE_MUTE:
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ case KeyEvent.META_SHIFT_RIGHT_ON:
+ case KeyEvent.KEYCODE_MEDIA_EJECT:
+ case KeyEvent.KEYCODE_MEDIA_RECORD:
+ case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK:
+
+ AudioManager audioManager = (AudioManager) mActivity.getApplicationContext()
+ .getSystemService(Context.AUDIO_SERVICE);
+ audioManager.dispatchMediaKeyEvent(event);
+ }
+ }
+ }
+
+ @Override
+ public boolean onUnhandledKeyEvent(KeyEvent event) {
+ if (!isActivityPaused()) {
+ handleMediaKeyEvent(event);
+ 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 or when disabling history
+ // for regular tabs is enabled
+ if (tab.isPrivateBrowsingEnabled() || BrowserConfig.getInstance(getContext())
+ .hasFeature(BrowserConfig.Feature.DISABLE_HISTORY))
+ 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) {
+ return (String[]) Browser.getVisitedHistory(mActivity.getContentResolver());
+ }
+ @Override
+ public void onPostExecute(String[] result) {
+ callback.onReceiveValue(result);
+ }
+ };
+ task.execute();
+ }
+
+ @Override
+ public void onReceivedHttpAuthRequest(Tab tab, WebView view,
+ final HttpAuthHandler handler, final String host,
+ final String realm) {
+ String username = null;
+ String password = null;
+
+ boolean reuseHttpAuthUsernamePassword
+ = handler.useHttpAuthUsernamePassword();
+
+ if (reuseHttpAuthUsernamePassword && view != null) {
+ String[] credentials = view.getHttpAuthUsernamePassword(host, realm);
+ if (credentials != null && credentials.length == 2) {
+ username = credentials[0];
+ password = credentials[1];
+ }
+ }
+
+ if (username != null && password != null) {
+ handler.proceed(username, password);
+ } else {
+ if (!tab.inForeground()) {
+ handler.cancel();
+ }
+ }
+ }
+
+ @Override
+ public void onDownloadStart(Tab tab, String url, String userAgent,
+ String contentDisposition, String mimetype, String referer,
+ long contentLength) {
+ WebView w = tab.getWebView();
+ if ( w == null) return;
+ 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.
+ if (tab.getDerivedFromIntent())
+ closeTab(tab);
+ else
+ goBackOnePageOrQuit();
+ } else {
+ // In this case, it is not.
+ closeTab(tab);
+ }
+ }
+ }
+
+ @Override
+ public Bitmap getDefaultVideoPoster() {
+ return mUi.getDefaultVideoPoster();
+ }
+
+ @Override
+ public View getVideoLoadingProgressView() {
+ return mUi.getVideoLoadingProgressView();
+ }
+
+ // helper method
+
+ /*
+ * Update the favorites icon if the private browsing isn't enabled and the
+ * icon is valid.
+ */
+ private void syncBookmarkFavicon(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();
+ } else if (action.equals(PreferenceKeys.ACTION_RELOAD_PAGE)) {
+ ArrayList<String> origins =
+ intent.getStringArrayListExtra(EXTRA_UPDATED_URLS);
+ if (origins.isEmpty()) {
+ mTabControl.reloadLiveTabs();
+ }
+ else{
+ for (String origin : origins){
+ mTabControl.findAndReload(origin);
+ }
+ }
+ }
+ }
+ 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();
+ getCurrentTopWebView().onActivityResult(requestCode, resultCode, intent);
+ }
+
+ /**
+ * 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;
+
+ /* Determine whether the ContextMenu got triggered because
+ * of user action of long click or because of the UNKNOWN_TYPE_MSG
+ * received. The mResult acts as a flag to identify, how it got trigerred
+ */
+ if (mResult == null){
+ result = webview.getHitTestResult();
+ } else {
+ result = mResult;
+ }
+
+ if (result == null) {
+ return;
+ }
+
+ int type = result.getType();
+ if (type == WebView.HitTestResult.UNKNOWN_TYPE) {
+
+ HashMap<String, Object> unknownTypeMap = new HashMap<String, Object>();
+ unknownTypeMap.put("webview", webview);
+ final Message msg = mHandler.obtainMessage(
+ UNKNOWN_TYPE_MSG, unknownTypeMap);
+ /* As defined in android developers guide
+ * when UNKNOWN_TYPE is received as a result of HitTest
+ * you need to determing the type by invoking requestFocusNodeHref
+ */
+ webview.requestFocusNodeHref(msg);
+
+ 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);
+
+ if (DownloadDirRestriction.getInstance().downloadsAllowed()) {
+ menu.findItem(R.id.save_link_context_menu_id).setEnabled(
+ UrlUtils.isDownloadableScheme(extra));
+ }
+ else {
+ menu.findItem(R.id.save_link_context_menu_id).setEnabled(false);
+ }
+ }
+ // 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, final Bitmap bitmap) {
+ final ContentResolver cr = mActivity.getContentResolver();
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... unused) {
+ boolean isMyNavigationUrl = MyNavigationUtil.isMyNavigationUrl(mActivity, itemUrl);
+ if(!isMyNavigationUrl)
+ return null;
+
+ 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();
+ bitmap.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());
+ }
+ mActivity.invalidateOptionsMenu();
+ }
+
+ public void invalidateOptionsMenu() {
+ mAppMenuHandler.invalidateAppMenu();
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ // Software menu key (toolbar key)
+ mAppMenuHandler.showAppMenu(mActivity.findViewById(R.id.more_browser_settings), false, false);
+ return true;
+ }
+
+ @Override
+ public void prepareMenu(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;
+ mUi.onPrepareOptionsMenu(menu);
+
+ IncognitoRestriction.getInstance()
+ .registerControl(menu.findItem(R.id.incognito_menu_id).getIcon());
+ EditBookmarksRestriction.getInstance()
+ .registerControl(menu.findItem(R.id.bookmark_this_page_id).getIcon());
+ }
+
+ private void setMenuItemVisibility(Menu menu, int id,
+ boolean visibility) {
+ MenuItem item = menu.findItem(id);
+ if (item != null) {
+ item.setVisible(visibility);
+ }
+ }
+
+ private int lookupBookmark(String title, String url) {
+ final ContentResolver cr = getActivity().getContentResolver();
+ int count = 0;
+ Cursor cursor = null;
+ try {
+ cursor = cr.query(BrowserContract.Bookmarks.CONTENT_URI,
+ BookmarksLoader.PROJECTION,
+ "title = ? OR url = ?",
+ new String[] {
+ title, url
+ },
+ null);
+
+ if (cursor != null)
+ count = cursor.getCount();
+
+ } catch (IllegalStateException e) {
+ Log.e(LOGTAG, "lookupBookmark ", e);
+ } finally {
+ if (null != cursor) {
+ cursor.close();
+ }
+ }
+ return count;
+ }
+
+ private void resetMenuItems(Menu menu) {
+ setMenuItemVisibility(menu, R.id.find_menu_id, true);
+
+ WebView w = getCurrentTopWebView();
+ MenuItem bookmark_icon = menu.findItem(R.id.bookmark_this_page_id);
+
+ String title = w.getTitle();
+ String url = w.getUrl();
+ mCurrentPageBookmarked = (lookupBookmark(title, url) > 0);
+ if (title != null && url != null && mCurrentPageBookmarked) {
+ bookmark_icon.setChecked(true);
+ } else {
+ bookmark_icon.setChecked(false);
+ }
+
+ // update reader mode checkbox
+ MenuItem readerSwitcher = menu.findItem(R.id.reader_mode_menu_id);
+ readerSwitcher.setVisible(false);
+ readerSwitcher.setChecked(false);
+ }
+
+ @Override
+ public void updateMenuState(Tab tab, Menu menu) {
+ boolean canGoForward = false;
+ boolean isDesktopUa = false;
+ boolean isLive = false;
+ // Following flag is used to identify schemes for which the LIVE_MENU
+ // items defined in res/menu/browser.xml should be enabled
+ boolean isLiveScheme = false;
+ boolean isPageFinished = false;
+ boolean isSavable = false;
+
+ boolean isDistillable = false;
+ boolean isDistilled = false;
+ resetMenuItems(menu);
+
+ if (tab != null) {
+ canGoForward = tab.canGoForward();
+ isDesktopUa = mSettings.hasDesktopUseragent(tab.getWebView());
+ isLive = !tab.isSnapshot();
+ isLiveScheme = UrlUtils.isLiveScheme(tab.getWebView().getUrl());
+ isPageFinished = (tab.getPageFinishedStatus() || !tab.inPageLoad());
+ isSavable = tab.getWebView().isSavable();
+
+ isDistillable = tab.isDistillable();
+ isDistilled = tab.isDistilled();
+ }
+
+ final MenuItem forward = menu.findItem(R.id.forward_menu_id);
+ forward.setEnabled(canGoForward);
+
+ // decide whether to show the share link option
+ PackageManager pm = mActivity.getPackageManager();
+ Intent send = new Intent(Intent.ACTION_SEND);
+ send.setType("text/plain");
+ ResolveInfo ri = pm.resolveActivity(send,
+ PackageManager.MATCH_DEFAULT_ONLY);
+ menu.findItem(R.id.share_page_menu_id).setVisible(ri != null);
+
+ boolean isNavDump = mSettings.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 && isLiveScheme);
+ menu.setGroupVisible(R.id.NAV_MENU, isLive && isLiveScheme);
+ setMenuItemVisibility(menu, R.id.find_menu_id, isLive && isLiveScheme);
+ menu.setGroupVisible(R.id.SNAPSHOT_MENU, !isLive);
+ setMenuItemVisibility(menu, R.id.add_to_homescreen,
+ isLive && isLiveScheme && isPageFinished);
+ setMenuItemVisibility(menu, R.id.save_snapshot_menu_id,
+ isLive && ( isLiveScheme || isDistilled ) && isPageFinished && isSavable);
+ // 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);
+
+
+ // update reader mode checkbox
+ final MenuItem readerSwitcher = menu.findItem(R.id.reader_mode_menu_id);
+ // The reader mode checkbox is hidden only
+ // when the current page is neither distillable nor distilled
+ readerSwitcher.setVisible(isDistillable || isDistilled);
+ readerSwitcher.setChecked(isDistilled);
+
+ 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:
+ getCurrentTab().capture();
+ openTabToHomePage();
+ break;
+
+ case R.id.incognito_menu_id:
+ getCurrentTab().capture();
+ openIncognitoTab();
+ break;
+
+ case R.id.goto_menu_id:
+ editUrl();
+ break;
+
+ case R.id.bookmarks_menu_id:
+ bookmarksOrHistoryPicker(ComboViews.Bookmarks);
+ break;
+
+ case R.id.snapshots_menu_id:
+ bookmarksOrHistoryPicker(ComboViews.Snapshots);
+ break;
+
+ case R.id.bookmark_this_page_id:
+ bookmarkCurrentPage();
+ break;
+
+ case R.id.stop_reload_menu_id:
+ if (isInLoad()) {
+ stopLoading();
+ } else {
+ Tab currentTab = mTabControl.getCurrentTab();
+ getCurrentTopWebView().reload();
+ }
+ 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.invokeMethod(
+ "android.os.SystemProperties","get", type, params);
+ if (ret != null && ret.equals("enable"))
+ break;
+ if (BrowserConfig.getInstance(getContext())
+ .hasFeature(BrowserConfig.Feature.EXIT_DIALOG))
+ 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;
+ createScreenshotAsync(
+ source.getWebView(),
+ getDesiredThumbnailWidth(mActivity),
+ getDesiredThumbnailHeight(mActivity),
+ new ValueCallback<Bitmap>() {
+ @Override
+ public void onReceiveValue(Bitmap bitmap) {
+ new SaveSnapshotTask(source, bitmap).execute();
+ }
+ });
+ break;
+
+ case R.id.page_info_menu_id:
+ showPageInfo();
+ break;
+
+ case R.id.snapshot_go_live:
+ // passing null to distinguish between
+ // "go live" button and navigating a web page
+ // on a snapshot tab
+ return goLive(null);
+ 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.reader_mode_menu_id:
+ toggleReaderMode();
+ 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;
+
+ case R.id.about_menu_id:
+ Bundle bundle = new Bundle();
+ bundle.putCharSequence("UA", Engine.getDefaultUserAgent());
+ bundle.putCharSequence("TabTitle", mTabControl.getCurrentTab().getTitle());
+ bundle.putCharSequence("TabURL", mTabControl.getCurrentTab().getUrl());
+ BrowserPreferencesPage.startPreferenceFragmentExtraForResult(mActivity,
+ AboutPreferencesFragment.class.getName(), bundle, 0);
+ break;
+
+ case R.id.add_to_homescreen:
+ final WebView w = getCurrentTopWebView();
+ final EditText input = new EditText(getContext());
+ input.setText(w.getTitle());
+ new AlertDialog.Builder(getContext())
+ .setTitle(getContext().getResources().getString(
+ R.string.add_to_homescreen))
+ .setMessage(R.string.my_navigation_name)
+ .setView(input)
+ .setPositiveButton(getContext().getResources().getString(
+ R.string.add_bookmark_short), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ mActivity.sendBroadcast(BookmarkUtils.createAddToHomeIntent(
+ getContext(),
+ w.getUrl(),
+ input.getText().toString(),
+ w.getViewportBitmap(),
+ w.getFavicon()));
+
+ mActivity.startActivity(new Intent(Intent.ACTION_MAIN)
+ .addCategory(Intent.CATEGORY_HOME));
+ }})
+ .setNegativeButton(getContext().getResources().getString(
+ R.string.cancel), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ // Do nothing.
+ }
+ })
+ .show();
+ 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 Bitmap mBitmap;
+
+ private SaveSnapshotTask(Tab tab, Bitmap bm) {
+ mTab = tab;
+ mBitmap = bm;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ CharSequence message = mActivity.getText(R.string.saving_snapshot);
+ mProgressDialog = ProgressDialog.show(mActivity, null, message,
+ true, true, this);
+ mValues = mTab.createSnapshotValues(mBitmap);
+ }
+
+ @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);
+ }
+
+ // This function calls the method in the webview to enable/disable
+ // the reader mode through the DOM distiller
+ public void toggleReaderMode() {
+ Tab t = mTabControl.getCurrentTab();
+ if (t.isDistilled()) {
+ closeTab(t);
+ } else if (t.isDistillable()) {
+ openTab(t.getDistilledUrl(), false, true, false, t);
+ }
+ }
+
+ @Override
+ public void findOnPage() {
+ getCurrentTopWebView().showFindDialog(null, true);
+ }
+
+ @Override
+ public void openPreferences() {
+ BrowserPreferencesPage.startPreferencesForResult(mActivity, getCurrentTopWebView().getUrl(), PREFERENCES_PAGE);
+ }
+
+ @Override
+ public void bookmarkCurrentPage() {
+ if(EditBookmarksRestriction.getInstance().isEnabled()) {
+ Toast.makeText(getContext(), R.string.mdm_managed_alert,
+ Toast.LENGTH_SHORT).show();
+ }
+ else {
+ WebView w = getCurrentTopWebView();
+ if (w == null)
+ return;
+ final Intent i = createBookmarkCurrentPageIntent(mCurrentPageBookmarked);
+ mActivity.startActivity(i);
+ }
+ }
+
+ public boolean goLive(String url) {
+ if (!getCurrentTab().isSnapshot())
+ return false;
+ SnapshotTab t = (SnapshotTab) getCurrentTab();
+
+ if (url == null) { // "go live" button was clicked
+ url = t.getLiveUrl();
+ closeTab(t);
+ }
+ Tab liveTab = createNewTab(false, true, false);
+ loadUrl(liveTab, url);
+ return true;
+ }
+
+ private void showExitDialog(final Activity activity) {
+ BrowserActivity.killOnExitDialog = false;
+ new AlertDialog.Builder(activity)
+ .setTitle(R.string.exit_browser_title)
+ /* disabled, was worrying people: .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) {
+ mCrashRecoveryHandler.clearState(true);
+ BrowserActivity.killOnExitDialog = true;
+ activity.finish();
+ dialog.dismiss();
+ }
+ })
+ .show();
+ }
+
+ @Override
+ public void showPageInfo() {
+ }
+
+ @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.save_link_bookmark_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 = getCurrentTab().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());
+ }
+ }
+ //SWE: Thumbnail will need to be set asynchronously
+ 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);
+ }
+
+ @Override
+ public void showFileChooser(ValueCallback<String[]> uploadFilePaths, String acceptTypes,
+ boolean capture) {
+ mUploadHandler = new UploadHandler(this);
+ mUploadHandler.showFileChooser(uploadFilePaths, acceptTypes, 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 void createScreenshotAsync(WebView view, int width, int height,
+ final ValueCallback<Bitmap> cb) {
+ if (view == null || width == 0 || height == 0) {
+ return;
+ }
+ view.getContentBitmapAsync(
+ (float) width / view.getWidth(),
+ new Rect(),
+ new ValueCallback<Bitmap>() {
+ @Override
+ public void onReceiveValue(Bitmap bitmap) {
+ if (bitmap != null)
+ bitmap = bitmap.copy(Bitmap.Config.RGB_565, false);
+ cb.onReceiveValue(bitmap);
+ }});
+ }
+
+ 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) {
+
+ //Not going to the Nav Screen AnyMore. Unless NavScreen is already showing.
+ mUi.cancelNavScreenRequest();
+ mTabControl.setCurrentTab(tab);
+ // the tab is guaranteed to have a webview after setCurrentTab
+ mUi.setActiveTab(tab);
+
+
+ tab.setTimeStamp();
+ //Purge active tabs
+ MemoryMonitor.purgeActiveTabs(mActivity.getApplicationContext(), this, mSettings);
+ }
+ }
+
+ protected void closeEmptyTab() {
+ Tab current = mTabControl.getCurrentTab();
+ if (current != null
+ && current.getWebView().copyBackForwardList().getSize() == 0) {
+ closeCurrentTab();
+ }
+ }
+
+ protected void reuseTab(Tab appTab, UrlData urlData) {
+ //Cancel navscreen request
+ mUi.cancelNavScreenRequest();
+ // 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 instanceof SnapshotTab) {
+ addTab(tab);
+ if (setActive)
+ setActiveTab(tab);
+ }else 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;
+ if (IncognitoRestriction.getInstance().isEnabled() && incognito) {
+ Toast.makeText(getContext(), R.string.mdm_managed_alert, Toast.LENGTH_SHORT).show();
+ } else {
+ if (mTabControl.canCreateNewTab()) {
+ tab = mTabControl.createNewTab(incognito, !setActive);
+ addTab(tab);
+ if (setActive) {
+ setActiveTab(tab);
+ } else {
+ tab.pause();
+ }
+ } 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, null);
+ 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);
+ mHomepageHandler.registerJsInterface(tab.getWebView(), url);
+ tab.loadUrl(url, headers);
+ 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
+ //In case of tab can go back (aka tab has navigation entry) do nothing
+ //else just load homepage in current tab.
+ if (!tab.canGoBack()) {
+ tab.loadUrl(mSettings.getHomePage(), null);
+ }
+ }
+
+ void goBackOnePageOrQuit() {
+ Tab current = mTabControl.getCurrentTab();
+ if (current == null) {
+ if (BrowserConfig.getInstance(getContext()).hasFeature(BrowserConfig.Feature.EXIT_DIALOG)) {
+ showExitDialog(mActivity);
+ } 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().
+ */
+ mActivity.moveTaskToBack(true);
+ }
+ 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 if (BrowserConfig.getInstance(getContext())
+ .hasFeature(BrowserConfig.Feature.EXIT_DIALOG)) {
+ showExitDialog(mActivity);
+ } 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().
+ */
+ mActivity.moveTaskToBack(true);
+ }
+ }
+ }
+
+ /**
+ * 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) {
+ if (keyCode == KeyEvent.KEYCODE_MENU && event.getRepeatCount() == 0) {
+ // Hardware menu key
+ if (!mUi.isComboViewShowing()) {
+ mAppMenuHandler.showAppMenu(mActivity.findViewById(R.id.taburlbar),
+ true, false);
+ }
+ return true;
+ }
+
+ 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.
+ mAutoFillSetupMessage = message;
+ BrowserPreferencesPage.startPreferenceFragmentForResult(mActivity,
+ AutoFillSettingsFragment.class.getName(), 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);
+ }
+
+ public void setWindowDimming(float level) {
+ if (mLevel == level)
+ return;
+ mLevel = level;
+ if (level != 0.0f) {
+ WindowManager.LayoutParams lp = mActivity.getWindow().getAttributes();
+ lp.dimAmount = level;
+ mActivity.getWindow().setAttributes(lp);
+ mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+ } else {
+ mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+ }
+ }
+
+ @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;
+ }
+
+ @Override
+ public boolean shouldShowAppMenu() {
+ return true;
+ }
+
+ @Override
+ public int getMenuThemeResourceId() {
+ return R.style.OverflowMenuTheme;
+ }
+}
diff --git a/src/src/com/android/browser/CrashLogExceptionHandler.java b/src/src/com/android/browser/CrashLogExceptionHandler.java
new file mode 100644
index 00000000..dc42d596
--- /dev/null
+++ b/src/src/com/android/browser/CrashLogExceptionHandler.java
@@ -0,0 +1,388 @@
+/*
+ * 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;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.os.Build;
+import android.os.Build.VERSION;
+import android.os.SystemClock;
+import android.net.http.AndroidHttpClient;
+import android.util.Log;
+import android.os.FileObserver;
+import android.os.Handler;
+
+import org.codeaurora.swe.BrowserCommandLine;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.entity.InputStreamEntity;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.entity.AbstractHttpEntity;
+import org.apache.http.entity.ByteArrayEntity;
+
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.json.JSONException;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.Integer;
+import java.lang.StringBuilder;
+import java.lang.System;
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.util.Calendar;
+import java.util.zip.GZIPOutputStream;
+
+public class CrashLogExceptionHandler implements Thread.UncaughtExceptionHandler {
+
+ private static final String CRASH_LOG_FILE = "crash.log";
+ private static final String CRASH_LOG_MAX_FILE_SIZE_CMD = "crash-log-max-file-size";
+ private static final String CRASH_REPORT_DIR = "Crash Reports";
+
+ private final static String LOGTAG = "CrashLog";
+
+ private Context mAppContext = null;
+
+ private UncaughtExceptionHandler mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
+
+ private String mLogServer = new String();
+
+ private boolean mOverrideHandler = false;
+
+ private int mMaxLogFileSize = 1024 * 1024;
+ // To avoid increasing startup time an upload delay is used
+ private static final int UPLOAD_DELAY = 3000;
+
+ private static FileObserver crashObserver;
+
+ private final Handler mCrashReportHandler = new Handler();
+
+ public CrashLogExceptionHandler(Context ctx) {
+ mAppContext = ctx;
+ if (BrowserCommandLine.hasSwitch(BrowserSwitches.CRASH_LOG_SERVER_CMD)) {
+ initNativeReporter(ctx);
+ mLogServer = BrowserCommandLine.getSwitchValue(BrowserSwitches.CRASH_LOG_SERVER_CMD);
+ if (mLogServer != null) {
+ uploadPastCrashLog();
+ mOverrideHandler = true;
+ }
+ }
+
+ try {
+ int size = Integer.parseInt(
+ BrowserCommandLine.getSwitchValue(CRASH_LOG_MAX_FILE_SIZE_CMD,
+ Integer.toString(mMaxLogFileSize)));
+ mMaxLogFileSize = size;
+ } catch (NumberFormatException nfe) {
+ Log.e(LOGTAG,"Max log file size is not configured properly. Using default: "
+ + mMaxLogFileSize);
+ }
+
+ }
+
+ private void initNativeReporter(Context ctx){
+ final File crashReports = new File(ctx.getCacheDir(),CRASH_REPORT_DIR);
+ // On fresh installs, make the directory before registering an observer
+ if (!crashReports.isDirectory()) {
+ crashReports.mkdir();
+ }
+ // Implement FileObserver for crashReports that don't bring the system down
+ crashObserver = new FileObserver(crashReports.getAbsolutePath()) {
+ @Override
+ public void onEvent(int event, String path){
+ if ((event == FileObserver.CREATE) || (event == FileObserver.MOVED_TO)){
+ Log.w(LOGTAG, "A crash report was generated");
+ checkNativeCrash(crashReports);
+ }
+ }
+ };
+ // Native Crash reporting if commandline is set
+ mCrashReportHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ checkNativeCrash(crashReports);
+ }
+ }, UPLOAD_DELAY);
+ // start watching the crash reports folder
+ crashObserver.startWatching();
+ }
+
+ private void saveCrashLog(String crashLog) {
+ // Check if log file exists and it's current size
+ try {
+ File file = new File(mAppContext.getFilesDir(), CRASH_LOG_FILE);
+ if (file.exists()) {
+ if (file.length() > mMaxLogFileSize) {
+ Log.e(LOGTAG,"CRASH Log file size(" + file.length()
+ + ") exceeded max log file size("
+ + mMaxLogFileSize + ")");
+ return;
+ }
+ }
+ } catch (NullPointerException npe) {
+ Log.e(LOGTAG,"Exception while checking file size: " + npe);
+ }
+
+ FileOutputStream crashLogFile = null;
+ try {
+ crashLogFile = mAppContext.openFileOutput(CRASH_LOG_FILE, Context.MODE_APPEND);
+ crashLogFile.write(crashLog.getBytes());
+ } catch(IOException ioe) {
+ Log.e(LOGTAG,"Exception while writing file: " + ioe);
+ } finally {
+ if (crashLogFile != null) {
+ try {
+ crashLogFile.close();
+ } catch (IOException ignore) {
+ }
+ }
+ }
+ }
+
+ private void uploadPastCrashLog() {
+ FileInputStream crashLogFile = null;
+ BufferedReader reader = null;
+ try {
+ crashLogFile = mAppContext.openFileInput(CRASH_LOG_FILE);
+
+ reader = new BufferedReader(new InputStreamReader(crashLogFile));
+ StringBuilder crashLog = new StringBuilder();
+ String line = reader.readLine();
+ if (line != null) {
+ crashLog.append(line);
+ }
+
+ // Typically there's only one line (JSON string) in the crash
+ // log file. This loop would not be executed.
+ while ((line = reader.readLine()) != null) {
+ crashLog.append("\n").append(line);
+ }
+
+ uploadCrashLog(crashLog.toString(), UPLOAD_DELAY);
+ } catch(FileNotFoundException fnfe) {
+ Log.v(LOGTAG,"No previous crash found");
+ } catch(IOException ioe) {
+ Log.e(LOGTAG,"Exception while reading crash file: " + ioe);
+ } finally {
+ if (crashLogFile != null) {
+ try {
+ crashLogFile.close();
+ } catch (IOException ignore) {
+ }
+ }
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException ignore) {
+ }
+ }
+ }
+ }
+
+ private void uploadCrashLog(String data, int after) {
+ final String crashLog = data;
+ final int waitFor = after;
+ new Thread(new Runnable() {
+ public void run(){
+ try {
+ SystemClock.sleep(waitFor);
+ AndroidHttpClient httpClient = AndroidHttpClient.newInstance("Android");;
+ HttpPost httpPost = new HttpPost(mLogServer);
+ HttpEntity se = new StringEntity(crashLog);
+ httpPost.setEntity(se);
+ HttpResponse response = httpClient.execute(httpPost);
+
+ File crashLogFile = new File(mAppContext.getFilesDir(),
+ CRASH_LOG_FILE);
+ if (crashLogFile != null) {
+ crashLogFile.delete();
+ } else {
+ Log.e(LOGTAG,"crash log file could not be opened for deletion");
+ }
+ } catch (ClientProtocolException pe) {
+ Log.e(LOGTAG,"Exception while sending http post: " + pe);
+ } catch (IOException ioe1) {
+ Log.e(LOGTAG,"Exception while sending http post: " + ioe1);
+ }
+ }
+ }).start();
+ }
+
+ public void uncaughtException(Thread t, Throwable e) {
+ if (!mOverrideHandler) {
+ mDefaultHandler.uncaughtException(t, e);
+ return;
+ }
+
+ String crashLog = new String();
+
+ try {
+ Calendar calendar = Calendar.getInstance();
+ JSONObject jsonBackTraceObj = new JSONObject();
+ String date = calendar.getTime().toString();
+ String aboutSWE = mAppContext.getResources().getString(R.string.about_text);
+ String sweVer = findValueFromAboutText(aboutSWE, "Version: ");
+ String sweHash = findValueFromAboutText(aboutSWE, "Hash: ");
+ String sweBuildDate = findValueFromAboutText(aboutSWE, "Built: ");
+
+ jsonBackTraceObj.put("date", date);
+ jsonBackTraceObj.put("android-model", android.os.Build.MODEL);
+ jsonBackTraceObj.put("android-device", android.os.Build.DEVICE);
+ jsonBackTraceObj.put("android-ver", android.os.Build.VERSION.RELEASE);
+ jsonBackTraceObj.put("browser-ver", sweVer);
+ jsonBackTraceObj.put("browser-hash", sweHash);
+ jsonBackTraceObj.put("browser-build-date", sweBuildDate);
+ jsonBackTraceObj.put("thread", t.toString());
+ jsonBackTraceObj.put("format", "crashmon-1");
+ jsonBackTraceObj.put("monkey-test", ActivityManager.isUserAMonkey());
+
+ JSONArray jsonStackArray = new JSONArray();
+
+ Throwable throwable = e;
+ String stackTag = "Exception thrown while running";
+ while (throwable != null) {
+ JSONObject jsonStackObj = new JSONObject();
+ StackTraceElement[] arr = throwable.getStackTrace();
+ JSONArray jsonStack = new JSONArray(arr);
+
+ jsonStackObj.put("cause", throwable.getCause());
+ jsonStackObj.put("message", throwable.getMessage());
+ jsonStackObj.put(stackTag, jsonStack);
+
+ jsonStackArray.put(jsonStackObj);
+
+ stackTag = "stack";
+ throwable = throwable.getCause();
+ }
+ jsonBackTraceObj.put("exceptions", jsonStackArray);
+
+ JSONObject jsonMainObj = new JSONObject();
+ jsonMainObj.put("backtraces", jsonBackTraceObj);
+
+ Log.e(LOGTAG, "Exception: " + jsonMainObj.toString(4));
+ crashLog = jsonMainObj.toString();
+
+ } catch (JSONException je) {
+ Log.e(LOGTAG, "Failed in JSON encoding: " + je);
+ }
+
+ saveCrashLog(crashLog);
+
+ uploadCrashLog(crashLog, 0);
+
+ mDefaultHandler.uncaughtException(t, e);
+ }
+
+ private String findValueFromAboutText(String aboutText, String aboutKey) {
+ int start = aboutText.indexOf(aboutKey);
+ int end = aboutText.indexOf("\n", start);
+ String value = "";
+
+ if (start != -1 && end != -1) {
+ start += aboutKey.length();
+ value = aboutText.substring(start, end);
+ }
+ return value;
+ }
+
+ private void checkNativeCrash(final File crashReportsDir) {
+ // Search cache/Crash Reports/ for any crashes
+ if (crashReportsDir.exists()) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ for (File f : crashReportsDir.listFiles()) {
+ uploadNativeCrashReport(f);
+ }
+ }
+ }).start();
+ }
+ }
+
+ private void uploadNativeCrashReport(final File report) {
+ Log.w(LOGTAG, "Preparing Crash Report for upload " + report.getName());
+ // get server url from commandline
+ String server = BrowserCommandLine.getSwitchValue(BrowserSwitches.CRASH_LOG_SERVER_CMD);
+ try {
+ HttpClient httpClient = new DefaultHttpClient();
+ HttpPost httpPost = new HttpPost(server);
+
+ // Compress the data
+ ByteArrayOutputStream arrayStream = new ByteArrayOutputStream();
+ OutputStream gzipData = new GZIPOutputStream(arrayStream);
+ InputStream inputStream = new FileInputStream(report);
+ long length = report.length();
+ byte[] data = new byte[(int)length];
+
+ // Read in the bytes
+ int offset = 0;
+ int numRead = 0;
+ while (offset < data.length
+ && (numRead=inputStream.read(data, offset, data.length-offset)) >= 0) {
+ offset += numRead;
+ }
+ gzipData.write(data);
+ gzipData.close();
+
+ AbstractHttpEntity entity = new ByteArrayEntity(arrayStream.toByteArray());
+
+ // Send the report as a compressed Binary
+ entity.setContentType("binary/octet-stream");
+ entity.setContentEncoding("gzip");
+ entity.setChunked(false);
+ httpPost.setEntity(entity);
+
+ HttpResponse response = httpClient.execute(httpPost);
+ int status = response.getStatusLine().getStatusCode();
+ if (status == 200)
+ report.delete();
+ else Log.w(LOGTAG, "Upload Failure. Will try again next time- " + status);
+
+ } catch (Exception e) {
+ Log.w(LOGTAG, "Crash Report failed to upload, will try again next time " + e);
+ }
+ }
+
+}
diff --git a/src/src/com/android/browser/CrashRecoveryHandler.java b/src/src/com/android/browser/CrashRecoveryHandler.java
new file mode 100644
index 00000000..bcdf8b03
--- /dev/null
+++ b/src/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/src/com/android/browser/DataController.java b/src/src/com/android/browser/DataController.java
new file mode 100644
index 00000000..936ef9c9
--- /dev/null
+++ b/src/src/com/android/browser/DataController.java
@@ -0,0 +1,303 @@
+/*
+ * 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.Browser;
+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 {
+ 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/src/com/android/browser/DataUri.java b/src/src/com/android/browser/DataUri.java
new file mode 100644
index 00000000..dae3caf6
--- /dev/null
+++ b/src/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:[&lt;MIME-type&gt;][;charset=&lt;encoding&gt;][;base64],&lt;data&gt;
+ * </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/src/com/android/browser/DateSortedExpandableListAdapter.java b/src/src/com/android/browser/DateSortedExpandableListAdapter.java
new file mode 100644
index 00000000..529e1edd
--- /dev/null
+++ b/src/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/src/com/android/browser/DownloadHandler.java b/src/src/com/android/browser/DownloadHandler.java
new file mode 100644
index 00000000..0148fda4
--- /dev/null
+++ b/src/src/com/android/browser/DownloadHandler.java
@@ -0,0 +1,685 @@
+/*
+ * 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.mdm.DownloadDirRestriction;
+import com.android.browser.platformsupport.WebAddress;
+import com.android.browser.reflect.ReflectHelper;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+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.setVisibleInDownloadsUi(!privateBrowsing);
+ request.setNotificationVisibility(
+ DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
+ final DownloadManager manager = (DownloadManager) activity
+ .getSystemService(Context.DOWNLOAD_SERVICE);
+ new Thread("Browser download") {
+ public void run() {
+ try {
+ manager.enqueue(request);
+ } catch (Exception e) {
+ Log.w("DLHandler", "Could not enqueue the download", e);
+ }
+ }
+ }.start();
+ showStartDownloadToast(activity, privateBrowsing);
+ }
+
+ private static boolean isAudioFileType(int fileType){
+ Object[] params = {Integer.valueOf(fileType)};
+ Class[] type = new Class[] {int.class};
+ Boolean result = (Boolean) ReflectHelper.invokeMethod("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.invokeMethod("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.invokeMethod("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 trimmedcontentDisposition = trimContentDisposition(contentDisposition);
+ String title = URLUtil.guessFileName(url, trimmedcontentDisposition, 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) {
+
+ contentDisposition = trimContentDisposition(contentDisposition);
+
+ 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 {
+ if (DownloadDirRestriction.getInstance().downloadsAllowed()) {
+ startDownloadSettings(activity, url, userAgent, contentDisposition, mimetype, referer,
+ privateBrowsing, contentLength, filename);
+ }
+ else {
+ Toast.makeText(activity, R.string.managed_by_your_administrator, Toast.LENGTH_SHORT)
+ .show();
+ }
+ }
+
+ }
+
+ static String trimContentDisposition(String contentDisposition) {
+ final Pattern CONTENT_DISPOSITION_PATTERN =
+ Pattern.compile("filename\\s*=\\s*(\"?)([^\";]*)\\1\\s*",
+ Pattern.CASE_INSENSITIVE);
+
+ if (contentDisposition != null) {
+
+ try {
+ Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
+ if (m.find()) {
+ contentDisposition = "attachment; filename="+m.group(2);
+ }
+
+ return contentDisposition;
+ } catch (IllegalStateException ex) {
+ // This function is defined as returning null when it can't parse the header
+ }
+ }
+ return null;
+ }
+
+ 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");
+
+ // Since there could be multiple browsers capable of handling
+ // the same intent we assure that the same package handles it
+ intent.setPackage(activity.getPackageName());
+
+ 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,
+ boolean privateBrowsing) {
+ if (!privateBrowsing) {
+ Intent intent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ activity.startActivity(intent);
+ } else {
+ activity.finish();
+ }
+ 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;
+ }
+
+ // assure that internal storage is initialized before
+ // comparing it with download path
+ if (mInternalStorage == null) {
+ initStorageDefaultPath(activity);
+ }
+
+ 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 + DownloadDirRestriction.getInstance().getDownloadDirectory();
+ 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]);
+ break;
+ }
+ }
+ 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(mStorageManager,
+ "getVolumeState", type, params);
+ }
+}
diff --git a/src/src/com/android/browser/DownloadSettings.java b/src/src/com/android/browser/DownloadSettings.java
new file mode 100644
index 00000000..7f1150a8
--- /dev/null
+++ b/src/src/com/android/browser/DownloadSettings.java
@@ -0,0 +1,393 @@
+/*
+ * Copyright (c) 2013,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;
+
+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.webkit.MimeTypeMap;
+import android.text.TextUtils;
+
+import com.android.browser.reflect.ReflectHelper;
+
+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";
+ private static final String APK_TYPE="apk";
+ private static final String OCTET_STREAM = "application/octet-stream";
+
+ 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_cancel);
+ 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);
+
+ String filenameExtension = DownloadHandler.getFilenameExtension(filename);
+
+ // introspect for octet stream mimetype what type of file extension it has
+ // and reassign mimetype
+ if (mimetype == null || mimetype.isEmpty() || mimetype.equals(OCTET_STREAM)) {
+
+ String updatedFileName = filenameBase + "." + filenameExtension;
+ Object[] params = {updatedFileName};
+ Class[] type = new Class[] {String.class};
+ mimetype = (String) ReflectHelper.invokeMethod("android.media.MediaFile",
+ "getMimeTypeForFile", type, params);
+ }
+
+ //Add special check for .apk files with octet-stream mimetype
+ if (filenameExtension.equals(APK_TYPE) && mimetype != null && mimetype.equals(OCTET_STREAM)) {
+ mimetype = "application/vnd.android.package-archive";
+ }
+
+ // last way to fetch for mimetype if its still null
+ if (mimetype == null || mimetype.isEmpty())
+ mimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(filenameExtension);
+
+ downloadPath = chooseFolderFromMimeType(BrowserSettings.getInstance().getDownloadPath(),
+ mimetype);
+ downloadPathForUser = DownloadHandler.getDownloadPathForUser(DownloadSettings.this,
+ downloadPath);
+
+ autoupdateFileName(filenameBase, DownloadHandler.getFilenameExtension(filename), downloadPath);
+ setDownloadPathForUserText(downloadPathForUser);
+ setDownloadFileSizeText();
+ setDownloadFileTimeText();
+ }
+
+ private void autoupdateFileName(String filenameBase, String extension, String downloadPath) {
+ String fullPath = downloadPath + "/" + filenameBase + "." + extension;
+ int count = 1;
+ String newFilenameBase = "";
+
+ while(new File(fullPath).exists()) {
+ newFilenameBase = filenameBase+"-"+count++;
+ fullPath = downloadPath + "/" + newFilenameBase + "." + extension;
+ }
+
+ if(!TextUtils.isEmpty(newFilenameBase)) {
+ filenameBase = newFilenameBase;
+ }
+
+ downloadFilenameET.setText(filenameBase);
+ }
+
+ private OnClickListener downloadPathListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+
+ final String filemanagerIntent =
+ getResources().getString(R.string.def_intent_file_manager);
+ if (!TextUtils.isEmpty(filemanagerIntent)) {
+ // start filemanager for getting download path
+ try {
+ Intent downloadPathIntent = new Intent(filemanagerIntent);
+ DownloadSettings.this.startActivityForResult(downloadPathIntent, DOWNLOAD_PATH);
+ } catch (Exception e) {
+ String err_msg = getString(R.string.activity_not_found,
+ filemanagerIntent);
+ Toast.makeText(DownloadSettings.this, err_msg, Toast.LENGTH_LONG).show();
+ }
+ } else {
+ Log.e(LOGTAG, "File Manager intent not defined !!");
+ }
+
+ }
+ };
+
+ private OnClickListener downloadStartListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ filenameBase = getFilenameBaseFromUserEnter();
+ // check the filename user enter is null or not
+ if (TextUtils.isEmpty(filenameBase) || TextUtils.isEmpty(downloadPath)) {
+ 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) {
+ final String result_dir_sel =
+ getResources().getString(R.string.def_file_manager_result_dir);
+ downloadPath = intent.getStringExtra(result_dir_sel);
+ // Fallback logic to stock browser
+ if (downloadPath == null) {
+ Uri uri = intent.getData();
+ if(uri != null)
+ downloadPath = uri.getPath();
+ }
+ 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/src/com/android/browser/DownloadTouchIcon.java b/src/src/com/android/browser/DownloadTouchIcon.java
new file mode 100644
index 00000000..fe139e89
--- /dev/null
+++ b/src/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.invokeMethod(
+ "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/src/com/android/browser/DraggableFrameLayout.java b/src/src/com/android/browser/DraggableFrameLayout.java
new file mode 100644
index 00000000..e2f96995
--- /dev/null
+++ b/src/src/com/android/browser/DraggableFrameLayout.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2015, 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.content.Context;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.widget.ViewDragHelper;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+public class DraggableFrameLayout extends FrameLayout {
+ private ViewDragHelper mDragger;
+ public DraggableFrameLayout(Context ctx) {
+ super(ctx);
+ }
+ public DraggableFrameLayout(Context ctx, AttributeSet set) {
+ super(ctx, set);
+ }
+ public DraggableFrameLayout(Context ctx, AttributeSet set, int defStyleAttr) {
+ super(ctx, set, defStyleAttr);
+ }
+ public DraggableFrameLayout(Context ctx, AttributeSet set, int defStyleAttr, int defStyleRes) {
+ super(ctx, set, defStyleAttr, defStyleRes);
+ }
+
+ public void setDragHelper(ViewDragHelper dragger) {
+ mDragger = dragger;
+ }
+
+ public final ViewGroup getParentViewGroup() {
+ return (ViewGroup) DraggableFrameLayout.this.getParent();
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ if (mDragger == null)
+ return super.onInterceptTouchEvent(event);
+
+ return mDragger.shouldInterceptTouchEvent(event) ?
+ true : super.onInterceptTouchEvent(event);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mDragger == null)
+ return super.onTouchEvent(event);
+
+ mDragger.processTouchEvent(event);
+ return true;
+ }
+
+ @Override
+ public void computeScroll() {
+ super.computeScroll();
+ if (mDragger != null && mDragger.continueSettling(true)) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ }
+}
+
diff --git a/src/src/com/android/browser/EdgeSwipeController.java b/src/src/com/android/browser/EdgeSwipeController.java
new file mode 100644
index 00000000..5d6edd52
--- /dev/null
+++ b/src/src/com/android/browser/EdgeSwipeController.java
@@ -0,0 +1,560 @@
+/*
+ * Copyright (c) 2015, 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.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Paint;
+import android.os.CountDownTimer;
+import android.support.v4.widget.ViewDragHelper;
+import android.view.View;
+
+import org.codeaurora.swe.WebHistoryItem;
+import org.codeaurora.swe.WebView;
+import org.codeaurora.swe.util.Activator;
+import org.codeaurora.swe.util.Observable;
+
+public class EdgeSwipeController extends ViewDragHelper.Callback {
+ private ViewDragHelper mDragHelper;
+ private int mState = ViewDragHelper.STATE_IDLE;
+ private int mFromEdge = ViewDragHelper.EDGE_LEFT;
+ private boolean mbNavigated = false;
+ private int mOldX = 0;
+ private int mOldDx = 0;
+ private Observable mPageLoadTarget;
+ private Observable mPageLoadObservable;
+
+ private boolean mbCurrBMSynced = false;
+
+ private Tab mActiveTab;
+ private TitleBar mTitleBar;
+
+ private static final float mMinAlpha = 0.5f;
+ private static final int mMinProgress = 85;
+ private static final int mProgressWaitMS = 1000;
+ private static final int EDGE_SWIPE_INVALID_INDEX = -2;
+
+ private CountDownTimer mLoadTimer, mCommitTimer;
+
+ private int mCurrIndex = EDGE_SWIPE_INVALID_INDEX;
+ private int mPrevIndex;
+ private int mNextIndex;
+ private int mMaxIndex;
+
+ private EdgeSwipeModel mModel;
+ private EdgeSwipeView mView;
+
+ public EdgeSwipeController(View container,
+ int stationaryViewId,
+ int slidingViewId,
+ int slidingViewShadowId,
+ int opacityViewId,
+ int liveViewId,
+ int viewGroupId,
+ BaseUi ui) {
+ DraggableFrameLayout viewGroup = (DraggableFrameLayout)
+ container.findViewById(viewGroupId);
+
+ mActiveTab = ui.getActiveTab();
+ mTitleBar = ui.getTitleBar();
+
+ mModel = new EdgeSwipeModel(mActiveTab, mTitleBar);
+ mView = new EdgeSwipeView(
+ container,
+ stationaryViewId,
+ slidingViewId,
+ slidingViewShadowId,
+ opacityViewId,
+ liveViewId,
+ viewGroupId,
+ mTitleBar);
+
+ mPageLoadTarget = mActiveTab.getTabHistoryUpdateObservable();
+ mPageLoadObservable = Activator.activate(
+ new Observable.Observer() {
+ @Override
+ public void onChange(Object... params) {
+ if (mDragHelper == null ||
+ mPageLoadTarget == null) {
+ return;
+ }
+
+ synchronized (this) {
+ int index = (int) params[0];
+ if (mState == ViewDragHelper.STATE_IDLE && index == mCurrIndex) {
+ monitorProgressAtHistoryUpdate(index);
+ }
+ }
+ }
+ },
+ mPageLoadTarget
+ );
+
+ mDragHelper = ViewDragHelper.create(viewGroup, 0.5f, this);
+ mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT | ViewDragHelper.EDGE_RIGHT);
+ viewGroup.setDragHelper(mDragHelper);
+ }
+
+ private void swipeSessionCleanup() {
+ mView.goLive();
+ mModel.cleanup();
+ mCurrIndex = EDGE_SWIPE_INVALID_INDEX;
+ mState = ViewDragHelper.STATE_IDLE;
+ }
+
+ private boolean setState(int curState, int newState) {
+ if (mState == curState) {
+ mState = newState;
+ return true;
+ }
+ return false;
+ }
+
+ public void cleanup() {
+ if (mPageLoadObservable != null) {
+ mPageLoadObservable.onOff(false);
+ synchronized (this) {
+ mDragHelper.cancel();
+ swipeSessionCleanup();
+ }
+ }
+ }
+
+ public void onConfigurationChanged() {
+ synchronized (this) {
+ swipeSessionCleanup();
+ }
+ }
+
+ private void showCurrBMInStationaryView() {
+ if (!mbCurrBMSynced) {
+ Bitmap currBM = mModel.readSnapshot(mCurrIndex);
+ if (currBM != null) {
+ mView.setStationaryViewBitmap(currBM);
+ mbCurrBMSynced = true;
+ }
+ }
+ }
+
+ private void showCurrBMInSlidingView() {
+ if (!mbCurrBMSynced) {
+ Bitmap currBM = mModel.readSnapshot(mCurrIndex);
+ mView.setSlidingViewBitmap(currBM);
+ if (currBM != null) {
+ mbCurrBMSynced = true;
+ }
+ }
+ }
+
+ private Bitmap getGrayscale(Bitmap bitmap)
+ {
+ if (bitmap == null)
+ return null;
+
+ int height = bitmap.getHeight();
+ int width = bitmap.getWidth();
+
+ Bitmap gray = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas c = new Canvas(gray);
+ Paint paint = new Paint();
+ ColorMatrix cm = new ColorMatrix();
+
+ cm.setSaturation(0);
+
+ ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
+
+ paint.setColorFilter(f);
+
+ c.drawBitmap(bitmap, 0, 0, paint);
+ return gray;
+ }
+
+ private void monitorProgressAtLoad(final int pageIndex) {
+ if (mLoadTimer != null) {
+ mLoadTimer.cancel();
+ }
+
+ mLoadTimer = new CountDownTimer(mProgressWaitMS * 5, mProgressWaitMS) {
+ boolean mGrayBM = false;
+
+ public void onTick(long msRemain) {
+ if (msRemain > mProgressWaitMS * 4) {
+ return;
+ }
+ synchronized (this) {
+ if (mTitleBar.getProgressView().getProgressPercent() >= mMinProgress) {
+ if (mState == ViewDragHelper.STATE_IDLE && pageIndex == mCurrIndex) {
+ swipeSessionCleanup();
+
+ }
+ cancel();
+ } else if(mState == ViewDragHelper.STATE_DRAGGING) {
+ if (mGrayBM) {
+ return;
+ }
+ switch (mFromEdge) {
+ case ViewDragHelper.EDGE_LEFT:
+ mView.setSlidingViewBitmap(
+ getGrayscale(getSnapshotOrFavicon(pageIndex)));
+ mGrayBM = true;
+ break;
+ case ViewDragHelper.EDGE_RIGHT:
+ mView.setStationaryViewBitmap(
+ getGrayscale(getSnapshotOrFavicon(pageIndex)));
+ mGrayBM = true;
+ break;
+ }
+ } else {
+ if (mGrayBM) {
+ return;
+ }
+ mView.setStationaryViewBitmap(
+ getGrayscale(getSnapshotOrFavicon(pageIndex)));
+ mGrayBM = true;
+ }
+ }
+ }
+
+ public void onFinish() {
+ mGrayBM = false;
+ synchronized (this) {
+ if (mTitleBar.getProgressView().getProgressPercent() >= mMinProgress) {
+ if (mState == ViewDragHelper.STATE_IDLE && pageIndex == mCurrIndex) {
+ swipeSessionCleanup();
+ }
+ cancel();
+ }
+ }
+ }
+ }.start();
+ }
+
+ private int lastCommittedHistoryIndex() {
+ WebView wv = mActiveTab.getWebView();
+ if (wv == null || wv.getLastCommittedHistoryIndex() == -1)
+ return 0; // WebView is null or No History has been committed for this tab
+ else
+ return wv.getLastCommittedHistoryIndex();
+ }
+
+ private void monitorProgressAtHistoryUpdate(final int pageIndex) {
+ if (mCommitTimer != null) {
+ mCommitTimer.cancel();
+ }
+
+ if (mTitleBar.getProgressView().getProgressPercent() >= mMinProgress
+ && lastCommittedHistoryIndex() == pageIndex) {
+ swipeSessionCleanup();
+ return;
+ }
+
+ mCommitTimer = new CountDownTimer(mProgressWaitMS * 5, mProgressWaitMS) {
+ public void onTick(long msRemain) {
+ synchronized (this) {
+ if (mTitleBar.getProgressView().getProgressPercent() >= mMinProgress) {
+ if (mState == ViewDragHelper.STATE_IDLE && pageIndex == mCurrIndex) {
+ swipeSessionCleanup();
+
+ }
+ cancel();
+ }
+ }
+ }
+
+ public void onFinish() {
+ synchronized (this) {
+ if (mState == ViewDragHelper.STATE_IDLE && pageIndex == mCurrIndex) {
+ swipeSessionCleanup();
+ }
+ }
+ }
+ }.start();
+ }
+
+ private boolean isPortrait(Bitmap bitmap) {
+ return (bitmap.getHeight() < bitmap.getWidth());
+ }
+
+ private Bitmap getSnapshotOrFavicon(int index) {
+ Bitmap bm = mModel.readSnapshot(index);
+ if (bm == null || mView.isPortrait() != isPortrait(bm)) {
+ WebHistoryItem item = mActiveTab.getWebView()
+ .copyBackForwardList().getItemAtIndex(index);
+ if (item != null) {
+ bm = item.getFavicon();
+ }
+ }
+ return bm;
+ }
+
+ public void onViewDragStateChanged(int state) {
+ synchronized (this) {
+ if (mState != ViewDragHelper.STATE_SETTLING || state != ViewDragHelper.STATE_IDLE) {
+ return;
+ }
+
+ mView.hideSlidingViews();
+
+ if (mbNavigated) {
+ mView.setStationaryViewBitmap(getSnapshotOrFavicon(mCurrIndex));
+ } else {
+ swipeSessionCleanup();
+ }
+
+ mView.setStationaryViewAlpha(1.0f);
+ mView.invalidate();
+
+ setState(ViewDragHelper.STATE_SETTLING, ViewDragHelper.STATE_IDLE);
+ }
+ }
+
+ public void onViewReleased(View releasedChild, float xvel, float yvel) {
+ synchronized (this) {
+ if (!setState(ViewDragHelper.STATE_DRAGGING, ViewDragHelper.STATE_SETTLING)) {
+ mOldX = 0;
+ mOldDx = 0;
+ return;
+ }
+
+ mbNavigated = true;
+
+ boolean bCrossedEventHorizon = Math.abs(mOldX) > mView.getWidth() / 2;
+
+ if (mCurrIndex >= 0) {
+ if ((xvel > 0 || (xvel == 0 && mOldX > 0 && bCrossedEventHorizon))
+ && mFromEdge == ViewDragHelper.EDGE_LEFT
+ && mActiveTab.getWebView().canGoToHistoryIndex(mCurrIndex - 1)) {
+ mCurrIndex -= 1;
+ mActiveTab.getWebView().stopLoading();
+ mActiveTab.getWebView().goToHistoryIndex(mCurrIndex);
+ monitorProgressAtLoad(mCurrIndex);
+ mDragHelper.settleCapturedViewAt(
+ releasedChild.getMeasuredWidth(),
+ releasedChild.getTop());
+ } else if ((xvel < 0 || (xvel == 0 && mOldX < 0 && bCrossedEventHorizon))
+ && mFromEdge == ViewDragHelper.EDGE_RIGHT
+ && mActiveTab.getWebView().canGoToHistoryIndex(mCurrIndex + 1)) {
+ mCurrIndex += 1;
+ mActiveTab.getWebView().stopLoading();
+ mActiveTab.getWebView().goToHistoryIndex(mCurrIndex);
+ monitorProgressAtLoad(mCurrIndex);
+ mDragHelper.settleCapturedViewAt(
+ -releasedChild.getMeasuredWidth(),
+ releasedChild.getTop());
+ mView.goDormant();
+ } else {
+ mbNavigated = false;
+ mDragHelper.settleCapturedViewAt(0, releasedChild.getTop());
+ }
+ }
+ mOldX = 0;
+ mOldDx = 0;
+
+ mView.invalidate();
+ }
+ }
+
+ public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
+ float alpha = ((float) Math.abs(left)) / mView.getMeasuredWidth();
+
+ synchronized (this) {
+ switch (mFromEdge) {
+ case ViewDragHelper.EDGE_LEFT:
+ if (mView.isLive()) {
+ return;
+ }
+ mView.setStationaryViewAlpha(mMinAlpha + alpha * (1 - mMinAlpha));
+
+ if (mState != ViewDragHelper.STATE_IDLE) {
+ mView.moveShadowView(left);
+ }
+
+ showCurrBMInSlidingView();
+
+ if (mPrevIndex >= 0) {
+ if (!mView.stationaryViewHasImage()) {
+ mView.setStationaryViewBitmap(getSnapshotOrFavicon(mPrevIndex));
+ }
+ }
+ break;
+ case ViewDragHelper.EDGE_RIGHT:
+ mView.setStationaryViewAlpha(mMinAlpha + (1 - alpha) * (1 - mMinAlpha));
+ if (mState != ViewDragHelper.STATE_IDLE) {
+ mView.moveShadowView(mView.getMeasuredWidth() + left);
+
+ if (!mView.slidingViewHasImage() && mNextIndex < mMaxIndex) {
+ mView.setSlidingViewBitmap(getSnapshotOrFavicon(mNextIndex));
+ }
+
+ showCurrBMInStationaryView();
+ if (mbCurrBMSynced) {
+ mView.goDormant();
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ public void onEdgeDragStarted(int edgeFlags, int pointerId) {
+ synchronized (this) {
+ if (mActiveTab.isPrivateBrowsingEnabled()) {
+ mDragHelper.abort();
+ return;
+ }
+
+ if (mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE ||
+ !setState(ViewDragHelper.STATE_IDLE, ViewDragHelper.STATE_DRAGGING)) {
+ mDragHelper.abort();
+ return;
+ }
+
+ if ((edgeFlags & mFromEdge) != mFromEdge || mCurrIndex == EDGE_SWIPE_INVALID_INDEX) {
+ onEdgeTouched(edgeFlags, pointerId);
+ }
+
+ mbCurrBMSynced = false;
+
+ switch (mFromEdge) {
+ case ViewDragHelper.EDGE_LEFT:
+ mView.showSlidingViews();
+ mView.goDormant();
+ mPrevIndex = mCurrIndex - 1;
+ mView.setStationaryViewBitmap(getSnapshotOrFavicon(mPrevIndex));
+ showCurrBMInSlidingView();
+ break;
+ case ViewDragHelper.EDGE_RIGHT:
+ mView.showSlidingViews();
+ mNextIndex = mCurrIndex + 1;
+ mView.setSlidingViewBitmap(getSnapshotOrFavicon(mNextIndex));
+ showCurrBMInStationaryView();
+ if (mbCurrBMSynced)
+ mView.goDormant();
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ public int getOrderedChildIndex(int index) {
+ return mView.slidingViewIndex();
+ }
+
+ public void onEdgeTouched (int edgeFlags, int pointerId) {
+ synchronized (this) {
+ if (mActiveTab.getWebView() == null ||
+ mActiveTab.isPrivateBrowsingEnabled() ||
+ mActiveTab.isKeyboardShowing()) {
+ mDragHelper.abort();
+ return;
+ }
+
+ if (mState != ViewDragHelper.STATE_IDLE && mCurrIndex != EDGE_SWIPE_INVALID_INDEX) {
+ mDragHelper.abort();
+ return;
+ }
+
+ mView.init();
+
+ if (mCurrIndex == EDGE_SWIPE_INVALID_INDEX) {
+ mCurrIndex = lastCommittedHistoryIndex();
+ }
+
+ mMaxIndex = mActiveTab.getWebView().copyBackForwardList().getSize() - 1;
+ mModel.updateSnapshot(mCurrIndex);
+
+ if (ViewDragHelper.EDGE_LEFT == (edgeFlags & ViewDragHelper.EDGE_LEFT)) {
+ mFromEdge = ViewDragHelper.EDGE_LEFT;
+ mView.slidingViewTouched(mFromEdge);
+ if (mCurrIndex > 0) {
+ mModel.fetchSnapshot(mCurrIndex - 1);
+ }
+ } else if (ViewDragHelper.EDGE_RIGHT == (edgeFlags & ViewDragHelper.EDGE_RIGHT)) {
+ mFromEdge = ViewDragHelper.EDGE_RIGHT;
+ mView.slidingViewTouched(mFromEdge);
+ if (mCurrIndex < mMaxIndex) {
+ mModel.fetchSnapshot(mCurrIndex + 1);
+ }
+ }
+ }
+ }
+
+ public int getViewHorizontalDragRange(View child) {
+ return child.getMeasuredWidth();
+ }
+
+ public boolean tryCaptureView(View child, int pointerId) {
+ return (mState == ViewDragHelper.STATE_DRAGGING && mView.allowCapture(child));
+ }
+
+ public int clampViewPositionHorizontal(View child, int left, int dx) {
+ if (mOldX != 0 && Math.signum(dx) != Math.signum(mOldDx)) {
+ mOldDx = dx;
+ return mOldX;
+ }
+
+ switch (mFromEdge) {
+ case ViewDragHelper.EDGE_LEFT:
+ if (left < 0) {
+ mOldDx = dx;
+ return mOldX;
+ }
+ if (!mActiveTab.getWebView().canGoToHistoryIndex(mPrevIndex)) {
+ if (Math.abs(left) >= child.getMeasuredWidth() / 3) {
+ return child.getMeasuredWidth() / 3;
+ }
+ }
+ break;
+ case ViewDragHelper.EDGE_RIGHT:
+ if (left > 0) {
+ mOldDx = dx;
+ return mOldX;
+ }
+ if (!mActiveTab.getWebView().canGoToHistoryIndex(mNextIndex)) {
+ if (Math.abs(left) >= child.getMeasuredWidth() / 3) {
+ return -child.getMeasuredWidth() / 3;
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ mOldX = left;
+ mOldDx = dx;
+ return left;
+ }
+}
+
diff --git a/src/src/com/android/browser/EdgeSwipeModel.java b/src/src/com/android/browser/EdgeSwipeModel.java
new file mode 100644
index 00000000..51a5dc9c
--- /dev/null
+++ b/src/src/com/android/browser/EdgeSwipeModel.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2015, 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.graphics.Bitmap;
+import android.util.SparseArray;
+import android.webkit.ValueCallback;
+
+public class EdgeSwipeModel {
+ private SparseArray<Bitmap> mBitmaps;
+ private Tab mTab;
+ private TitleBar mBar;
+
+ private static final int mMinProgress = 85;
+
+ private static final int mMaxBitmaps = 5;
+
+ public EdgeSwipeModel(Tab tab, TitleBar bar) {
+ mTab = tab;
+ mBar = bar;
+ mBitmaps = new SparseArray<>();
+ }
+
+ public void updateSnapshot(final int index) {
+ if (mBitmaps.get(index) != null) {
+ return;
+ }
+
+ int captureIndex = mTab.getCaptureIndex(index);
+
+ boolean bitmapExists = mTab.getWebView().hasSnapshot(captureIndex);
+
+ if (!mTab.isFirstVisualPixelPainted()) {
+ fetchSnapshot(index);
+ return;
+ }
+
+ int progress = mBar.getProgressView().getProgressPercent();
+
+ if (bitmapExists && progress < mMinProgress) {
+ fetchSnapshot(index);
+ return;
+ }
+
+ mTab.getWebView().captureSnapshot(captureIndex,
+ new ValueCallback<Bitmap>() {
+ @Override
+ public void onReceiveValue(Bitmap value) {
+ mBitmaps.put(index, value);
+ }
+ }
+ );
+ }
+
+ public void fetchSnapshot(final int index) {
+ if (mBitmaps.get(index) != null) {
+ return;
+ }
+
+ int captureIndex = mTab.getCaptureIndex(index);
+
+ mTab.getWebView().getSnapshot(captureIndex,
+ new ValueCallback<Bitmap>() {
+ @Override
+ public void onReceiveValue(Bitmap bitmap) {
+ mBitmaps.put(index, bitmap);
+ }
+ }
+ );
+ }
+
+ public Bitmap readSnapshot(int index) {
+ if (index < 0) {
+ return null;
+ }
+
+ if (index > (mTab.getWebView().copyBackForwardList().getSize() - 1)) {
+ return null;
+ }
+
+ return mBitmaps.get(index);
+ }
+
+ public void deleteSnapshot(int index) {
+ mBitmaps.delete(index);
+ }
+
+ public void cleanup() {
+ mBitmaps.clear();
+ }
+}
diff --git a/src/src/com/android/browser/EdgeSwipeSettings.java b/src/src/com/android/browser/EdgeSwipeSettings.java
new file mode 100644
index 00000000..71c8a565
--- /dev/null
+++ b/src/src/com/android/browser/EdgeSwipeSettings.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (c) 2015, 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.graphics.Bitmap;
+import android.os.AsyncTask;
+import android.support.v4.widget.ViewDragHelper;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+import android.widget.Toast;
+
+public class EdgeSwipeSettings extends ViewDragHelper.Callback {
+ private ViewDragHelper mDragHelper;
+ private int mFromEdge = ViewDragHelper.EDGE_TOP;
+ private int mLeft = 0;
+
+ private ImageView mSlidingViewShadow;
+ private LinearLayout mSettingsView;
+ private DraggableFrameLayout mViewGroup;
+ private View mLiveView;
+ private ImageView mStationaryView;
+
+ private int mSlidingViewIndex;
+ private int mCurrIndex;
+
+ private EdgeSwipeModel mModel;
+ private Tab mActiveTab;
+ private TitleBar mTitleBar;
+
+ private boolean mbWaitForSettings = false;
+
+ public EdgeSwipeSettings(final View container,
+ int stationaryViewId,
+ int settingViewId,
+ int slidingViewShadowId,
+ int liveViewId,
+ int viewGroupId,
+ final BaseUi ui) {
+ DraggableFrameLayout viewGroup = (DraggableFrameLayout)
+ container.findViewById(viewGroupId);
+
+ mSlidingViewShadow = (ImageView) container.findViewById(slidingViewShadowId);
+ mSettingsView = (LinearLayout) container.findViewById(settingViewId);
+ mStationaryView = (ImageView) container.findViewById(stationaryViewId);
+ mLiveView = container.findViewById(liveViewId);
+ mViewGroup = (DraggableFrameLayout) container.findViewById(viewGroupId);
+
+ final int childCount = mViewGroup.getChildCount();
+
+ for (int i = childCount - 1; i >= 0; i--) {
+ final View child = mViewGroup.getChildAt(i);
+ if (mSettingsView == child) {
+ mSlidingViewIndex = i;
+ break;
+ }
+ }
+
+ mActiveTab = ui.getActiveTab();
+ mTitleBar = ui.getTitleBar();
+ mModel = new EdgeSwipeModel(mActiveTab, mTitleBar);
+
+ mDragHelper = ViewDragHelper.create(viewGroup, 0.5f, this);
+ mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT | ViewDragHelper.EDGE_RIGHT);
+ mViewGroup.setDragHelper(mDragHelper);
+
+ final Button closeBtn =
+ (Button) container.findViewById(R.id.edge_sliding_settings_close_btn);
+ closeBtn.setOnClickListener(
+ new View.OnClickListener() {
+ public void onClick(View v) {
+ goLive();
+ }
+ }
+ );
+
+ final RadioButton temporalNavButton =
+ (RadioButton) container.findViewById(R.id.edge_sliding_settings_options_temporal);
+ temporalNavButton.setOnClickListener(
+ new View.OnClickListener() {
+ public void onClick(View v) {
+ BrowserSettings.getInstance().setEdgeSwipeTemporal();
+ goLive();
+ applySettingsAndRefresh(ui, container);
+ Toast toast = Toast.makeText(ui.getActivity().getApplicationContext(),
+ R.string.pref_temporal_edge_swipe_enabled_toast, Toast.LENGTH_SHORT);
+ toast.show();
+ }
+ }
+ );
+
+ final RadioButton spatialNavButton =
+ (RadioButton) container.findViewById(R.id.edge_sliding_settings_options_spatial);
+ spatialNavButton.setOnClickListener(
+ new View.OnClickListener() {
+ public void onClick(View v) {
+ BrowserSettings.getInstance().setEdgeSwipeSpatial();
+ goLive();
+ applySettingsAndRefresh(ui, container);
+ Toast toast = Toast.makeText(ui.getActivity().getApplicationContext(),
+ R.string.pref_spatial_edge_swipe_enabled_toast, Toast.LENGTH_SHORT);
+ toast.show();
+ }
+ }
+ );
+
+ final RadioButton disabledNavButton =
+ (RadioButton) container.findViewById(R.id.edge_sliding_settings_options_disabled);
+ disabledNavButton.setOnClickListener(
+ new View.OnClickListener() {
+ public void onClick(View v) {
+ BrowserSettings.getInstance().setEdgeSwipeDisabled();
+ goLive();
+ applySettingsAndRefresh(ui, container);
+ Toast toast = Toast.makeText(ui.getActivity().getApplicationContext(),
+ R.string.pref_edge_swipe_disabled_toast, Toast.LENGTH_SHORT);
+ toast.show();
+ }
+ }
+ );
+ }
+
+ private void applySettingsAndRefresh(final BaseUi ui, final View container) {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... unused) {
+ mDragHelper = null;
+ ui.refreshEdgeSwipeController(container);
+ return null;
+ }
+ }.execute();
+ }
+
+ private void goLive() {
+ mbWaitForSettings = false;
+ mFromEdge = ViewDragHelper.EDGE_TOP;
+ mLiveView.setVisibility(View.VISIBLE);
+ mStationaryView.setVisibility(View.GONE);
+ mSlidingViewShadow.setVisibility(View.GONE);
+ mSettingsView.setVisibility(View.GONE);
+ mViewGroup.invalidate();
+ }
+
+ private void goDormant() {
+ mLiveView.setVisibility(View.GONE);
+ mStationaryView.setVisibility(View.VISIBLE);
+ mViewGroup.invalidate();
+ }
+
+ public void onConfigurationChanged() {
+ goLive();
+ }
+
+ private void showCurrBitmap() {
+ if (mStationaryView.getVisibility() == View.VISIBLE) {
+ return;
+ }
+
+ Bitmap currBM = mModel.readSnapshot(mCurrIndex);
+ if (currBM != null) {
+ clampViewIfNeeded(mStationaryView);
+ mStationaryView.setImageBitmap(currBM);
+ goDormant();
+ mModel.deleteSnapshot(mCurrIndex);
+ }
+ }
+
+ private void clampViewIfNeeded(View view) {
+ int offset = 0;
+ if (mTitleBar.getY() >= 0) {
+ offset = mTitleBar.getNavigationBar().getMeasuredHeight();
+ }
+ view.setPadding(0, offset - view.getTop(), 0, 0);
+ }
+
+ public void onViewDragStateChanged(int state) {
+ if (ViewDragHelper.STATE_IDLE == state && !mbWaitForSettings) {
+ goLive();
+ }
+ }
+
+ public void onViewReleased(View releasedChild, float xvel, float yvel) {
+ boolean bCrossedEventHorizon = Math.abs(mLeft) > mViewGroup.getWidth() / 2;
+
+ switch (mFromEdge) {
+ case ViewDragHelper.EDGE_LEFT:
+ if (xvel > 0 || (xvel == 0 && mLeft > 0 && bCrossedEventHorizon)) {
+ showCurrBitmap();
+ mbWaitForSettings = true;
+ mDragHelper.settleCapturedViewAt(
+ releasedChild.getMeasuredWidth(),
+ releasedChild.getTop());
+ break;
+ }
+ mDragHelper.settleCapturedViewAt(0, releasedChild.getTop());
+ break;
+ case ViewDragHelper.EDGE_RIGHT:
+ if (xvel < 0 || (xvel == 0 && mLeft < 0 && bCrossedEventHorizon)) {
+ showCurrBitmap();
+ mbWaitForSettings = true;
+ mDragHelper.settleCapturedViewAt(
+ -releasedChild.getMeasuredWidth(),
+ releasedChild.getTop());
+ break;
+ }
+ mDragHelper.settleCapturedViewAt(0, releasedChild.getTop());
+ break;
+ default:
+ mDragHelper.settleCapturedViewAt(0, releasedChild.getTop());
+ break;
+ }
+ mLeft = 0;
+ mViewGroup.invalidate();
+ }
+
+ public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
+ showCurrBitmap();
+ switch (mFromEdge) {
+ case ViewDragHelper.EDGE_LEFT:
+ mSlidingViewShadow.setX(left);
+ mViewGroup.invalidate();
+ break;
+ case ViewDragHelper.EDGE_RIGHT:
+ mSlidingViewShadow.setX(mViewGroup.getMeasuredWidth() + left
+ - mSlidingViewShadow.getMeasuredWidth());
+ mViewGroup.invalidate();
+ break;
+ default:
+ break;
+ }
+ }
+
+ public void onEdgeDragStarted(int edgeFlags, int pointerId) {
+ if (mFromEdge != ViewDragHelper.EDGE_TOP) {
+ return;
+ }
+
+ mCurrIndex = mActiveTab.getWebView().copyBackForwardList().getCurrentIndex();
+
+ mModel.updateSnapshot(mCurrIndex);
+
+ clampViewIfNeeded(mSettingsView);
+
+ if (ViewDragHelper.EDGE_LEFT == (edgeFlags & ViewDragHelper.EDGE_LEFT)) {
+ mFromEdge = ViewDragHelper.EDGE_LEFT;
+
+ mSettingsView.setTranslationX(-mViewGroup.getMeasuredWidth());
+ mSlidingViewShadow.setBackgroundResource(R.drawable.right_shade);
+ } else if (ViewDragHelper.EDGE_RIGHT == (edgeFlags & ViewDragHelper.EDGE_RIGHT)) {
+ mFromEdge = ViewDragHelper.EDGE_RIGHT;
+
+ mSettingsView.setTranslationX(mViewGroup.getMeasuredWidth());
+ mSlidingViewShadow.setBackgroundResource(R.drawable.left_shade);
+ }
+
+ mSettingsView.setVisibility(View.VISIBLE);
+ mSlidingViewShadow.setVisibility(View.VISIBLE);
+
+ showCurrBitmap();
+
+ mViewGroup.invalidate();
+ }
+
+ public int getOrderedChildIndex(int index) {
+ return mSlidingViewIndex;
+ }
+
+ public int getViewHorizontalDragRange(View child) {
+ return child.getMeasuredWidth();
+ }
+
+ public boolean tryCaptureView(View child, int pointerId) {
+ return (mFromEdge != ViewDragHelper.EDGE_TOP && child == mSettingsView);
+ }
+
+ public int clampViewPositionHorizontal(View child, int left, int dx) {
+ mLeft = left;
+ return left;
+ }
+}
+
diff --git a/src/src/com/android/browser/EdgeSwipeView.java b/src/src/com/android/browser/EdgeSwipeView.java
new file mode 100644
index 00000000..a777c26f
--- /dev/null
+++ b/src/src/com/android/browser/EdgeSwipeView.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (c) 2015, 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.graphics.Bitmap;
+import android.graphics.Color;
+import android.support.v4.widget.ViewDragHelper;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+public class EdgeSwipeView {
+ private ImageView mStationaryView;
+ private ImageView mSlidingView;
+ private ImageView mSlidingViewShadow;
+ private View mOpacityView;
+ private FrameLayout mLiveView;
+ private DraggableFrameLayout mViewGroup;
+
+ private int mSlidingViewIndex;
+
+ private boolean mbShowingLive = true;
+
+ private boolean mbStationaryViewBMSet = false;
+ private boolean mbSlidingViewBMSet = false;
+
+ private TitleBar mTitleBar;
+
+ public EdgeSwipeView(
+ View container,
+ int stationaryViewId,
+ int slidingViewId,
+ int slidingViewShadowId,
+ int opacityViewId,
+ int liveViewId,
+ int viewGroupId,
+ TitleBar titleBar) {
+ mStationaryView = (ImageView) container.findViewById(stationaryViewId);
+ mSlidingView = (ImageView) container.findViewById(slidingViewId);
+ mSlidingViewShadow = (ImageView) container.findViewById(slidingViewShadowId);
+ mOpacityView = container.findViewById(opacityViewId);
+ mLiveView = (FrameLayout) container.findViewById(liveViewId);
+ mViewGroup = (DraggableFrameLayout) container.findViewById(viewGroupId);
+ mSlidingViewShadow.setBackgroundResource(R.drawable.left_shade);
+
+ mSlidingView.setVisibility(View.GONE);
+ mSlidingViewShadow.setVisibility(View.GONE);
+ mOpacityView.setVisibility(View.GONE);
+
+ final int childCount = mViewGroup.getChildCount();
+ for (int i = childCount - 1; i >= 0; i--) {
+ final View child = mViewGroup.getChildAt(i);
+ if (mSlidingView == child) {
+ mSlidingViewIndex = i;
+ break;
+ }
+ }
+
+ mTitleBar = titleBar;
+ }
+
+ public void goLive() {
+ if (mbShowingLive)
+ return;
+
+ mLiveView.setVisibility(View.VISIBLE);
+ mStationaryView.setVisibility(View.GONE);
+ mSlidingView.setVisibility(View.GONE);
+ mSlidingViewShadow.setVisibility(View.GONE);
+ mOpacityView.setVisibility(View.GONE);
+ mbShowingLive = true;
+ }
+
+ public void goDormant() {
+ if (!mbShowingLive)
+ return;
+
+ mSlidingView.setVisibility(View.VISIBLE);
+ mStationaryView.setVisibility(View.VISIBLE);
+ mOpacityView.setVisibility(View.VISIBLE);
+ mLiveView.setVisibility(View.GONE);
+ mbShowingLive = false;
+ }
+
+ public boolean isLive() {
+ return mbShowingLive;
+ }
+
+ private Bitmap getColorBitmap(int color)
+ {
+ int height = mViewGroup.getMeasuredHeight();
+ int width = mViewGroup.getMeasuredWidth();
+ height -= (mTitleBar.getY()>= 0) ? mTitleBar.getNavigationBar().getMeasuredHeight() : 0;
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444);
+ bitmap.eraseColor(color);
+ return bitmap;
+ }
+
+ private void clampViewIfNeeded(View view) {
+ int offset = 0;
+ if (mTitleBar.getY() >= 0) {
+ offset = mTitleBar.getNavigationBar().getMeasuredHeight();
+ }
+ view.setPadding(0, offset - view.getTop(), 0, 0);
+ }
+
+ public boolean isPortrait() {
+ return (mViewGroup.getHeight() < mViewGroup.getWidth());
+ }
+
+ private void setBitmap(ImageView view, Bitmap bitmap) {
+ clampViewIfNeeded(view);
+ if (bitmap == null) {
+ bitmap = getColorBitmap(Color.DKGRAY);
+ }
+
+ int offset = 0;
+ if (mTitleBar.getY() >= 0) {
+ offset = mTitleBar.getNavigationBar().getMeasuredHeight();
+ }
+
+ int bitmap_height = bitmap.getHeight();
+
+ if (view.getMeasuredHeight() != 0) {
+ bitmap_height = (view.getMeasuredHeight() - offset) * bitmap.getWidth() /
+ view.getMeasuredWidth();
+ }
+
+ if ((bitmap.getHeight() - bitmap_height) > 5) {
+ Bitmap cropped = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap_height);
+ view.setImageBitmap(cropped);
+ } else {
+ view.setImageBitmap(bitmap);
+ }
+ }
+
+ public void setStationaryViewBitmap(Bitmap bitmap) {
+ mbStationaryViewBMSet = null != bitmap;
+ setBitmap(mStationaryView, bitmap);
+ }
+
+ public void setStationaryViewAlpha(float alpha) {
+ mStationaryView.setAlpha(alpha);
+ }
+
+ public void setSlidingViewBitmap(Bitmap bitmap) {
+ mbSlidingViewBMSet = null != bitmap;
+ setBitmap(mSlidingView, bitmap);
+ }
+
+ public boolean slidingViewHasImage() {
+ return mbSlidingViewBMSet;
+ }
+
+ public boolean stationaryViewHasImage() {
+ return mbStationaryViewBMSet;
+ }
+
+ public void slidingViewTouched(int edge) {
+ if (edge == ViewDragHelper.EDGE_RIGHT) {
+ mSlidingView.setTranslationX(mViewGroup.getMeasuredWidth());
+ } else {
+ mSlidingView.setTranslationX(0);
+ }
+ }
+
+ public void hideSlidingViews() {
+ mSlidingViewShadow.setVisibility(View.GONE);
+ mSlidingView.setVisibility(View.GONE);
+ }
+
+ public void showSlidingViews() {
+ mSlidingViewShadow.setVisibility(View.VISIBLE);
+ mSlidingView.setVisibility(View.VISIBLE);
+ }
+
+ public int slidingViewIndex() {
+ return mSlidingViewIndex;
+ }
+
+ public void moveShadowView(float x) {
+ x -= mSlidingViewShadow.getMeasuredWidth();
+ mSlidingViewShadow.setX(x);
+ mSlidingViewShadow.setVisibility(View.VISIBLE);
+ mOpacityView.setVisibility(View.VISIBLE);
+ }
+
+ public boolean allowCapture(View view) {
+ return (view == mSlidingView);
+ }
+
+ public int getMeasuredWidth() {
+ return mViewGroup.getMeasuredWidth();
+ }
+
+ public int getWidth() {
+ return mViewGroup.getWidth();
+ }
+
+ public void init() {
+ clampViewIfNeeded(mSlidingView);
+ clampViewIfNeeded(mStationaryView);
+ }
+
+ public void invalidate() {
+ mViewGroup.invalidate();
+ }
+}
diff --git a/src/src/com/android/browser/EngineInitializer.java b/src/src/com/android/browser/EngineInitializer.java
new file mode 100644
index 00000000..1a1b0707
--- /dev/null
+++ b/src/src/com/android/browser/EngineInitializer.java
@@ -0,0 +1,436 @@
+/*
+ * Copyright (c) 2014-2015 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.content.Context;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.StrictMode;
+import android.util.Log;
+import android.view.ViewTreeObserver;
+
+import com.android.browser.mdm.DevToolsRestriction;
+
+import org.codeaurora.swe.BrowserCommandLine;
+import org.codeaurora.swe.Engine;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+
+import org.chromium.base.VisibleForTesting;
+
+public class EngineInitializer {
+
+ private final static String LOGTAG = "EngineInitializer";
+
+ private static boolean mInitializationStarted = false;
+ private static boolean mSynchronousInitialization = false;
+ private static boolean mInitializationCompleted = false;
+ private static Handler mUiThreadHandler;
+
+ static class ActivityResult
+ {
+ public Intent data;
+ public int requestCode;
+ public int resultCode;
+
+ public ActivityResult(int requestCode, int resultCode, Intent data)
+ {
+ this.requestCode = requestCode;
+ this.resultCode = resultCode;
+ this.data = data;
+ }
+ }
+
+ public static class ActivityScheduler implements ViewTreeObserver.OnPreDrawListener
+ {
+ private BrowserActivity mActivity = null;
+ private ArrayList<ActivityResult> mPendingActivityResults = null;
+ private ArrayList<Intent> mPendingIntents = null;
+
+ private boolean mFirstDrawCompleted = false;
+ private boolean mOnStartPending = false;
+ private boolean mOnPausePending = false;
+ private boolean mEngineInitialized = false;
+ private boolean mCanForwardEvents = false;
+
+ public ActivityScheduler(BrowserActivity activity)
+ {
+ mActivity = activity;
+ mFirstDrawCompleted = false;
+ mOnStartPending = false;
+ mOnPausePending = false;
+ mPendingIntents = null;
+ mPendingActivityResults = null;
+ mEngineInitialized = false;
+ mCanForwardEvents = false;
+ }
+
+ @VisibleForTesting
+ public boolean firstDrawCompleted() { return mFirstDrawCompleted; }
+ @VisibleForTesting
+ public boolean onStartPending() { return mOnStartPending; }
+ @VisibleForTesting
+ public boolean onPausePending() { return mOnPausePending; }
+ @VisibleForTesting
+ public boolean engineInitialized() { return mEngineInitialized; }
+ @VisibleForTesting
+ public boolean canForwardEvents() { return mCanForwardEvents; }
+
+ public void processPendingEvents() {
+ assert runningOnUiThread() : "Tried to initialize the engine on the wrong thread.";
+
+ if (mOnStartPending) {
+ mOnStartPending = false;
+ mActivity.handleOnStart();
+ }
+ if (mOnPausePending) {
+ mActivity.handleOnPause();
+ mOnPausePending = false;
+ }
+ if (mPendingIntents != null) {
+ for (int i = 0; i < mPendingIntents.size(); i++) {
+ mActivity.handleOnNewIntent(mPendingIntents.get(i));
+ }
+ mPendingIntents = null;
+ }
+ if (mPendingActivityResults != null) {
+ for (int i = 0; i < mPendingActivityResults.size(); i++) {
+ ActivityResult result = mPendingActivityResults.get(i);
+ mActivity.handleOnActivityResult(result.requestCode, result.resultCode, result.data);
+ }
+ mPendingActivityResults = null;
+ }
+ mCanForwardEvents = true;
+ }
+
+ public void onActivityCreate(boolean engineInitialized) {
+ mEngineInitialized = engineInitialized;
+ if (!mEngineInitialized) {
+ // Engine initialization is not completed, we should wait for the onPreDraw() notification.
+ final ViewTreeObserver observer = mActivity.getWindow().getDecorView().getViewTreeObserver();
+ observer.addOnPreDrawListener(this);
+ } else {
+ mFirstDrawCompleted = true;
+ mCanForwardEvents = true;
+ }
+ }
+
+ @Override
+ public boolean onPreDraw() {
+ final ViewTreeObserver observer = mActivity.getWindow().getDecorView().getViewTreeObserver();
+ observer.removeOnPreDrawListener(this);
+
+ if (mFirstDrawCompleted)
+ return true;
+
+ mFirstDrawCompleted = true;
+ if (mEngineInitialized) {
+ postOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.startController();
+ processPendingEvents();
+ }
+ });
+ }
+ return true;
+ }
+
+ public void onEngineInitializationCompletion(boolean synchronous) {
+ if (synchronous) {
+ // Don't wait for pre-draw notification if it is synchronous
+ onPreDraw();
+ }
+ mEngineInitialized = true;
+ if (mFirstDrawCompleted) {
+ mActivity.startController();
+ processPendingEvents();
+ }
+ }
+
+ public void onActivityPause() {
+ if (mCanForwardEvents) {
+ mActivity.handleOnPause();
+ return;
+ }
+ mOnPausePending = true;
+ }
+
+ public void onActivityResume() {
+ if (mCanForwardEvents) {
+ mActivity.handleOnResume();
+ return;
+ }
+ mOnPausePending = false;
+ }
+
+ public void onActivityStart() {
+ if (mCanForwardEvents) {
+ mActivity.handleOnStart();
+ // TODO: We have no reliable mechanism to know when the app goes background.
+ //ChildProcessLauncher.onBroughtToForeground();
+ return;
+ }
+ mOnStartPending = true;
+ }
+
+ public void onActivityStop() {
+ if (!mCanForwardEvents) {
+ initializeSync(mActivity.getApplicationContext());
+ }
+ mActivity.handleOnStop();
+ }
+
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (mCanForwardEvents) {
+ mActivity.handleOnActivityResult(requestCode, resultCode, data);
+ return;
+ }
+ if (mPendingActivityResults == null) {
+ mPendingActivityResults = new ArrayList<ActivityResult>(1);
+ }
+ mPendingActivityResults.add(new ActivityResult(requestCode, resultCode, data));
+ }
+
+ public void onNewIntent(Intent intent) {
+ if (mCanForwardEvents) {
+ mActivity.handleOnNewIntent(intent);
+ return;
+ }
+
+ if (mPendingIntents == null) {
+ mPendingIntents = new ArrayList<Intent>(1);
+ }
+ mPendingIntents.add(intent);
+ }
+ }
+
+ private static HashMap<BrowserActivity, ActivityScheduler> mActivitySchedulerMap = null;
+ private static long sDelayForTesting = 0;
+
+ @VisibleForTesting
+ public static void setDelayForTesting(long delay)
+ {
+ sDelayForTesting = delay;
+ }
+
+ @VisibleForTesting
+ public static boolean isInitialized()
+ {
+ return mInitializationCompleted;
+ }
+
+ public static boolean runningOnUiThread() {
+ return mUiThreadHandler.getLooper() == Looper.myLooper();
+ }
+
+ public static void postOnUiThread(Runnable task) {
+ mUiThreadHandler.post(task);
+ }
+
+ private static class InitializeTask extends AsyncTask<Void, Void, Boolean> {
+ private Context mApplicationContext;
+ public InitializeTask(Context ctx) {
+ mApplicationContext = ctx;
+ }
+ @Override
+ protected Boolean doInBackground(Void... unused) {
+ try
+ {
+ // For testing.
+ if (sDelayForTesting > 0) {
+ Thread.sleep(sDelayForTesting);
+ }
+
+ Engine.loadNativeLibraries(mApplicationContext);
+ if (!BrowserCommandLine.hasSwitch(BrowserSwitches.SINGLE_PROCESS)) {
+ Engine.warmUpChildProcess(mApplicationContext);
+ }
+ return true;
+ }
+ catch (Exception e)
+ {
+ Log.e(LOGTAG, "Unable to load native library.", e);
+ }
+ return false;
+ }
+
+ @Override
+ protected void onPostExecute (Boolean result) {
+ completeInitializationOnUiThread(mApplicationContext);
+ }
+ }
+ private static InitializeTask mInitializeTask = null;
+
+ public static void initializeSync(Context ctx) {
+ assert runningOnUiThread() : "Tried to initialize the engine on the wrong thread.";
+ mSynchronousInitialization = true;
+ if (mInitializeTask != null) {
+ try {
+ // Wait for the InitializeTask to finish.
+ mInitializeTask.get();
+ } catch (CancellationException e1) {
+ Log.e(LOGTAG, "Native library load cancelled", e1);
+ } catch (ExecutionException e2) {
+ Log.e(LOGTAG, "Native library load failed", e2);
+ } catch (InterruptedException e3) {
+ Log.e(LOGTAG, "Native library load interrupted", e3);
+ }
+ }
+ completeInitializationOnUiThread(ctx);
+ mSynchronousInitialization = false;
+ }
+
+ private static void initialize(Context ctx) {
+ if (!mInitializationCompleted) {
+ if (!mInitializationStarted) {
+ mInitializationStarted = true;
+ mUiThreadHandler = new Handler(Looper.getMainLooper());
+ Engine.initializeCommandLine(ctx, CommandLineManager.getCommandLineSwitches(ctx));
+ mInitializeTask = new InitializeTask(ctx);
+ mInitializeTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ mActivitySchedulerMap = new HashMap<BrowserActivity, ActivityScheduler>();
+ } else {
+ // This is not the first activity, wait for the engine initialization to finish.
+ initializeSync(ctx);
+ }
+ }
+ }
+
+ public static ActivityScheduler onActivityCreate(BrowserActivity activity) {
+ assert runningOnUiThread() : "Tried to initialize the engine on the wrong thread.";
+
+ Context ctx = activity.getApplicationContext();
+ ActivityScheduler scheduler = new ActivityScheduler(activity);
+ initialize(ctx);
+
+ scheduler.onActivityCreate(mInitializationCompleted);
+ if (!mInitializationCompleted) {
+ mActivitySchedulerMap.put(activity, scheduler);
+ }
+ return scheduler;
+ }
+
+ public static void onPostActivityCreate(BrowserActivity activity) {
+ EngineInitializer.initializeResourceExtractor(activity);
+ if (EngineInitializer.isInitialized()) {
+ activity.startController();
+ }
+ }
+
+ private static void completeInitializationOnUiThread(Context ctx) {
+ assert runningOnUiThread() : "Tried to initialize the engine on the wrong thread.";
+
+ if (!mInitializationCompleted) {
+
+ // TODO: Evaluate the benefit of async Engine.initialize()
+ Engine.initialize(ctx, CommandLineManager.getCommandLineSwitches(ctx));
+ // Add the browser commandline options
+ BrowserConfig.getInstance(ctx).initCommandLineSwitches();
+
+ //Note: Only enable this for debugging.
+ if (BrowserCommandLine.hasSwitch(BrowserSwitches.STRICT_MODE)) {
+ Log.v(LOGTAG, "StrictMode enabled");
+ StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
+ .detectDiskReads()
+ .detectDiskWrites()
+ .detectNetwork()
+ .penaltyLog()
+ .build());
+ StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
+ .detectLeakedSqlLiteObjects()
+ .detectLeakedClosableObjects()
+ .penaltyLog()
+ .penaltyDeath()
+ .build());
+ }
+
+ //Enable remote debugging by default as long as MDM restriction is not enabled
+ Engine.setWebContentsDebuggingEnabled(!DevToolsRestriction.getInstance().isEnabled());
+ mInitializationCompleted = true;
+ mInitializationStarted = true;
+ BrowserSettings.getInstance().onEngineInitializationComplete();
+ Engine.resumeTracing(ctx);
+
+ if (mActivitySchedulerMap != null) {
+ for (Map.Entry<BrowserActivity, ActivityScheduler> entry : mActivitySchedulerMap.entrySet()) {
+ entry.getValue().onEngineInitializationCompletion(mSynchronousInitialization);
+ }
+ mActivitySchedulerMap.clear();
+ }
+ }
+ }
+
+ public static void initializeResourceExtractor(Context ctx) {
+ Engine.startExtractingResources(ctx);
+ }
+
+ public static void onPreDraw(BrowserActivity activity) {
+ activity.getScheduler().onPreDraw();
+ }
+
+ public static void onActivityPause(BrowserActivity activity) {
+ activity.getScheduler().onActivityPause();
+ }
+
+ public static void onActivityStop(BrowserActivity activity) {
+ activity.getScheduler().onActivityStop();
+ }
+
+ public static void onActivityResume(BrowserActivity activity) {
+ activity.getScheduler().onActivityResume();
+ }
+
+ public static void onActivityStart(BrowserActivity activity) {
+ activity.getScheduler().onActivityStart();
+ }
+
+ public static void onActivityResult(BrowserActivity activity, int requestCode, int resultCode, Intent data) {
+ activity.getScheduler().onActivityResult(requestCode, resultCode, data);
+ }
+
+ public static void onNewIntent(BrowserActivity activity, Intent intent) {
+ if (BrowserActivity.ACTION_RESTART.equals(intent.getAction())) {
+ Engine.releaseSpareChildProcess();
+ }
+ activity.getScheduler().onNewIntent(intent);
+ }
+
+ public static void onActivityDestroy(BrowserActivity activity) {
+ Engine.releaseSpareChildProcess();
+ }
+}
diff --git a/src/src/com/android/browser/EventLogTags.logtags b/src/src/com/android/browser/EventLogTags.logtags
new file mode 100644
index 00000000..b3834cf1
--- /dev/null
+++ b/src/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/src/com/android/browser/FetchUrlMimeType.java b/src/src/com/android/browser/FetchUrlMimeType.java
new file mode 100644
index 00000000..56ea4c30
--- /dev/null
+++ b/src/src/com/android/browser/FetchUrlMimeType.java
@@ -0,0 +1,294 @@
+/*
+ * 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 java.net.URI;
+import java.net.URISyntaxException;
+
+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.invokeMethod("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;
+ try {
+ URI uriObj = new URI(mUri);
+ request = new HttpHead(uriObj.toString());
+ } catch (URISyntaxException e) {
+ Log.e(LOGTAG,"Encode URI failed: " + e);
+ client.close();
+ return;
+ }
+
+ 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) {
+ if (request != null)
+ request.abort();
+ } catch (IOException ex) {
+ if (request != null)
+ 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/src/com/android/browser/FolderTileView.java b/src/src/com/android/browser/FolderTileView.java
new file mode 100644
index 00000000..2d23ebfe
--- /dev/null
+++ b/src/src/com/android/browser/FolderTileView.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (c) 2015, 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.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+public class FolderTileView extends ViewGroup {
+
+ // created in the constructor
+ private View mInnerLayout;
+ private TextView mTextView;
+ private TextView mLabelView;
+ private int mPaddingLeft = 0;
+ private int mPaddingTop = 0;
+ private int mPaddingRight = 0;
+ private int mPaddingBottom = 0;
+
+ // runtime params set on Layout
+ private int mCurrentWidth;
+ private int mCurrentHeight;
+
+ // static objects, to be recycled amongst instances (this is an optimization)
+ private static Paint sBackgroundPaint;
+ private String mText;
+ private String mLabel;
+
+
+ /* XML constructors */
+
+ public FolderTileView(Context context) {
+ super(context);
+ xmlInit(null, 0);
+ }
+
+ public FolderTileView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ xmlInit(attrs, 0);
+ }
+
+ public FolderTileView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ xmlInit(attrs, defStyle);
+ }
+
+
+ /* Programmatic Constructors */
+
+ public FolderTileView(Context context, String text, String label) {
+ super(context);
+ mText = text;
+ mLabel = label;
+ init();
+ }
+
+
+ /**
+ * Replaces the main text of the bookmark tile
+ */
+ public void setText(String text) {
+ mText = text;
+ if (mTextView != null)
+ mTextView.setText(mText);
+ }
+
+ /**
+ * Replaces the subtitle, for example "32 items"
+ */
+ public void setLabel(String label) {
+ mLabel = label;
+ if (mLabelView != null)
+ mLabelView.setText(mLabel);
+ }
+
+
+ /* private stuff ahead */
+
+ private void xmlInit(AttributeSet attrs, int defStyle) {
+ // load attributes
+ final TypedArray a = getContext().obtainStyledAttributes(attrs,
+ R.styleable.FolderTileView, defStyle, 0);
+
+ // saves the text for later
+ setText(a.getString(R.styleable.FolderTileView_android_text));
+
+ // saves the label for later
+ setLabel(a.getString(R.styleable.FolderTileView_android_label));
+
+ // delete attribute resolution
+ a.recycle();
+
+ // proceed with real initialization
+ init();
+ }
+
+ private void init() {
+ // create new Views for us from the XML (and automatically add them)
+ inflate(getContext(), R.layout.folder_tile_view, this);
+
+ // we make the assumption that the XML file will always have 1 and only 1 child
+ mInnerLayout = getChildAt(0);
+ mInnerLayout.setVisibility(View.VISIBLE);
+
+ // reference objects
+ mTextView = (TextView) mInnerLayout.findViewById(android.R.id.text1);
+ if (mText != null && !mText.isEmpty())
+ mTextView.setText(mText);
+ mLabelView = (TextView) mInnerLayout.findViewById(android.R.id.text2);
+ if (mLabel != null && !mLabel.isEmpty())
+ mLabelView.setText(mLabel);
+
+ // load the common statics, also for the SiteTileView if needed
+ final Resources resources = getResources();
+ SiteTileView.ensureCommonLoaded(resources);
+ ensureCommonLoaded(resources);
+
+ // get the padding rect from the Tile View (to stay synced in size)
+ final Rect padding = SiteTileView.getBackgroundDrawablePadding();
+ mPaddingLeft = padding.left;
+ mPaddingTop = padding.top;
+ mPaddingRight = padding.right;
+ mPaddingBottom = padding.bottom;
+
+ // we'll draw our background (usually ViewGroups don't)
+ setWillNotDraw(false);
+ }
+
+ private static void ensureCommonLoaded(Resources r) {
+ if (sBackgroundPaint != null)
+ return;
+
+ // shared tiles background paint
+ sBackgroundPaint = new Paint();
+ sBackgroundPaint.setColor(r.getColor(R.color.FolderTileBackground));
+ sBackgroundPaint.setAntiAlias(true);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // layout the xml inflated contents
+ if (mInnerLayout != null) {
+ final int desiredWidth = MeasureSpec.getSize(widthMeasureSpec);
+ final int desiredHeight = MeasureSpec.getSize(heightMeasureSpec);
+ if (desiredHeight > 0 || desiredHeight > 0)
+ mInnerLayout.measure(
+ MeasureSpec.EXACTLY | desiredWidth - mPaddingLeft - mPaddingRight,
+ MeasureSpec.EXACTLY | desiredHeight - mPaddingTop - mPaddingBottom);
+ else
+ mInnerLayout.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ // update current params
+ mCurrentWidth = right - left;
+ mCurrentHeight = bottom - top;
+
+ // layout the inflated XML layout using the same Padding as the SiteTileView
+ if (mInnerLayout != null)
+ mInnerLayout.layout(mPaddingLeft, mPaddingTop,
+ mCurrentWidth - mPaddingRight, mCurrentHeight - mPaddingBottom);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ final int left = mPaddingLeft;
+ final int top = mPaddingTop;
+ final int right = mCurrentWidth - mPaddingRight;
+ final int bottom = mCurrentHeight - mPaddingBottom;
+
+ // draw the background rectangle
+ float roundedRadius = SiteTileView.sRoundedRadius;
+ if (roundedRadius >= 1.) {
+ SiteTileView.sRectF.set(left, top, right, bottom);
+ canvas.drawRoundRect(SiteTileView.sRectF, roundedRadius, roundedRadius, sBackgroundPaint);
+ } else
+ canvas.drawRect(left, top, right, bottom, sBackgroundPaint);
+ }
+
+}
diff --git a/src/src/com/android/browser/HistoryItem.java b/src/src/com/android/browser/HistoryItem.java
new file mode 100644
index 00000000..5153d9ca
--- /dev/null
+++ b/src/src/com/android/browser/HistoryItem.java
@@ -0,0 +1,109 @@
+/*
+ * 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.View;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.ExpandableListView;
+
+import com.android.browser.platformsupport.Browser;
+/**
+ * Layout representing a history item in the classic history viewer.
+ */
+/* package */ class HistoryItem extends BookmarkItem
+ implements OnCheckedChangeListener, View.OnClickListener {
+
+ 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);
+ }
+
+ mTileView.setOnClickListener(this);
+ }
+
+ /* package */ void copyTo(HistoryItem item) {
+ item.mTextView.setText(mTextView.getText());
+ item.mUrlText.setText(mUrlText.getText());
+ item.setIsBookmark(mStar.isChecked());
+ item.mTileView.replaceFavicon(mBitmap);
+ }
+
+ /**
+ * 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());
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == mTileView) {
+ ExpandableListView list = (ExpandableListView) getTag(R.id.combo_view_container);
+ int group = (int) getTag(R.id.group_position);
+ int pos = (int) getTag(R.id.child_position);
+ if (list != null) {
+ long packedPos = list.getPackedPositionForChild(group, pos);
+ int flatPos = list.getFlatListPosition(packedPos);
+ list.performItemClick(
+ list.getAdapter().getView(flatPos, null, null),
+ flatPos, list.getAdapter().getItemId(flatPos));
+ }
+ performClick();
+ }
+ }
+}
diff --git a/src/src/com/android/browser/HomepageHandler.java b/src/src/com/android/browser/HomepageHandler.java
new file mode 100644
index 00000000..f447226a
--- /dev/null
+++ b/src/src/com/android/browser/HomepageHandler.java
@@ -0,0 +1,94 @@
+/*
+ * 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;
+
+import com.android.browser.UI.ComboViews;
+
+import android.content.Context;
+import android.os.Handler;
+import android.app.Activity;
+
+import android.webkit.JavascriptInterface;
+
+import org.codeaurora.swe.WebView;
+
+public class HomepageHandler {
+
+ private Activity mActivity;
+ private Controller mController;
+ private Handler mHandler = new Handler();
+
+ HomepageHandler(Activity activity, Controller controller ){
+ mActivity = activity;
+ mController = controller;
+ }
+
+ // add for carrier homepage feature
+ @JavascriptInterface
+ public void loadBookmarks() {
+ Tab t = mController.mTabControl.getCurrentTab();
+ if (isDefaultLandingPage(t.mCurrentState.mUrl)) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mController.bookmarksOrHistoryPicker(ComboViews.Bookmarks);
+ }
+ });
+ }
+ }
+
+ // add for carrier homepage feature
+ @JavascriptInterface
+ public void loadHistory() {
+ Tab t = mController.mTabControl.getCurrentTab();
+ if (isDefaultLandingPage(t.mCurrentState.mUrl)) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mController.bookmarksOrHistoryPicker(ComboViews.History);
+ }
+ });
+ }
+ }
+
+ public void registerJsInterface(WebView webview, String url){
+ if (isDefaultLandingPage(url)) {
+ webview.getSettings().setJavaScriptEnabled(true);
+ webview.addJavascriptInterface(this, "default_homepage");
+ }
+ }
+
+ public boolean isDefaultLandingPage(String url) {
+ return (url != null &&
+ url.equals(mActivity.getResources().getString(R.string.def_landing_page)) &&
+ url.startsWith("file:///"));
+ }
+}
diff --git a/src/src/com/android/browser/HttpAuthenticationDialog.java b/src/src/com/android/browser/HttpAuthenticationDialog.java
new file mode 100644
index 00000000..2981e65f
--- /dev/null
+++ b/src/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/src/com/android/browser/IntentHandler.java b/src/src/com/android/browser/IntentHandler.java
new file mode 100644
index 00000000..9e2bddfd
--- /dev/null
+++ b/src/src/com/android/browser/IntentHandler.java
@@ -0,0 +1,392 @@
+/*
+ * 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.MediaStore;
+import android.text.TextUtils;
+import android.util.Patterns;
+
+import com.android.browser.UI.ComboViews;
+import com.android.browser.platformsupport.Browser;
+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) {
+ int requestCode = intent.getIntExtra(Controller.EXTRA_REQUEST_CODE, -1);
+ if (requestCode >= Controller.COMBO_VIEW && requestCode <= Controller.MY_NAVIGATION) {
+ int resultCode = intent.getIntExtra(Controller.EXTRA_RESULT_CODE, Activity.RESULT_OK);
+ mController.onActivityResult(requestCode, resultCode, intent);
+ return;
+ }
+
+ 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);
+ t.setDerivedFromIntent(true);
+ 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
+ Tab jsTab = mController.openTab(urlData);
+ jsTab.setDerivedFromIntent(true);
+ 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);
+ tab.setDerivedFromIntent(true);
+ 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/src/com/android/browser/LogTag.java b/src/src/com/android/browser/LogTag.java
new file mode 100644
index 00000000..b2393c7e
--- /dev/null
+++ b/src/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/src/com/android/browser/MemoryMonitor.java b/src/src/com/android/browser/MemoryMonitor.java
new file mode 100644
index 00000000..62bfe991
--- /dev/null
+++ b/src/src/com/android/browser/MemoryMonitor.java
@@ -0,0 +1,90 @@
+/*
+ * 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 java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+public class MemoryMonitor {
+
+ /**
+ * if number of tabs whose native tab is active, is greater
+ * than MAX_ACTIVE_TABS destroy the nativetab of oldest used Tab
+ */
+ public static void purgeActiveTabs(Context context,
+ Controller controller,
+ BrowserSettings settings) {
+ if(!settings.enableMemoryMonitor())
+ return;
+
+ int maxActiveTabs = getMaxActiveTabs(context);
+ TabControl tabControl = controller.getTabControl();
+
+ ArrayList<Tab> activeTabList = new ArrayList<Tab>();
+
+ for (int i = 0; i < tabControl.getTabCount(); i++) {
+ Tab tab = tabControl.getTab(i);
+ if(tab.isNativeActive())
+ activeTabList.add(tab);
+ }
+
+ int numActiveTabsToRelease = activeTabList.size() - maxActiveTabs;
+
+ if(numActiveTabsToRelease < 1)
+ return;
+ // sort tabs in order of LRU first
+ Collections.sort(activeTabList, new Comparator<Tab>() {
+ @Override
+ public int compare(Tab tab1, Tab tab2) {
+ return tab1.getTimestamp().compareTo(tab2.getTimestamp());
+ }
+ });
+
+ for(int i = 0; i < numActiveTabsToRelease; i++) {
+ if(tabControl.getCurrentTab()!= activeTabList.get(i)) {
+ activeTabList.get(i).destroyThroughMemoryMonitor();
+ }
+ }
+ }
+
+ /**
+ * Returns the default max number of active tabs based on device's
+ * memory class.
+ */
+ private static int getMaxActiveTabs(Context context) {
+ return context.getResources()
+ .getInteger(R.integer.feature_num_min_active_tabs);
+ }
+}
diff --git a/src/src/com/android/browser/MessagesReceiver.java b/src/src/com/android/browser/MessagesReceiver.java
new file mode 100644
index 00000000..d59ae847
--- /dev/null
+++ b/src/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/src/com/android/browser/NavScreen.java b/src/src/com/android/browser/NavScreen.java
new file mode 100644
index 00000000..d77d31c1
--- /dev/null
+++ b/src/src/com/android/browser/NavScreen.java
@@ -0,0 +1,254 @@
+/*
+ * 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.ObjectAnimator;
+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.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+import android.widget.PopupMenu.OnMenuItemClickListener;
+import android.widget.RelativeLayout;
+
+import com.android.browser.NavTabScroller.OnRemoveListener;
+import com.android.browser.mdm.IncognitoRestriction;
+
+import java.util.HashMap;
+
+public class NavScreen extends RelativeLayout
+ implements OnClickListener, OnMenuItemClickListener {
+
+
+ private final UiController mUiController;
+ private final PhoneUi mUi;
+ private final Activity mActivity;
+
+ private View mToolbarLayout;
+ private ImageButton mMore;
+ private ImageButton mNewTab;
+ private ImageButton mNewIncognitoTab;
+
+ private NavTabScroller mScroller;
+ private TabAdapter mAdapter;
+ private int mOrientation;
+ private 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 showPopupMenu() {
+ if (mUiController instanceof Controller) {
+ PopupMenu popup = new PopupMenu(getContext(), mMore);
+ Menu menu = popup.getMenu();
+
+ Controller controller = (Controller) mUiController;
+ controller.onPrepareOptionsMenu(menu);
+ }
+ }
+
+ public NavTabScroller getScroller() {
+ return mScroller;
+ }
+
+ public ObjectAnimator createToolbarInAnimator() {
+ return ObjectAnimator.ofFloat(mToolbarLayout, "translationY",
+ -getResources().getDimensionPixelSize(R.dimen.toolbar_height), 0f);
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ return mUiController.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newconfig) {
+ if (newconfig.orientation != mOrientation) {
+ mOrientation = newconfig.orientation;
+
+ // the only thing we need to change is the orientation. see nav_screen.xml
+ //final int prevScroll = mScroller.getScrollValue();
+ mScroller.setOrientation(mOrientation == Configuration.ORIENTATION_LANDSCAPE
+ ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
+ mScroller.setScrollOnNextLayout();
+ }
+ }
+
+ 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));
+ mToolbarLayout = findViewById(R.id.nav_toolbar_animate);
+ mNewIncognitoTab = (ImageButton) findViewById(R.id.newincognitotab);
+ IncognitoRestriction.getInstance().registerControl(mNewIncognitoTab);
+ mNewTab = (ImageButton) findViewById(R.id.newtab);
+ mMore = (ImageButton) findViewById(R.id.more);
+ mNewIncognitoTab.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);
+ }
+ });
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mNewTab == v) {
+ openNewTab();
+ } else if (mNewIncognitoTab == v) {
+ openNewIncognitoTab();
+ } else if (mMore == v) {
+ showPopupMenu();
+ }
+ }
+
+ private void onCloseTab(Tab tab) {
+ if (tab != null) {
+ if (tab == mUiController.getCurrentTab()) {
+ mUiController.closeCurrentTab();
+ } else {
+ mUiController.closeTab(tab);
+ }
+ mTabViews.remove(tab);
+ }
+ }
+
+ private void openNewIncognitoTab() {
+ final Tab tab = mUiController.openIncognitoTab();
+ if (tab != null) {
+ mUiController.setBlockEvents(true);
+ final int tix = mUi.mTabControl.getTabPosition(tab);
+ switchToTab(tab);
+ mUi.hideNavScreen(tix, true);
+ mScroller.handleDataChanged(tix);
+ mUiController.setBlockEvents(false);
+ }
+ }
+
+ 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);
+ switchToTab(tab);
+ mUi.hideNavScreen(tix, true);
+ 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.isTitle(v)) {
+ switchToTab(tab);
+ close(position, false);
+ mUi.editUrl(false, true);
+ } else if (tabview.isWebView(v)) {
+ close(position);
+ }
+ }
+ });
+ return tabview;
+ }
+
+ }
+}
diff --git a/src/src/com/android/browser/NavTabScroller.java b/src/src/com/android/browser/NavTabScroller.java
new file mode 100644
index 00000000..220f44c4
--- /dev/null
+++ b/src/src/com/android/browser/NavTabScroller.java
@@ -0,0 +1,587 @@
+/*
+ * 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.FrameLayout;
+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);
+
+ // update the layout parameters of existing views (to not destroy/recreate all)
+ final int childCount = mContentView.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View view = mContentView.getChildAt(i);
+ final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ lp.gravity = (mHorizontal ? Gravity.CENTER_VERTICAL : Gravity.CENTER_HORIZONTAL);
+ view.setLayoutParams(lp);
+ if (mGapPosition > INVALID_POSITION)
+ adjustViewGap(view, i);
+ }
+ }
+
+ @Override
+ protected void onMeasure(int wspec, int hspec) {
+ super.onMeasure(wspec, hspec);
+ calcPadding();
+ }
+
+ private void calcPadding() {
+ if (mAdapter != null && 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 setScrollOnNextLayout() {
+ mNeedsScroll = true;
+ }
+
+ 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);
+ view.setTranslationY(0);
+ } else {
+ view.setTranslationY(mGap);
+ view.setTranslationX(0);
+ }
+ }
+ }
+
+ 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/src/com/android/browser/NavTabView.java b/src/src/com/android/browser/NavTabView.java
new file mode 100644
index 00000000..8be8c3d1
--- /dev/null
+++ b/src/src/com/android/browser/NavTabView.java
@@ -0,0 +1,132 @@
+/*
+ * 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 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.nav_tab);
+ mTitle = (TextView) findViewById(R.id.title);
+ mTitleBar = findViewById(R.id.titlebar);
+ mImage = (ImageView) findViewById(R.id.tab_preview);
+ }
+
+ 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_suggest_history_normal);
+ } else if (mTab.isDistilled()) {
+ setTitleIcon(R.drawable.ic_deco_reader_mode_normal);
+ } else if (mTab.isPrivateBrowsingEnabled()) {
+ mContent.setBackgroundResource(R.drawable.nav_tab_title_incognito);
+ mTitle.setTextColor(getResources().getColor(R.color.white));
+ setTitleIcon(R.drawable.ic_deco_incognito_normal);
+ } 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);
+ if (mImage != null) {
+ mImage.setOnClickListener(mClickListener);
+ }
+ }
+
+}
diff --git a/src/src/com/android/browser/NavigationBarBase.java b/src/src/com/android/browser/NavigationBarBase.java
new file mode 100644
index 00000000..5591558e
--- /dev/null
+++ b/src/src/com/android/browser/NavigationBarBase.java
@@ -0,0 +1,822 @@
+/*
+ * 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.ArgbEvaluator;
+import android.animation.ValueAnimator;
+import android.app.Activity;
+import android.app.SearchManager;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.net.Uri;
+import android.net.http.SslCertificate;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+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.PopupMenu;
+import android.widget.Toast;
+
+import com.android.browser.UrlInputView.UrlInputListener;
+import com.android.browser.preferences.SiteSpecificPreferencesFragment;
+
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+import org.codeaurora.net.NetworkServices;
+import org.codeaurora.swe.WebRefiner;
+import org.codeaurora.swe.WebView;
+import org.codeaurora.swe.util.ColorUtils;
+
+public class NavigationBarBase extends LinearLayout implements
+ OnClickListener, UrlInputListener, OnFocusChangeListener,
+ TextWatcher, UrlInputView.StateListener,
+ PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener {
+
+ private final static String TAG = "NavigationBarBase";
+
+ protected BaseUi mBaseUi;
+ protected TitleBar mTitleBar;
+ protected UiController mUiController;
+ protected UrlInputView mUrlInput;
+ protected ImageView mStopButton;
+
+ private SiteTileView mFaviconTile;
+ private View mVoiceButton;
+ private ImageView mClearButton;
+ private View mMore;
+ private PopupMenu mPopupMenu;
+ private boolean mOverflowMenuShowing;
+
+ private static Bitmap mDefaultFavicon;
+
+ private int mStatusBarColor;
+ private static int mDefaultStatusBarColor = -1;
+
+ private static final int WEBREFINER_COUNTER_MSG = 4242;
+ private static final int WEBREFINER_COUNTER_MSG_DELAY = 3000;
+ private Handler mHandler;
+
+ protected int mTrustLevel = SiteTileView.TRUST_UNKNOWN;
+
+ private static final String noSitePrefs[] = {
+ "chrome://",
+ "about:",
+ "content:",
+ };
+
+ 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();
+ mUrlInput = (UrlInputView) findViewById(R.id.url);
+ mUrlInput.setUrlInputListener(this);
+ mUrlInput.setOnFocusChangeListener(this);
+ mUrlInput.setSelectAllOnFocus(true);
+ mUrlInput.addTextChangedListener(this);
+ mMore = findViewById(R.id.more_browser_settings);
+ mMore.setOnClickListener(this);
+ mFaviconTile = (SiteTileView) findViewById(R.id.favicon_view);
+ mFaviconTile.setOnClickListener(this);
+ mVoiceButton = findViewById(R.id.voice);
+ mVoiceButton.setOnClickListener(this);
+ mClearButton = (ImageView) findViewById(R.id.clear);
+ mClearButton.setOnClickListener(this);
+ mStopButton = (ImageView) findViewById(R.id.stop);
+ mStopButton.setOnClickListener(this);
+
+ mDefaultFavicon = BitmapFactory.decodeResource(getResources(),
+ R.drawable.ic_deco_favicon_normal);
+
+ mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message m) {
+ switch (m.what) {
+ case WEBREFINER_COUNTER_MSG:
+ WebView wv = mUiController.getCurrentTopWebView();
+ if (wv != null && WebRefiner.isInitialized()) {
+ int count = WebRefiner.getInstance().getBlockedURLCount(wv);
+ if (count > 0) {
+ mFaviconTile.setBadgeBlockedObjectsCount(count);
+ }
+ }
+ mHandler.sendEmptyMessageDelayed(WEBREFINER_COUNTER_MSG,
+ WEBREFINER_COUNTER_MSG_DELAY);
+ break;
+ }
+ }
+ };
+ }
+
+ public void setTitleBar(TitleBar titleBar) {
+ mTitleBar = titleBar;
+ mBaseUi = mTitleBar.getUi();
+ mUiController = mTitleBar.getUiController();
+ mUrlInput.setController(mUiController);
+ }
+
+ public void setSecurityState(Tab.SecurityState securityState) {
+ switch (securityState) {
+ case SECURITY_STATE_SECURE:
+ mTrustLevel = SiteTileView.TRUST_TRUSTED;
+ mFaviconTile.setBadgeHasCertIssues(false);
+ break;
+ case SECURITY_STATE_MIXED:
+ mTrustLevel = SiteTileView.TRUST_UNTRUSTED;
+ mFaviconTile.setBadgeHasCertIssues(true);
+ mTitleBar.showTopControls(false);
+ break;
+ case SECURITY_STATE_BAD_CERTIFICATE:
+ mTrustLevel = SiteTileView.TRUST_AVOID;
+ mFaviconTile.setBadgeHasCertIssues(true);
+ mTitleBar.showTopControls(false);
+ break;
+ case SECURITY_STATE_NOT_SECURE:
+ default:
+ mTrustLevel = SiteTileView.TRUST_UNKNOWN;
+ }
+ mFaviconTile.setTrustLevel(mTrustLevel);
+ }
+
+ public int getTrustLevel() {
+ return mTrustLevel;
+ }
+
+ public static int adjustColor(int color, float hueMultiplier,
+ float saturationMultiplier, float valueMultiplier) {
+ float[] hsv = new float[3];
+ Color.colorToHSV(color, hsv);
+ hsv[0] *= hueMultiplier;
+ hsv[1] *= saturationMultiplier;
+ hsv[2] *= valueMultiplier;
+ return Color.HSVToColor(Color.alpha(color), hsv);
+ }
+
+ public static void setStatusAndNavigationBarColor(final Activity activity, int color) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ int currentColor = activity.getWindow().getStatusBarColor();
+ Integer from = currentColor;
+ Integer to = color;
+ ValueAnimator animator = ValueAnimator.ofObject(new ArgbEvaluator(), from, to);
+
+ if (mDefaultStatusBarColor == -1) {
+ mDefaultStatusBarColor = activity.getWindow().getStatusBarColor();
+ }
+
+ animator.addUpdateListener(
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ Integer value = (Integer) animation.getAnimatedValue();
+ activity.getWindow().setStatusBarColor(value.intValue());
+ //activity.getWindow().setNavigationBarColor(value.intValue());
+ }
+ }
+ );
+ animator.start();
+ }
+ }
+
+ private void updateSiteIconColor(String urlString, int color) {
+ try {
+ URL url = new URL(urlString);
+ String host = url.getHost();
+ SharedPreferences prefs = BrowserSettings.getInstance().getPreferences();
+ int currentColor = prefs.getInt(host + ":color", 0);
+ if (currentColor != color) {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.remove(host + ":color");
+ editor.putInt(host + ":color", color);
+ editor.commit();
+ }
+ } catch (MalformedURLException e) {
+ }
+ }
+
+ public static int getSiteIconColor(String urlString) {
+ try {
+ URL url = new URL(urlString);
+ String host = url.getHost();
+ SharedPreferences prefs = BrowserSettings.getInstance().getPreferences();
+ return prefs.getInt(host + ":color", 0);
+ } catch (MalformedURLException e) {
+ return 0;
+ }
+ }
+
+ public static int getDefaultStatusBarColor() {
+ return mDefaultStatusBarColor;
+ }
+
+ // Sets the favicon for the given tab if it's in the foreground
+ // If the tab doesn't have a favicon, it sets the default favicon
+ public void showCurrentFavicon(Tab tab) {
+ int color;
+ if (tab == null) { return; }
+
+ if (tab.inForeground()) {
+ if (tab.hasFavicon()) {
+ color = ColorUtils.getDominantColorForBitmap(tab.getFavicon());
+ updateSiteIconColor(tab.getUrl(), color);
+ setStatusAndNavigationBarColor(mUiController.getActivity(),
+ adjustColor(color, 1, 1, 0.7f));
+
+ } else {
+ color = getSiteIconColor(tab.getUrl());
+ if (color != 0) {
+ setStatusAndNavigationBarColor(mUiController.getActivity(),
+ adjustColor(color, 1, 1, 0.7f));
+ } else {
+ setStatusAndNavigationBarColor(mUiController.getActivity(),
+ mDefaultStatusBarColor);
+ }
+ }
+ if (mFaviconTile != null) {
+ mFaviconTile.replaceFavicon(tab.getFavicon()); // Always set the tab's favicon
+ }
+ }
+ }
+
+ protected void showSiteSpecificSettings() {
+ WebView wv = mUiController.getCurrentTopWebView();
+ int ads = 0;
+ int tracker = 0;
+ int malware = 0;
+
+ WebRefiner webRefiner = WebRefiner.getInstance();
+ if (wv != null && webRefiner != null) {
+ WebRefiner.PageInfo pageInfo = webRefiner.getPageInfo(wv);
+ if (pageInfo != null) {
+ for (WebRefiner.MatchedURLInfo urlInfo : pageInfo.mMatchedURLInfoList) {
+ if (urlInfo.mActionTaken == WebRefiner.MatchedURLInfo.ACTION_BLOCKED) {
+ switch (urlInfo.mMatchedFilterCategory) {
+ case WebRefiner.RuleSet.CATEGORY_ADS:
+ ads++;
+ break;
+ case WebRefiner.RuleSet.CATEGORY_TRACKERS:
+ tracker++;
+ break;
+ case WebRefiner.RuleSet.CATEGORY_MALWARE_DOMAINS:
+ malware++;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ Bundle bundle = new Bundle();
+ bundle.putCharSequence(SiteSpecificPreferencesFragment.EXTRA_SITE,
+ mUiController.getCurrentTab().getUrl());
+ bundle.putCharSequence(SiteSpecificPreferencesFragment.EXTRA_SITE_TITLE,
+ mUiController.getCurrentTab().getTitle());
+ bundle.putInt(SiteSpecificPreferencesFragment.EXTRA_WEB_REFINER_ADS_INFO, ads);
+ bundle.putInt(SiteSpecificPreferencesFragment.EXTRA_WEB_REFINER_TRACKER_INFO, tracker);
+ bundle.putInt(SiteSpecificPreferencesFragment.EXTRA_WEB_REFINER_MALWARE_INFO, malware);
+
+ bundle.putParcelable(SiteSpecificPreferencesFragment.EXTRA_SECURITY_CERT,
+ SslCertificate.saveState(wv.getCertificate()));
+
+ Tab.SecurityState securityState = Tab.getWebViewSecurityState(
+ mUiController.getCurrentTab().getWebView());
+ if (securityState == Tab.SecurityState.SECURITY_STATE_MIXED) {
+ bundle.putBoolean(SiteSpecificPreferencesFragment.EXTRA_SECURITY_CERT_MIXED, true);
+ } else if (securityState == Tab.SecurityState.SECURITY_STATE_BAD_CERTIFICATE) {
+ bundle.putBoolean(SiteSpecificPreferencesFragment.EXTRA_SECURITY_CERT_BAD, true);
+ }
+
+ Bitmap favicon = mUiController.getCurrentTopWebView().getFavicon();
+ if (favicon != null) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ favicon.compress(Bitmap.CompressFormat.PNG, 100, baos);
+ bundle.putByteArray(SiteSpecificPreferencesFragment.EXTRA_FAVICON,
+ baos.toByteArray());
+ }
+ BrowserPreferencesPage.startPreferenceFragmentExtraForResult(
+ mUiController.getActivity(), SiteSpecificPreferencesFragment.class.getName(),
+ bundle, Controller.PREFERENCES_PAGE);
+ }
+
+ @Override
+ public void onClick(View v) {
+ Tab currentTab = mUiController.getCurrentTab();
+ WebView wv = currentTab.getWebView();
+ String url = null;
+ if (currentTab != null){
+ url = currentTab.getUrl();
+ }
+ if (mMore == v) {
+ showMenu(mMore);
+ } else if (mFaviconTile == v) {
+ if (urlHasSitePrefs(url) && (wv != null && !wv.isShowingInterstitialPage()) ){
+ showSiteSpecificSettings();
+ }
+ } else if (mVoiceButton == v) {
+ mUiController.startVoiceRecognizer();
+ } else if (mStopButton == v) {
+ stopOrRefresh();
+ } else if (mClearButton == v) {
+ clearOrClose();
+ mUrlInput.setText("");
+ }
+ }
+
+ private static boolean urlHasSitePrefs(String url) {
+ if (TextUtils.isEmpty(url)) {
+ return false;
+ }
+
+ for (int i = 0; i < noSitePrefs.length; i++) {
+ if (url.startsWith(noSitePrefs[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private void stopOrRefresh() {
+ if (mUiController == null) return;
+ if (mTitleBar.isInLoad()) {
+ mUiController.stopLoading();
+ } else {
+ if (mUiController.getCurrentTopWebView() != null) {
+ stopEditingUrl();
+ mUiController.getCurrentTopWebView().reload();
+ }
+ }
+ }
+
+ private void clearOrClose() {
+ if (TextUtils.isEmpty(mUrlInput.getText())) {
+ // close
+ mUrlInput.clearFocus();
+ } else {
+ // clear
+ mUrlInput.setText("");
+ }
+ }
+
+ 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 (mUiController instanceof Controller) {
+ Controller controller = (Controller) mUiController;
+ if (controller.onPrepareOptionsMenu(menu)) {
+ mOverflowMenuShowing = true;
+ }
+ }
+ }
+
+ @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();
+ if (!BrowserSettings.getInstance().isPowerSaveModeEnabled()) {
+ //Notify about anticipated network activity
+ NetworkServices.hintUpcomingUserActivity();
+ }
+ } else if (!mUrlInput.needsUpdate()) {
+ mUrlInput.dismissDropDown();
+ mUrlInput.hideIME();
+ if (mUrlInput.getText().length() == 0) {
+ Tab currentTab = mUiController.getTabControl().getCurrentTab();
+ if (currentTab != null) {
+ setDisplayTitle(currentTab.getTitle(), currentTab.getUrl());
+ }
+ }
+ }
+ 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, String url) {
+ if (!isEditingUrl()) {
+ if (!TextUtils.isEmpty(title)) {
+ if (mTrustLevel == SiteTileView.TRUST_TRUSTED) {
+ if (!title.equals(mUrlInput.getText().toString())) {
+ mUrlInput.setText(title, false);
+ }
+ return;
+ }
+ }
+ if (!url.equals(mUrlInput.getText().toString())) {
+ mUrlInput.setText(url, 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;
+ boolean wap2estore = BrowserConfig.getInstance(getContext())
+ .hasFeature(BrowserConfig.Feature.WAP2ESTORE);
+ if ((wap2estore && isEstoreTypeUrl(text)) || isRtspTypeUrl(text)
+ || isMakeCallTypeUrl(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(null, text);
+ return;
+ }
+
+ // add for carrier wap2estore feature
+ if (url != null && t != null && wap2estore && isEstoreTypeUrl(url)) {
+ if (handleEstoreTypeUrl(url)) {
+ setDisplayTitle(null, text);
+ return;
+ }
+ }
+ // add for rtsp scheme feature
+ if (url != null && t != null && isRtspTypeUrl(url)) {
+ if (handleRtspTypeUrl(url)) {
+ setDisplayTitle(null, text);
+ return;
+ }
+ }
+ // add for "wtai://wp/mc;" scheme feature
+ if (url != null && t != null && isMakeCallTypeUrl(url)) {
+ if (handleMakeCallTypeUrl(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(null, text);
+ }
+
+ private boolean isMakeCallTypeUrl(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(UrlHandler.SCHEME_WTAI_MC)) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean handleMakeCallTypeUrl(String url) {
+ // wtai://wp/mc;number
+ // number=string(phone-number)
+ if (url.startsWith(UrlHandler.SCHEME_WTAI_MC)) {
+ Intent intent = new Intent(Intent.ACTION_VIEW,
+ Uri.parse(WebView.SCHEME_TEL +
+ url.substring(UrlHandler.SCHEME_WTAI_MC.length())));
+ getContext().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.
+ Tab current = mUiController.getCurrentTab();
+ if (current != null
+ && current.getWebView().copyBackForwardList().getSize() == 0) {
+ mUiController.closeCurrentTab();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isEstoreTypeUrl(String url) {
+ if (url != null && url.startsWith("estore:")) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean handleEstoreTypeUrl(String url) {
+ if (url.getBytes().length > 256) {
+ Toast.makeText(getContext(), R.string.estore_url_warning, Toast.LENGTH_LONG).show();
+ return false;
+ }
+
+ Intent intent;
+ // perform generic parsing of the URI to turn it into an Intent.
+ try {
+ intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
+ } catch (URISyntaxException ex) {
+ Log.w("Browser", "Bad URI " + url + ": " + ex.getMessage());
+ return false;
+ }
+
+ 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();
+ }
+
+ return true;
+ }
+
+ 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.getTitle(), 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);
+ }
+
+ public void setCurrentUrlIsBookmark(boolean isBookmark) {
+ }
+
+ @Override
+ public boolean dispatchKeyEventPreIme(KeyEvent evt) {
+ if (evt.getKeyCode() == KeyEvent.KEYCODE_BACK) {
+ if (mUiController.getCurrentTab() != null &&
+ mUiController.getCurrentTab().isKeyboardShowing()){
+ stopEditingUrl();
+ return true;
+ }
+ // catch back key in order to do slightly more cleanup than usual
+ stopEditingUrl();
+ }
+ 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 (clearInput) {
+ mUrlInput.setText("");
+ }
+ if (!mUrlInput.hasFocus()) {
+ mUrlInput.requestFocus();
+ }
+ if (forceIME) {
+ mUrlInput.showIME();
+ }
+ }
+
+ public void onProgressStarted() {
+ mFaviconTile.setBadgeBlockedObjectsCount(0);
+ mFaviconTile.setTrustLevel(SiteTileView.TRUST_UNKNOWN);
+ mFaviconTile.setBadgeHasCertIssues(false);
+ setSecurityState(Tab.SecurityState.SECURITY_STATE_NOT_SECURE);
+ mHandler.removeMessages(WEBREFINER_COUNTER_MSG);
+ mHandler.sendEmptyMessageDelayed(WEBREFINER_COUNTER_MSG,
+ WEBREFINER_COUNTER_MSG_DELAY);
+ mStopButton.setImageResource(R.drawable.ic_action_stop);
+ mStopButton.setContentDescription(getResources().
+ getString(R.string.accessibility_button_stop));
+ }
+
+ public void onProgressStopped() {
+ if (!isEditingUrl()) {
+ mFaviconTile.setVisibility(View.VISIBLE);
+ }
+ mStopButton.setImageResource(R.drawable.ic_action_reload);
+ mStopButton.setContentDescription(getResources().
+ getString(R.string.accessibility_button_refresh));
+ }
+
+ 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) { }
+
+ @Override
+ public void onStateChanged(int state) {
+ mVoiceButton.setVisibility(View.GONE);
+ mClearButton.setVisibility(View.GONE);
+ switch(state) {
+ case STATE_NORMAL:
+ mFaviconTile.setVisibility(View.VISIBLE);
+ mMore.setVisibility(View.VISIBLE);
+ if (mUiController != null) {
+ Tab currentTab = mUiController.getCurrentTab();
+ if (currentTab != null){
+ if (TextUtils.isEmpty(currentTab.getUrl())) {
+ mFaviconTile.setVisibility(View.GONE);
+ }
+ setDisplayTitle(currentTab.getTitle(), currentTab.getUrl());
+ }
+ mUiController.setWindowDimming(0.0f);
+ }
+
+ break;
+ case STATE_HIGHLIGHTED:
+ mFaviconTile.setVisibility(View.GONE);
+ mClearButton.setVisibility(View.VISIBLE);
+ mMore.setVisibility(View.GONE);
+ if (mUiController != null) {
+ mUiController.setWindowDimming(0.75f);
+ if (mTrustLevel == SiteTileView.TRUST_TRUSTED) {
+ Tab currentTab = mUiController.getCurrentTab();
+ if (currentTab != null) {
+ mUrlInput.setText(currentTab.getUrl(), false);
+ mUrlInput.selectAll();
+ }
+ }
+ }
+ break;
+ case STATE_EDITED:
+ if (TextUtils.isEmpty(mUrlInput.getText()) &&
+ mUiController != null &&
+ mUiController.supportsVoice()) {
+ mVoiceButton.setVisibility(View.VISIBLE);
+ }
+ else {
+ mClearButton.setVisibility(View.VISIBLE);
+ }
+ mFaviconTile.setVisibility(View.GONE);
+ mMore.setVisibility(View.GONE);
+ break;
+ }
+ }
+
+ public boolean isMenuShowing() {
+ return mOverflowMenuShowing;
+ }
+
+
+ @Override
+ public void onDismiss(PopupMenu popupMenu) {
+ if (popupMenu == mPopupMenu) {
+ onMenuHidden();
+ }
+ }
+
+ private void onMenuHidden() {
+ mOverflowMenuShowing = false;
+ }
+
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ return mUiController.onOptionsItemSelected(item);
+ }
+}
diff --git a/src/src/com/android/browser/NavigationBarPhone.java b/src/src/com/android/browser/NavigationBarPhone.java
new file mode 100644
index 00000000..5fa093be
--- /dev/null
+++ b/src/src/com/android/browser/NavigationBarPhone.java
@@ -0,0 +1,165 @@
+/*
+ * 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.util.TypedValue;
+import android.view.View;
+
+import org.codeaurora.swe.util.Activator;
+import org.codeaurora.swe.util.Observable;
+
+import android.widget.TextView;
+import com.android.browser.UrlInputView.StateListener;
+
+public class NavigationBarPhone extends NavigationBarBase implements StateListener {
+
+ private View mTabSwitcher;
+ private TextView mTabText;
+ private View mIncognitoIcon;
+ private float mTabSwitcherInitialTextSize = 0;
+ private float mTabSwitcherCompressedTextSize = 0;
+
+ 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();
+ mTabSwitcher = findViewById(R.id.tab_switcher);
+ mTabSwitcher.setOnClickListener(this);
+ mTabText = (TextView) findViewById(R.id.tab_switcher_text);
+ setFocusState(false);
+ mUrlInput.setContainer(this);
+ mUrlInput.setStateListener(this);
+ mIncognitoIcon = findViewById(R.id.incognito_icon);
+
+ if (mTabSwitcherInitialTextSize == 0) {
+ mTabSwitcherInitialTextSize = mTabText.getTextSize();
+ mTabSwitcherCompressedTextSize = (float) (mTabSwitcherInitialTextSize / 1.2);
+ }
+ }
+
+ @Override
+ public void setTitleBar(TitleBar titleBar) {
+ super.setTitleBar(titleBar);
+ Activator.activate(
+ new Observable.Observer() {
+ @Override
+ public void onChange(Object... params) {
+ if ((Integer)params[0] > 9) {
+ mTabText.setTextSize(TypedValue.COMPLEX_UNIT_PX,
+ mTabSwitcherCompressedTextSize);
+ } else {
+ mTabText.setTextSize(TypedValue.COMPLEX_UNIT_PX,
+ mTabSwitcherInitialTextSize);
+ }
+
+ mTabText.setText(Integer.toString((Integer) params[0]));
+ }
+ },
+ mUiController.getTabControl().getTabCountObservable());
+ }
+
+ @Override
+ public void onProgressStopped() {
+ super.onProgressStopped();
+ onStateChanged(mUrlInput.getState());
+ }
+
+ /**
+ * Update the text displayed in the title bar.
+ * @param title String to display. If null, the new tab string will be
+ * @param url
+ */
+ @Override
+ void setDisplayTitle(String title, String url) {
+ mUrlInput.setTag(title);
+ if (!isEditingUrl()) {
+ // add for carrier requirement - show title from native instead of url
+ if ((BrowserConfig.getInstance(getContext())
+ .hasFeature(BrowserConfig.Feature.TITLE_IN_URL_BAR) ||
+ mTrustLevel == SiteTileView.TRUST_TRUSTED) && title != null) {
+ mUrlInput.setText(title, false);
+ } else if (url == null) {
+ mUrlInput.setText(R.string.new_tab);
+ } else {
+ mUrlInput.setText(UrlUtils.stripUrl(url), false);
+ }
+ mUrlInput.setSelection(0);
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == mTabSwitcher) {
+ ((PhoneUi) mBaseUi).toggleNavScreen();
+ } else {
+ super.onClick(v);
+ }
+ }
+
+ @Override
+ public void onFocusChange(View view, boolean hasFocus) {
+ if (view == mUrlInput && !hasFocus) {
+ Tab currentTab = mUiController.getTabControl().getCurrentTab();
+ setDisplayTitle(currentTab.getTitle(), currentTab.getUrl());
+ }
+ super.onFocusChange(view, hasFocus);
+ }
+
+ @Override
+ public void onStateChanged(int state) {
+ switch(state) {
+ case StateListener.STATE_NORMAL:
+ mStopButton.setVisibility(View.GONE);
+ mTabSwitcher.setVisibility(View.VISIBLE);
+ mTabText.setVisibility(View.VISIBLE);
+ break;
+ case StateListener.STATE_HIGHLIGHTED:
+ mTabSwitcher.setVisibility(View.GONE);
+ mTabText.setVisibility(View.GONE);
+ break;
+ case StateListener.STATE_EDITED:
+ mStopButton.setVisibility(View.GONE);
+ mTabSwitcher.setVisibility(View.GONE);
+ mTabText.setVisibility(View.GONE);
+ break;
+ }
+ super.onStateChanged(state);
+ }
+
+ @Override
+ public void onTabDataChanged(Tab tab) {
+ super.onTabDataChanged(tab);
+ boolean isPrivate = tab.isPrivateBrowsingEnabled();
+ mIncognitoIcon.setVisibility(isPrivate ? View.VISIBLE : View.GONE);
+ // change the background to a darker tone to reflect the 'incognito' state
+ setBackgroundColor(getResources().getColor(isPrivate ?
+ R.color.NavigationBarBackgroundIncognito : R.color.NavigationBarBackground));
+
+ }
+}
diff --git a/src/src/com/android/browser/NavigationBarTablet.java b/src/src/com/android/browser/NavigationBarTablet.java
new file mode 100644
index 00000000..ce482d94
--- /dev/null
+++ b/src/src/com/android/browser/NavigationBarTablet.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.content.Intent;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+
+import com.android.browser.UI.ComboViews;
+import com.android.browser.UrlInputView.StateListener;
+
+public class NavigationBarTablet extends NavigationBarBase implements StateListener {
+
+ private View mUrlContainer;
+ private ImageButton mBackButton;
+ private ImageButton mForwardButton;
+ private ImageView mStar;
+ private ImageView mSearchButton;
+ private View mAllButton;
+ private View mNavButtons;
+ private boolean mHideNavButtons;
+
+ 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) {
+ mHideNavButtons = getResources().getBoolean(R.bool.hide_nav_buttons);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mAllButton = findViewById(R.id.all_btn);
+ mNavButtons = findViewById(R.id.navbuttons);
+ mBackButton = (ImageButton) findViewById(R.id.back);
+ mForwardButton = (ImageButton) findViewById(R.id.forward);
+ mStar = (ImageView) findViewById(R.id.star);
+ mSearchButton = (ImageView) findViewById(R.id.search);
+ mUrlContainer = findViewById(R.id.urlbar_focused);
+ mBackButton.setOnClickListener(this);
+ mForwardButton.setOnClickListener(this);
+ mStar.setOnClickListener(this);
+ mAllButton.setOnClickListener(this);
+ mSearchButton.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)) {
+ hideNavButtons();
+ } else if (!mHideNavButtons && (mNavButtons.getVisibility() == View.GONE)) {
+ showNavButtons();
+ }
+ }
+ }
+
+ @Override
+ public void setTitleBar(TitleBar titleBar) {
+ super.setTitleBar(titleBar);
+ setFocusState(false);
+ }
+
+ void updateNavigationState(Tab tab) {
+ if (tab != null) {
+ mBackButton.setEnabled(tab.canGoBack());
+ mForwardButton.setEnabled(tab.canGoForward());
+ }
+ }
+
+ @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 {
+ super.onClick(v);
+ }
+ }
+
+ @Override
+ protected void setFocusState(boolean focus) {
+ super.setFocusState(focus);
+ if (focus) {
+ if (mHideNavButtons) {
+ hideNavButtons();
+ }
+ mSearchButton.setVisibility(View.GONE);
+ mStar.setVisibility(View.GONE);
+ } else {
+ if (mHideNavButtons) {
+ showNavButtons();
+ }
+ showHideStar(mUiController.getCurrentTab());
+ mSearchButton.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void hideNavButtons() {
+ int aw = mNavButtons.getMeasuredWidth();
+ mNavButtons.setVisibility(View.GONE);
+ mNavButtons.setAlpha(0f);
+ mNavButtons.setTranslationX(-aw);
+ }
+
+ private void showNavButtons() {
+ mNavButtons.setVisibility(View.VISIBLE);
+ mNavButtons.setAlpha(1f);
+ mNavButtons.setTranslationX(0);
+ }
+
+ 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);
+ }
+ }
+}
diff --git a/src/src/com/android/browser/NetworkStateHandler.java b/src/src/com/android/browser/NetworkStateHandler.java
new file mode 100644
index 00000000..9faa9964
--- /dev/null
+++ b/src/src/com/android/browser/NetworkStateHandler.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.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)) {
+ final ConnectivityManager cm = (ConnectivityManager)
+ context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo info = cm.getActiveNetworkInfo();
+
+ if (info == null) {
+ onNetworkToggle(false);
+ return;
+ }
+
+ String typeName = info.getTypeName();
+ String subtypeName = info.getSubtypeName();
+ sendNetworkType(typeName.toLowerCase(),
+ (subtypeName != null ? subtypeName.toLowerCase() : ""));
+ BrowserSettings.getInstance().updateConnectionType();
+
+ onNetworkToggle(info.isConnectedOrConnecting());
+ }
+ }
+ };
+
+ }
+
+ 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);
+ }
+ Tab t = mController.getCurrentTab();
+ if (t != null) {
+ t.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/src/com/android/browser/NfcHandler.java b/src/src/com/android/browser/NfcHandler.java
new file mode 100644
index 00000000..0dd85769
--- /dev/null
+++ b/src/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/src/com/android/browser/OpenDownloadReceiver.java b/src/src/com/android/browser/OpenDownloadReceiver.java
new file mode 100644
index 00000000..4277ff49
--- /dev/null
+++ b/src/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/src/com/android/browser/OptionsMenuHandler.java b/src/src/com/android/browser/OptionsMenuHandler.java
new file mode 100644
index 00000000..d602c7d3
--- /dev/null
+++ b/src/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/src/com/android/browser/PageProgressView.java b/src/src/com/android/browser/PageProgressView.java
new file mode 100644
index 00000000..c316c684
--- /dev/null
+++ b/src/src/com/android/browser/PageProgressView.java
@@ -0,0 +1,125 @@
+
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.browser;
+
+import android.content.Context;
+import android.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);
+ }
+
+ public int getProgressPercent() {
+ return (100* mCurrentProgress) / MAX_PROGRESS;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+// super.onDraw(canvas);
+ Drawable d = getDrawable();
+ d.setBounds(mBounds);
+ d.draw(canvas);
+ }
+
+ public void onProgressStarted() {
+ mCurrentProgress = 0;
+ mTargetProgress = 0;
+ }
+}
diff --git a/src/src/com/android/browser/Performance.java b/src/src/com/android/browser/Performance.java
new file mode 100644
index 00000000..330f47e9
--- /dev/null
+++ b/src/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/src/com/android/browser/PhoneUi.java b/src/src/com/android/browser/PhoneUi.java
new file mode 100644
index 00000000..dd18c193
--- /dev/null
+++ b/src/src/com/android/browser/PhoneUi.java
@@ -0,0 +1,638 @@
+/*
+ * 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.ActionBar;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.os.Bundle;
+import android.os.CountDownTimer;
+import android.util.Log;
+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.ViewStub;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.DecelerateInterpolator;
+
+import org.codeaurora.swe.WebView;
+
+import android.widget.ImageView;
+
+import com.android.browser.UrlInputView.StateListener;
+
+import org.codeaurora.net.NetworkServices;
+
+/**
+ * Ui for regular phone screen sizes
+ */
+public class PhoneUi extends BaseUi {
+
+ private static final String LOGTAG = "PhoneUi";
+
+ private NavScreen mNavScreen;
+ private AnimScreen mAnimScreen;
+ private final NavigationBarPhone mNavigationBar;
+ private boolean mNavScreenRequested = false;
+
+ boolean mShowNav = false;
+ private ComboView mComboView;
+
+ private CountDownTimer mCaptureTimer;
+ private static final int mCaptureMaxWaitMS = 1000;
+
+ /**
+ * @param browser
+ * @param controller
+ */
+ public PhoneUi(Activity browser, UiController controller) {
+ super(browser, controller);
+ mNavigationBar = (NavigationBarPhone) mTitleBar.getNavigationBar();
+ }
+
+ @Override
+ public void onDestroy() {
+ hideTitleBar();
+ // Free the allocated memory for GC to clear it from the heap.
+ mAnimScreen = null;
+ }
+
+ @Override
+ public void editUrl(boolean clearInput, boolean forceIME) {
+ //Do nothing while at Nav show screen.
+ if (mShowNav) return;
+ super.editUrl(clearInput, forceIME);
+ }
+
+ @Override
+ public void showComboView(ComboViews startingView, Bundle extras) {
+
+ if (mComboView == null) {
+ if (mNavScreen != null) {
+ mNavScreen.setVisibility(View.GONE);
+ }
+ ViewStub stub = (ViewStub) mActivity.getWindow().
+ getDecorView().findViewById(R.id.combo_view_stub);
+ mComboView = (ComboView) stub.inflate();
+ mComboView.setVisibility(View.GONE);
+ mComboView.setupViews(mActivity);
+ }
+
+ Bundle b = new Bundle();
+ b.putString(ComboViewActivity.EXTRA_INITIAL_VIEW, startingView.name());
+ b.putBundle(ComboViewActivity.EXTRA_COMBO_ARGS, extras);
+ Tab t = getActiveTab();
+ if (t != null) {
+ b.putString(ComboViewActivity.EXTRA_CURRENT_URL, t.getUrl());
+ }
+
+ mComboView.showViews(mActivity, b);
+ }
+
+ @Override
+ public void hideComboView() {
+ mComboView.hideViews();
+ }
+
+ @Override
+ public boolean onBackKey() {
+ if (showingNavScreen()) {
+ mNavScreen.close(mUiController.getTabControl().getCurrentPosition());
+ return true;
+ }
+ if (isComboViewShowing()) {
+ hideComboView();
+ 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 setActiveTab(final Tab tab) {
+ 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.
+ view.setTitleBar(mTitleBar);
+
+ // update nav bar state
+ mNavigationBar.onStateChanged(StateListener.STATE_NORMAL);
+ }
+
+ // 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 info = menu.findItem(R.id.page_info_menu_id);
+ if (info != null) {
+ info.setVisible(false);
+ }
+
+ if (showingNavScreen()) {
+ setMenuItemVisibility(menu, R.id.find_menu_id, false);
+ menu.setGroupVisible(R.id.LIVE_MENU, false);
+ setMenuItemVisibility(menu, R.id.save_snapshot_menu_id, false);
+ menu.setGroupVisible(R.id.SNAPSHOT_MENU, false);
+ menu.setGroupVisible(R.id.NAV_MENU, false);
+ }
+
+ if (isComboViewShowing()) {
+ menu.setGroupVisible(R.id.MAIN_MENU, false);
+ menu.setGroupEnabled(R.id.MAIN_MENU, false);
+ menu.setGroupEnabled(R.id.MAIN_SHORTCUT_MENU, false);
+ }
+
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (showingNavScreen()
+ && (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) {
+ super.onActionModeStarted(mode);
+ if (!isEditingUrl()) {
+ mTitleBar.setVisibility(View.GONE);
+ }
+
+ ActionBar actionBar = mActivity.getActionBar();
+ if (actionBar != null) {
+ actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ actionBar.hide();
+ }
+ }
+
+ @Override
+ public void onActionModeFinished(boolean inLoad) {
+ super.onActionModeFinished(inLoad);
+ mTitleBar.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public boolean isWebShowing() {
+ return super.isWebShowing() && !showingNavScreen() && !isComboViewShowing();
+ }
+
+ @Override
+ public boolean isComboViewShowing() {
+ return mComboView != null && mComboView.getVisibility() == View.VISIBLE;
+ }
+
+ @Override
+ public void showWeb(boolean animate) {
+ super.showWeb(animate);
+ hideNavScreen(mUiController.getTabControl().getCurrentPosition(), animate);
+ }
+
+ //Unblock touch events
+ private void unblockEvents() {
+ mUiController.setBlockEvents(false);
+ }
+ //Block touch events
+ private void blockEvents() {
+ mUiController.setBlockEvents(true);
+ }
+
+ @Override
+ public void cancelNavScreenRequest() {
+ mNavScreenRequested = false;
+ }
+
+ private void thumbnailUpdated(Tab t) {
+ mTabControl.setOnThumbnailUpdatedListener(null);
+
+ // Discard the callback if the req is interrupted
+ if (!mNavScreenRequested) {
+ unblockEvents();
+ return;
+ }
+
+ Bitmap bm = t.getScreenshot();
+ if (bm == null) {
+ t.initCaptureBitmap();
+ bm = t.getScreenshot();
+ }
+
+ Bitmap sbm;
+ WebView webView = getWebView();
+ if (webView != null) {
+ int view_width = webView.getWidth();
+ int capture_width = mActivity.getResources().getDimensionPixelSize(
+ R.dimen.tab_thumbnail_width);
+
+ float scale = (float) view_width / capture_width;
+
+ //Upscale the low-res bitmap to the needed size
+ Matrix m = new Matrix();
+ m.postScale(scale, scale);
+ sbm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(),
+ bm.getHeight(), m, false);
+ } else {
+ sbm = bm;
+ }
+
+ onShowNavScreenContinue(sbm);
+ }
+
+ private void startCaptureTimer(final Tab tab) {
+ mCaptureTimer = new CountDownTimer(mCaptureMaxWaitMS, mCaptureMaxWaitMS) {
+ @Override
+ public void onTick(long millisUntilFinished) {
+ // Do nothing
+ }
+
+ @Override
+ public void onFinish() {
+ Log.e(LOGTAG, "Screen capture timed out while showing navigation screen");
+ thumbnailUpdated(tab);
+ }
+ }.start();
+ }
+
+ private void stopCaptureTimer() {
+ if (mCaptureTimer != null) {
+ mCaptureTimer.cancel();
+ mCaptureTimer = null;
+ }
+ }
+
+ void showNavScreen() {
+ blockEvents();
+ stopCaptureTimer();
+
+ mNavScreenRequested = true;
+ mTabControl.setOnThumbnailUpdatedListener(
+ new TabControl.OnThumbnailUpdatedListener() {
+ @Override
+ public void onThumbnailUpdated(Tab t) {
+ stopCaptureTimer();
+ thumbnailUpdated(t);
+ }
+ });
+ if (!BrowserSettings.getInstance().isPowerSaveModeEnabled()) {
+ //Notify about anticipated network activity
+ NetworkServices.hintUpcomingUserActivity();
+ }
+ mActiveTab.capture();
+ startCaptureTimer(mActiveTab);
+ }
+
+ void onShowNavScreenContinue(Bitmap viewportBitmap) {
+ dismissIME();
+ mShowNav = true;
+ mNavScreenRequested = false;
+ 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();
+ }
+ if (mAnimScreen == null) {
+ mAnimScreen = new AnimScreen(mActivity);
+ } else {
+ mAnimScreen.mMain.setAlpha(1f);
+ mAnimScreen.setScaleFactor(1f);
+ }
+ mAnimScreen.set(getTitleBar(), viewportBitmap);
+ 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 fromBottom = mContentView.getHeight();
+ 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 toScaleFactor = width / (float) mContentView.getWidth();
+ 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 sx = ObjectAnimator.ofFloat(mAnimScreen, "scaleFactor", 1f, toScaleFactor);
+ ObjectAnimator navTabsIn = mNavScreen.createToolbarInAnimator();
+ mAnimScreen.mContent.layout(fromLeft, fromTop, fromRight, fromBottom);
+ mAnimScreen.setScaleFactor(1f);
+
+ AnimatorSet inanim = new AnimatorSet();
+ inanim.playTogether(tx, ty, tr, tb, sx, navTabsIn);
+ inanim.setInterpolator(new DecelerateInterpolator());
+ inanim.setDuration(200);
+
+ ObjectAnimator disappear = ObjectAnimator.ofFloat(mAnimScreen.mMain, "alpha", 1f, 0f);
+ disappear.setInterpolator(new DecelerateInterpolator());
+ disappear.setDuration(100);
+
+ AnimatorSet set1 = new AnimatorSet();
+ set1.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mContentView.setVisibility(View.GONE);
+ }
+ @Override
+ public void onAnimationEnd(Animator anim) {
+ mCustomViewContainer.removeView(mAnimScreen.mMain);
+ finishAnimationIn();
+ unblockEvents();
+ }
+ });
+ set1.playSequentially(inanim, disappear);
+ set1.start();
+ unblockEvents();
+ }
+
+ private void finishAnimationIn() {
+ if (showingNavScreen()) {
+ // notify accessibility manager about the screen change
+ mNavScreen.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ }
+ }
+
+ 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;
+ }
+ blockEvents();
+ mUiController.setActiveTab(tab);
+ mContentView.setVisibility(View.VISIBLE);
+ if (mAnimScreen == null) {
+ mAnimScreen = new AnimScreen(mActivity);
+ }
+ ImageView target = tabview.mImage;
+ int width = target.getDrawable().getIntrinsicWidth();
+ int height = target.getDrawable().getIntrinsicHeight();
+ Bitmap bm = tab.getScreenshot();
+ if (bm == null)
+ bm = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
+ mAnimScreen.set(bm);
+ if (mAnimScreen.mMain.getParent() == null) {
+ mCustomViewContainer.addView(mAnimScreen.mMain, COVER_SCREEN_PARAMS);
+ }
+ mAnimScreen.mMain.layout(0, 0, mContentView.getWidth(),
+ mContentView.getHeight());
+ mNavScreen.getScroller().finishScroller();
+ int toLeft = 0;
+ int toTop = mTitleBar.calculateEmbeddedHeight();
+ int toRight = mContentView.getWidth();
+ int fromLeft = tabview.getLeft() + target.getLeft() - mNavScreen.getScroller().getScrollX();
+ int fromTop = tabview.getTop() + target.getTop() - mNavScreen.getScroller().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);
+ //ObjectAnimator fade2 = ObjectAnimator.ofFloat(mNavScreen, "alpha", 1f, 0f);
+ //fade2.setDuration(100);
+ AnimatorSet set = new AnimatorSet();
+ ObjectAnimator animAppear = ObjectAnimator.ofFloat(mAnimScreen.mMain, "alpha", 0f, 1f);
+ animAppear.setDuration(100);
+ 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);
+ set.playTogether(animAppear, l, t, r, b, scale);
+ set.setInterpolator(new DecelerateInterpolator());
+ set.setDuration(200);
+ set.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator anim) {
+ checkTabReady();
+ }
+ });
+ set.start();
+ }
+
+
+ private int mNumTries = 0;
+ private void checkTabReady() {
+ boolean isready = true;
+ boolean zeroTries = mNumTries == 0;
+ Tab tab = mUiController.getTabControl().getCurrentTab();
+ BrowserWebView webview = null;
+ if (tab == null)
+ isready = false;
+ else {
+ webview = (BrowserWebView)tab.getWebView();
+ if (webview == null) {
+ isready = false;
+ } else {
+ isready = webview.isReady();
+ }
+ }
+ // Post only when not ready and not crashed
+ if (!isready && mNumTries++ < 150) {
+ mCustomViewContainer.postDelayed(new Runnable() {
+ public void run() {
+ checkTabReady();
+ }
+ }, 17); //WebView is not ready. check again in for next frame.
+ return;
+ }
+ mNumTries = 0;
+ final boolean hasCrashed = (webview == null) ? false :false;
+ // fast path: don't wait if we've been ready for a while
+ if (zeroTries) {
+ fadeOutCustomViewContainer();
+ return;
+ }
+ mCustomViewContainer.postDelayed(new Runnable() {
+ public void run() {
+ fadeOutCustomViewContainer();
+ }
+ }, 32); //WebView is ready, but give it extra 2 frame's time to display and finish the swaps
+ }
+
+ private void fadeOutCustomViewContainer() {
+ ObjectAnimator otheralpha = ObjectAnimator.ofFloat(mCustomViewContainer, "alpha", 1f, 0f);
+ otheralpha.setDuration(100);
+ otheralpha.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator anim) {
+ mCustomViewContainer.removeView(mAnimScreen.mMain);
+ finishAnimateOut();
+ unblockEvents();
+ }
+ });
+ otheralpha.setInterpolator(new DecelerateInterpolator());
+ otheralpha.start();
+ }
+
+ private void finishAnimateOut() {
+ if (mNavScreen != null) {
+ mNavScreen.setVisibility(View.GONE);
+ }
+ mCustomViewContainer.setAlpha(1f);
+ mCustomViewContainer.setVisibility(View.GONE);
+ mAnimScreen.set(null);
+ }
+
+ @Override
+ public boolean needsRestoreAllTabs() {
+ return false;
+ }
+
+ public void toggleNavScreen() {
+ if (!showingNavScreen()) {
+ showNavScreen();
+ } else {
+ hideNavScreen(mUiController.getTabControl().getCurrentPosition(), false);
+ }
+ }
+
+ static class AnimScreen {
+
+ private View mMain;
+ private ImageView mContent;
+ private float mScale;
+
+ public AnimScreen(Context ctx) {
+ mMain = LayoutInflater.from(ctx).inflate(R.layout.anim_screen, null);
+ mMain.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // just eat clicks when this view is visible
+ }
+ });
+ mContent = (ImageView) mMain.findViewById(R.id.anim_screen_content);
+ mContent.setScaleType(ImageView.ScaleType.MATRIX);
+ mContent.setImageMatrix(new Matrix());
+ mScale = 1.0f;
+ setScaleFactor(getScaleFactor());
+ }
+
+ public void set(TitleBar tbar, Bitmap viewportBitmap) {
+ if (tbar == null) {
+ return;
+ }
+ mContent.setImageBitmap(viewportBitmap);
+ }
+
+ public void set(Bitmap image) {
+ 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/src/com/android/browser/PowerConnectionReceiver.java b/src/src/com/android/browser/PowerConnectionReceiver.java
new file mode 100644
index 00000000..f68d3fc7
--- /dev/null
+++ b/src/src/com/android/browser/PowerConnectionReceiver.java
@@ -0,0 +1,60 @@
+/* Copyright (c) 2015, 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.preferences.GeneralPreferencesFragment;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.DownloadManager;
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.PowerManager;
+
+public class PowerConnectionReceiver extends BroadcastReceiver {
+ static final String POWER_MODE_TOGGLE = "android.os.action.POWER_SAVE_MODE_CHANGED";
+ static final String POWER_OKAY = Intent.ACTION_BATTERY_OKAY;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ BrowserSettings settings = BrowserSettings.getInstance();
+ String action = intent.getAction();
+ if (POWER_MODE_TOGGLE.equals(action)) {
+ // This feature is only on android L+
+ PowerManager pm = (PowerManager) context.getSystemService(context.POWER_SERVICE);
+ settings.setPowerSaveModeEnabled(pm.isPowerSaveMode());
+ } else if (POWER_OKAY.equals(action)) {
+ settings.setPowerSaveModeEnabled(false);
+ } else {
+ if (settings.isPowerSaveModeEnabled())
+ return;
+ Bundle bundle = new Bundle();
+ bundle.putBoolean("LowPower", true);
+ BrowserPreferencesPage.startPreferenceFragmentExtraForResult((Activity) context, GeneralPreferencesFragment.class.getName(), bundle, 0);
+ }
+ }
+}
diff --git a/src/src/com/android/browser/PreferenceKeys.java b/src/src/com/android/browser/PreferenceKeys.java
new file mode 100644
index 00000000..15ec6a07
--- /dev/null
+++ b/src/src/com/android/browser/PreferenceKeys.java
@@ -0,0 +1,148 @@
+/*
+ * 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_POPUP_WINDOWS = "popup_windows";
+ static final String PREF_DEFAULT_TEXT_ENCODING = "default_text_encoding";
+ 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_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_DISABLE_PERF = "disable_perf";
+
+ // ----------------------
+ // 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_POWERSAVE_ENABLED = "powersave_enabled";
+ static final String PREF_NIGHTMODE_ENABLED = "nightmode_enabled";
+ 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_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_FULLSCREEN = "fullscreen";
+
+ // ----------------------
+ // Keys for privacy_security_preferences.xml
+ // ----------------------
+ static final String PREF_CLEAR_SELECTED_DATA = "privacy_clear_selected";
+ 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";
+ static final String PREF_DO_NOT_TRACK = "do_not_track";
+ static final String PREF_WEB_REFINER = "web_refiner";
+
+ // ----------------------
+ // 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";
+
+ // ----------------------
+ // Keys for about_preferences.xml
+ // ----------------------
+ static final String PREF_ABOUT = "about";
+ static final String PREF_VERSION = "version";
+ static final String PREF_BUILD_DATE = "built";
+ static final String PREF_BUILD_HASH = "hash";
+ static final String PREF_USER_AGENT = "user_agent";
+ static final String PREF_HELP = "help_about";
+ static final String PREF_FEEDBACK = "feedback";
+ static final String PREF_AUTO_UPDATE = "update_notification";
+ static final String PREF_EDGE_SWIPE = "edge_swiping_action";
+ static final String PREF_LEGAL = "legal";
+
+ // ----------------------
+ // Keys for legal_preferences.xml
+ // ----------------------
+ static final String PREF_LEGAL_CREDITS = "legal_credits";
+ static final String PREF_LEGAL_EULA = "legal_eula";
+ static final String PREF_LEGAL_PRIVACY_POLICY = "legal_privacy_policy";
+
+ static final String ACTION_RELOAD_PAGE = "reload";
+}
diff --git a/src/src/com/android/browser/PreloadController.java b/src/src/com/android/browser/PreloadController.java
new file mode 100644
index 00000000..8bccf2f6
--- /dev/null
+++ b/src/src/com/android/browser/PreloadController.java
@@ -0,0 +1,255 @@
+/*
+ * 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.os.Message;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import org.codeaurora.swe.HttpAuthHandler;
+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 onUserCanceledSsl(Tab tab) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "onUserCanceledSsl()");
+ }
+
+ @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 showFileChooser(ValueCallback<String[]> uploadFilePaths, String acceptTypes,
+ boolean capture) {
+ if (LOGD_ENABLED) Log.d(LOGTAG, "showFileChooser()");
+ }
+
+ @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 boolean shouldCaptureThumbnails() {
+ return false;
+ }
+
+}
diff --git a/src/src/com/android/browser/PreloadRequestReceiver.java b/src/src/com/android/browser/PreloadRequestReceiver.java
new file mode 100644
index 00000000..b75da0c0
--- /dev/null
+++ b/src/src/com/android/browser/PreloadRequestReceiver.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.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.browser.platformsupport.Browser;
+
+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/src/com/android/browser/PreloadedTabControl.java b/src/src/com/android/browser/PreloadedTabControl.java
new file mode 100644
index 00000000..21dafa9d
--- /dev/null
+++ b/src/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/src/com/android/browser/Preloader.java b/src/src/com/android/browser/Preloader.java
new file mode 100644
index 00000000..5f90487e
--- /dev/null
+++ b/src/src/com/android/browser/Preloader.java
@@ -0,0 +1,177 @@
+/*
+ * 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/src/com/android/browser/ShareDialog.java b/src/src/com/android/browser/ShareDialog.java
new file mode 100644
index 00000000..0f44e5cb
--- /dev/null
+++ b/src/src/com/android/browser/ShareDialog.java
@@ -0,0 +1,125 @@
+/*
+ * 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;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ResolveInfo;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.os.Build;
+
+import java.util.List;
+import java.util.Collections;
+
+import android.util.Log;
+
+
+public class ShareDialog extends AppItem {
+ private Activity activity = null;
+ public String title = null;
+ public String url = null;
+ public Bitmap favicon = null;
+ public Bitmap screenshot = null;
+ private List<ResolveInfo>apps = null;
+ public final static String EXTRA_SHARE_SCREENSHOT = "share_screenshot";
+ public final static String EXTRA_SHARE_FAVICON = "share_favicon";
+
+
+ public ShareDialog (Activity activity, String title, String url, Bitmap favicon, Bitmap screenshot) {
+ super(null);
+ this.activity = activity;
+ this.apps = getShareableApps();
+ this.title = title;
+ this.url = url;
+ this.favicon = favicon;
+ this.screenshot = screenshot;
+ }
+
+ private List<ResolveInfo> getShareableApps() {
+ Intent shareIntent = new Intent("android.intent.action.SEND");
+ shareIntent.setType("text/plain");
+ PackageManager pm = activity.getPackageManager();
+ List<ResolveInfo> launchables = pm.queryIntentActivities(shareIntent, 0);
+
+ Collections.sort(launchables,
+ new ResolveInfo.DisplayNameComparator(pm));
+
+ return launchables;
+ }
+
+
+ public List<ResolveInfo> getApps() {
+ return apps;
+ }
+
+ public void loadView(final AppAdapter adapter) {
+ AlertDialog.Builder builderSingle = new AlertDialog.Builder(activity);
+ builderSingle.setIcon(R.mipmap.ic_launcher_browser);
+ builderSingle.setTitle(activity.getString(R.string.choosertitle_sharevia));
+ builderSingle.setAdapter(adapter, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int position) {
+ dialog.dismiss();
+ ResolveInfo launchable = adapter.getItem(position);
+ ActivityInfo activityInfo = launchable.activityInfo;
+ ComponentName name = new android.content.ComponentName(activityInfo.applicationInfo.packageName,
+ activityInfo.name);
+ Intent i = new Intent(Intent.ACTION_SEND);
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
+ // This flag clears the called app from the activity stack,
+ // so users arrive in the expected place next time this application is restarted
+ i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ } else {
+ // flag used from Lollipop onwards
+ i.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+ }
+
+ i.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT |
+ Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
+ i.setType("text/plain");
+ i.putExtra(Intent.EXTRA_TEXT, url);
+ i.putExtra(Intent.EXTRA_SUBJECT, title);
+ i.putExtra(EXTRA_SHARE_FAVICON, favicon);
+ i.putExtra(EXTRA_SHARE_SCREENSHOT, screenshot);
+ i.setComponent(name);
+ activity.startActivity(i);
+ }
+ });
+
+ builderSingle.show();
+ }
+}
diff --git a/src/src/com/android/browser/ShortcutActivity.java b/src/src/com/android/browser/ShortcutActivity.java
new file mode 100644
index 00000000..dcc176f0
--- /dev/null
+++ b/src/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/src/com/android/browser/SiteTileView.java b/src/src/com/android/browser/SiteTileView.java
new file mode 100644
index 00000000..8d69dbaf
--- /dev/null
+++ b/src/src/com/android/browser/SiteTileView.java
@@ -0,0 +1,680 @@
+/*
+ * Copyright (c) 2015, 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.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.View;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This represents a WebSite Tile that is created from a Drawable and will scale across any
+ * area this is externally layouted to. There are 3 possible looks:
+ * - just the favicon (TYPE_SMALL)
+ * - drop-shadow plus a thin overlay border (1dp) (TYPE_MEDIUM)
+ * - centered favicon, extended color, rounded base (TYPE_LARGE)
+ *
+ * By centralizing everything in this class we make customization of looks much easier.
+ *
+ * NOTES:
+ * - do not set a background from the outside; this overrides it automatically
+ */
+public class SiteTileView extends View {
+
+ // public trust level constants
+ public static final int TRUST_UNKNOWN = 0; // default
+ public static final int TRUST_AVOID = 0x01;
+ public static final int TRUST_UNTRUSTED = 0x02;
+ public static final int TRUST_TRUSTED = 0x04;
+ private static final int TRUST_MASK = 0x07;
+
+
+ // static configuration
+ private static final int THRESHOLD_MEDIUM_DP = 32;
+ private static final int THRESHOLD_LARGE_DP = 64;
+ private static final int LARGE_FAVICON_SIZE_DP = 48;
+ private static final int BACKGROUND_DRAWABLE_RES = R.drawable.img_tile_background;
+ private static final int DEFAULT_SITE_FAVICON = 0;
+ private static final float FILLER_RADIUS_DP = 2f; // sync with the bg image radius
+ private static final int FILLER_FALLBACK_COLOR = Color.WHITE; // in case there is no favicon
+ private static final boolean BADGE_SHOW_BLOCKED_COUNT = false;
+
+ // internal enums
+ private static final int TYPE_SMALL = 1;
+ private static final int TYPE_MEDIUM = 2;
+ private static final int TYPE_LARGE = 3;
+ private static final int TYPE_AUTO = 0;
+ private static final int COLOR_AUTO = 0;
+
+
+ // configuration
+ private Bitmap mFaviconBitmap = null;
+ private Paint mFundamentalPaint = null;
+ private int mFaviconWidth = 0;
+ private int mFaviconHeight = 0;
+ private int mForcedFundamentalColor = COLOR_AUTO;
+ private boolean mBackgroundDisabled = false;
+ private int mTrustLevel = TRUST_UNKNOWN;
+ private int mBadgeBlockedObjectsCount = 0;
+ private boolean mBadgeHasCertIssues = false;
+
+ // runtime params set on Layout
+ private int mCurrentWidth = 0;
+ private int mCurrentHeight = 0;
+ private int mCurrentType = TYPE_MEDIUM;
+ private int mPaddingLeft = 0;
+ private int mPaddingTop = 0;
+ private int mPaddingRight = 0;
+ private int mPaddingBottom = 0;
+ private boolean mCurrentShadowDrawn = false;
+
+ // static objects, to be recycled amongst instances (this is an optimization)
+ // NOTE: package-visible statics are for optimized usage inside FolderTileView as well
+ private static int sMediumPxThreshold = -1;
+ private static int sLargePxThreshold = -1;
+ private static int sLargeFaviconPx = -1;
+ /* package */ static float sRoundedRadius = -1;
+ private static Paint sBitmapPaint = null;
+ private static Paint sBadgeTextPaint = null;
+ private static Rect sSrcRect = new Rect();
+ private static Rect sDstRect = new Rect();
+ /* package */ static RectF sRectF = new RectF();
+ private static Drawable sBackgroundDrawable = null;
+ private static class BadgeAssets {
+ Drawable back;
+ Drawable accent;
+ int textColor;
+ }
+ private static Map<Integer, BadgeAssets> sBadges;
+ private static Bitmap sDefaultSiteBitmap = null;
+ /* package */ static Rect sBackgroundDrawablePadding = new Rect();
+
+
+ /* XML constructors */
+
+ public SiteTileView(Context context) {
+ super(context);
+ xmlInit(null, 0);
+ }
+
+ public SiteTileView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ xmlInit(attrs, 0);
+ }
+
+ public SiteTileView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ xmlInit(attrs, defStyle);
+ }
+
+
+ /* Programmatic Constructors */
+
+ public SiteTileView(Context context, Bitmap favicon) {
+ super(context);
+ init(favicon, COLOR_AUTO);
+ }
+
+ public SiteTileView(Context context, Bitmap favicon, int fundamentalColor) {
+ super(context);
+ init(favicon, fundamentalColor);
+ }
+
+
+ /**
+ * Changes the current favicon (and associated fundamental color) on the fly
+ */
+ public void replaceFavicon(Bitmap favicon) {
+ replaceFavicon(favicon, COLOR_AUTO);
+ }
+
+ /**
+ * Changes the current favicon (and associated fundamental color) on the fly
+ * @param favicon the new favicon
+ * @param fundamentalColor the new fudamental color, or COLOR_AUTO
+ */
+ public void replaceFavicon(Bitmap favicon, int fundamentalColor) {
+ init(favicon, fundamentalColor);
+ requestLayout();
+ }
+
+ /**
+ * Disables the automatic background and filling. Useful for things that are not really
+ * "Website Tiles", like folders.
+ * @param disabled true to disable the background (defaults to false)
+ */
+ public void setBackgroundDisabled(boolean disabled) {
+ if (mBackgroundDisabled != disabled) {
+ mBackgroundDisabled = disabled;
+ invalidate();
+ }
+ }
+
+ /**
+ * This results in the Badge being updated
+ * @param trustLevel one of the TRUST_ constants
+ */
+ public void setTrustLevel(int trustLevel) {
+ if (mTrustLevel != trustLevel) {
+ mTrustLevel = trustLevel;
+ invalidate();
+ }
+ }
+
+ /**
+ * Tells that there will be some message about issues inside
+ * @param certIssues true if there are issues.
+ */
+ public void setBadgeHasCertIssues(boolean certIssues) {
+ if (certIssues != mBadgeHasCertIssues) {
+ mBadgeHasCertIssues = certIssues;
+ invalidate();
+ }
+ }
+
+ /**
+ * Sets the number of objects blocked (a positive contribution to the page). Presentation
+ * may or may not have the number indication.
+ * @param sessionCounter Counter of blocked objects. Use 0 to not display anything.
+ */
+ public void setBadgeBlockedObjectsCount(int sessionCounter) {
+ if (sessionCounter != mBadgeBlockedObjectsCount) {
+ // repaint if going from or to 0, or if showing the ads count
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (mBadgeBlockedObjectsCount == 0 || sessionCounter == 0 || BADGE_SHOW_BLOCKED_COUNT)
+ invalidate();
+ mBadgeBlockedObjectsCount = sessionCounter;
+ }
+ }
+
+
+ /**
+ * @return The fundamental color representing the site.
+ */
+ public int getFundamentalColor() {
+ if (mForcedFundamentalColor != COLOR_AUTO)
+ return mForcedFundamentalColor;
+ if (mFundamentalPaint == null)
+ mFundamentalPaint = createFundamentalPaint(mFaviconBitmap, COLOR_AUTO);
+ return mFundamentalPaint.getColor();
+ }
+
+
+ /*** private stuff ahead ***/
+
+ private boolean requiresBadge() {
+ return !mBackgroundDisabled && (mTrustLevel != TRUST_UNKNOWN || mBadgeHasCertIssues
+ || mBadgeBlockedObjectsCount > 0);
+ }
+
+ private int computeBadgeMessages() {
+ // special case, for TRUST_AVOID, always show the common accent
+ if (mTrustLevel == TRUST_AVOID)
+ return 0;
+
+ // recompute number of 'messages' inside the badge
+ int count = 0;
+ if (mBadgeHasCertIssues)
+ count++;
+ if (mBadgeBlockedObjectsCount > 0)
+ count++;
+
+ // add the number of blocked objects (-1, for having already counted the message) if needed
+ if (BADGE_SHOW_BLOCKED_COUNT)
+ count += mBadgeBlockedObjectsCount - 1;
+
+ return count;
+ }
+
+ private void xmlInit(AttributeSet attrs, int defStyle) {
+ // load attributes
+ final TypedArray a = getContext().obtainStyledAttributes(attrs,
+ R.styleable.SiteTileView, defStyle, 0);
+
+ // fetch the drawable, if defined - then just extract and use the bitmap
+ final Drawable drawable = a.getDrawable(R.styleable.SiteTileView_android_src);
+ final Bitmap favicon = drawable instanceof BitmapDrawable ?
+ ((BitmapDrawable) drawable).getBitmap() : null;
+
+ // check if we want it background-less (disable shadow and filler)
+ setBackgroundDisabled(a.getBoolean(R.styleable.SiteTileView_disableBackground, false));
+
+ // read the trust level (unknown, aka 'default', if not present)
+ setTrustLevel(a.getInteger(R.styleable.SiteTileView_trustLevel, TRUST_UNKNOWN)
+ & TRUST_MASK);
+
+ // read the amount of blocked objects (or 0 if not present)
+ setBadgeBlockedObjectsCount(a.getInteger(R.styleable.SiteTileView_blockedObjects, 0));
+
+ // delete attribute resolution
+ a.recycle();
+
+ // proceed with real initialization
+ init(favicon, COLOR_AUTO);
+ }
+
+ private void init(Bitmap favicon, int fundamentalColor) {
+ mFaviconBitmap = favicon;
+
+ // show a default favicon if nothing is set (consider removing this, it's ugly)
+ if (mFaviconBitmap == null && DEFAULT_SITE_FAVICON != 0) {
+ if (sDefaultSiteBitmap == null)
+ sDefaultSiteBitmap = BitmapFactory.decodeResource(getResources(),
+ DEFAULT_SITE_FAVICON);
+ mFaviconBitmap = sDefaultSiteBitmap;
+ fundamentalColor = 0xFF262626;
+ }
+
+ if (mFaviconBitmap != null) {
+ mFaviconWidth = mFaviconBitmap.getWidth();
+ mFaviconHeight = mFaviconBitmap.getHeight();
+ }
+
+ // don't compute the paint right now, just save any hint for later
+ mFundamentalPaint = null;
+ mForcedFundamentalColor = fundamentalColor;
+
+ // shared (static) resources initialization; except for background, inited on-demand
+ ensureCommonLoaded(getResources());
+
+ // change when clicked
+ setClickable(true);
+ }
+
+ static void ensureCommonLoaded(Resources r) {
+ // check if already initialized
+ if (sMediumPxThreshold != -1)
+ return;
+
+ // heuristics thresholds
+ final DisplayMetrics displayMetrics = r.getDisplayMetrics();
+ sMediumPxThreshold = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+ THRESHOLD_MEDIUM_DP, displayMetrics);
+ sLargePxThreshold = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+ THRESHOLD_LARGE_DP, displayMetrics);
+ sLargeFaviconPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+ LARGE_FAVICON_SIZE_DP, displayMetrics);
+
+ // rounded radius
+ sRoundedRadius = FILLER_RADIUS_DP > 0 ? TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP, FILLER_RADIUS_DP, displayMetrics) : 0;
+
+ // bitmap paint (copy, smooth scale)
+ sBitmapPaint = new Paint();
+ sBitmapPaint.setColor(Color.BLACK);
+ sBitmapPaint.setFilterBitmap(true);
+
+ // badge text paint (anti-aliased)
+ sBadgeTextPaint = new Paint();
+ sBadgeTextPaint.setAntiAlias(true);
+ Typeface badgeTypeface = Typeface.create("sans-serif-medium", Typeface.NORMAL);
+ if (badgeTypeface != null)
+ sBadgeTextPaint.setTypeface(badgeTypeface);
+
+ // load the background (could be loaded on demand, but in the end it's always needed)
+ sBackgroundDrawable = r.getDrawable(BACKGROUND_DRAWABLE_RES);
+ if (sBackgroundDrawable != null)
+ sBackgroundDrawable.getPadding(sBackgroundDrawablePadding);
+
+ // load all the badge drawables
+ sBadges = new HashMap<>();
+ loadBadgeResources(r, TRUST_AVOID, R.drawable.img_deco_tile_avoid,
+ R.drawable.img_deco_tile_avoid_accent, R.color.TileBadgeTextAvoid);
+ loadBadgeResources(r, TRUST_UNTRUSTED, R.drawable.img_deco_tile_untrusted,
+ R.drawable.img_deco_tile_untrusted_accent, R.color.TileBadgeTextUntrusted);
+ loadBadgeResources(r, TRUST_UNKNOWN, R.drawable.img_deco_tile_unknown,
+ R.drawable.img_deco_tile_unknown_accent, R.color.TileBadgeTextUnknown);
+ loadBadgeResources(r, TRUST_TRUSTED, R.drawable.img_deco_tile_verified,
+ R.drawable.img_deco_tile_verified_accent, R.color.TileBadgeTextVerified);
+ }
+
+ private static void loadBadgeResources(Resources r, int t, int back, int accent, int color) {
+ BadgeAssets ba = new BadgeAssets();
+ ba.back = back == 0 ? null : r.getDrawable(back);
+ ba.accent = accent == 0 ? null : r.getDrawable(accent);
+ ba.textColor = color == 0 ? Color.TRANSPARENT : r.getColor(color);
+ sBadges.put(t, ba);
+ }
+
+ static Rect getBackgroundDrawablePadding() {
+ return sBackgroundDrawablePadding != null ? sBackgroundDrawablePadding : new Rect();
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ mCurrentWidth = right - left;
+ mCurrentHeight = bottom - top;
+
+ // auto-determine the "TYPE_" from the physical size of the layout
+ if (mCurrentWidth < sMediumPxThreshold && mCurrentHeight < sMediumPxThreshold)
+ mCurrentType = TYPE_SMALL;
+ else if (mCurrentWidth < sLargePxThreshold && mCurrentHeight < sLargePxThreshold)
+ mCurrentType = TYPE_MEDIUM;
+ else
+ mCurrentType = TYPE_LARGE;
+
+ // set or remove the background (if the need changed!)
+ boolean requiresBackgroundDrawable = mCurrentType >= TYPE_MEDIUM;
+ if (requiresBackgroundDrawable && !mCurrentShadowDrawn) {
+ // draw the background
+ mCurrentShadowDrawn = mCurrentType >= TYPE_LARGE;
+
+ // background -> padding
+ mPaddingLeft = sBackgroundDrawablePadding.left;
+ mPaddingTop = sBackgroundDrawablePadding.top;
+ mPaddingRight = sBackgroundDrawablePadding.right;
+ mPaddingBottom = sBackgroundDrawablePadding.bottom;
+ } else if (!requiresBackgroundDrawable && mCurrentShadowDrawn) {
+ // turn off background drawing
+ mCurrentShadowDrawn = false;
+
+ // no background -> no padding
+ mPaddingLeft = 0;
+ mPaddingTop = 0;
+ mPaddingRight = 0;
+ mPaddingBottom = 0;
+ }
+
+ // just proceed, do nothing here
+ super.onLayout(changed, left, top, right, bottom);
+ }
+
+ @Override
+ public void setPressed(boolean pressed) {
+ super.setPressed(pressed);
+ // schedule a repaint to show pressed/released
+ invalidate();
+ }
+
+ @Override
+ public void setSelected(boolean selected) {
+ super.setSelected(selected);
+ // schedule a repaint to show selected
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ // Selection State: make everything smaller
+ if (isSelected()) {
+ float scale = 0.8f;
+ canvas.translate(mCurrentWidth * (1 - scale) / 2, mCurrentHeight * (1 - scale) / 2);
+ canvas.scale(scale, scale);
+ }
+
+ // Pressed state: make the button reach the finger
+ if (isPressed()) {
+ float scale = 1.1f;
+ canvas.translate(mCurrentWidth * (1 - scale) / 2, mCurrentHeight * (1 - scale) / 2);
+ canvas.scale(scale, scale);
+ }
+
+ final int left = mPaddingLeft;
+ final int top = mPaddingTop;
+ final int right = mCurrentWidth - mPaddingRight;
+ final int bottom = mCurrentHeight - mPaddingBottom;
+ final int contentWidth = right - left;
+ final int contentHeight = bottom - top;
+
+ // A. the background drawable (if set)
+ boolean requiresBackground = mCurrentShadowDrawn && sBackgroundDrawable != null
+ && !isPressed() && !mBackgroundDisabled;
+ if (requiresBackground) {
+ sBackgroundDrawable.setBounds(0, 0, mCurrentWidth, mCurrentHeight);
+ sBackgroundDrawable.draw(canvas);
+ }
+
+ // B. (when needed) draw the background rectangle; sharp our rounded
+ boolean requiresFundamentalFiller = mCurrentType >= TYPE_LARGE && !mBackgroundDisabled;
+ if (requiresFundamentalFiller) {
+ // create the filler paint on demand (not all icons need it)
+ if (mFundamentalPaint == null)
+ mFundamentalPaint = createFundamentalPaint(mFaviconBitmap, mForcedFundamentalColor);
+
+ // paint if not white, since requiresBackground already painted it white
+ int fundamentalColor = mFundamentalPaint.getColor();
+ if (fundamentalColor != COLOR_AUTO &&
+ (fundamentalColor != Color.WHITE || !requiresBackground)) {
+ if (sRoundedRadius >= 1.) {
+ sRectF.set(left, top, right, bottom);
+ canvas.drawRoundRect(sRectF, sRoundedRadius, sRoundedRadius, mFundamentalPaint);
+ } else
+ canvas.drawRect(left, top, right, bottom, mFundamentalPaint);
+ }
+ }
+
+ // C. (if present) draw the favicon
+ boolean requiresFavicon = mFaviconBitmap != null
+ && mFaviconWidth > 1 && mFaviconHeight > 1;
+ if (requiresFavicon) {
+ // destination can either fill, or auto-center
+ boolean fillSpace = mCurrentType <= TYPE_MEDIUM;
+ if (fillSpace || contentWidth < sLargeFaviconPx || contentHeight < sLargeFaviconPx) {
+ sDstRect.set(left, top, right, bottom);
+ } else {
+ int dstLeft = left + (contentWidth - sLargeFaviconPx) / 2;
+ int dstTop = top + (contentHeight - sLargeFaviconPx) / 2;
+ sDstRect.set(dstLeft, dstTop, dstLeft + sLargeFaviconPx, dstTop + sLargeFaviconPx);
+ }
+
+ // source has to 'crop proportionally' to keep the dest aspect ratio
+ sSrcRect.set(0, 0, mFaviconWidth, mFaviconHeight);
+ int sW = sSrcRect.width();
+ int sH = sSrcRect.height();
+ int dW = sDstRect.width();
+ int dH = sDstRect.height();
+ if (sW > 4 && sH > 4 && dW > 4 && dH > 4) {
+ float hScale = (float) dW / (float) sW;
+ float vScale = (float) dH / (float) sH;
+ if (hScale == vScale) {
+ // no transformation needed, just zoom
+ } else if (hScale < vScale) {
+ // horizontal crop
+ float hCrop = 1 - hScale / vScale;
+ int hCropPx = (int) (sW * hCrop / 2 + 0.5);
+ sSrcRect.left += hCropPx;
+ sSrcRect.right -= hCropPx;
+ canvas.drawBitmap(mFaviconBitmap, sSrcRect, sDstRect, sBitmapPaint);
+ } else {
+ // vertical crop
+ float vCrop = 1 - vScale / hScale;
+ int vCropPx = (int) (sH * vCrop / 2 + 0.5);
+ sSrcRect.top += vCropPx;
+ sSrcRect.bottom -= vCropPx;
+ }
+ }
+
+ // blit favicon, croppped, scaled
+ canvas.drawBitmap(mFaviconBitmap, sSrcRect, sDstRect, sBitmapPaint);
+ }
+
+ // D. show badge, if requested
+ if (requiresBadge()) {
+ // retrieve the badge resources
+ final BadgeAssets ba = sBadges.get(mTrustLevel);
+ if (ba != null) {
+
+ // paint back
+ final Drawable back = ba.back;
+ int badgeL = 0, badgeT = 0, badgeW = 0, badgeH = 0;
+ if (back != null) {
+ badgeW = back.getIntrinsicWidth();
+ badgeH = back.getIntrinsicHeight();
+ badgeL = mCurrentWidth - mPaddingRight / 3 - badgeW;
+ badgeT = mCurrentHeight - mPaddingBottom / 3 - badgeH;
+ back.setBounds(badgeL, badgeT, badgeL + badgeW, badgeT + badgeH);
+ back.draw(canvas);
+ }
+ int messagesCount = computeBadgeMessages();
+
+ // paint accent, if 0 messages
+ if (messagesCount < 1) {
+ final Drawable accent = ba.accent;
+ if (accent != null && badgeW > 0 && badgeH > 0) {
+ int accentW = accent.getIntrinsicWidth();
+ int accentH = accent.getIntrinsicHeight();
+ int accentL = badgeL + (badgeW - accentW) / 2;
+ int accentT = badgeT + (badgeH - accentH) / 2;
+ accent.setBounds(accentL, accentT, accentL + accentW, accentT + accentH);
+ accent.draw(canvas);
+ }
+ }
+ // at least 1 message, draw text
+ else if (Color.alpha(ba.textColor) > 0) {
+ float textSize = Math.min(2 * contentWidth / 5, sMediumPxThreshold / 4) * 1.1f;
+ sBadgeTextPaint.setTextSize(textSize);
+ sBadgeTextPaint.setColor(ba.textColor);
+ final String text = String.valueOf(messagesCount);
+ int textWidth = Math.round(sBadgeTextPaint.measureText(text) / 2);
+ int textCx = badgeL + badgeW / 2;
+ int textCy = badgeT + badgeH / 2;
+ canvas.drawText(text, textCx - textWidth, textCy + textSize / 3 + 1,
+ sBadgeTextPaint);
+ }
+ }
+ }
+
+ /*if (true) { // DEBUG TYPE
+ Paint paint = new Paint();
+ paint.setColor(Color.BLACK);
+ paint.setTextSize(20);
+ canvas.drawText(String.valueOf(mCurrentType), 30, 30, paint);
+ }*/
+ }
+
+
+ /**
+ * Creates a fill Paint from the favicon, or using the forced color (if not COLOR_AUTO)
+ */
+ private static Paint createFundamentalPaint(Bitmap favicon, int forceFillColor) {
+ final Paint fillPaint = new Paint();
+ if (forceFillColor != COLOR_AUTO)
+ fillPaint.setColor(forceFillColor);
+ else
+ fillPaint.setColor(guessFundamentalColor(favicon));
+ return fillPaint;
+ }
+
+ /**
+ * This uses very stupid mechanism - a 9x9 grid sample on the borders and center - and selects
+ * the color with the most frequency, or the center.
+ *
+ * @param bitmap the bitmap to guesss the color about
+ * @return a Color
+ */
+ private static int guessFundamentalColor(Bitmap bitmap) {
+ if (bitmap == null)
+ return FILLER_FALLBACK_COLOR;
+ int height = bitmap.getHeight();
+ int width = bitmap.getWidth();
+ if (height < 2 || width < 2)
+ return FILLER_FALLBACK_COLOR;
+
+ // pick up to 9 colors
+ // NOTE: the order of sampling sets the precendece, in case of ties
+ int[] pxColors = new int[9];
+ int idx = 0;
+ if ((pxColors[idx] = sampleColor(bitmap, width / 2, height / 2)) != 0) idx++;
+ if ((pxColors[idx] = sampleColor(bitmap, width / 2, height - 1)) != 0) idx++;
+ if ((pxColors[idx] = sampleColor(bitmap, width - 1, height - 1)) != 0) idx++;
+ if ((pxColors[idx] = sampleColor(bitmap, width - 1, height / 2)) != 0) idx++;
+ if ((pxColors[idx] = sampleColor(bitmap, 0, 0 )) != 0) idx++;
+ if ((pxColors[idx] = sampleColor(bitmap, width / 2, 0 )) != 0) idx++;
+ if ((pxColors[idx] = sampleColor(bitmap, width - 1, 0 )) != 0) idx++;
+ if ((pxColors[idx] = sampleColor(bitmap, 0 , height / 2)) != 0) idx++;
+ if ((pxColors[idx] = sampleColor(bitmap, 0 , height - 1)) != 0) idx++;
+
+ // find the most popular
+ int popColor = -1;
+ int popCount = -1;
+ for (int i = 0; i < idx; i++) {
+ int thisColor = pxColors[i];
+ int thisCount = 0;
+ for (int j = 0; j < idx; j++) {
+ if (pxColors[j] == thisColor)
+ thisCount++;
+ }
+ if (thisCount > popCount) {
+ popColor = thisColor;
+ popCount = thisCount;
+ }
+ }
+ return popCount > -1 ? popColor : FILLER_FALLBACK_COLOR;
+ }
+
+ /**
+ * @return Color, but if it's 0, you should discard it (not representative)
+ */
+ private static int sampleColor(Bitmap bitmap, int x, int y) {
+ final int color = bitmap.getPixel(x, y);
+
+ // discard semi-transparent pixels, because they're probably from a spurious border
+ //if ((color >>> 24) <= 128)
+ // return 0;
+
+ // compose transparent pixels with white, since the BG will be white anyway
+ final int alpha = Color.alpha(color);
+ if (alpha == 0)
+ return Color.WHITE;
+ if (alpha < 255) {
+ // perform simplified Porter-Duff source-over
+ int dstContribution = 255 - alpha;
+ return Color.argb(255,
+ ((alpha * Color.red(color)) >> 8) + dstContribution,
+ ((alpha * Color.green(color)) >> 8) + dstContribution,
+ ((alpha * Color.blue(color)) >> 8) + dstContribution
+ );
+ }
+
+ // discard black pixels, because black is not a color (well, not a good looking one)
+ if ((color & 0xFFFFFF) == 0)
+ return 0;
+
+ return color;
+ }
+
+}
diff --git a/src/src/com/android/browser/SnapshotBar.java b/src/src/com/android/browser/SnapshotBar.java
new file mode 100644
index 00000000..eeb300bb
--- /dev/null
+++ b/src/src/com/android/browser/SnapshotBar.java
@@ -0,0 +1,269 @@
+/*
+ * 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.util.TypedValue;
+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 org.codeaurora.swe.util.Activator;
+import org.codeaurora.swe.util.Observable;
+
+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 = 1500;
+
+ private ImageView mFavicon;
+ private ImageView mSnapshoticon;
+ private ImageView mReadericon;
+ private TextView mDate;
+ private TextView mTitle;
+ private View mBookmarks;
+ private TitleBar mTitleBar;
+ private View mTabSwitcher;
+ private TextView mTabText;
+ private View mOverflowMenu;
+ private View mToggleContainer;
+ private boolean mIsAnimating;
+ private ViewPropertyAnimator mTitleAnimator, mDateAnimator;
+ private float mAnimRadius = 20f;
+ private float mTabSwitcherInitialTextSize = 0;
+ private float mTabSwitcherCompressedTextSize = 0;
+
+ 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);
+ Activator.activate(
+ new Observable.Observer() {
+ @Override
+ public void onChange(Object... params) {
+ if (mTabText == null)
+ return;
+ if ((Integer) params[0] > 9) {
+ mTabText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTabSwitcherCompressedTextSize);
+ } else {
+ mTabText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTabSwitcherInitialTextSize);
+ }
+
+ mTabText.setText(Integer.toString((Integer) params[0]));
+ }
+ },
+ mTitleBar.getUiController().getTabControl().getTabCountObservable());
+ }
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_SHOW_TITLE) {
+ mIsAnimating = false;
+ showTitle();
+ }
+ }
+ };
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mFavicon = (ImageView) findViewById(R.id.favicon);
+ mSnapshoticon = (ImageView) findViewById(R.id.snapshot_icon);
+ mReadericon = (ImageView) findViewById(R.id.reader_icon);
+ mDate = (TextView) findViewById(R.id.date);
+ mTitle = (TextView) findViewById(R.id.title);
+ mBookmarks = findViewById(R.id.all_btn);
+ mTabSwitcher = findViewById(R.id.tab_switcher);
+ mTabText = (TextView) findViewById(R.id.tab_switcher_text);
+ 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);
+ mOverflowMenu.setVisibility(VISIBLE);
+ }
+ if (mToggleContainer != null) {
+ mToggleContainer.setOnClickListener(this);
+ resetAnimation();
+ }
+
+ if (mTabSwitcherInitialTextSize == 0 && mTabText != null) {
+ mTabSwitcherInitialTextSize = mTabText.getTextSize();
+ mTabSwitcherCompressedTextSize = (float) (mTabSwitcherInitialTextSize / 1.2);
+ }
+ }
+
+ @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 (navBar instanceof NavigationBarTablet) {
+ ((NavigationBarTablet)navBar).showMenu(mOverflowMenu);
+ }
+ } else if (mToggleContainer == v && !mIsAnimating) {
+ mIsAnimating = true;
+ showDate();
+ mTitleBar.showTopControls(false);
+ 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)
+ mFavicon.setImageDrawable(mTitleBar.getUi().getFaviconDrawable(icon));
+ }
+
+ public boolean isAnimating() {
+ return mIsAnimating;
+ }
+
+ public void setTitle(String title) {
+ mTitle.setText(title);
+ }
+
+ public void setDate(String date) {
+ mDate.setText(date);
+ }
+
+ public void setSnapshoticonVisibility(int visibility) {
+ if (mSnapshoticon.getVisibility() != visibility) {
+ mSnapshoticon.setVisibility(visibility);
+ }
+ }
+
+ public void setReadericonVisibility(int visibility) {
+ if (mReadericon != null && mReadericon.getVisibility() != visibility) {
+ mReadericon.setVisibility(visibility);
+ }
+ }
+
+ public void setFaviconVisibility(int visibility) {
+ if (mFavicon != null && mFavicon.getVisibility() != visibility) {
+ mFavicon.setVisibility(visibility);
+ }
+ }
+
+}
diff --git a/src/src/com/android/browser/SnapshotTab.java b/src/src/com/android/browser/SnapshotTab.java
new file mode 100644
index 00000000..c63bf102
--- /dev/null
+++ b/src/src/com/android/browser/SnapshotTab.java
@@ -0,0 +1,254 @@
+/*
+ * 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.Bitmap;
+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;
+
+ // Used for saving and restoring each Tab
+ static final String SNAPSHOT_ID = "snapshotId";
+ static final String ID = "ID";
+
+
+ public SnapshotTab(WebViewController wvcontroller,
+ long snapshotId,
+ Bundle state) {
+ super(wvcontroller, null, state);
+ mSnapshotId = snapshotId;
+ mWebViewFactory = mWebViewController.getWebViewFactory();
+ WebView web = mWebViewFactory.createWebView(false);
+ setWebView(web);
+ loadData();
+ mIsLive = false;
+ }
+
+ @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(Bitmap bm) {
+ if (mIsLive) {
+ return super.createSnapshotValues(bm);
+ }
+ return null;
+ }
+
+ @Override
+ public Bundle saveState() {
+ if (mIsLive) {
+ return super.saveState();
+ }
+
+ Bundle savedState = new Bundle();
+ savedState.putLong(SNAPSHOT_ID, mSnapshotId);
+ savedState.putLong(ID, getId());
+
+ return savedState;
+ }
+
+ public long getDateCreated() {
+ return mDateCreated;
+ }
+
+ public String getLiveUrl() {
+ return mLiveUrl;
+ }
+
+ @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/src/com/android/browser/SuggestionsAdapter.java b/src/src/com/android/browser/SuggestionsAdapter.java
new file mode 100644
index 00000000..8ab78e24
--- /dev/null
+++ b/src/src/com/android/browser/SuggestionsAdapter.java
@@ -0,0 +1,598 @@
+/*
+ * 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_suggest_search_normal;
+ break;
+ case TYPE_BOOKMARK:
+ id = R.drawable.ic_suggest_bookmark_normal;
+ break;
+ case TYPE_HISTORY:
+ id = R.drawable.ic_suggest_history_normal;
+ break;
+ case TYPE_SUGGEST_URL:
+ id = R.drawable.ic_suggest_browser_normal;
+ 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++) {
+ SuggestItem item = cursor.getItem();
+ if(item != null)
+ results.add(item);
+ 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++) {
+ SuggestItem item = s.getItem();
+ if(item != null)
+ results.add(item);
+ 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[] colIndexList = {
+ SearchManager.SUGGEST_COLUMN_TEXT_1,
+ SearchManager.SUGGEST_COLUMN_TEXT_2,
+ SearchManager.SUGGEST_COLUMN_TEXT_2_URL,
+ SearchManager.SUGGEST_COLUMN_INTENT_DATA,
+ SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA
+ };
+
+ for (String currentColIndex: colIndexList) {
+ /*
+ * As defined in documentation getColumnIndex can return
+ * a value of -1,
+ * if the column does not exists, so we need to return back
+ */
+ if (mCursor.getColumnIndex(currentColIndex) == -1) {
+ return 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/src/com/android/browser/SystemAllowGeolocationOrigins.java b/src/src/com/android/browser/SystemAllowGeolocationOrigins.java
new file mode 100644
index 00000000..f4b58357
--- /dev/null
+++ b/src/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/src/com/android/browser/Tab.java b/src/src/com/android/browser/Tab.java
new file mode 100644
index 00000000..d3ce38c6
--- /dev/null
+++ b/src/src/com/android/browser/Tab.java
@@ -0,0 +1,2133 @@
+/*
+ * 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.Configuration;
+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.graphics.Rect;
+import android.net.Uri;
+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.webkit.ConsoleMessage;
+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 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.BrowserCommandLine;
+import org.codeaurora.swe.BrowserDownloadListener;
+import org.codeaurora.swe.HttpAuthHandler;
+import org.codeaurora.swe.WebBackForwardList;
+import org.codeaurora.swe.WebChromeClient;
+import org.codeaurora.swe.WebView;
+import org.codeaurora.swe.WebView.PictureListener;
+import org.codeaurora.swe.WebView.CreateWindowParams;
+import org.codeaurora.swe.WebViewClient;
+import org.codeaurora.swe.util.Observable;
+import org.codeaurora.swe.DomDistillerUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.UUID;
+import java.util.Vector;
+import java.util.List;
+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;
+ private boolean mIsKeyboardUp = 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;
+
+ // 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 mPageFinished;
+ private boolean mDisableOverrideUrlLoading;
+ private boolean mFirstVisualPixelPainted = false;
+ // 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;
+ // flag to indicate if the tab was opened from an intent
+ private boolean mDerivedFromIntent = false;
+ // The listener that gets invoked when a download is started from the
+ // mMainView
+ private final BrowserDownloadListener mDownloadListener;
+ private DataController mDataController;
+
+ // 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;
+ private boolean mFullScreen = false;
+ private boolean mReceivedError;
+
+ // determine if webview is destroyed to MemoryMonitor
+ private boolean mWebViewDestroyedByMemoryMonitor;
+
+ private String mTouchIconUrl;
+
+ private Observable mFirstPixelObservable;
+ private Observable mTabHistoryUpdateObservable;
+
+ Observable getFirstPixelObservable() {
+ return mFirstPixelObservable;
+ }
+
+ Observable getTabHistoryUpdateObservable() {
+ return mTabHistoryUpdateObservable;
+ }
+
+ // dertermines if the tab contains a disllable page
+ private boolean mIsDistillable = false;
+
+ private static synchronized Bitmap getDefaultFavicon(Context context) {
+ if (sDefaultFavicon == null) {
+ sDefaultFavicon = BitmapFactory.decodeResource(
+ context.getResources(), R.drawable.ic_deco_favicon_normal);
+ }
+ return sDefaultFavicon;
+ }
+
+ // All the state needed for a page
+ protected static class PageState {
+ String mUrl;
+ String mOriginalUrl;
+ String mTitle;
+ // This is non-null only when mSecurityState is SECURITY_STATE_BAD_CERTIFICATE.
+ SecurityState mSecurityState;
+ // This is non-null only when onReceivedIcon is called or SnapshotTab restores it.
+ Bitmap mFavicon;
+ boolean mIsBookmarkedSite;
+ boolean mIncognito;
+
+ PageState(Context c, boolean incognito) {
+ mIncognito = incognito;
+ mOriginalUrl = mUrl = "";
+ if (mIncognito) {
+ mTitle = c.getString(R.string.new_incognito_tab);
+ } else {
+ mTitle = c.getString(R.string.new_tab);
+ }
+ mSecurityState = SecurityState.SECURITY_STATE_NOT_SECURE;
+ }
+
+ PageState(Context c, boolean incognito, String url) {
+ mIncognito = incognito;
+ if (mIncognito)
+ mOriginalUrl = mUrl = "";
+ else
+ mOriginalUrl = mUrl = url;
+ mSecurityState = SecurityState.SECURITY_STATE_NOT_SECURE;
+ }
+
+ }
+
+ // 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";
+
+ public void setNetworkAvailable(boolean networkUp) {
+ if (networkUp && mReceivedError && (mMainView != null)) {
+ mMainView.reload();
+ }
+ }
+
+ public boolean isFirstVisualPixelPainted() {
+ return mFirstVisualPixelPainted;
+ }
+
+ public int getCaptureIndex(int navIndex) {
+ int orientation = mWebViewController.getActivity().
+ getResources().getConfiguration().orientation;
+
+ int orientationBit = (orientation == Configuration.ORIENTATION_LANDSCAPE) ? 0 : 1;
+
+ int index = orientationBit << 31 | (((int)mId & 0x7f) << 24) | (navIndex & 0xffffff);
+ return index;
+ }
+
+ public int getTabIdxFromCaptureIdx(int index) {
+ return (index & 0x7f000000) >> 24;
+ }
+
+ public int getOrientationFromCaptureIdx(int index) {
+ return ((index & 0x80000000) == 0) ? Configuration.ORIENTATION_LANDSCAPE :
+ Configuration.ORIENTATION_PORTRAIT;
+
+ }
+
+ public int getNavIdxFromCaptureIdx(int index) {
+ return (index & 0xffffff);
+ }
+
+ public static SecurityState getWebViewSecurityState(WebView view) {
+ switch (view.getSecurityLevel()) {
+ case WebView.SecurityLevel.EV_SECURE:
+ case WebView.SecurityLevel.SECURE:
+ return SecurityState.SECURITY_STATE_SECURE;
+ case WebView.SecurityLevel.SECURITY_ERROR:
+ return SecurityState.SECURITY_STATE_BAD_CERTIFICATE;
+ case WebView.SecurityLevel.SECURITY_POLICY_WARNING:
+ case WebView.SecurityLevel.SECURITY_WARNING:
+ return SecurityState.SECURITY_STATE_MIXED;
+ }
+ return SecurityState.SECURITY_STATE_NOT_SECURE;
+ }
+
+ // -------------------------------------------------------------------------
+ // 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) {
+ setIsDistillable(false);
+ mInPageLoad = true;
+ mPageFinished = false;
+ mFirstVisualPixelPainted = false;
+ mFirstPixelObservable.set(false);
+ mReceivedError = false;
+ mUpdateThumbnail = true;
+ mPageLoadProgress = INITIAL_PROGRESS;
+ mCurrentState = new PageState(mContext,
+ view.isPrivateBrowsingEnabled(), url);
+ mLoadStartTime = SystemClock.uptimeMillis();
+ // Need re-enable FullScreenMode on Page navigation if needed
+ if (BrowserSettings.getInstance().useFullscreen()){
+ Controller controller = (Controller) mWebViewController;
+ BaseUi ui = (BaseUi) controller.getUi();
+ ui.forceDisableFullscreenMode(false);
+ }
+ // 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;
+ }
+
+ // 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);
+ setSecurityState(getWebViewSecurityState(view));
+ }
+
+ @Override
+ public void onFirstVisualPixel(WebView view) {
+ mFirstVisualPixelPainted = true;
+ mFirstPixelObservable.set(true);
+ }
+
+ // 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;
+ }
+ }
+
+ @Override
+ public boolean shouldDownloadFavicon(WebView view, String url) {
+ return true;
+ }
+
+ /**
+ * 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) {
+ // Used for the syncCurrentState to use
+ // the failing url instead of using webview url
+ mReceivedError = true;
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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
+
+ // show only incognito content and webview has private
+ // and cannot go back(only supported if explicit from UI )
+ if (view.isPrivateBrowsingEnabled() &&
+ !view.canGoBack() &&
+ url.startsWith(Controller.INCOGNITO_URI)) {
+ 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 beforeNavigation(WebView view, String url) {
+ mTouchIconUrl = null;
+ TitleBar titleBar = null;
+ Controller controller = (Controller)mWebViewController;
+ UI ui = controller.getUi();
+
+ // Clear the page state
+ mCurrentState = new PageState(mContext,
+ view.isPrivateBrowsingEnabled(), url);
+
+ if (ui instanceof BaseUi) {
+ titleBar = ((BaseUi)ui).getTitleBar();
+ if (titleBar != null) {
+ NavigationBarBase navBar = titleBar.getNavigationBar();
+ navBar.showCurrentFavicon(Tab.this); // Show the default Favicon while loading a new page
+ }
+ }
+
+ if (BaseUi.isUiLowPowerMode()) {
+ return;
+ }
+
+ if (isPrivateBrowsingEnabled()) {
+ return;
+ }
+
+ if (!mFirstVisualPixelPainted) {
+ return;
+ }
+
+ final int idx = view.copyBackForwardList().getCurrentIndex();
+ boolean bitmapExists = view.hasSnapshot(idx);
+
+ int progress = 100;
+ if (titleBar != null) {
+ progress = titleBar.getProgressView().getProgressPercent();
+ }
+
+ if (bitmapExists && progress < 85) {
+ return;
+ }
+
+ int index = getCaptureIndex(view.getLastCommittedHistoryIndex());
+ view.captureSnapshot(index, null);
+ }
+
+ @Override
+ public void onHistoryItemCommit(WebView view, int index) {
+ if (BaseUi.isUiLowPowerMode()) {
+ return;
+ }
+
+ // prevent snapshot tab from commiting any history
+ if (isSnapshot()) {
+ return;
+ }
+
+ mTabHistoryUpdateObservable.set(index);
+ final int maxIdx = view.copyBackForwardList().getSize();
+ final WebView wv = view;
+ view.getSnapshotIds(new ValueCallback <List<Integer>>() {
+ @Override
+ public void onReceiveValue(List<Integer> ids) {
+ int currentTabIdx = mWebViewController.getTabControl().getCurrentPosition();
+ for (Integer id : ids) {
+ if (getTabIdxFromCaptureIdx(id) == currentTabIdx &&
+ getNavIdxFromCaptureIdx(id) >= maxIdx) {
+ wv.deleteSnapshot(id);
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onKeyboardStateChange(boolean popup) {
+ boolean keyboardWasShowing = isKeyboardShowing();
+ mIsKeyboardUp = popup;
+ Controller controller = (Controller)mWebViewController;
+ BaseUi ui = (BaseUi) controller.getUi();
+ // lock the title bar
+ if (popup)
+ ui.getTitleBar().showTopControls(true);
+ if (keyboardWasShowing && popup)
+ ui.getTitleBar().enableTopControls(true);
+ if (BrowserSettings.getInstance().useFullscreen()) {
+ ui.forceDisableFullscreenMode(popup);
+ }
+ }
+
+ @Override
+ public void onAttachInterstitialPage(WebView mWebView) {
+ Controller controller = (Controller)mWebViewController;
+ BaseUi ui = (BaseUi) controller.getUi();
+ ui.getTitleBar().showTopControls(false);
+ }
+
+ @Override
+ public void onDetachInterstitialPage(WebView mWebView) {
+ Controller controller = (Controller)mWebViewController;
+ BaseUi ui = (BaseUi) controller.getUi();
+ ui.getTitleBar().enableTopControls(true);
+ }
+ };
+
+ private void syncCurrentState(WebView view, String url) {
+ // Sync state (in case of stop/timeout)
+
+
+
+ if (mReceivedError) {
+ mCurrentState.mUrl = url;
+ mCurrentState.mOriginalUrl = url;
+ } else if (view.isPrivateBrowsingEnabled() &&
+ !TextUtils.isEmpty(url) &&
+ url.contains(Controller.INCOGNITO_URI)) {
+ mCurrentState.mUrl = mCurrentState.mOriginalUrl = "";
+ }
+
+ else {
+ mCurrentState.mUrl = view.getUrl();
+ mCurrentState.mOriginalUrl = view.getOriginalUrl();
+ }
+
+ if (mCurrentState.mUrl == null) {
+ mCurrentState.mUrl = "";
+ }
+ mCurrentState.mTitle = view.getTitle();
+
+
+ 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.mIncognito = view.isPrivateBrowsingEnabled();
+ }
+
+ public String getTouchIconUrl() {
+ return mTouchIconUrl;
+ }
+
+ public boolean isKeyboardShowing() {
+ Controller controller = (Controller)mWebViewController;
+ return (mIsKeyboardUp || controller.getUi().isEditingUrl());
+ }
+
+ public boolean isTabFullScreen() {
+ return mFullScreen;
+ }
+
+ protected void setTabFullscreen(boolean fullScreen) {
+ Controller controller = (Controller)mWebViewController;
+ controller.getUi().showFullscreen(fullScreen);
+ mFullScreen = fullScreen;
+ }
+
+ public boolean exitFullscreen() {
+ if (mFullScreen) {
+ Controller controller = (Controller)mWebViewController;
+ controller.getUi().showFullscreen(false);
+ if (getWebView() != null)
+ getWebView().exitFullscreen();
+ mFullScreen = false;
+ return true;
+ }
+ return false;
+ }
+
+
+
+
+ // -------------------------------------------------------------------------
+ // 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) {
+ this.createWindow(dialog, msg, null, false);
+ }
+
+ private void createWindow(final boolean dialog, final Message msg, final String url,
+ final boolean opener_suppressed) {
+ WebView.WebViewTransport transport =
+ (WebView.WebViewTransport) msg.obj;
+ if (dialog) {
+ createSubWindow();
+ mWebViewController.attachSubWindow(Tab.this);
+ transport.setWebView(mSubView);
+ } else {
+ capture();
+
+ final Tab newTab = mWebViewController.openTab(url,
+ Tab.this, true, true);
+ // This is special case for rendering links on a webpage in
+ // a new tab. If opener is suppressed, the WebContents created
+ // by the content layer are not fully initialized. This check
+ // will prevent content layer from overriding WebContents
+ // created by new tab with the uninitialized instance.
+ if (!opener_suppressed) {
+ transport.setWebView(newTab.getWebView());
+ }
+ }
+ msg.sendToTarget();
+ }
+
+ @Override
+ public void toggleFullscreenModeForTab(boolean enterFullscreen) {
+ if (mWebViewController instanceof Controller) {
+ setTabFullscreen(enterFullscreen);
+ }
+ }
+
+ @Override
+ public void onOffsetsForFullscreenChanged(float topControlsOffsetYPix,
+ float contentOffsetYPix,
+ float overdrawBottomHeightPix) {
+ if (mWebViewController instanceof Controller) {
+ Controller controller = (Controller)mWebViewController;
+ controller.getUi().translateTitleBar(topControlsOffsetYPix);
+ // Resize the viewport if top controls is not visible
+ if (mMainView != null &&
+ (topControlsOffsetYPix == 0.0f || contentOffsetYPix == 0.0f))
+ ((BrowserWebView)mMainView).enableTopControls(
+ (topControlsOffsetYPix == 0.0f) ? true : false);
+ }
+ }
+
+ @Override
+ public boolean isTabFullScreen() {
+ return mFullScreen;
+ }
+
+ @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()) {
+ WebView.WebViewTransport transport =
+ (WebView.WebViewTransport) resultMsg.obj;
+ CreateWindowParams windowParams = transport.getCreateWindowParams();
+ if (windowParams.mOpenerSuppressed) {
+ createWindow(dialog, resultMsg, windowParams.mURL, true);
+ // This is special case for rendering links on a webpage in
+ // a new tab. If opener is suppressed, the WebContents created
+ // by the content layer are not fully initialized. Returning false
+ // will prevent content layer from overriding WebContents
+ // created by new tab with the uninitialized instance.
+ return false;
+ }
+
+ createWindow(dialog, resultMsg);
+ return true;
+ }
+
+ createWindow(dialog, resultMsg);
+ 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) {
+ 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);
+ }
+ mTouchIconUrl = 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);
+ }
+
+ /* 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) {
+ // 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);
+ }
+ }
+
+ @Override
+ public void showFileChooser(ValueCallback<String[]> uploadFilePaths, String acceptTypes,
+ boolean capture) {
+ if (mInForeground) {
+ mWebViewController.showFileChooser(uploadFilePaths, acceptTypes, capture);
+ } else {
+ uploadFilePaths.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 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);
+ setTimeStamp();
+ mInPageLoad = false;
+ mInForeground = false;
+ mWebViewDestroyedByMemoryMonitor = 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);
+ }
+ };
+
+ mCaptureWidth = mContext.getResources().getDimensionPixelSize(R.dimen.tab_thumbnail_width);
+ mCaptureHeight =mContext.getResources().getDimensionPixelSize(R.dimen.tab_thumbnail_height);
+
+ initCaptureBitmap();
+
+ restoreState(state);
+ if (getId() == -1) {
+ mId = TabControl.getNextId();
+ }
+ setWebView(w);
+
+ UI ui = ((Controller)mWebViewController).getUi();
+ if (ui instanceof BaseUi) {
+ TitleBar titleBar = ((BaseUi)ui).getTitleBar();
+ if (titleBar != null) {
+ NavigationBarBase navBar = titleBar.getNavigationBar();
+ navBar.showCurrentFavicon(this); // Show the default Favicon while loading a new page
+ }
+ }
+
+ mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message m) {
+ switch (m.what) {
+ case MSG_CAPTURE:
+ capture();
+ break;
+ }
+ }
+ };
+
+ mFirstPixelObservable = new Observable();
+ mFirstPixelObservable.set(false);
+ mTabHistoryUpdateObservable = new Observable();
+ }
+
+ public void initCaptureBitmap() {
+ mCapture = Bitmap.createBitmap(mCaptureWidth, mCaptureHeight, Bitmap.Config.RGB_565);
+ mCapture.eraseColor(Color.WHITE);
+ }
+
+ /**
+ * 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 setController(WebViewController ctl) {
+ mWebViewController = ctl;
+
+ if (mWebViewController.shouldCaptureThumbnails()) {
+ synchronized (Tab.this) {
+ if (mCapture == null) {
+ initCaptureBitmap();
+ if (mInForeground && !mHandler.hasMessages(MSG_CAPTURE)) {
+ mHandler.sendEmptyMessageDelayed(MSG_CAPTURE, CAPTURE_DELAY);
+ }
+ }
+ }
+ } else {
+ synchronized (Tab.this) {
+ mCapture = null;
+ deleteThumbnail();
+ }
+ }
+ }
+
+ 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;
+ }
+
+ mWebViewController.onSetWebView(this, w);
+
+ if (mMainView != null) {
+ mMainView.setPictureListener(null);
+ if (w != null) {
+ syncCurrentState(w, null);
+ } else {
+ mCurrentState = new PageState(mContext, mMainView.isPrivateBrowsingEnabled());
+
+ if (mWebViewDestroyedByMemoryMonitor) {
+ /*
+ * If tab was destroyed as a result of the MemoryMonitor
+ * then we need to restore the state properties
+ * from the old WebView (mMainView)
+ */
+ syncCurrentState(mMainView, null);
+ mWebViewDestroyedByMemoryMonitor = false;
+ }
+ }
+ }
+ // 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);
+ 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;
+ }
+ }
+ }
+
+ public void destroyThroughMemoryMonitor() {
+ mWebViewDestroyedByMemoryMonitor = true;
+ destroy();
+ }
+
+ /**
+ * Destroy the tab's main WebView and subWindow if any
+ */
+ void destroy() {
+
+ if (mPostponeDestroy) {
+ mShouldDestroy = true;
+ return;
+ }
+ mShouldDestroy = false;
+ if (mMainView != null) {
+ dismissSubWindow();
+ // save the WebView to call destroy() after detach it from the tab
+ final WebView webView = mMainView;
+ setWebView(null);
+ if (!mWebViewDestroyedByMemoryMonitor && !BaseUi.isUiLowPowerMode()) {
+ // Tabs can be reused with new instance of WebView so delete the snapshots
+ webView.getSnapshotIds(new ValueCallback<List<Integer>>() {
+ @Override
+ public void onReceiveValue(List<Integer> ids) {
+ int currentTabIdx = mWebViewController.getTabControl().getCurrentPosition();
+ for (Integer id : ids) {
+ if (getTabIdxFromCaptureIdx(id) == currentTabIdx) {
+ webView.deleteSnapshot(id);
+ }
+ }
+ webView.destroy();
+ }
+ });
+ } else {
+ webView.destroy();
+ }
+ }
+ }
+
+ private boolean mPostponeDestroy = false;
+ private boolean mShouldDestroy = false;
+
+ public void postponeDestroy() {
+ mPostponeDestroy = true;
+ }
+
+ public void performPostponedDestroy() {
+ mPostponeDestroy = false;
+ if (mShouldDestroy) {
+ 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);
+ }
+
+ mCapture = null;
+ 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);
+ }
+
+ mWebViewController.bookmarkedStatusHasChanged(this);
+ }
+
+ void putInBackground() {
+ if (!mInForeground) {
+ return;
+ }
+ 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 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;
+ }
+
+ boolean getDerivedFromIntent() {
+ return mDerivedFromIntent;
+ }
+
+ void setDerivedFromIntent(boolean derived) {
+ mDerivedFromIntent = derived;
+ }
+
+ String getUrl() {
+ return UrlUtils.filteredUrl(mCurrentState.mUrl);
+ }
+
+
+ protected void onPageFinished() {
+ mPageFinished = true;
+ isDistillable();
+ }
+
+ public boolean getPageFinishedStatus() {
+ return mPageFinished;
+ }
+
+ String getOriginalUrl() {
+ if (mCurrentState.mOriginalUrl == null) {
+ return getUrl();
+ }
+ return UrlUtils.filteredUrl(mCurrentState.mOriginalUrl);
+ }
+
+ /**
+ * Get the title of this tab.
+ */
+ String getTitle() {
+ return mCurrentState.mTitle;
+ }
+
+ /**
+ * Get the favicon of this tab.
+ */
+ Bitmap getFavicon() {
+ if (mCurrentState.mFavicon != null) {
+ return mCurrentState.mFavicon;
+ }
+ return getDefaultFavicon(mContext);
+ }
+
+ public boolean hasFavicon() {
+ return mCurrentState.mFavicon != null;
+ }
+
+ public boolean isBookmarkedSite() {
+ return mCurrentState.mIsBookmarkedSite;
+ }
+
+ /**
+ * Sets the security state, clears the SSL certificate error and informs
+ * the controller.
+ */
+ private void setSecurityState(SecurityState securityState) {
+ mCurrentState.mSecurityState = securityState;
+ mWebViewController.onUpdatedSecurityState(this);
+ }
+
+ /**
+ * @return The tab's security state.
+ */
+ SecurityState getSecurityState() {
+ return mCurrentState.mSecurityState;
+ }
+
+ 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);
+ 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(Bitmap bm) {
+ 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()));
+ values.put(Snapshots.THUMBNAIL, compressBitmap(bm));
+ 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();
+ // could be that saving of file failed
+ if (path == null) {
+ return false;
+ }
+
+ 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;
+ mCurrentState = new PageState(
+ mContext, mMainView.isPrivateBrowsingEnabled(), url);
+ mMainView.loadUrl(url, headers);
+ }
+ }
+
+ public void disableUrlOverridingForLoad() {
+ mDisableOverrideUrlLoading = true;
+ }
+
+ private void thumbnailUpdated() {
+ mHandler.removeMessages(MSG_CAPTURE);
+
+ TabControl tc = mWebViewController.getTabControl();
+ if (tc != null) {
+ OnThumbnailUpdatedListener updateListener = tc.getOnThumbnailUpdatedListener();
+ if (updateListener != null) {
+ updateListener.onThumbnailUpdated(this);
+ }
+ }
+ }
+
+ protected void capture() {
+ if (mMainView == null || mCapture == null || !mMainView.isReady() ||
+ mMainView.getContentWidth() <= 0 || mMainView.getContentHeight() <= 0 ||
+ !mFirstVisualPixelPainted || mMainView.isShowingCrashView()) {
+
+ initCaptureBitmap();
+ thumbnailUpdated();
+ return;
+ }
+
+ mMainView.getContentBitmapAsync((float) mCaptureWidth / mMainView.getWidth(), new Rect(),
+ new ValueCallback<Bitmap>() {
+ @Override
+ public void onReceiveValue(Bitmap bitmap) {
+ if (mCapture == null) {
+ initCaptureBitmap();
+ }
+
+ if (bitmap == null) {
+ thumbnailUpdated();
+ return;
+ }
+
+ Canvas c = new Canvas(mCapture);
+ mCapture.eraseColor(Color.WHITE);
+ c.drawBitmap(bitmap, 0, 0, null);
+
+ // 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);
+
+ persistThumbnail();
+ thumbnailUpdated();
+ }
+ }
+ );
+ }
+
+ @Override
+ public void onNewPicture(WebView view, Picture picture) {
+ }
+
+ 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();
+ }
+ }
+
+ 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();
+ }
+
+ // dertermines if the tab contains a dislled page
+ public boolean isDistilled() {
+ if (!BrowserCommandLine.hasSwitch("reader-mode")) {
+ return false;
+ }
+ try {
+ return DomDistillerUtils.isUrlDistilled(getUrl());
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ //determines if the tab contains a distillable page
+ public boolean isDistillable() {
+ if (!BrowserCommandLine.hasSwitch("reader-mode")) {
+ mIsDistillable = false;
+ return mIsDistillable;
+ }
+ final ValueCallback<String> onIsDistillable = new ValueCallback<String>() {
+ @Override
+ public void onReceiveValue(String str) {
+ mIsDistillable = Boolean.parseBoolean(str);
+ }
+ };
+
+ if (isDistilled()) {
+ mIsDistillable = true;
+ return mIsDistillable;
+ }
+
+ try {
+ DomDistillerUtils.isWebViewDistillable(getWebView(), onIsDistillable);
+ } catch (Exception e) {
+ mIsDistillable = false;
+ }
+
+ return mIsDistillable;
+ }
+
+ // Function that sets the mIsDistillable variable
+ public void setIsDistillable(boolean value) {
+ if (!BrowserCommandLine.hasSwitch("reader-mode")) {
+ mIsDistillable = false;
+ }
+ mIsDistillable = value;
+ }
+
+ // Function that returns the distilled url of the current url
+ public String getDistilledUrl() {
+ if (getUrl() != null) {
+ return DomDistillerUtils.getDistilledUrl(getUrl());
+ }
+ return new String();
+ }
+
+ // function that returns the non-distilled version of the current url
+ public String getNonDistilledUrl() {
+ if (getUrl() != null) {
+ return DomDistillerUtils.getOriginalUrlFromDistilledUrl(getUrl());
+ }
+ return new String();
+ }
+}
diff --git a/src/src/com/android/browser/TabBar.java b/src/src/com/android/browser/TabBar.java
new file mode 100644
index 00000000..378557e2
--- /dev/null
+++ b/src/src/com/android/browser/TabBar.java
@@ -0,0 +1,515 @@
+/*
+ * 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;
+
+/**
+ * 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;
+
+ 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();
+ }
+
+ 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
+ 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;
+ mButtonWidth = mNewTab.getMeasuredWidth() - mAddTabOverlap;
+ if (w-sw < mButtonWidth) {
+ sw = w - mButtonWidth;
+ }
+ mTabs.layout(pl, pt, pl + sw, bottom - top);
+ // adjust for overlap
+ 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 (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 mFaviconView;
+ 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);
+ mFaviconView = (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);
+ mFaviconView.setVisibility((mSelected || mTab.isSnapshot()) ?
+ 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) {
+ mFaviconView.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 = Bitmap.createBitmap(
+ mCurrentTextureWidth, mCurrentTextureHeight, Bitmap.Config.ARGB_8888);
+ activeTexture.eraseColor(getResources().
+ getColor(R.color.NavigationBarBackground));
+ Bitmap inactiveTexture = Bitmap.createBitmap(
+ mCurrentTextureWidth, mCurrentTextureHeight, Bitmap.Config.ARGB_8888);
+ inactiveTexture.eraseColor(getResources().
+ getColor(R.color.TabNavBackgroundColor));
+
+ 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);
+ }
+
+ @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) {
+ }
+
+ @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/src/com/android/browser/TabControl.java b/src/src/com/android/browser/TabControl.java
new file mode 100644
index 00000000..81da6966
--- /dev/null
+++ b/src/src/com/android/browser/TabControl.java
@@ -0,0 +1,772 @@
+/*
+ * 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.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+import org.codeaurora.swe.GeolocationPermissions;
+import org.codeaurora.swe.WebView;
+import org.codeaurora.swe.util.Observable;
+
+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";
+
+
+ /*
+ Find and reload any live tabs that have loaded the given URL.
+ Note - Upto 2 tabs are live at any given moment.
+ */
+ public void findAndReload(String origin) {
+ for (Tab tab : mTabs){
+ if (tab.getWebView() != null) {
+ Uri url = Uri.parse(tab.getWebView().getUrl());
+ if (url.getHost().equals(origin)){
+ tab.getWebView().reload();
+ }
+ }
+ }
+ }
+
+ // Reload the all the live tabs
+ public void reloadLiveTabs() {
+ for (Tab tab : mTabs) {
+ if (tab.getWebView() != null) {
+ tab.getWebView().reload();
+ }
+ }
+ }
+
+ 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;
+ // number of incognito tabs
+ private int mNumIncognito = 0;
+
+ private OnThumbnailUpdatedListener mOnThumbnailUpdatedListener;
+
+ private Observable mTabCountObservable;
+
+ /**
+ * Construct a new TabControl object
+ */
+ TabControl(Controller controller) {
+ mController = controller;
+ mMaxTabs = mController.getMaxTabs();
+ mTabs = new ArrayList<Tab>(mMaxTabs);
+ mTabQueue = new ArrayList<Tab>(mMaxTabs);
+ mTabCountObservable = new Observable();
+ mTabCountObservable.set(0);
+ }
+
+ synchronized static long getNextId() {
+ return sNextId++;
+ }
+
+ Observable getTabCountObservable() {
+ return mTabCountObservable;
+ }
+
+ /**
+ * 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);
+ mTabCountObservable.set(mTabs.size());
+ 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, false);
+ }
+
+ Tab createNewTab(boolean privateBrowsing, boolean backgroundTab) {
+ return createNewTab(null, privateBrowsing, backgroundTab);
+ }
+
+ Tab createNewTab(Bundle state, boolean privateBrowsing) {
+ return createNewTab(state, privateBrowsing, false);
+ }
+
+ Tab createNewTab(Bundle state, boolean privateBrowsing, boolean backgroundTab) {
+ int size = mTabs.size();
+ // Return false if we have maxed out on tabs
+ if (!canCreateNewTab()) {
+ return null;
+ }
+
+ final WebView w = createNewWebView(privateBrowsing, backgroundTab);
+
+ // Create a new tab and add it to the tab list
+ Tab t = new Tab(mController, w, state);
+ mTabs.add(t);
+ mTabCountObservable.set(mTabs.size());
+ if (privateBrowsing) {
+ mNumIncognito += 1;
+ }
+ // 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, Bundle state) {
+ SnapshotTab t = new SnapshotTab(mController, snapshotId, state);
+ mTabs.add(t);
+ mTabCountObservable.set(mTabs.size());
+ 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);
+ mTabCountObservable.set(mTabs.size());
+
+ //Clear incognito geolocation state if this is the last incognito tab.
+ if (t.isPrivateBrowsingEnabled()) {
+ mNumIncognito -= 1;
+ if (mNumIncognito == 0) {
+ GeolocationPermissions.onIncognitoTabsRemoved();
+ }
+ }
+
+ // 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.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 = null;
+ // Add special check to restore Snapshot Tab if needed
+ if (state.getLong(SnapshotTab.SNAPSHOT_ID, -1) != -1 ) {
+ t = (SnapshotTab) createSnapshotTab( state.getLong(SnapshotTab.SNAPSHOT_ID), state);
+ } else {
+ // presume its a normal Tab
+ t = createNewTab(state, false);
+ }
+
+ if (t == null) {
+ // We could "break" at this point, but we want
+ // sNextId to be set correctly.
+ continue;
+ }
+ 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);
+ mTabCountObservable.set(mTabs.size());
+ // 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 createNewWebView(privateBrowsing, false);
+ }
+
+ private WebView createNewWebView(boolean privateBrowsing, boolean backgroundTab) {
+ return mController.getWebViewFactory().createWebView(privateBrowsing, backgroundTab);
+ }
+
+ /**
+ * 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/src/com/android/browser/TabScrollView.java b/src/src/com/android/browser/TabScrollView.java
new file mode 100644
index 00000000..1df88cc8
--- /dev/null
+++ b/src/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/src/com/android/browser/TitleBar.java b/src/src/com/android/browser/TitleBar.java
new file mode 100644
index 00000000..fd19a414
--- /dev/null
+++ b/src/src/com/android/browser/TitleBar.java
@@ -0,0 +1,329 @@
+/*
+ * 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.content.res.Resources;
+import android.graphics.Rect;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityManager;
+
+import org.codeaurora.swe.BrowserCommandLine;
+import org.codeaurora.swe.WebView;
+
+import android.widget.FrameLayout;
+
+/**
+ * Base class for a title bar used by the browser.
+ */
+public class TitleBar extends FrameLayout implements ViewTreeObserver.OnPreDrawListener {
+
+ 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 NavigationBarBase mNavBar;
+ private SnapshotBar mSnapshotBar;
+
+ //state
+ private boolean mShowing;
+ private boolean mInLoad;
+ private boolean mIsFixedTitleBar;
+ private float mCurrentTranslationY;
+ private boolean mUpdateTranslationY = false;
+
+ 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 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);
+ mBaseUi.setContentViewMarginTop(0);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ mCurrentTranslationY = this.getTranslationY();
+ if (mCurrentTranslationY < 0) {
+ mUpdateTranslationY = true;
+ this.setTranslationY(0);
+
+ final ViewTreeObserver observer = this.getViewTreeObserver();
+ observer.addOnPreDrawListener(this);
+ }
+ }
+
+ @Override
+ public boolean onPreDraw() {
+ if (mUpdateTranslationY) {
+ this.setTranslationY(mCurrentTranslationY);
+ mUpdateTranslationY = false;
+ }
+ final ViewTreeObserver observer = this.getViewTreeObserver();
+ observer.removeOnPreDrawListener(this);
+ return true;
+ }
+
+ private void setFixedTitleBar() {
+ boolean isFixed = !getContext().getResources().getBoolean(R.bool.hide_title) ||
+ BrowserCommandLine.hasSwitch(BrowserSwitches.DISABLE_TOP_CONTROLS);
+
+ isFixed |= mAccessibilityManager.isEnabled() &&
+ mAccessibilityManager.isTouchExplorationEnabled();
+ // If getParent() returns null, we are initializing
+ ViewGroup parent = (ViewGroup)getParent();
+ if (mIsFixedTitleBar == isFixed && parent != null) return;
+ mIsFixedTitleBar = isFixed;
+ showTopControls(false);
+ if (parent != null) {
+ parent.removeView(this);
+ }
+ mContentView.addView(this, makeLayoutParams());
+ mBaseUi.setContentViewMarginTop(0);
+ }
+
+ public BaseUi getUi() {
+ return mBaseUi;
+ }
+
+ public UiController getUiController() {
+ return mUiController;
+ }
+
+ void setShowProgressOnly(boolean progress) {
+ if (progress && !wantsToBeVisible()) {
+ mNavBar.setVisibility(View.GONE);
+ } else {
+ mNavBar.setVisibility(View.VISIBLE);
+ }
+ }
+
+ boolean isShowing() {
+ return mShowing;
+ }
+
+ private int getVisibleTitleHeight() {
+ Tab tab = mBaseUi.getActiveTab();
+ WebView webview = tab != null ? tab.getWebView() : null;
+ return webview != null ? webview.getVisibleTitleHeight() : 0;
+ }
+
+ protected void hideTopControls(boolean animate) {
+ if (mIsFixedTitleBar)
+ return;
+ Tab tab = mBaseUi.getActiveTab();
+ WebView view = tab != null ? tab.getWebView() : null;
+ if (view != null) {
+ view.updateTopControls(true, false, animate);
+ }
+ mShowing = false;
+ }
+
+ protected void showTopControls(boolean animate) {
+ Tab tab = mBaseUi.getActiveTab();
+ WebView view = tab != null ? tab.getWebView() : null;
+ if (view != null) {
+ view.updateTopControls(false, true, animate);
+ }
+ mShowing = true;
+ }
+
+ protected void enableTopControls(boolean animate) {
+ if (mIsFixedTitleBar || mNavBar.getTrustLevel() == SiteTileView.TRUST_AVOID
+ || mNavBar.getTrustLevel() == SiteTileView.TRUST_UNTRUSTED)
+ return;
+ Tab tab = mBaseUi.getActiveTab();
+ WebView view = tab != null ? tab.getWebView() : null;
+ if (view != null)
+ view.updateTopControls(true, true, animate);
+ }
+
+
+ /**
+ * 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();
+ //onPageFinished
+ enableTopControls(true);
+
+ } else {
+ if (!mInLoad) {
+ mProgress.setVisibility(View.VISIBLE);
+ mInLoad = true;
+ mNavBar.onProgressStarted();
+ mProgress.onProgressStarted();
+ //onPageStarted
+ }
+ mProgress.setProgress(newProgress * PageProgressView.MAX_PROGRESS
+ / PROGRESS_MAX);
+ showTopControls(false);
+ }
+ }
+
+ public int getEmbeddedHeight() {
+ if (mIsFixedTitleBar) return 0;
+ return calculateEmbeddedHeight();
+ }
+
+ public boolean isFixed() {
+ return mIsFixedTitleBar;
+ }
+
+ int calculateEmbeddedHeight() {
+ int height = mNavBar.getHeight();
+ return height;
+ }
+
+ public boolean wantsToBeVisible() {
+ return (mSnapshotBar != null && mSnapshotBar.getVisibility() == View.VISIBLE
+ && mSnapshotBar.isAnimating());
+ }
+
+ 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 isInLoad() {
+ return mInLoad;
+ }
+
+ private ViewGroup.LayoutParams makeLayoutParams() {
+ return new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
+ LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
+ return mBaseUi.isCustomViewShowing() ? false :
+ super.requestFocus(direction, previouslyFocusedRect);
+ }
+
+ @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() || tab.isDistilled()) {
+ inflateSnapshotBar();
+ mSnapshotBar.setVisibility(VISIBLE);
+ mNavBar.setVisibility(GONE);
+ if (tab.isDistilled()) {
+ mSnapshotBar.setTitle(tab.getWebView().getTitle());
+ mSnapshotBar.setDate(tab.getNonDistilledUrl());
+ mSnapshotBar.setSnapshoticonVisibility(View.GONE);
+ mSnapshotBar.setFaviconVisibility(View.GONE);
+ mSnapshotBar.setReadericonVisibility(View.VISIBLE);
+ } else {
+ mSnapshotBar.setSnapshoticonVisibility(View.VISIBLE);
+ mSnapshotBar.setFaviconVisibility(View.GONE); // Snapshot Tabs don't have a Favicon
+ mSnapshotBar.setReadericonVisibility(View.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/src/com/android/browser/UI.java b/src/src/com/android/browser/UI.java
new file mode 100644
index 00000000..1f881b12
--- /dev/null
+++ b/src/src/com/android/browser/UI.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.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 cancelNavScreenRequest();
+
+ 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 hideComboView();
+
+ 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);
+
+ // returns if the web page is clear of any overlays (not including sub windows)
+ public boolean isWebShowing();
+
+ public boolean isComboViewShowing();
+
+ 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 setFullscreen(boolean enabled);
+ void showFullscreen(boolean show);
+
+ void translateTitleBar(float topControlsOffsetYPix);
+
+ public boolean shouldCaptureThumbnails();
+
+ boolean blockFocusAnimations();
+
+ void onVoiceResult(String result);
+}
diff --git a/src/src/com/android/browser/UiController.java b/src/src/com/android/browser/UiController.java
new file mode 100644
index 00000000..cf989e8e
--- /dev/null
+++ b/src/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);
+
+ 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();
+
+ void setWindowDimming(float level);
+
+}
diff --git a/src/src/com/android/browser/UpdateNotificationService.java b/src/src/com/android/browser/UpdateNotificationService.java
new file mode 100644
index 00000000..5ecb6613
--- /dev/null
+++ b/src/src/com/android/browser/UpdateNotificationService.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (c) 2015, 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.IntentService;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.TaskStackBuilder;
+import android.content.Intent;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.support.v4.app.NotificationCompat;
+
+
+import org.codeaurora.swe.BrowserCommandLine;
+import org.codeaurora.swe.Engine;
+import org.codeaurora.swe.utils.Logger;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.net.URLConnection;
+
+public class UpdateNotificationService extends IntentService {
+ private static final String LOGTAG = "UpdateNotificationService";
+ private static final String ACTION_CHECK_UPDATES = BrowserConfig.AUTHORITY +
+ ".action.check.update";
+ public static final int DEFAULT_UPDATE_INTERVAL = 172800000; // two days
+ public static final String UPDATE_SERVICE_PREF = "browser_update_service";
+ public static final String UPDATE_JSON_VERSION_CODE = "versioncode";
+ public static final String UPDATE_JSON_VERSION_STRING = "versionstring";
+ public static final String UPDATE_JSON_MIN_INTERVAL = "interval";
+ public static final String UPDATE_INTERVAL = "update_interval";
+ public static final String UPDATE_VERSION_CODE = "version_code";
+ public static final String UPDATE_VERSION = "update_version";
+ public static final String UPDATE_URL = "update_url";
+ public static final String UPDATE_TIMESTAMP = "update_timestamp";
+ private static int NOTIFICATION_ID = 1000;
+ private static boolean sIntentServiceInitialized = false;
+ private static boolean sNotifyAlways = false;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ initEngine(this);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (sIntentServiceInitialized)
+ Engine.pauseTracing(this);
+ }
+
+ private static void initEngine(Context context) {
+ if (!EngineInitializer.isInitialized()) {
+ sIntentServiceInitialized = true;
+ EngineInitializer.initializeSync((Context) context);
+ }
+ }
+
+ public static void startActionUpdateNotificationService(Context context) {
+ Intent intent = new Intent(context, UpdateNotificationService.class);
+ intent.setAction(ACTION_CHECK_UPDATES);
+ context.startService(intent);
+ }
+
+ public static String getFlavor(Context ctx) {
+ String flavor = "";
+ try {
+ ApplicationInfo ai = ctx.getPackageManager().getApplicationInfo(
+ ctx.getPackageName(),PackageManager.GET_META_DATA);
+ String compiler = (String) ai.metaData.get("Compiler");
+ String arch = (String) ai.metaData.get("Architecture");
+ flavor = "url-" + compiler + "-" + arch;
+ } catch (Exception e) {
+ Logger.e(LOGTAG, "getFlavor Exception : " + e.toString());
+ }
+ return flavor;
+ }
+
+ public static void updateCheck(Context context) {
+ initEngine(context.getApplicationContext());
+ if (!BrowserCommandLine.hasSwitch(BrowserSwitches.AUTO_UPDATE_SERVER_CMD)) {
+ Logger.v(LOGTAG, "skip no command line: ");
+ return;
+ }
+ long interval = getInterval(context);
+ Long last_update_time = getLastUpdateTimestamp(context);
+ if ((last_update_time + interval) < System.currentTimeMillis()) {
+ Logger.v(LOGTAG, "check for update now: ");
+ startActionUpdateNotificationService(context);
+ }
+ }
+
+ public static int getLatestVersionCode(Context ctx) {
+ SharedPreferences sharedPref = ctx.getSharedPreferences(
+ UPDATE_SERVICE_PREF, Context.MODE_PRIVATE);
+ return sharedPref.getInt(UPDATE_VERSION_CODE, 0);
+ }
+
+ public static String getLatestDownloadUrl(Context ctx) {
+ SharedPreferences sharedPref = ctx.getSharedPreferences(
+ UPDATE_SERVICE_PREF, Context.MODE_PRIVATE);
+ return sharedPref.getString(UPDATE_URL,"");
+ }
+
+ public static String getLatestVersion(Context ctx) {
+ SharedPreferences sharedPref = ctx.getSharedPreferences(
+ UPDATE_SERVICE_PREF, Context.MODE_PRIVATE);
+ return sharedPref.getString(UPDATE_VERSION, "");
+ }
+
+ private static long getLastUpdateTimestamp(Context ctx) {
+ SharedPreferences sharedPref = ctx.getSharedPreferences(
+ UPDATE_SERVICE_PREF, Context.MODE_PRIVATE);
+ return sharedPref.getLong(UPDATE_TIMESTAMP, 0);
+ }
+
+ private static int getInterval(Context ctx) {
+ SharedPreferences sharedPref = ctx.getSharedPreferences(
+ UPDATE_SERVICE_PREF, Context.MODE_PRIVATE);
+ return sharedPref.getInt(UPDATE_INTERVAL, DEFAULT_UPDATE_INTERVAL);
+ }
+
+ public UpdateNotificationService() {
+ super("UpdateNotificationService");
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ if (intent != null) {
+ final String action = intent.getAction();
+ if (ACTION_CHECK_UPDATES.equals(action)) {
+ handleUpdateCheck();
+ }
+ }
+ }
+
+ private void updateTimeStamp() {
+ SharedPreferences sharedPref = getSharedPreferences(
+ UPDATE_SERVICE_PREF, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.putLong(UPDATE_TIMESTAMP, System.currentTimeMillis());
+ editor.commit();
+ }
+
+ private void persist(int versionCode, String url, String version, int interval) {
+ SharedPreferences sharedPref = getSharedPreferences(
+ UPDATE_SERVICE_PREF, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.putInt(UPDATE_VERSION_CODE, versionCode);
+ editor.putInt(UPDATE_INTERVAL, interval);
+ editor.putString(UPDATE_VERSION, version);
+ editor.putString(UPDATE_URL, url);
+ Logger.v(LOGTAG, "persist version code : " + versionCode);
+ Logger.v(LOGTAG, "persist version : " + version);
+ Logger.v(LOGTAG, "persist download url : " + url);
+ editor.commit();
+ }
+
+ private void handleUpdateCheck() {
+ String server_url = BrowserCommandLine.getSwitchValue(
+ BrowserSwitches.AUTO_UPDATE_SERVER_CMD);
+ int interval = DEFAULT_UPDATE_INTERVAL;
+ InputStream stream = null;
+ if (server_url != null && !server_url.isEmpty()) {
+ try {
+ URLConnection connection = new URL(server_url).openConnection();
+ stream = connection.getInputStream();
+ String result = readContents(stream);
+ Logger.v(LOGTAG, "handleUpdateCheck result : " + result);
+ JSONObject jsonResult = (JSONObject) new JSONTokener(result).nextValue();
+ int versionCode = Integer.parseInt((String) jsonResult.get(UPDATE_JSON_VERSION_CODE));
+ String url = (String) jsonResult.get(getFlavor(this));
+ String version = (String) jsonResult.get(UPDATE_JSON_VERSION_STRING);
+ if (jsonResult.has(UPDATE_JSON_MIN_INTERVAL))
+ interval = Integer.parseInt((String) jsonResult.get(UPDATE_JSON_MIN_INTERVAL));
+ if (getCurrentVersionCode(this) < versionCode &&
+ (sNotifyAlways || getLatestVersionCode(this) != versionCode)) {
+ persist(versionCode, url, version, interval);
+ // notify only once per version change
+ showNotification(this, url, version);
+ }
+ stream.close();
+ } catch (Exception e) {
+ Logger.e(LOGTAG, "handleUpdateCheck Exception : " + e.toString());
+ } finally {
+ // always update the timestamp
+ updateTimeStamp();
+ }
+ }
+ }
+
+ public static int getCurrentVersionCode(Context ctx) {
+ PackageInfo pInfo = null;
+ try {
+ pInfo = ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ Logger.e(LOGTAG, "getCurrentVersionCode Exception : " + e.toString());
+ }
+ return pInfo.versionCode;
+ }
+
+ private static void showNotification(Context ctx, String url, String version) {
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(ctx)
+ .setSmallIcon(R.drawable.img_notify_update_white)
+ .setContentTitle(ctx.getString(R.string.update))
+ .setContentText(ctx.getString(R.string.update_msg) + version);
+ Intent resultIntent = new Intent(ctx, BrowserActivity.class);
+ resultIntent.setAction(Intent.ACTION_VIEW);
+ resultIntent.setData(Uri.parse(url));
+ TaskStackBuilder stackBuilder = TaskStackBuilder.create(ctx);
+ stackBuilder.addParentStack(BrowserActivity.class);
+ stackBuilder.addNextIntent(resultIntent);
+ PendingIntent resultPendingIntent =
+ stackBuilder.getPendingIntent(
+ 0,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ );
+ builder.setContentIntent(resultPendingIntent);
+ NotificationManager mNotificationManager =
+ (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
+ Notification notification = builder.build();
+ notification.flags = Notification.DEFAULT_LIGHTS | Notification.FLAG_AUTO_CANCEL;
+ mNotificationManager.notify(NOTIFICATION_ID, notification);
+ }
+
+ private static void removeNotification(Context ctx) {
+ NotificationManager mNotificationManager =
+ (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
+ mNotificationManager.cancel(NOTIFICATION_ID);
+ }
+
+ private static String readContents(InputStream is) {
+ String line = null;
+ BufferedReader reader = new BufferedReader(new InputStreamReader(is));
+ StringBuilder sb = new StringBuilder();
+ try {
+ line = reader.readLine();
+ while (line != null) {
+ line = line.replaceFirst("channel = ","");
+ sb.append(line + "\n");
+ line = reader.readLine();
+ }
+ } catch (Exception e) {
+ Logger.e(LOGTAG, "convertStreamToString Exception : " + e.toString());
+ } finally {
+ try {
+ is.close();
+ } catch (Exception e) {
+ Logger.e(LOGTAG, "convertStreamToString Exception : " + e.toString());
+ }
+ }
+ return sb.toString();
+ }
+
+}
diff --git a/src/src/com/android/browser/UploadDialog.java b/src/src/com/android/browser/UploadDialog.java
new file mode 100644
index 00000000..7505ef71
--- /dev/null
+++ b/src/src/com/android/browser/UploadDialog.java
@@ -0,0 +1,91 @@
+/*
+ * 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;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ResolveInfo;
+import android.content.pm.PackageManager;
+
+import java.util.List;
+
+public class UploadDialog extends AppItem {
+ public List<ResolveInfo> apps;
+ private Activity activity;
+ private List<Intent> uploadIntents;
+
+ public UploadDialog(Activity activity) {
+ super(null);
+ this.activity = activity;
+ this.apps = null;
+ }
+
+ public void getUploadableApps(List<ResolveInfo> apps, List<Intent> intents) {
+ this.apps = apps;
+ this.uploadIntents = intents;
+ }
+
+ public void loadView(final UploadHandler uploadHandler) {
+
+ final AppAdapter adapter = new AppAdapter(activity, activity.getPackageManager(),
+ R.layout.app_row, this.apps);
+
+ AlertDialog.Builder builderSingle = new AlertDialog.Builder(activity);
+ builderSingle.setIcon(R.mipmap.ic_launcher_browser);
+ builderSingle.setTitle(activity.getString(R.string.choose_upload));
+
+ builderSingle.setAdapter(adapter, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int position) {
+ dialog.dismiss();
+ uploadHandler.initiateActivity(uploadIntents.get(position));
+ }
+ });
+
+ builderSingle.setOnCancelListener(new DialogInterface.OnCancelListener()
+ {
+ @Override
+ public void onCancel(DialogInterface dialog)
+ {
+ uploadHandler.setHandled(false);
+ dialog.dismiss();
+ }
+ });
+
+ builderSingle.show();
+ }
+}
diff --git a/src/src/com/android/browser/UploadHandler.java b/src/src/com/android/browser/UploadHandler.java
new file mode 100644
index 00000000..fb747636
--- /dev/null
+++ b/src/src/com/android/browser/UploadHandler.java
@@ -0,0 +1,584 @@
+/*
+ * 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.ComponentName;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ActivityInfo;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.webkit.ValueCallback;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.android.browser.R;
+import com.android.browser.reflect.ReflectHelper;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Handle the file upload callbacks from WebView here
+ */
+public class UploadHandler {
+
+ private static final String TAG = "UploadHandler";
+ /*
+ * The Object used to inform the WebView of the file to upload.
+ */
+ private ValueCallback<Uri> mUploadMessage;
+ private ValueCallback<String[]> mUploadFilePaths;
+ private String mCameraFilePath;
+
+ private boolean mHandled;
+ private boolean mCaughtActivityNotFoundException;
+
+ private Controller mController;
+
+ public UploadHandler(Controller controller) {
+ mController = controller;
+ }
+
+ String getFilePath() {
+ return mCameraFilePath;
+ }
+
+ protected boolean handled() {
+ return mHandled;
+ }
+
+ protected void setHandled(boolean handled) {
+ mHandled = handled;
+ mCaughtActivityNotFoundException = false;
+ // If upload dialog shown to the user got dismissed
+ if (!mHandled) {
+ mUploadFilePaths.onReceiveValue(null);
+ }
+ mUploadFilePaths = null;
+ }
+
+ 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));
+ }
+ }
+
+ boolean hasGoodFilePath = false;
+ String filePath = null;
+ if (result != null) {
+ String scheme = result.getScheme();
+ // try to get local file path from uri
+ if ("file".equals(scheme)) {
+ filePath = result.getPath();
+ hasGoodFilePath = filePath != null && !filePath.isEmpty();
+ } else if ("content".equals(scheme)) {
+ filePath = getFilePath(mController.getContext(), result);
+ hasGoodFilePath = filePath != null && !filePath.isEmpty();
+ }
+
+ // The native layer only accepts path based on file scheme
+ // and skips anything else passed to it
+ filePath = "file://"+filePath;
+ }
+
+ // Add for carrier feature - prevent uploading DRM type files based on file extension. This
+ // is not a secure implementation since malicious users can trivially modify the filename.
+ // DRM files can be securely detected by inspecting their integrity protected content.
+ boolean drmUploadEnabled = BrowserConfig.getInstance(mController.getContext())
+ .hasFeature(BrowserConfig.Feature.DRM_UPLOADS);
+ boolean isDRMFileType = false;
+ if (drmUploadEnabled && filePath != null
+ && (filePath.endsWith(".fl") || filePath.endsWith(".dm")
+ || filePath.endsWith(".dcf") || filePath.endsWith(".dr")
+ || filePath.endsWith(".dd"))) {
+ isDRMFileType = true;
+ Toast.makeText(mController.getContext(), R.string.drm_file_unsupported,
+ Toast.LENGTH_LONG).show();
+ }
+
+ if (mUploadMessage != null) {
+ if (!isDRMFileType) {
+ mUploadMessage.onReceiveValue(result);
+ } else {
+ mUploadMessage.onReceiveValue(null);
+ }
+ }
+
+ if (mUploadFilePaths != null) {
+ if (hasGoodFilePath && !isDRMFileType) {
+ Log.d(TAG, "upload file path:" + filePath);
+ mUploadFilePaths.onReceiveValue(new String[]{filePath});
+ } else {
+ mUploadFilePaths.onReceiveValue(null);
+ }
+ }
+
+ setHandled(true);
+ }
+
+
+ public String getDocumentId(final Uri uri) {
+ String id = null;
+ try {
+ Object[] params = {(android.net.Uri)uri};
+ Class[] type = new Class[] {Class.forName("android.net.Uri") };
+ id = (String) ReflectHelper.invokeMethod(
+ "android.provider.DocumentsContract","getDocumentId",
+ type, params);
+
+ } catch(java.lang.ClassNotFoundException e) {
+
+ }
+ return id;
+ }
+
+
+ public String getFilePath(final Context context, final Uri uri) {
+ String id = getDocumentId(uri);
+
+ // DocumentProvider is new API exposed in Kitkat
+ // Its a way of exposing unified file explorer
+ if (id != null) {
+ // ExternalStorageProvider
+ if (isExternalStorageDocument(uri)) {
+ final String docId = id;
+ final String[] split = docId.split(":");
+ final String type = split[0];
+
+ if ("primary".equalsIgnoreCase(type)) {
+ return Environment.getExternalStorageDirectory() + "/" + split[1];
+ }
+ }
+ // DownloadsProvider
+ else if (isDownloadsDocument(uri)) {
+ final Uri contentUri = ContentUris.withAppendedId(
+ Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
+ return getDataColumn(context, contentUri, null, null);
+ }
+ // MediaProvider
+ else if (isMediaDocument(uri)) {
+ final String docId = id;
+ final String[] split = docId.split(":");
+ final String type = split[0];
+
+ Uri contentUri = null;
+ if ("image".equals(type)) {
+ contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+ } else if ("video".equals(type)) {
+ contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+ } else if ("audio".equals(type)) {
+ contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+ }
+
+ final String selection = "_id=?";
+ final String[] selectionArgs = new String[] {
+ split[1]
+ };
+
+ return getDataColumn(context, contentUri, selection, selectionArgs);
+ }
+ }
+ // MediaStore (and general)
+ else if ("content".equalsIgnoreCase(uri.getScheme())) {
+ return getDataColumn(context, uri, null, null);
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the value of the data column for this Uri. This is useful for
+ * MediaStore Uris, and other file-based ContentProviders.
+ * @return The value of the _data column, which is typically a file path.
+ */
+ private String getDataColumn(Context context, Uri uri, String selection,
+ String[] selectionArgs) {
+
+ Cursor cursor = null;
+ final String column = "_data";
+ final String[] projection = { column };
+
+ try {
+ cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
+ null);
+ if (cursor != null && cursor.moveToFirst()) {
+ final int column_index = cursor.getColumnIndexOrThrow(column);
+ return cursor.getString(column_index);
+ }
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+ return null;
+ }
+
+ /**
+ * @return Whether the Uri authority is ExternalStorageProvider.
+ */
+ private boolean isExternalStorageDocument(Uri uri) {
+ return "com.android.externalstorage.documents".equals(uri.getAuthority());
+ }
+
+ /**
+ * @return Whether the Uri authority is DownloadsProvider.
+ */
+ private boolean isDownloadsDocument(Uri uri) {
+ return "com.android.providers.downloads.documents".equals(uri.getAuthority());
+ }
+
+ /**
+ * @return Whether the Uri authority is MediaProvider.
+ */
+ public static boolean isMediaDocument(Uri uri) {
+ return "com.android.providers.media.documents".equals(uri.getAuthority());
+ }
+
+
+ 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());
+ }
+
+ void showFileChooser(ValueCallback<String[]> uploadFilePaths, String acceptTypes,
+ boolean capture) {
+
+ final String imageMimeType = "image/*";
+ final String videoMimeType = "video/*";
+ final String audioMimeType = "audio/*";
+
+ if (mUploadFilePaths != null) {
+ // Already a file picker operation in progress.
+ return;
+ }
+
+ mUploadFilePaths = uploadFilePaths;
+
+ // Parse the accept type.
+ String params[] = acceptTypes.split(";");
+ String mimeType = params[0];
+
+ // Ensure it is not still set from a previous upload.
+ mCameraFilePath = null;
+ List<Intent> intentList = new ArrayList<Intent>();
+
+ if (mimeType.equals(imageMimeType)) {
+ if (capture) {
+ // Specified 'image/*' and capture=true, so go ahead and launch the
+ // camera directly.
+ startActivity(createCameraIntent());
+ return;
+ } else {
+ // Specified just 'image/*', capture=false, or no capture value.
+ // In all these cases we show a traditional picker filetered on accept type
+ // so launch an intent for both the Camera and image/* OPENABLE.
+ intentList.add(createCameraIntent());
+ createUploadDialog(imageMimeType, intentList);
+ return;
+ }
+ } else if (mimeType.equals(videoMimeType)) {
+ if (capture) {
+ // Specified 'video/*' and capture=true, so go ahead and launch the
+ // camcorder directly.
+ startActivity(createCamcorderIntent());
+ return;
+ } else {
+ // Specified just 'video/*', capture=false, or no capture value.
+ // 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.
+ intentList.add(createCamcorderIntent());
+ createUploadDialog(videoMimeType, intentList);
+ return;
+ }
+ } else if (mimeType.equals(audioMimeType)) {
+ if (capture) {
+ // Specified 'audio/*' and capture=true, so go ahead and launch the sound
+ // recorder.
+ startActivity(createSoundRecorderIntent());
+ return;
+ } else {
+ // Specified just 'audio/*', capture=false, or no capture value.
+ // In all these cases so go ahead and launch an intent for both the sound
+ // recorder and audio/* OPENABLE.
+ intentList.add(createSoundRecorderIntent());
+ createUploadDialog(audioMimeType, intentList);
+ return;
+ }
+ }
+
+ // No special handling based on the accept type was necessary, so trigger the default
+ // file upload chooser.
+ createUploadDialog("*/*", null);
+ }
+
+
+ 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 void createUploadDialog(String openableMimeType, List<Intent> intentList) {
+
+ Intent openable = new Intent(Intent.ACTION_GET_CONTENT);
+ openable.addCategory(Intent.CATEGORY_OPENABLE);
+ openable.setType(openableMimeType);
+
+ if (openableMimeType.equals("*/*") && intentList == null) {
+ intentList = new ArrayList<Intent>();
+ intentList.add(createCameraIntent());
+ intentList.add(createCamcorderIntent());
+ intentList.add(createSoundRecorderIntent());
+ }
+
+ PackageManager pm = mController.getActivity().getPackageManager();
+ ArrayList<ResolveInfo> uploadApps = new ArrayList<ResolveInfo>();
+
+ //Step 1:- resolve all apps for IntentList passed
+ for (Iterator<Intent> iterator = intentList.iterator(); iterator.hasNext();) {
+ List<ResolveInfo> intentAppsList = pm.queryIntentActivities(iterator.next(),
+ PackageManager.MATCH_DEFAULT_ONLY);
+
+ // Check whether any apps are available
+ if (intentAppsList!= null && intentAppsList.size() > 0){
+ // limit only to first activity
+ uploadApps.add(intentAppsList.get(0));
+ } else {
+ iterator.remove();
+ }
+
+ }
+
+ // Step 2:- get all openable apps list and create corresponding intents
+ List<ResolveInfo> openableAppsList = pm.queryIntentActivities(openable,
+ PackageManager.MATCH_DEFAULT_ONLY);
+ // limit only to first activity
+ ResolveInfo topOpenableApp = openableAppsList.get(0);
+ uploadApps.add(topOpenableApp);
+ ActivityInfo activityInfo = topOpenableApp.activityInfo;
+ ComponentName name = new ComponentName(activityInfo.applicationInfo.packageName,
+ activityInfo.name);
+ Intent i = new Intent(Intent.ACTION_GET_CONTENT);
+ i.setType(openableMimeType);
+ i.setComponent(name);
+ i.addCategory(Intent.CATEGORY_OPENABLE);
+ intentList.add(i);
+
+ // Step 3: Pass all the apps and their corresponding intents to uploaddialog
+ UploadDialog upDialog = new UploadDialog(mController.getActivity());
+ upDialog.getUploadableApps(uploadApps, intentList);
+ upDialog.loadView(this);
+ }
+
+ public void initiateActivity(Intent intent) {
+ startActivity(intent);
+ }
+
+ 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/src/com/android/browser/UrlHandler.java b/src/src/com/android/browser/UrlHandler.java
new file mode 100755
index 00000000..8ecffe46
--- /dev/null
+++ b/src/src/com/android/browser/UrlHandler.java
@@ -0,0 +1,292 @@
+/*
+ * 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.util.Log;
+import android.widget.Toast;
+
+import com.android.browser.R;
+import com.android.browser.platformsupport.Browser;
+
+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;";
+ /* package */ final static String SCHEME_MAILTO = "mailto:";
+ 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 feature - recognize additional website format
+ // here add to support "mailto:" scheme
+ if (url.startsWith(SCHEME_MAILTO) && handleMailtoTypeUrl(url)) {
+ return true;
+ }
+
+ // add for carrier feature - wap2estore
+ boolean wap2estore = BrowserConfig.getInstance(mController.getContext())
+ .hasFeature(BrowserConfig.Feature.WAP2ESTORE);
+ if (wap2estore && isEstoreTypeUrl(url) && handleEstoreTypeUrl(url)) {
+ return true;
+ }
+
+ if (startActivityForUrl(tab, url)) {
+ return true;
+ }
+
+ if (handleMenuClick(tab, url)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean handleMailtoTypeUrl(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);
+ mActivity.startActivity(intent);
+ } catch (URISyntaxException ex) {
+ Log.w("Browser", "Bad URI " + url + ": " + ex.getMessage());
+ return false;
+ } catch (ActivityNotFoundException ex) {
+ Log.w("Browser", "No Activity Found for " + url);
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean isEstoreTypeUrl(String url) {
+ if (url != null && url.startsWith("estore:")) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean handleEstoreTypeUrl(String url) {
+ if (url.getBytes().length > 256) {
+ Toast.makeText(mActivity, R.string.estore_url_warning, Toast.LENGTH_LONG).show();
+ return false;
+ }
+
+ Intent intent;
+ // perform generic parsing of the URI to turn it into an Intent.
+ try {
+ intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
+ mActivity.startActivity(intent);
+ } catch (URISyntaxException ex) {
+ Log.w("Browser", "Bad URI " + url + ": " + ex.getMessage());
+ return false;
+ } 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();
+ }
+
+ return true;
+ }
+
+
+ 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) {
+ try {
+ 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;
+ } catch (ActivityNotFoundException e) {
+ Log.w(TAG, "Play store not found while searching for : " + packagename);
+ CharSequence alert = mActivity.getResources().getString(
+ R.string.msg_no_google_play);
+ Toast t = Toast.makeText(mActivity , alert, Toast.LENGTH_SHORT);
+ t.show();
+ return false;
+ }
+ } 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/src/com/android/browser/UrlInputView.java b/src/src/com/android/browser/UrlInputView.java
new file mode 100755
index 00000000..8a70b6d7
--- /dev/null
+++ b/src/src/com/android/browser/UrlInputView.java
@@ -0,0 +1,369 @@
+/*
+ * 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.InputFilter;
+import android.text.InputFilter.LengthFilter;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.util.Patterns;
+import android.view.Gravity;
+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 android.widget.Toast;
+
+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 String LATIN_INPUTMETHOD_PACKAGE_NAME = "com.android.inputmethod.latin";
+
+ static final int POST_DELAY = 100;
+ static final int URL_MAX_LENGTH = 2048;
+
+ 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 Context mContext;
+
+ private int mState;
+ private StateListener mStateListener;
+
+ public UrlInputView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ 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);
+ setDropDownAnchor(R.id.taburlbar);
+ mState = StateListener.STATE_NORMAL;
+ mContext = ctx;
+
+ this.setFilters(new InputFilter[] {
+ new UrlLengthFilter(URL_MAX_LENGTH)
+ });
+ }
+
+ 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)) {
+ dismissDropDown();
+ showDropDown();
+ performFiltering(getText(), 0);
+ }
+ }
+
+ @Override
+ public void dismissDropDown() {
+ super.dismissDropDown();
+ mAdapter.clearCache();
+ }
+
+ @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 ||
+ StateListener.STATE_EDITED == mState) {
+ changeState(StateListener.STATE_EDITED);
+ }
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) { }
+
+ /**
+ * It will prompt the toast if the text length greater than the given length.
+ */
+ private class UrlLengthFilter extends LengthFilter {
+ public UrlLengthFilter(int max) {
+ super(max);
+ }
+
+ @Override
+ public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
+ int dstart, int dend) {
+ CharSequence result = super.filter(source, start, end, dest, dstart, dend);
+ if (result != null) {
+ // If the result is not null, it means the text length is greater than
+ // the given length. We will prompt the toast to alert the user.
+ CharSequence alert = getResources().getString(R.string.max_url_character_limit_msg);
+ Toast t = Toast.makeText(mContext , alert, Toast.LENGTH_SHORT);
+ t.setGravity(Gravity.CENTER, 0, 0);
+ t.show();
+ }
+ return result;
+ }
+ }
+
+}
diff --git a/src/src/com/android/browser/UrlSelectionActionMode.java b/src/src/com/android/browser/UrlSelectionActionMode.java
new file mode 100644
index 00000000..87446ece
--- /dev/null
+++ b/src/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/src/com/android/browser/UrlUtils.java b/src/src/com/android/browser/UrlUtils.java
new file mode 100755
index 00000000..4d3dee42
--- /dev/null
+++ b/src/src/com/android/browser/UrlUtils.java
@@ -0,0 +1,216 @@
+/*
+ * 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;
+import java.util.HashSet;
+import java.util.Arrays;
+import java.net.URI;
+
+/**
+ * Utility methods for Url manipulation
+ */
+public class UrlUtils {
+ public static final String[] DOWNLOADABLE_SCHEMES_VALUES = new String[]
+ { "data", "filesystem", "http", "https" };
+
+ private static final HashSet<String> DOWNLOADABLE_SCHEMES =
+ new HashSet<String>(Arrays.asList(DOWNLOADABLE_SCHEMES_VALUES));
+
+ // Schemes for which the LIVE_MENU items defined in res/menu/browser.xml
+ // should be enabled
+ public static final String[] LIVE_SCHEMES_VALUES = new String[]
+ { "http", "https" };
+
+ private static final HashSet<String> LIVE_SCHEMES =
+ new HashSet<String>(Arrays.asList(LIVE_SCHEMES_VALUES));
+
+ static final Pattern ACCEPTED_URI_SCHEMA = Pattern.compile(
+ "(?i)" + // switch on case insensitive matching
+ "(" + // begin group for schema
+ "(?:http|https|file|chrome):\\/\\/" +
+ "|(?: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);
+ }
+
+ public static boolean isDownloadableScheme(Uri uri) {
+ return DOWNLOADABLE_SCHEMES.contains(uri.getScheme());
+ }
+
+ public static boolean isDownloadableScheme(String uri) {
+ try {
+ URI uriObj = new URI(uri);
+ return isDownloadableScheme(Uri.parse(uriObj.toString()));
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ public static boolean isLiveScheme(Uri uri) {
+ return LIVE_SCHEMES.contains(uri.getScheme());
+ }
+
+ public static boolean isLiveScheme(String uri) {
+ try {
+ return isLiveScheme(Uri.parse(uri));
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /**
+ * 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/src/com/android/browser/WallpaperHandler.java b/src/src/com/android/browser/WallpaperHandler.java
new file mode 100644
index 00000000..5c539b51
--- /dev/null
+++ b/src/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/src/com/android/browser/WebStorageSizeManager.java b/src/src/com/android/browser/WebStorageSizeManager.java
new file mode 100644
index 00000000..0a6a514a
--- /dev/null
+++ b/src/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/src/com/android/browser/WebViewController.java b/src/src/com/android/browser/WebViewController.java
new file mode 100644
index 00000000..f7b926ef
--- /dev/null
+++ b/src/src/com/android/browser/WebViewController.java
@@ -0,0 +1,115 @@
+/*
+ * 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.os.Message;
+import android.view.KeyEvent;
+import android.view.View;
+import org.codeaurora.swe.HttpAuthHandler;
+import android.webkit.ValueCallback;
+import android.webkit.WebChromeClient.CustomViewCallback;
+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 onUserCanceledSsl(Tab tab);
+
+ void onUpdatedSecurityState(Tab tab);
+
+ void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture);
+
+ void showFileChooser(ValueCallback<String[]> uploadFilePaths, String acceptTypes,
+ boolean 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);
+
+ boolean shouldCaptureThumbnails();
+}
diff --git a/src/src/com/android/browser/WebViewFactory.java b/src/src/com/android/browser/WebViewFactory.java
new file mode 100644
index 00000000..12327daf
--- /dev/null
+++ b/src/src/com/android/browser/WebViewFactory.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import org.codeaurora.swe.WebView;
+
+/**
+ * Factory for WebViews
+ */
+public interface WebViewFactory {
+
+ public WebView createWebView(boolean privateBrowsing);
+
+ public WebView createWebView(boolean privateBrowsing, boolean backgroundTab);
+
+ public WebView createSubWebView(boolean privateBrowsing);
+
+}
diff --git a/src/src/com/android/browser/WebViewProperties.java b/src/src/com/android/browser/WebViewProperties.java
new file mode 100644
index 00000000..c6629579
--- /dev/null
+++ b/src/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/src/com/android/browser/WebViewTimersControl.java b/src/src/com/android/browser/WebViewTimersControl.java
new file mode 100644
index 00000000..ac74fa1a
--- /dev/null
+++ b/src/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/src/com/android/browser/XLargeUi.java b/src/src/com/android/browser/XLargeUi.java
new file mode 100644
index 00000000..92e30ad2
--- /dev/null
+++ b/src/src/com/android/browser/XLargeUi.java
@@ -0,0 +1,337 @@
+/*
+ * 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 android.view.View;
+import android.view.ViewStub;
+import android.webkit.WebChromeClient;
+
+import org.codeaurora.swe.WebView;
+
+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 ComboView mComboView;
+
+ 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();
+ }
+
+ private void setupActionBar() {
+ mActionBar.setHomeButtonEnabled(false);
+ mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
+ mActionBar.setCustomView(mTabBar);
+ }
+
+ public void showComboView(ComboViews startWith, Bundle extras) {
+ if (mComboView == null) {
+ ViewStub stub = (ViewStub) mActivity.getWindow().getDecorView().
+ findViewById(R.id.combo_view_stub);
+ mComboView = (ComboView) stub.inflate();
+ mComboView.setVisibility(View.GONE);
+ mComboView.setupViews(mActivity);
+ }
+ mNavBar.setVisibility(View.GONE);
+ if (mActionBar != null)
+ mActionBar.hide();
+ Bundle b = new Bundle();
+ b.putString(ComboViewActivity.EXTRA_INITIAL_VIEW, startWith.name());
+ b.putBundle(ComboViewActivity.EXTRA_COMBO_ARGS, extras);
+ Tab t = getActiveTab();
+ if (t != null) {
+ b.putString(ComboViewActivity.EXTRA_CURRENT_URL, t.getUrl());
+ }
+ mComboView.showViews(mActivity, b);
+ }
+
+ @Override
+ public void hideComboView() {
+ if (isComboViewShowing()) {
+ mComboView.hideViews();
+ mActionBar = mActivity.getActionBar();
+ setupActionBar();
+ if (mActionBar != null)
+ mActionBar.show();
+ mNavBar.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public boolean onBackKey() {
+ if (isComboViewShowing()) {
+ hideComboView();
+ return true;
+ }
+ return super.onBackKey();
+ }
+
+ @Override
+ public boolean isComboViewShowing() {
+ return mComboView != null && mComboView.getVisibility() == View.VISIBLE;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mNavBar.clearCompletions();
+ }
+
+ @Override
+ public void onProgressChanged(Tab tab) {
+ super.onProgressChanged(tab);
+ if (mComboView != null && !mComboView.isShowing()) {
+ mActionBar = mActivity.getActionBar();
+ setupActionBar();
+ if (mActionBar != null)
+ mActionBar.show();
+ if (mNavBar != null)
+ mNavBar.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @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);
+ }
+
+ menu.setGroupVisible(R.id.NAV_MENU, false);
+
+ return true;
+ }
+
+
+ // WebView callbacks
+
+ @Override
+ public void addTab(Tab tab) {
+ mTabBar.onNewTab(tab);
+ }
+
+ @Override
+ public void setActiveTab(final Tab tab) {
+ 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);
+ }
+
+ @Override
+ public void updateTabs(List<Tab> tabs) {
+ mTabBar.updateTabs(tabs);
+ }
+
+ @Override
+ public void removeTab(Tab tab) {
+ super.removeTab(tab);
+ mTabBar.onRemoveTab(tab);
+ }
+
+ @Override
+ public void showCustomView(View view, int requestedOrientation,
+ WebChromeClient.CustomViewCallback callback) {
+ super.showCustomView(view, requestedOrientation, callback);
+ if (mActionBar != null)
+ mActionBar.hide();
+ }
+
+ int getContentWidth() {
+ if (mContentView != null) {
+ return mContentView.getWidth();
+ }
+ return 0;
+ }
+
+ @Override
+ public void editUrl(boolean clearInput, boolean forceIME) {
+ 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) {
+ if (inLoad) {
+ // the titlebar was removed when the CAB was shown
+ // if the page is loading, show it again
+ 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());
+/*
+ if (mActiveTab == tab) {
+ int color = NavigationBarBase.getSiteIconColor(tab.getUrl());
+ if (tab.hasFavicon()) {
+ color = ColorUtils.getDominantColorForBitmap(tab.getFavicon());
+ }
+ mActionBar.setBackgroundDrawable(new ColorDrawable(color));
+ }
+*/
+ }
+
+ @Override
+ public void onHideCustomView() {
+ super.onHideCustomView();
+ if (mActionBar != null)
+ mActionBar.show();
+ }
+
+ @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;
+ }
+
+ @Override
+ public boolean shouldCaptureThumbnails() {
+ return false;
+ }
+
+ 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) {
+ if (ENABLE_BORDER_AROUND_FAVICON) {
+ Drawable[] array = new Drawable[2];
+ array[0] = getFaviconBackground();
+ if (icon == null) {
+ array[1] = getGenericFavicon();
+ } else {
+ array[1] = new BitmapDrawable(mActivity.getResources(), icon);
+ }
+ LayerDrawable d = new LayerDrawable(array);
+ d.setLayerInset(1, 2, 2, 2, 2);
+ return d;
+ }
+ return icon == null ? getGenericFavicon() :
+ new BitmapDrawable(mActivity.getResources(), icon);
+ }
+
+}
diff --git a/src/src/com/android/browser/addbookmark/FolderSpinner.java b/src/src/com/android/browser/addbookmark/FolderSpinner.java
new file mode 100644
index 00000000..dd85cda3
--- /dev/null
+++ b/src/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/src/com/android/browser/addbookmark/FolderSpinnerAdapter.java b/src/src/com/android/browser/addbookmark/FolderSpinnerAdapter.java
new file mode 100644
index 00000000..2321002e
--- /dev/null
+++ b/src/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_deco_home_normal;
+ break;
+ case ROOT_FOLDER:
+ labelResource = R.string.bookmarks;
+ drawableResource = R.drawable.ic_deco_bookmarks_normal;
+ 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_deco_folder_normal;
+ 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/src/com/android/browser/appmenu/AppMenu.java b/src/src/com/android/browser/appmenu/AppMenu.java
new file mode 100644
index 00000000..c961305a
--- /dev/null
+++ b/src/src/com/android/browser/appmenu/AppMenu.java
@@ -0,0 +1,372 @@
+// Copyright 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package com.android.browser.appmenu;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.Surface;
+import android.view.View;
+import android.view.View.OnKeyListener;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ImageButton;
+import android.widget.ListPopupWindow;
+import android.widget.PopupWindow;
+import android.widget.PopupWindow.OnDismissListener;
+
+import org.chromium.base.SysUtils;
+import com.android.browser.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.codeaurora.swe.Engine;
+
+/**
+ * Shows a popup of menuitems anchored to a host view. When a item is selected we call
+ * Activity.onOptionsItemSelected with the appropriate MenuItem.
+ * - Only visible MenuItems are shown.
+ * - Disabled items are grayed out.
+ */
+public class AppMenu implements OnItemClickListener, OnKeyListener {
+ /** Whether or not to show the software menu button in the menu. */
+ private static final boolean SHOW_SW_MENU_BUTTON = false;
+
+ private static final float LAST_ITEM_SHOW_FRACTION = 0.5f;
+
+ private final Menu mMenu;
+ private final int mItemRowHeight;
+ private final int mItemDividerHeight;
+ private final int mVerticalFadeDistance;
+ private final int mNegativeSoftwareVerticalOffset;
+ private ListPopupWindow mPopup;
+ private AppMenuAdapter mAdapter;
+ private AppMenuHandler mHandler;
+ private int mCurrentScreenRotation = -1;
+ private boolean mIsByHardwareButton;
+
+ /**
+ * Creates and sets up the App Menu.
+ * @param menu Original menu created by the framework.
+ * @param itemRowHeight Desired height for each app menu row.
+ * @param itemDividerHeight Desired height for the divider between app menu items.
+ * @param handler AppMenuHandler receives callbacks from AppMenu.
+ * @param res Resources object used to get dimensions and style attributes.
+ */
+ AppMenu(Menu menu, int itemRowHeight, int itemDividerHeight, AppMenuHandler handler,
+ Resources res) {
+ mMenu = menu;
+
+ mItemRowHeight = itemRowHeight;
+ assert mItemRowHeight > 0;
+
+ mHandler = handler;
+
+ mItemDividerHeight = itemDividerHeight;
+ assert mItemDividerHeight >= 0;
+
+ mNegativeSoftwareVerticalOffset =
+ res.getDimensionPixelSize(R.dimen.menu_negative_software_vertical_offset);
+ mVerticalFadeDistance = res.getDimensionPixelSize(R.dimen.menu_vertical_fade_distance);
+ }
+
+ /**
+ * Creates and shows the app menu anchored to the specified view.
+ *
+ * @param context The context of the AppMenu (ensure the proper theme is set on
+ * this context).
+ * @param anchorView The anchor {@link View} of the {@link ListPopupWindow}.
+ * @param isByHardwareButton Whether or not hardware button triggered it. (oppose to software
+ * button)
+ * @param screenRotation Current device screen rotation.
+ * @param visibleDisplayFrame The display area rect in which AppMenu is supposed to fit in.
+ * @param screenHeight Current device screen height.
+ */
+ void show(Context context, View anchorView, boolean isByHardwareButton, int screenRotation,
+ Rect visibleDisplayFrame, int screenHeight) {
+ mPopup = new ListPopupWindow(context, null, android.R.attr.popupMenuStyle);
+ mPopup.setModal(true);
+ mPopup.setAnchorView(anchorView);
+ mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+ mPopup.setOnDismissListener(new OnDismissListener() {
+ @Override
+ public void onDismiss() {
+ if (mPopup.getAnchorView() instanceof ImageButton) {
+ ((ImageButton) mPopup.getAnchorView()).setSelected(false);
+ }
+ mHandler.onMenuVisibilityChanged(false);
+ }
+ });
+
+ // Some OEMs don't actually let us change the background... but they still return the
+ // padding of the new background, which breaks the menu height. If we still have a
+ // drawable here even though our style says @null we should use this padding instead...
+ Drawable originalBgDrawable = mPopup.getBackground();
+
+ if (!isByHardwareButton) {
+ mPopup.setAnimationStyle(R.style.OverflowMenuAnim);
+ }
+
+ // Turn off window animations for low end devices.
+ if (SysUtils.isLowEndDevice()) mPopup.setAnimationStyle(0);
+
+ Rect bgPadding = new Rect();
+ mPopup.getBackground().getPadding(bgPadding);
+
+ int popupWidth = context.getResources().getDimensionPixelSize(R.dimen.menu_width) +
+ bgPadding.left + bgPadding.right;
+
+ mPopup.setWidth(popupWidth);
+
+ mCurrentScreenRotation = screenRotation;
+ mIsByHardwareButton = isByHardwareButton;
+
+ // Extract visible items from the Menu.
+ int numItems = mMenu.size();
+ List<MenuItem> menuItems = new ArrayList<MenuItem>();
+ for (int i = 0; i < numItems; ++i) {
+ MenuItem item = mMenu.getItem(i);
+ if (item.isVisible()) {
+ menuItems.add(item);
+ }
+ }
+
+ Rect sizingPadding = new Rect(bgPadding);
+ if (isByHardwareButton && originalBgDrawable != null) {
+ Rect originalPadding = new Rect();
+ originalBgDrawable.getPadding(originalPadding);
+ sizingPadding.top = originalPadding.top;
+ sizingPadding.bottom = originalPadding.bottom;
+ }
+
+ boolean showMenuButton = !mIsByHardwareButton;
+ if (!SHOW_SW_MENU_BUTTON) showMenuButton = false;
+ // A List adapter for visible items in the Menu. The first row is added as a header to the
+ // list view.
+ mAdapter = new AppMenuAdapter(
+ this, menuItems, LayoutInflater.from(context), showMenuButton);
+ mPopup.setAdapter(mAdapter);
+
+ setMenuHeight(menuItems.size(), visibleDisplayFrame, screenHeight, sizingPadding);
+ setPopupOffset(mPopup, mCurrentScreenRotation, visibleDisplayFrame, sizingPadding);
+ mPopup.setOnItemClickListener(this);
+ mPopup.show();
+ mPopup.getListView().setItemsCanFocus(true);
+ mPopup.getListView().setOnKeyListener(this);
+
+ mHandler.onMenuVisibilityChanged(true);
+
+ if (mVerticalFadeDistance > 0) {
+ mPopup.getListView().setVerticalFadingEdgeEnabled(true);
+ mPopup.getListView().setFadingEdgeLength(mVerticalFadeDistance);
+ }
+
+ // Don't animate the menu items for low end devices.
+ if (!SysUtils.isLowEndDevice()) {
+ mPopup.getListView().addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ mPopup.getListView().removeOnLayoutChangeListener(this);
+ runMenuItemEnterAnimations();
+ }
+ });
+ }
+ }
+
+ public void invalidate(Context context, Menu menu) {
+ assert(mMenu == menu);
+ // Extract visible items from the Menu.
+ int numItems = mMenu.size();
+ List<MenuItem> menuItems = new ArrayList<MenuItem>();
+ for (int i = 0; i < numItems; ++i) {
+ MenuItem item = mMenu.getItem(i);
+ if (item.isVisible()) {
+ menuItems.add(item);
+ }
+ }
+
+ boolean showMenuButton = !mIsByHardwareButton;
+ if (!SHOW_SW_MENU_BUTTON) showMenuButton = false;
+
+ mAdapter = new AppMenuAdapter(
+ this, menuItems, LayoutInflater.from(context), showMenuButton);
+ mPopup.setAdapter(mAdapter);
+
+ mPopup.show();
+ mPopup.getListView().setItemsCanFocus(true);
+ mPopup.getListView().setOnKeyListener(this);
+ }
+
+ private void setPopupOffset(
+ ListPopupWindow popup, int screenRotation, Rect appRect, Rect padding) {
+ int[] anchorLocation = new int[2];
+ popup.getAnchorView().getLocationInWindow(anchorLocation);
+ int anchorHeight = popup.getAnchorView().getHeight();
+
+ // If we have a hardware menu button, locate the app menu closer to the estimated
+ // hardware menu button location.
+ if (mIsByHardwareButton) {
+ int horizontalOffset = -anchorLocation[0];
+ switch (screenRotation) {
+ case Surface.ROTATION_0:
+ case Surface.ROTATION_180:
+ horizontalOffset += (appRect.width() - popup.getWidth()) / 2;
+ break;
+ case Surface.ROTATION_90:
+ horizontalOffset += appRect.width() - popup.getWidth();
+ break;
+ case Surface.ROTATION_270:
+ break;
+ default:
+ assert false;
+ break;
+ }
+ popup.setHorizontalOffset(horizontalOffset);
+ // The menu is displayed above the anchored view, so shift the menu up by the bottom
+ // padding of the background.
+ int verticalOffset = appRect.height() - popup.getHeight() + padding.bottom;
+ if (anchorLocation[1] > 0) {
+ verticalOffset -= anchorHeight;
+ }
+ popup.setVerticalOffset(verticalOffset);
+ } else {
+ // The menu is displayed over and below the anchored view, so shift the menu up by the
+ // height of the anchor view.
+ popup.setVerticalOffset(-mNegativeSoftwareVerticalOffset - anchorHeight);
+ }
+ }
+
+ /**
+ * Handles clicks on the AppMenu popup.
+ * @param menuItem The menu item in the popup that was clicked.
+ */
+ void onItemClick(MenuItem menuItem) {
+ if (menuItem.isEnabled()) {
+ dismiss();
+ mHandler.onOptionsItemSelected(menuItem);
+ }
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ onItemClick(mAdapter.getItem(position));
+ }
+
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (mPopup == null || mPopup.getListView() == null) return false;
+
+ if (event.getKeyCode() == KeyEvent.KEYCODE_MENU) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
+ event.startTracking();
+ v.getKeyDispatcherState().startTracking(event, this);
+ return true;
+ } else if (event.getAction() == KeyEvent.ACTION_UP) {
+ v.getKeyDispatcherState().handleUpEvent(event);
+ if (event.isTracking() && !event.isCanceled()) {
+ dismiss();
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Dismisses the app menu and cancels the drag-to-scroll if it is taking place.
+ */
+ void dismiss() {
+ mHandler.appMenuDismissed();
+ if (isShowing()) {
+ mPopup.dismiss();
+ }
+ }
+
+ /**
+ * @return Whether the app menu is currently showing.
+ */
+ boolean isShowing() {
+ if (mPopup == null) {
+ return false;
+ }
+ return mPopup.isShowing();
+ }
+
+ /**
+ * @return ListPopupWindow that displays all the menu options.
+ */
+ ListPopupWindow getPopup() {
+ return mPopup;
+ }
+
+ private void setMenuHeight(
+ int numMenuItems, Rect appDimensions, int screenHeight, Rect padding) {
+ assert mPopup.getAnchorView() != null;
+ View anchorView = mPopup.getAnchorView();
+ int[] anchorViewLocation = new int[2];
+ anchorView.getLocationOnScreen(anchorViewLocation);
+ anchorViewLocation[1] -= appDimensions.top;
+ int anchorViewImpactHeight = mIsByHardwareButton ? anchorView.getHeight() : 0;
+
+ // Set appDimensions.height() for abnormal anchorViewLocation.
+ if (anchorViewLocation[1] > screenHeight) {
+ anchorViewLocation[1] = appDimensions.height();
+ }
+ int availableScreenSpace = Math.max(anchorViewLocation[1],
+ appDimensions.height() - anchorViewLocation[1] - anchorViewImpactHeight);
+
+ availableScreenSpace -= padding.bottom;
+ if (mIsByHardwareButton) availableScreenSpace -= padding.top;
+
+ int numCanFit = availableScreenSpace / (mItemRowHeight + mItemDividerHeight);
+
+ // Fade out the last item if we cannot fit all items.
+ if (numCanFit < numMenuItems) {
+ int spaceForFullItems = numCanFit * (mItemRowHeight + mItemDividerHeight);
+ int spaceForPartialItem = (int) (LAST_ITEM_SHOW_FRACTION * mItemRowHeight);
+ // Determine which item needs hiding.
+ if (spaceForFullItems + spaceForPartialItem < availableScreenSpace) {
+ mPopup.setHeight(spaceForFullItems + spaceForPartialItem +
+ padding.top + padding.bottom);
+ } else {
+ mPopup.setHeight(spaceForFullItems - mItemRowHeight + spaceForPartialItem +
+ padding.top + padding.bottom);
+ }
+ } else {
+ mPopup.setHeight(numMenuItems * (mItemRowHeight + mItemDividerHeight) +
+ padding.top + padding.bottom);
+ }
+ }
+
+ private void runMenuItemEnterAnimations() {
+ AnimatorSet animation = new AnimatorSet();
+ AnimatorSet.Builder builder = null;
+
+ ViewGroup list = mPopup.getListView();
+ for (int i = 0; i < list.getChildCount(); i++) {
+ View view = list.getChildAt(i);
+ Object animatorObject = view.getTag(R.id.menu_item_enter_anim_id);
+ if (animatorObject != null) {
+ if (builder == null) {
+ builder = animation.play((Animator) animatorObject);
+ } else {
+ builder.with((Animator) animatorObject);
+ }
+ }
+ }
+
+ animation.start();
+ }
+}
diff --git a/src/src/com/android/browser/appmenu/AppMenuAdapter.java b/src/src/com/android/browser/appmenu/AppMenuAdapter.java
new file mode 100644
index 00000000..b0e69429
--- /dev/null
+++ b/src/src/com/android/browser/appmenu/AppMenuAdapter.java
@@ -0,0 +1,499 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package com.android.browser.appmenu;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.CheckBox;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import org.chromium.base.ApiCompatibilityUtils;
+import com.android.browser.R;
+import org.chromium.ui.base.LocalizationUtils;
+import org.chromium.ui.interpolators.BakedBezierInterpolator;
+
+import java.util.List;
+
+/**
+ * ListAdapter to customize the view of items in the list.
+ */
+class AppMenuAdapter extends BaseAdapter {
+ private static final int VIEW_TYPE_COUNT = 9;
+
+ /**
+ * Regular Android menu item that contains a title and an icon if icon is specified.
+ */
+ private static final int STANDARD_MENU_ITEM = 0;
+ /**
+ * Menu item that has two buttons, the first one is a title and the second one is an icon.
+ * It is different from the regular menu item because it contains two separate buttons.
+ */
+ private static final int TITLE_BUTTON_MENU_ITEM = 1;
+ /**
+ * Menu item that has one button plus menu button. Every one of these buttons is displayed as an icon.
+ */
+ private static final int ONE_BUTTON_PLUS_MENU_ITEM = 2;
+ /**
+ * Menu item that has two buttons. Every one of these buttons is displayed as an icon.
+ */
+ private static final int TWO_BUTTON_MENU_ITEM = 3;
+ /**
+ * Menu item that has two buttons plus menu. Every one of these buttons is displayed as an icon.
+ */
+ private static final int TWO_BUTTON_PLUS_MENU_ITEM = 4;
+ /**
+ * Menu item that has three buttons. Every one of these buttons is displayed as an icon.
+ */
+ private static final int THREE_BUTTON_MENU_ITEM = 5;
+ /**
+ * Menu item that has three buttons plus menu. Every one of these buttons is displayed as an icon.
+ */
+ private static final int THREE_BUTTON_PLUS_MENU_ITEM = 6;
+ /**
+ * Menu item that has four buttons. Every one of these buttons is displayed as an icon.
+ */
+ private static final int FOUR_BUTTON_MENU_ITEM = 7;
+ /**
+ * Menu item that has two buttons, the first one is a title and the second is a menu icon.
+ * This is similar to {@link #TITLE_BUTTON_MENU_ITEM} but has some slight layout differences.
+ */
+ private static final int MENU_BUTTON_MENU_ITEM = 8;
+
+ /** MenuItem Animation Constants */
+ private static final int ENTER_ITEM_DURATION_MS = 350;
+ private static final int ENTER_ITEM_BASE_DELAY_MS = 80;
+ private static final int ENTER_ITEM_ADDL_DELAY_MS = 30;
+ private static final float ENTER_STANDARD_ITEM_OFFSET_Y_DP = -10.f;
+ private static final float ENTER_STANDARD_ITEM_OFFSET_X_DP = 10.f;
+
+ /** Menu Button Layout Constants */
+ private static final float MENU_BUTTON_WIDTH_DP = 59.f;
+ private static final float MENU_BUTTON_START_PADDING_DP = 21.f;
+
+ private final AppMenu mAppMenu;
+ private final LayoutInflater mInflater;
+ private final List<MenuItem> mMenuItems;
+ private final int mNumMenuItems;
+ private final boolean mShowMenuButton;
+ private final float mDpToPx;
+
+ public AppMenuAdapter(AppMenu appMenu, List<MenuItem> menuItems, LayoutInflater inflater,
+ boolean showMenuButton) {
+ mAppMenu = appMenu;
+ mMenuItems = menuItems;
+ mInflater = inflater;
+ mNumMenuItems = menuItems.size();
+ mShowMenuButton = showMenuButton;
+ mDpToPx = inflater.getContext().getResources().getDisplayMetrics().density;
+ }
+
+ @Override
+ public int getCount() {
+ return mNumMenuItems;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ MenuItem item = getItem(position);
+ boolean hasMenuButton = mShowMenuButton && position == 0;
+ int viewCount = item.hasSubMenu() ? item.getSubMenu().size() : 1;
+
+ if (viewCount == 4) {
+ return FOUR_BUTTON_MENU_ITEM;
+ } else if (viewCount == 3) {
+ if (hasMenuButton) {
+ return THREE_BUTTON_PLUS_MENU_ITEM;
+ }
+ return THREE_BUTTON_MENU_ITEM;
+ } else if (viewCount == 2) {
+ if (hasMenuButton) {
+ return TWO_BUTTON_PLUS_MENU_ITEM;
+ }
+ return TWO_BUTTON_MENU_ITEM;
+ } else if (hasMenuButton) {
+ return ONE_BUTTON_PLUS_MENU_ITEM;
+ }
+ return STANDARD_MENU_ITEM;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return getItem(position).getItemId();
+ }
+
+ @Override
+ public MenuItem getItem(int position) {
+ if (position == ListView.INVALID_POSITION) return null;
+ assert position >= 0;
+ assert position < mMenuItems.size();
+ return mMenuItems.get(position);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final boolean hasMenuButton = mShowMenuButton && position == 0;
+ final MenuItem item = getItem(position);
+ switch (getItemViewType(position)) {
+ case STANDARD_MENU_ITEM: {
+ StandardMenuItemViewHolder holder = null;
+ if (convertView == null) {
+ holder = new StandardMenuItemViewHolder();
+ convertView = mInflater.inflate(R.layout.swe_menu_item, parent, false);
+ holder.text = (TextView) convertView.findViewById(R.id.menu_item_text);
+ holder.image = (AppMenuItemIcon) convertView.findViewById(R.id.menu_item_icon);
+ holder.checkbox = (CheckBox) convertView.findViewById(R.id.menu_item_checkbox);
+ convertView.setTag(holder);
+ convertView.setTag(R.id.menu_item_enter_anim_id,
+ buildStandardItemEnterAnimator(convertView, position));
+ } else {
+ holder = (StandardMenuItemViewHolder) convertView.getTag();
+ }
+
+ convertView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mAppMenu.onItemClick(item);
+ }
+ });
+ // Set up the icon.
+ Drawable icon = item.getIcon();
+ holder.image.setImageDrawable(icon);
+ holder.image.setVisibility(icon == null ? View.GONE : View.VISIBLE);
+
+ holder.checkbox.setVisibility(item.isCheckable() ? View.VISIBLE : View.GONE);
+ holder.checkbox.setChecked(item.isChecked());
+ holder.checkbox.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mAppMenu.onItemClick(item);
+ }
+ });
+
+ holder.text.setText(item.getTitle());
+ boolean isEnabled = item.isEnabled();
+ // Set the text color (using a color state list).
+ holder.text.setEnabled(isEnabled);
+ // This will ensure that the item is not highlighted when selected.
+ convertView.setEnabled(isEnabled);
+ break;
+ }
+ case ONE_BUTTON_PLUS_MENU_ITEM: {
+ TwoButtonMenuItemViewHolder holder = null;
+ if (convertView == null) {
+ holder = new TwoButtonMenuItemViewHolder();
+ convertView = mInflater.inflate(R.layout.one_button_plus_menu_item, parent, false);
+ holder.buttons[0] = (ImageButton) convertView.findViewById(R.id.button_one);
+ holder.buttons[1] = (ImageButton) convertView.findViewById(R.id.button_two);
+ convertView.setTag(holder);
+ convertView.setTag(R.id.menu_item_enter_anim_id,
+ buildIconItemEnterAnimator(holder.buttons, hasMenuButton));
+ } else {
+ holder = (TwoButtonMenuItemViewHolder) convertView.getTag();
+ }
+ setupImageButton(holder.buttons[0], item.getSubMenu().getItem(0));
+ setupMenuButton(holder.buttons[1]);
+ convertView.setFocusable(false);
+ convertView.setEnabled(false);
+ break;
+ }
+ case TWO_BUTTON_MENU_ITEM: {
+ TwoButtonMenuItemViewHolder holder = null;
+ if (convertView == null) {
+ holder = new TwoButtonMenuItemViewHolder();
+ convertView = mInflater.inflate(R.layout.two_button_menu_item, parent, false);
+ holder.buttons[0] = (ImageButton) convertView.findViewById(R.id.button_one);
+ holder.buttons[1] = (ImageButton) convertView.findViewById(R.id.button_two);
+ convertView.setTag(holder);
+ convertView.setTag(R.id.menu_item_enter_anim_id,
+ buildIconItemEnterAnimator(holder.buttons, hasMenuButton));
+ } else {
+ holder = (TwoButtonMenuItemViewHolder) convertView.getTag();
+ }
+ setupImageButton(holder.buttons[0], item.getSubMenu().getItem(0));
+ setupImageButton(holder.buttons[1], item.getSubMenu().getItem(1));
+ convertView.setFocusable(false);
+ convertView.setEnabled(false);
+ break;
+ }
+ case TWO_BUTTON_PLUS_MENU_ITEM: {
+ ThreeButtonMenuItemViewHolder holder = null;
+ if (convertView == null) {
+ holder = new ThreeButtonMenuItemViewHolder();
+ convertView = mInflater.inflate(R.layout.two_button_plus_menu_item, parent, false);
+ holder.buttons[0] = (ImageButton) convertView.findViewById(R.id.button_one);
+ holder.buttons[1] = (ImageButton) convertView.findViewById(R.id.button_two);
+ holder.buttons[2] = (ImageButton) convertView.findViewById(R.id.button_three);
+ convertView.setTag(holder);
+ convertView.setTag(R.id.menu_item_enter_anim_id,
+ buildIconItemEnterAnimator(holder.buttons, hasMenuButton));
+ } else {
+ holder = (ThreeButtonMenuItemViewHolder) convertView.getTag();
+ }
+ setupImageButton(holder.buttons[0], item.getSubMenu().getItem(0));
+ setupImageButton(holder.buttons[1], item.getSubMenu().getItem(1));
+ setupMenuButton(holder.buttons[2]);
+ convertView.setFocusable(false);
+ convertView.setEnabled(false);
+ break;
+ }
+ case THREE_BUTTON_MENU_ITEM: {
+ ThreeButtonMenuItemViewHolder holder = null;
+ if (convertView == null) {
+ holder = new ThreeButtonMenuItemViewHolder();
+ convertView = mInflater.inflate(R.layout.three_button_menu_item, parent, false);
+ holder.buttons[0] = (ImageButton) convertView.findViewById(R.id.button_one);
+ holder.buttons[1] = (ImageButton) convertView.findViewById(R.id.button_two);
+ holder.buttons[2] = (ImageButton) convertView.findViewById(R.id.button_three);
+ convertView.setTag(holder);
+ convertView.setTag(R.id.menu_item_enter_anim_id,
+ buildIconItemEnterAnimator(holder.buttons, hasMenuButton));
+ } else {
+ holder = (ThreeButtonMenuItemViewHolder) convertView.getTag();
+ }
+ setupImageButton(holder.buttons[0], item.getSubMenu().getItem(0));
+ setupImageButton(holder.buttons[1], item.getSubMenu().getItem(1));
+ setupImageButton(holder.buttons[2], item.getSubMenu().getItem(2));
+ convertView.setFocusable(false);
+ convertView.setEnabled(false);
+ break;
+ }
+ case THREE_BUTTON_PLUS_MENU_ITEM: {
+ FourButtonMenuItemViewHolder holder = null;
+ if (convertView == null) {
+ holder = new FourButtonMenuItemViewHolder();
+ convertView = mInflater.inflate(R.layout.three_button_plus_menu_item, parent, false);
+ holder.buttons[0] = (ImageButton) convertView.findViewById(R.id.button_one);
+ holder.buttons[1] = (ImageButton) convertView.findViewById(R.id.button_two);
+ holder.buttons[2] = (ImageButton) convertView.findViewById(R.id.button_three);
+ holder.buttons[3] = (ImageButton) convertView.findViewById(R.id.button_four);
+ convertView.setTag(holder);
+ convertView.setTag(R.id.menu_item_enter_anim_id,
+ buildIconItemEnterAnimator(holder.buttons, hasMenuButton));
+ } else {
+ holder = (FourButtonMenuItemViewHolder) convertView.getTag();
+ }
+ setupImageButton(holder.buttons[0], item.getSubMenu().getItem(0));
+ setupImageButton(holder.buttons[1], item.getSubMenu().getItem(1));
+ setupImageButton(holder.buttons[2], item.getSubMenu().getItem(2));
+ setupMenuButton(holder.buttons[3]);
+ convertView.setFocusable(false);
+ convertView.setEnabled(false);
+ break;
+ }
+ case FOUR_BUTTON_MENU_ITEM: {
+ FourButtonMenuItemViewHolder holder = null;
+ if (convertView == null) {
+ holder = new FourButtonMenuItemViewHolder();
+ convertView = mInflater.inflate(R.layout.four_button_menu_item, parent, false);
+ holder.buttons[0] = (ImageButton) convertView.findViewById(R.id.button_one);
+ holder.buttons[1] = (ImageButton) convertView.findViewById(R.id.button_two);
+ holder.buttons[2] = (ImageButton) convertView.findViewById(R.id.button_three);
+ holder.buttons[3] = (ImageButton) convertView.findViewById(R.id.button_four);
+ convertView.setTag(holder);
+ convertView.setTag(R.id.menu_item_enter_anim_id,
+ buildIconItemEnterAnimator(holder.buttons, hasMenuButton));
+ } else {
+ holder = (FourButtonMenuItemViewHolder) convertView.getTag();
+ }
+ setupImageButton(holder.buttons[0], item.getSubMenu().getItem(0));
+ setupImageButton(holder.buttons[1], item.getSubMenu().getItem(1));
+ setupImageButton(holder.buttons[2], item.getSubMenu().getItem(2));
+ setupImageButton(holder.buttons[3], item.getSubMenu().getItem(3));
+ convertView.setFocusable(false);
+ convertView.setEnabled(false);
+ break;
+ }
+ case TITLE_BUTTON_MENU_ITEM:
+ // Fall through.
+ case MENU_BUTTON_MENU_ITEM: {
+ TitleButtonMenuItemViewHolder holder = null;
+ if (convertView == null) {
+ holder = new TitleButtonMenuItemViewHolder();
+ convertView = mInflater.inflate(R.layout.title_button_menu_item, parent, false);
+ holder.title = (TextView) convertView.findViewById(R.id.title);
+ holder.button = (ImageButton) convertView.findViewById(R.id.button);
+
+ View animatedView = hasMenuButton ? holder.title : convertView;
+
+ convertView.setTag(holder);
+ convertView.setTag(R.id.menu_item_enter_anim_id,
+ buildStandardItemEnterAnimator(animatedView, position));
+ } else {
+ holder = (TitleButtonMenuItemViewHolder) convertView.getTag();
+ }
+ final MenuItem titleItem = item.hasSubMenu() ? item.getSubMenu().getItem(0) : item;
+ holder.title.setText(titleItem.getTitle());
+ holder.title.setEnabled(titleItem.isEnabled());
+ holder.title.setFocusable(titleItem.isEnabled());
+ holder.title.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mAppMenu.onItemClick(titleItem);
+ }
+ });
+
+ if (hasMenuButton) {
+ holder.button.setVisibility(View.VISIBLE);
+ setupMenuButton(holder.button);
+ } else if (item.getSubMenu().getItem(1).getIcon() != null) {
+ holder.button.setVisibility(View.VISIBLE);
+ setupImageButton(holder.button, item.getSubMenu().getItem(1));
+ } else {
+ holder.button.setVisibility(View.GONE);
+ }
+ convertView.setFocusable(false);
+ convertView.setEnabled(false);
+ break;
+ }
+ default:
+ assert false : "Unexpected MenuItem type";
+ }
+ return convertView;
+ }
+
+ private void setupImageButton(ImageButton button, final MenuItem item) {
+ // Store and recover the level of image as button.setimageDrawable
+ // resets drawable to default level.
+ int currentLevel = item.getIcon().getLevel();
+ button.setImageDrawable(item.getIcon());
+ item.getIcon().setLevel(currentLevel);
+ button.setContentDescription(item.getTitle());
+ button.setEnabled(item.isEnabled());
+ button.setFocusable(item.isEnabled());
+ button.setSelected(item.isChecked());
+ button.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mAppMenu.onItemClick(item);
+ }
+ });
+ }
+
+ private void setupMenuButton(ImageButton button) {
+ button.setSelected(true);
+ button.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mAppMenu.dismiss();
+ }
+ });
+ }
+
+ /**
+ * This builds an {@link Animator} for the enter animation of a standard menu item. This means
+ * it will animate the alpha from 0 to 1 and translate the view from -10dp to 0dp on the y axis.
+ *
+ * @param view The menu item {@link View} to be animated.
+ * @param position The position in the menu. This impacts the start delay of the animation.
+ * @return The {@link Animator}.
+ */
+ private Animator buildStandardItemEnterAnimator(final View view, int position) {
+ final float offsetYPx = ENTER_STANDARD_ITEM_OFFSET_Y_DP * mDpToPx;
+ final int startDelay = ENTER_ITEM_BASE_DELAY_MS + ENTER_ITEM_ADDL_DELAY_MS * position;
+
+ AnimatorSet animation = new AnimatorSet();
+ animation.playTogether(
+ ObjectAnimator.ofFloat(view, View.ALPHA, 0.f, 1.f),
+ ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, offsetYPx, 0.f));
+ animation.setDuration(ENTER_ITEM_DURATION_MS);
+ animation.setStartDelay(startDelay);
+ animation.setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE);
+
+ animation.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ view.setAlpha(0.f);
+ }
+ });
+ return animation;
+ }
+
+ /**
+ * This builds an {@link Animator} for the enter animation of icon row menu items. This means
+ * it will animate the alpha from 0 to 1 and translate the views from 10dp to 0dp on the x axis.
+ *
+ * @param views The list if icons in the menu item that should be animated.
+ * @param skipLastItem Whether or not the last item should be animated or not.
+ * @return The {@link Animator}.
+ */
+ private Animator buildIconItemEnterAnimator(final ImageView[] views, boolean skipLastItem) {
+ final boolean rtl = false; //LocalizationUtils.isLayoutRtl();
+ final float offsetXPx = ENTER_STANDARD_ITEM_OFFSET_X_DP * mDpToPx * (rtl ? -1.f : 1.f);
+ final int maxViewsToAnimate = views.length - (skipLastItem ? 1 : 0);
+
+ AnimatorSet animation = new AnimatorSet();
+ AnimatorSet.Builder builder = null;
+ for (int i = 0; i < maxViewsToAnimate; i++) {
+ final int startDelay = ENTER_ITEM_ADDL_DELAY_MS * i;
+
+ Animator alpha = ObjectAnimator.ofFloat(views[i], View.ALPHA, 0.f, 1.f);
+ Animator translate = ObjectAnimator.ofFloat(views[i], View.TRANSLATION_X, offsetXPx, 0);
+ alpha.setStartDelay(startDelay);
+ translate.setStartDelay(startDelay);
+ alpha.setDuration(ENTER_ITEM_DURATION_MS);
+ translate.setDuration(ENTER_ITEM_DURATION_MS);
+
+ if (builder == null) {
+ builder = animation.play(alpha);
+ } else {
+ builder.with(alpha);
+ }
+ builder.with(translate);
+ }
+ animation.setStartDelay(ENTER_ITEM_BASE_DELAY_MS);
+ animation.setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE);
+
+ animation.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ for (int i = 0; i < maxViewsToAnimate; i++) {
+ views[i].setAlpha(0.f);
+ }
+ }
+ });
+ return animation;
+ }
+
+ static class StandardMenuItemViewHolder {
+ public TextView text;
+ public AppMenuItemIcon image;
+ public CheckBox checkbox;
+ }
+
+ static class TwoButtonMenuItemViewHolder {
+ public ImageButton[] buttons = new ImageButton[2];
+ }
+
+ static class ThreeButtonMenuItemViewHolder {
+ public ImageButton[] buttons = new ImageButton[3];
+ }
+
+ static class FourButtonMenuItemViewHolder {
+ public ImageButton[] buttons = new ImageButton[4];
+ }
+
+ static class TitleButtonMenuItemViewHolder {
+ public TextView title;
+ public ImageButton button;
+ }
+} \ No newline at end of file
diff --git a/src/src/com/android/browser/appmenu/AppMenuButtonHelper.java b/src/src/com/android/browser/appmenu/AppMenuButtonHelper.java
new file mode 100644
index 00000000..364183a9
--- /dev/null
+++ b/src/src/com/android/browser/appmenu/AppMenuButtonHelper.java
@@ -0,0 +1,93 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package com.android.browser.appmenu;
+
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+
+/**
+ * A helper class for a menu button to decide when to show the app menu and forward touch
+ * events.
+ *
+ * Simply construct this class and pass the class instance to a menu button as TouchListener.
+ * Then this class will handle everything regarding showing app menu for you.
+ */
+public class AppMenuButtonHelper implements OnTouchListener {
+ private final View mMenuButton;
+ private final AppMenuHandler mMenuHandler;
+ private Runnable mOnAppMenuShownListener;
+
+ /**
+ * @param menuButton Menu button instance that will trigger the app menu.
+ * @param menuHandler MenuHandler implementation that can show and get the app menu.
+ */
+ public AppMenuButtonHelper(View menuButton, AppMenuHandler menuHandler) {
+ mMenuButton = menuButton;
+ mMenuHandler = menuHandler;
+ }
+
+ /**
+ * @param onAppMenuShownListener This is called when the app menu is shown by this class.
+ */
+ public void setOnAppMenuShownListener(Runnable onAppMenuShownListener) {
+ mOnAppMenuShownListener = onAppMenuShownListener;
+ }
+
+ /**
+ * Shows the app menu if it is not already shown.
+ * @param startDragging Whether dragging is started.
+ * @return Whether or not if the app menu is successfully shown.
+ */
+ private boolean showAppMenu(boolean startDragging) {
+ if (!mMenuHandler.isAppMenuShowing() &&
+ mMenuHandler.showAppMenu(mMenuButton, false, startDragging)) {
+
+ if (mOnAppMenuShownListener != null) {
+ mOnAppMenuShownListener.run();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @return Whether app menu is active. That is, AppMenu is showing or menu button is consuming
+ * touch events to prepare AppMenu showing.
+ */
+ public boolean isAppMenuActive() {
+ return mMenuButton.isPressed() || mMenuHandler.isAppMenuShowing();
+ }
+
+ /**
+ * Handle the key press event on a menu button.
+ * @return Whether the app menu was shown as a result of this action.
+ */
+ public boolean onEnterKeyPress() {
+ return showAppMenu(false);
+ }
+
+ @Override
+
+ public boolean onTouch(View view, MotionEvent event) {
+ boolean isTouchEventConsumed = false;
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ isTouchEventConsumed |= true;
+ mMenuButton.setPressed(true);
+ showAppMenu(true);
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ isTouchEventConsumed |= true;
+ mMenuButton.setPressed(false);
+ break;
+ default:
+ }
+
+ return isTouchEventConsumed;
+ }
+} \ No newline at end of file
diff --git a/src/src/com/android/browser/appmenu/AppMenuDragHelper.java b/src/src/com/android/browser/appmenu/AppMenuDragHelper.java
new file mode 100644
index 00000000..ee2a8c75
--- /dev/null
+++ b/src/src/com/android/browser/appmenu/AppMenuDragHelper.java
@@ -0,0 +1,269 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package com.android.browser.appmenu;
+
+import android.animation.TimeAnimator;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.view.GestureDetector;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.ListPopupWindow;
+import android.widget.ListView;
+
+import com.android.browser.R;
+
+import java.util.ArrayList;
+
+/**
+ * Handles the drag touch events on AppMenu that start from the menu button.
+ *
+ * Lint suppression for NewApi is added because we are using TimeAnimator class that was marked
+ * hidden in API 16.
+ */
+@SuppressLint("NewApi")
+class AppMenuDragHelper {
+ private final Activity mActivity;
+ private final AppMenu mAppMenu;
+
+ // Internally used action constants for dragging.
+ private static final int ITEM_ACTION_HIGHLIGHT = 0;
+ private static final int ITEM_ACTION_PERFORM = 1;
+ private static final int ITEM_ACTION_CLEAR_HIGHLIGHT_ALL = 2;
+
+ private static final float AUTO_SCROLL_AREA_MAX_RATIO = 0.25f;
+
+ // Dragging related variables, i.e., menu showing initiated by touch down and drag to navigate.
+ private final float mAutoScrollFullVelocity;
+ private final TimeAnimator mDragScrolling = new TimeAnimator();
+ private float mDragScrollOffset;
+ private int mDragScrollOffsetRounded;
+ private volatile float mDragScrollingVelocity;
+ private volatile float mLastTouchX;
+ private volatile float mLastTouchY;
+ private final int mItemRowHeight;
+ private boolean mIsSingleTapUpHappened;
+ GestureDetector mGestureSingleTapDetector;
+
+ // These are used in a function locally, but defined here to avoid heap allocation on every
+ // touch event.
+ private final Rect mScreenVisibleRect = new Rect();
+ private final int[] mScreenVisiblePoint = new int[2];
+
+ AppMenuDragHelper(Activity activity, AppMenu appMenu, int itemRowHeight) {
+ mActivity = activity;
+ mAppMenu = appMenu;
+ mItemRowHeight = itemRowHeight;
+ Resources res = mActivity.getResources();
+ mAutoScrollFullVelocity = res.getDimensionPixelSize(R.dimen.auto_scroll_full_velocity);
+ // If user is dragging and the popup ListView is too big to display at once,
+ // mDragScrolling animator scrolls mPopup.getListView() automatically depending on
+ // the user's touch position.
+ mDragScrolling.setTimeListener(new TimeAnimator.TimeListener() {
+ @Override
+ public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
+ ListPopupWindow popup = mAppMenu.getPopup();
+ if (popup == null || popup.getListView() == null) return;
+
+ // We keep both mDragScrollOffset and mDragScrollOffsetRounded because
+ // the actual scrolling is by the rounded value but at the same time we also
+ // want to keep the precise scroll value in float.
+ mDragScrollOffset += (deltaTime * 0.001f) * mDragScrollingVelocity;
+ int diff = Math.round(mDragScrollOffset - mDragScrollOffsetRounded);
+ mDragScrollOffsetRounded += diff;
+ popup.getListView().smoothScrollBy(diff, 0);
+
+ // Force touch move event to highlight items correctly for the scrolled position.
+ if (!Float.isNaN(mLastTouchX) && !Float.isNaN(mLastTouchY)) {
+ menuItemAction(Math.round(mLastTouchX), Math.round(mLastTouchY),
+ ITEM_ACTION_HIGHLIGHT);
+ }
+ }
+ });
+ mGestureSingleTapDetector = new GestureDetector(activity, new SimpleOnGestureListener() {
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ mIsSingleTapUpHappened = true;
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Sets up all the internal state to prepare for menu dragging.
+ * @param startDragging Whether dragging is started. For example, if the app menu
+ * is showed by tapping on a button, this should be false. If it is
+ * showed by start dragging down on the menu button, this should be
+ * true.
+ */
+ void onShow(boolean startDragging) {
+ mLastTouchX = Float.NaN;
+ mLastTouchY = Float.NaN;
+ mDragScrollOffset = 0.0f;
+ mDragScrollOffsetRounded = 0;
+ mDragScrollingVelocity = 0.0f;
+ mIsSingleTapUpHappened = false;
+
+ if (startDragging) mDragScrolling.start();
+ }
+
+ /**
+ * Dragging mode will be stopped by calling this function. Note that it will fall back to normal
+ * non-dragging mode.
+ */
+ void finishDragging() {
+ menuItemAction(0, 0, ITEM_ACTION_CLEAR_HIGHLIGHT_ALL);
+ mDragScrolling.cancel();
+ }
+
+ /**
+ * Gets all the touch events and updates dragging related logic. Note that if this app menu
+ * is initiated by software UI control, then the control should set onTouchListener and forward
+ * all the events to this method because the initial UI control that processed ACTION_DOWN will
+ * continue to get all the subsequent events.
+ *
+ * @param event Touch event to be processed.
+ * @return Whether the event is handled.
+ */
+ boolean handleDragging(MotionEvent event) {
+ if (!mAppMenu.isShowing() || !mDragScrolling.isRunning()) return false;
+
+ // We will only use the screen space coordinate (rawX, rawY) to reduce confusion.
+ // This code works across many different controls, so using local coordinates will be
+ // a disaster.
+
+ final float rawX = event.getRawX();
+ final float rawY = event.getRawY();
+ final int roundedRawX = Math.round(rawX);
+ final int roundedRawY = Math.round(rawY);
+ final int eventActionMasked = event.getActionMasked();
+ final ListView listView = mAppMenu.getPopup().getListView();
+
+ mLastTouchX = rawX;
+ mLastTouchY = rawY;
+
+ if (eventActionMasked == MotionEvent.ACTION_CANCEL) {
+ mAppMenu.dismiss();
+ return true;
+ }
+
+ if (!mIsSingleTapUpHappened) {
+ mGestureSingleTapDetector.onTouchEvent(event);
+ if (mIsSingleTapUpHappened) {
+ finishDragging();
+ }
+ }
+
+ // After this line, drag scrolling is happening.
+ if (!mDragScrolling.isRunning()) return false;
+
+ boolean didPerformClick = false;
+ int itemAction = ITEM_ACTION_CLEAR_HIGHLIGHT_ALL;
+ switch (eventActionMasked) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_MOVE:
+ itemAction = ITEM_ACTION_HIGHLIGHT;
+ break;
+ case MotionEvent.ACTION_UP:
+ itemAction = ITEM_ACTION_PERFORM;
+ break;
+ default:
+ break;
+ }
+ didPerformClick = menuItemAction(roundedRawX, roundedRawY, itemAction);
+
+ if (eventActionMasked == MotionEvent.ACTION_UP && !didPerformClick) {
+ mAppMenu.dismiss();
+ } else if (eventActionMasked == MotionEvent.ACTION_MOVE) {
+ // Auto scrolling on the top or the bottom of the listView.
+ if (listView.getHeight() > 0) {
+ float autoScrollAreaRatio = Math.min(AUTO_SCROLL_AREA_MAX_RATIO,
+ mItemRowHeight * 1.2f / listView.getHeight());
+ float normalizedY =
+ (rawY - getScreenVisibleRect(listView).top) / listView.getHeight();
+ if (normalizedY < autoScrollAreaRatio) {
+ // Top
+ mDragScrollingVelocity = (normalizedY / autoScrollAreaRatio - 1.0f)
+ * mAutoScrollFullVelocity;
+ } else if (normalizedY > 1.0f - autoScrollAreaRatio) {
+ // Bottom
+ mDragScrollingVelocity = ((normalizedY - 1.0f) / autoScrollAreaRatio + 1.0f)
+ * mAutoScrollFullVelocity;
+ } else {
+ // Middle or not scrollable.
+ mDragScrollingVelocity = 0.0f;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Performs the specified action on the menu item specified by the screen coordinate position.
+ * @param screenX X in screen space coordinate.
+ * @param screenY Y in screen space coordinate.
+ * @param action Action type to perform, it should be one of ITEM_ACTION_* constants.
+ * @return true whether or not a menu item is performed (executed).
+ */
+ private boolean menuItemAction(int screenX, int screenY, int action) {
+ ListView listView = mAppMenu.getPopup().getListView();
+
+ ArrayList<View> itemViews = new ArrayList<View>();
+ for (int i = 0; i < listView.getChildCount(); ++i) {
+ boolean hasImageButtons = false;
+ if (listView.getChildAt(i) instanceof LinearLayout) {
+ LinearLayout layout = (LinearLayout) listView.getChildAt(i);
+ for (int j = 0; j < layout.getChildCount(); ++j) {
+ itemViews.add(layout.getChildAt(j));
+ if (layout.getChildAt(j) instanceof ImageButton) hasImageButtons = true;
+ }
+ }
+ if (!hasImageButtons) itemViews.add(listView.getChildAt(i));
+ }
+
+ boolean didPerformClick = false;
+ for (int i = 0; i < itemViews.size(); ++i) {
+ View itemView = itemViews.get(i);
+
+ boolean shouldPerform = itemView.isEnabled() && itemView.isShown() &&
+ getScreenVisibleRect(itemView).contains(screenX, screenY);
+
+ switch (action) {
+ case ITEM_ACTION_HIGHLIGHT:
+ itemView.setPressed(shouldPerform);
+ break;
+ case ITEM_ACTION_PERFORM:
+ if (shouldPerform) {
+ itemView.performClick();
+ didPerformClick = true;
+ }
+ break;
+ case ITEM_ACTION_CLEAR_HIGHLIGHT_ALL:
+ itemView.setPressed(false);
+ break;
+ default:
+ assert false;
+ break;
+ }
+ }
+ return didPerformClick;
+ }
+
+ /**
+ * @return Visible rect in screen coordinates for the given View.
+ */
+ private Rect getScreenVisibleRect(View view) {
+ view.getLocalVisibleRect(mScreenVisibleRect);
+ view.getLocationOnScreen(mScreenVisiblePoint);
+ mScreenVisibleRect.offset(mScreenVisiblePoint[0], mScreenVisiblePoint[1]);
+ return mScreenVisibleRect;
+ }
+}
diff --git a/src/src/com/android/browser/appmenu/AppMenuHandler.java b/src/src/com/android/browser/appmenu/AppMenuHandler.java
new file mode 100644
index 00000000..dd1c1af0
--- /dev/null
+++ b/src/src/com/android/browser/appmenu/AppMenuHandler.java
@@ -0,0 +1,187 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package com.android.browser.appmenu;
+
+import android.app.Activity;
+import android.content.res.TypedArray;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.ContextThemeWrapper;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.PopupMenu;
+
+import org.chromium.base.VisibleForTesting;
+
+import java.util.ArrayList;
+
+/**
+ * Object responsible for handling the creation, showing, hiding of the AppMenu and notifying the
+ * AppMenuObservers about these actions.
+ */
+public class AppMenuHandler {
+ private AppMenu mAppMenu;
+ private AppMenuDragHelper mAppMenuDragHelper;
+ private Menu mMenu;
+ private final ArrayList<AppMenuObserver> mObservers;
+ private final int mMenuResourceId;
+
+ private final AppMenuPropertiesDelegate mDelegate;
+ private final Activity mActivity;
+ private boolean mInvalidateInProgress = false;
+
+ /**
+ * Constructs an AppMenuHandler object.
+ * @param activity Activity that is using the AppMenu.
+ * @param delegate Delegate used to check the desired AppMenu properties on show.
+ * @param menuResourceId Resource Id that should be used as the source for the menu items.
+ * It is assumed to have back_menu_id, forward_menu_id, bookmark_this_page_id.
+ */
+ public AppMenuHandler(Activity activity, AppMenuPropertiesDelegate delegate,
+ int menuResourceId) {
+ mActivity = activity;
+ mDelegate = delegate;
+ mObservers = new ArrayList<AppMenuObserver>();
+ mMenuResourceId = menuResourceId;
+ }
+
+ /**
+ * Show the app menu.
+ * @param anchorView Anchor view (usually a menu button) to be used for the popup.
+ * @param isByHardwareButton True if hardware button triggered it. (oppose to software
+ * button)
+ * @param startDragging Whether dragging is started. For example, if the app menu is
+ * showed by tapping on a button, this should be false. If it is
+ * showed by start dragging down on the menu button, this should
+ * be true. Note that if isByHardwareButton is true, this must
+ * be false since we no longer support hardware menu button
+ * dragging.
+ * @return True, if the menu is shown, false, if menu is not shown, example reasons:
+ * the menu is not yet available to be shown, or the menu is already showing.
+ */
+ public boolean showAppMenu(View anchorView, boolean isByHardwareButton, boolean startDragging) {
+ assert !(isByHardwareButton && startDragging);
+ if (!mDelegate.shouldShowAppMenu() || isAppMenuShowing()) return false;
+
+ if (mMenu == null) {
+ // Use a PopupMenu to create the Menu object. Note this is not the same as the
+ // AppMenu (mAppMenu) created below.
+ PopupMenu tempMenu = new PopupMenu(mActivity, anchorView);
+ tempMenu.inflate(mMenuResourceId);
+ mMenu = tempMenu.getMenu();
+ }
+ mDelegate.prepareMenu(mMenu);
+
+ ContextThemeWrapper wrapper = new ContextThemeWrapper(mActivity,
+ mDelegate.getMenuThemeResourceId());
+
+ if (mAppMenu == null) {
+ TypedArray a = wrapper.obtainStyledAttributes(new int[]
+ {android.R.attr.listPreferredItemHeightSmall, android.R.attr.listDivider});
+ int itemRowHeight = a.getDimensionPixelSize(0, 0);
+ Drawable itemDivider = a.getDrawable(1);
+ int itemDividerHeight = itemDivider != null ? itemDivider.getIntrinsicHeight() : 0;
+ a.recycle();
+ mAppMenu = new AppMenu(mMenu, itemRowHeight, itemDividerHeight, this,
+ mActivity.getResources());
+ mAppMenuDragHelper = new AppMenuDragHelper(mActivity, mAppMenu, itemRowHeight);
+ }
+
+ // Get the height and width of the display.
+ Rect appRect = new Rect();
+ mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(appRect);
+
+ // Use full size of window for abnormal appRect.
+ if (appRect.left < 0 && appRect.top < 0) {
+ appRect.left = 0;
+ appRect.top = 0;
+ appRect.right = mActivity.getWindow().getDecorView().getWidth();
+ appRect.bottom = mActivity.getWindow().getDecorView().getHeight();
+ }
+ int rotation = mActivity.getWindowManager().getDefaultDisplay().getRotation();
+ Point pt = new Point();
+ mActivity.getWindowManager().getDefaultDisplay().getSize(pt);
+ mAppMenu.show(wrapper, anchorView, isByHardwareButton, rotation, appRect, pt.y);
+ return true;
+ }
+
+ public void invalidateAppMenu() {
+ if (!isAppMenuShowing()) return;
+ if (mInvalidateInProgress) return;
+
+ mInvalidateInProgress = true;
+
+ assert(mMenu != null);
+ assert(mAppMenu != null);
+ mDelegate.prepareMenu(mMenu);
+
+ ContextThemeWrapper wrapper = new ContextThemeWrapper(mActivity,
+ mDelegate.getMenuThemeResourceId());
+
+ mAppMenu.invalidate(wrapper, mMenu);
+ mInvalidateInProgress = false;
+ }
+
+ void appMenuDismissed() {
+ }
+
+ /**
+ * @return Whether the App Menu is currently showing.
+ */
+ public boolean isAppMenuShowing() {
+ return mAppMenu != null && mAppMenu.isShowing();
+ }
+
+ /**
+ * @return The App Menu that the menu handler is interacting with.
+ */
+ @VisibleForTesting
+ AppMenu getAppMenu() {
+ return mAppMenu;
+ }
+
+ AppMenuDragHelper getAppMenuDragHelper() {
+ return null;
+ }
+
+ /**
+ * Requests to hide the App Menu.
+ */
+ public void hideAppMenu() {
+ if (mAppMenu != null && mAppMenu.isShowing()) mAppMenu.dismiss();
+ }
+
+ /**
+ * Adds the observer to App Menu.
+ * @param observer Observer that should be notified about App Menu changes.
+ */
+ public void addObserver(AppMenuObserver observer) {
+ mObservers.add(observer);
+ }
+
+ /**
+ * Removes the observer from the App Menu.
+ * @param observer Observer that should no longer be notified about App Menu changes.
+ */
+ public void removeObserver(AppMenuObserver observer) {
+ mObservers.remove(observer);
+ }
+
+ void onOptionsItemSelected(MenuItem item) {
+ mActivity.onOptionsItemSelected(item);
+ }
+
+ /**
+ * Called by AppMenu to report that the App Menu visibility has changed.
+ * @param isVisible Whether the App Menu is showing.
+ */
+ void onMenuVisibilityChanged(boolean isVisible) {
+ for (int i = 0; i < mObservers.size(); ++i) {
+ mObservers.get(i).onMenuVisibilityChanged(isVisible);
+ }
+ }
+}
diff --git a/src/src/com/android/browser/appmenu/AppMenuItemIcon.java b/src/src/com/android/browser/appmenu/AppMenuItemIcon.java
new file mode 100644
index 00000000..ceecf10b
--- /dev/null
+++ b/src/src/com/android/browser/appmenu/AppMenuItemIcon.java
@@ -0,0 +1,46 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package com.android.browser.appmenu;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+/**
+ * A menu icon that supports the checkable state.
+ */
+class AppMenuItemIcon extends ImageView {
+ private static final int[] CHECKED_STATE_SET = new int[] {android.R.attr.state_checked};
+ private boolean mCheckedState;
+
+ public AppMenuItemIcon(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /**
+ * Sets whether the item is checked and refreshes the View if necessary.
+ */
+ protected void setChecked(boolean state) {
+ if (state == mCheckedState) return;
+ mCheckedState = state;
+ refreshDrawableState();
+ }
+
+ @Override
+ public void setPressed(boolean state) {
+ // We don't want to highlight the checkbox icon since the parent item is already
+ // highlighted.
+ return;
+ }
+
+ @Override
+ public int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+ if (mCheckedState) {
+ mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+ }
+ return drawableState;
+ }
+} \ No newline at end of file
diff --git a/src/src/com/android/browser/appmenu/AppMenuObserver.java b/src/src/com/android/browser/appmenu/AppMenuObserver.java
new file mode 100644
index 00000000..febca8ff
--- /dev/null
+++ b/src/src/com/android/browser/appmenu/AppMenuObserver.java
@@ -0,0 +1,16 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package com.android.browser.appmenu;
+
+/**
+ * Allows monitoring of application menu actions.
+ */
+public interface AppMenuObserver {
+ /**
+ * Informs when the App Menu visibility changes.
+ * @param isVisible Whether the menu is now visible.
+ */
+ public void onMenuVisibilityChanged(boolean isVisible);
+}
diff --git a/src/src/com/android/browser/appmenu/AppMenuPropertiesDelegate.java b/src/src/com/android/browser/appmenu/AppMenuPropertiesDelegate.java
new file mode 100644
index 00000000..e094a826
--- /dev/null
+++ b/src/src/com/android/browser/appmenu/AppMenuPropertiesDelegate.java
@@ -0,0 +1,29 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package com.android.browser.appmenu;
+
+import android.view.Menu;
+
+/**
+ * Interface for the App Handler to query the desired state of the App Menu.
+ */
+public interface AppMenuPropertiesDelegate {
+
+ /**
+ * @return Whether the App Menu should be shown.
+ */
+ boolean shouldShowAppMenu();
+
+ /**
+ * Allows the delegate to show and hide items before the App Menu is shown.
+ * @param mMenu Menu that will be used as the source for the App Menu pop up.
+ */
+ void prepareMenu(Menu mMenu);
+
+ /**
+ * @return The theme resource to use for displaying the App Menu.
+ */
+ int getMenuThemeResourceId();
+}
diff --git a/src/src/com/android/browser/appmenu/OWNERS b/src/src/com/android/browser/appmenu/OWNERS
new file mode 100644
index 00000000..99f087eb
--- /dev/null
+++ b/src/src/com/android/browser/appmenu/OWNERS
@@ -0,0 +1,2 @@
+aurimas@chromium.org
+kkimlabs@chromium.org
diff --git a/src/src/com/android/browser/homepages/HomeProvider.java b/src/src/com/android/browser/homepages/HomeProvider.java
new file mode 100644
index 00000000..291348d6
--- /dev/null
+++ b/src/src/com/android/browser/homepages/HomeProvider.java
@@ -0,0 +1,126 @@
+
+/*
+ * 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.BrowserConfig;
+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 = BrowserConfig.AUTHORITY + ".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;
+ }
+
+}
diff --git a/src/src/com/android/browser/homepages/RequestHandler.java b/src/src/com/android/browser/homepages/RequestHandler.java
new file mode 100644
index 00000000..e084f4ad
--- /dev/null
+++ b/src/src/com/android/browser/homepages/RequestHandler.java
@@ -0,0 +1,270 @@
+
+/*
+ * 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.nio.charset.Charset;
+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);
+ private static final Charset UTF8_CHARSET = Charset.forName("UTF-8");
+
+ 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(UTF8_CHARSET);
+ }
+
+ // 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) {
+ id = res.getIdentifier(fileName, null, mContext.getPackageName());
+ }
+
+ 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/src/com/android/browser/homepages/Template.java b/src/src/com/android/browser/homepages/Template.java
new file mode 100644
index 00000000..60cfe2f0
--- /dev/null
+++ b/src/src/com/android/browser/homepages/Template.java
@@ -0,0 +1,284 @@
+
+/*
+ * 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();
+ 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 {
+ final String packageName = R.class.getPackage().getName();
+ int id = res.getIdentifier(name, null, packageName);
+ if(id == 0) {
+ id = res.getIdentifier(name, null, context.getPackageName());
+ }
+ 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/src/com/android/browser/mdm/AutoFillRestriction.java b/src/src/com/android/browser/mdm/AutoFillRestriction.java
new file mode 100644
index 00000000..6fe9bbc1
--- /dev/null
+++ b/src/src/com/android/browser/mdm/AutoFillRestriction.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2015, 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.mdm;
+
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.util.Log;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.PreferenceKeys;
+
+public class AutoFillRestriction extends Restriction implements PreferenceKeys {
+
+ private final static String TAG = "AutoFillRestriction";
+
+ public static final String AUTO_FILL_RESTRICTION_ENABLED = "AutoFillRestrictionEnabled";
+ public static final String AUTO_FILL_ALLOWED = "AutoFillAllowed";
+
+ private static AutoFillRestriction sInstance;
+ private MdmCheckBoxPreference mPref = null;
+
+ private boolean m_bAfAllowed;
+
+ private AutoFillRestriction() {
+ super(TAG);
+ }
+
+ public static AutoFillRestriction getInstance() {
+ synchronized (AutoFillRestriction.class) {
+ if (sInstance == null) {
+ sInstance = new AutoFillRestriction();
+ }
+ }
+ return sInstance;
+ }
+
+ @Override
+ protected void doCustomInit() {
+ }
+
+
+ public void registerPreference (Preference pref) {
+ mPref = (MdmCheckBoxPreference) pref;
+ updatePref();
+ }
+
+ private void updatePref() {
+ if (null != mPref) {
+ if (isEnabled()) {
+ mPref.setChecked(getValue());
+ mPref.disablePref();
+ }
+ else {
+ mPref.enablePref();
+ }
+ mPref.setMdmRestrictionState(isEnabled());
+ }
+ }
+
+ @Override
+ public void enforce(Bundle restrictions) {
+ SharedPreferences.Editor editor = BrowserSettings.getInstance().getPreferences().edit();
+
+ boolean restrictionEnabled = restrictions.getBoolean(AUTO_FILL_RESTRICTION_ENABLED, false);
+ enable(restrictionEnabled);
+
+ if(isEnabled()) {
+ m_bAfAllowed = true;
+ if (restrictions.containsKey(AUTO_FILL_ALLOWED)) {
+ m_bAfAllowed = restrictions.getBoolean(AUTO_FILL_ALLOWED);
+ }
+
+ Log.i(TAG, "Enforce [" + m_bAfAllowed + "]");
+
+ editor.putBoolean(PREF_AUTOFILL_ENABLED, m_bAfAllowed);
+ editor.apply();
+ }
+ updatePref();
+ }
+
+ public boolean getValue() {
+ return m_bAfAllowed;
+ }
+}
diff --git a/src/src/com/android/browser/mdm/DevToolsRestriction.java b/src/src/com/android/browser/mdm/DevToolsRestriction.java
new file mode 100644
index 00000000..36acce83
--- /dev/null
+++ b/src/src/com/android/browser/mdm/DevToolsRestriction.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2015, 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.mdm;
+
+import android.os.Bundle;
+import android.util.Log;
+
+import org.codeaurora.swe.Engine;
+
+public class DevToolsRestriction extends Restriction {
+
+ private final static String TAG = "DevToolsRestriction";
+
+ public static final String DEV_TOOLS_RESTRICTION = "DevToolsEnabled";
+
+ private static DevToolsRestriction sInstance;
+
+ private DevToolsRestriction() {
+ super(TAG);
+ }
+
+ public static DevToolsRestriction getInstance() {
+ synchronized (DevToolsRestriction.class) {
+ if (sInstance == null) {
+ sInstance = new DevToolsRestriction();
+ }
+ }
+ return sInstance;
+ }
+
+ @Override
+ protected void doCustomInit() {
+ }
+
+ /*
+ * Note reversed logic:
+ * [x] 'Restrict' true = DevToolsEnabled : false => disable DevTools in swe
+ * [ ] 'Restrict' false = DevToolsEnabled : true => enable DevTools in swe
+ */
+ @Override
+ public void enforce(Bundle restrictions) {
+
+ boolean bEnable = false;
+ if (restrictions.containsKey(DEV_TOOLS_RESTRICTION)) {
+ bEnable = ! restrictions.getBoolean(DEV_TOOLS_RESTRICTION);
+ }
+ Log.d(TAG, "Enforce [" + bEnable + "]");
+ enable(bEnable);
+
+ Engine.setWebContentsDebuggingEnabled(!isEnabled());
+ }
+}
diff --git a/src/src/com/android/browser/mdm/DoNotTrackRestriction.java b/src/src/com/android/browser/mdm/DoNotTrackRestriction.java
new file mode 100644
index 00000000..c1e56054
--- /dev/null
+++ b/src/src/com/android/browser/mdm/DoNotTrackRestriction.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2015, 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.mdm;
+
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.Preference;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.PreferenceKeys;
+
+public class DoNotTrackRestriction extends Restriction implements PreferenceKeys {
+
+ private final static String TAG = "DoNotTrackRestriction";
+
+ public static final String DO_NOT_TRACK_ENABLED = "DoNotTrackEnabled"; // boolean
+ public static final String DO_NOT_TRACK_VALUE = "DoNotTrackValue"; // boolean
+
+ private static DoNotTrackRestriction sInstance;
+ private boolean mDntValue;
+
+ private MdmCheckBoxPreference mPref = null;
+
+ private DoNotTrackRestriction() {
+ super(TAG);
+ }
+
+ public static DoNotTrackRestriction getInstance() {
+ synchronized (DoNotTrackRestriction.class) {
+ if (sInstance == null) {
+ sInstance = new DoNotTrackRestriction();
+ }
+ }
+ return sInstance;
+ }
+
+ @Override
+ public void enforce(Bundle restrictions) {
+ SharedPreferences.Editor editor = BrowserSettings.getInstance().getPreferences().edit();
+ // Possible states
+ // DNT_enabled DNT_value | menu-item-enabled check-box-value
+ // -----------------------------------------------------------------
+ // not set x | Yes curr-sys-value
+ // 0 x | Yes curr-sys-value
+ // 1 0 | No 0
+ // 1 1 | No 1
+
+ boolean dntEnabled = restrictions.getBoolean(DO_NOT_TRACK_ENABLED,false);
+ if (dntEnabled) {
+ mDntValue = restrictions.getBoolean(DO_NOT_TRACK_VALUE, true); // default to true
+
+ editor.putBoolean(PREF_DO_NOT_TRACK, mDntValue);
+ editor.apply();
+
+ // enable the restriction : controls enable of the menu item
+ // Log.i(TAG, "DNT Restriction enabled. new val [" + mDntValue + "]");
+ enable(true);
+ }
+ else {
+ enable(false);
+ }
+
+ // Real time update of the Preference if it is registered
+ updatePref();
+ }
+
+ private void updatePref() {
+ if (null != mPref) {
+ if (isEnabled()) {
+ mPref.setChecked(getValue());
+ mPref.disablePref();
+ }
+ else {
+ mPref.enablePref();
+ }
+ mPref.setMdmRestrictionState(isEnabled());
+ }
+ }
+
+ public boolean getValue() {
+ return mDntValue;
+ }
+
+ public void registerPreference (Preference pref) {
+ mPref = (MdmCheckBoxPreference) pref;
+ updatePref();
+ }
+}
diff --git a/src/src/com/android/browser/mdm/DownloadDirRestriction.java b/src/src/com/android/browser/mdm/DownloadDirRestriction.java
new file mode 100644
index 00000000..696b0b9f
--- /dev/null
+++ b/src/src/com/android/browser/mdm/DownloadDirRestriction.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2015, 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.mdm;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import com.android.browser.Browser;
+import com.android.browser.R;
+
+public class DownloadDirRestriction extends Restriction {
+ private final static String TAG = "DownloadDirRestriction";
+ public static final String RESTRICTION_ENABLED = "DownloadRestrictionEnabled";
+ public static final String DOWNLOADS_ALLOWED = "DownloadsAllowed";
+ public static final String DOWNLOADS_DIR = "DownloadDirectory";
+
+ private static DownloadDirRestriction sInstance;
+
+ public static final boolean defaultDownloadsAllowed = true;
+ public static String defaultDownloadDir;
+
+ private String mCurrDownloadDir;
+ private boolean mCurrDownloadsAllowed;
+
+ private DownloadDirRestriction() {
+ super(TAG);
+ }
+
+ @Override
+ protected void doCustomInit() {
+ mCurrDownloadsAllowed = defaultDownloadsAllowed;
+ Context c = Browser.getContext();
+ defaultDownloadDir = c.getString(R.string.download_default_path);
+ mCurrDownloadDir = defaultDownloadDir;
+ }
+
+ public static DownloadDirRestriction getInstance() {
+ synchronized (DownloadDirRestriction.class) {
+ if (sInstance == null) {
+ sInstance = new DownloadDirRestriction();
+ }
+ }
+ return sInstance;
+ }
+
+ @Override
+ public void enforce(Bundle restrictions) {
+ enable(restrictions.getBoolean(RESTRICTION_ENABLED, false));
+
+ if (isEnabled()) {
+ mCurrDownloadsAllowed = restrictions.getBoolean(DOWNLOADS_ALLOWED, defaultDownloadsAllowed);
+ mCurrDownloadDir = restrictions.getString(DOWNLOADS_DIR, defaultDownloadDir);
+ }
+ else {
+ doCustomInit();
+ }
+ }
+
+ public boolean downloadsAllowed() {
+ return mCurrDownloadsAllowed;
+ }
+
+ public String getDownloadDirectory() {
+ return mCurrDownloadDir;
+ }
+}
diff --git a/src/src/com/android/browser/mdm/EditBookmarksRestriction.java b/src/src/com/android/browser/mdm/EditBookmarksRestriction.java
new file mode 100644
index 00000000..bbfe88a6
--- /dev/null
+++ b/src/src/com/android/browser/mdm/EditBookmarksRestriction.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) 2015, 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.mdm;
+
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Button;
+import android.widget.ExpandableListView;
+
+import java.util.ArrayList;
+
+public class EditBookmarksRestriction extends Restriction {
+
+ private final static String TAG = "EditBookmarkRestriction";
+
+ public static final String EDIT_BOOKMARKS_RESTRICTION = "EditBookmarksEnabled";
+
+ private static EditBookmarksRestriction sInstance;
+
+ private ExpandableListView targetListView;
+ private ArrayList<Button> registeredButtons;
+ private ArrayList<Drawable> registeredDrawables;
+ private ColorStateList initialButtonColors;
+ private int disabledColor;
+
+ private EditBookmarksRestriction() {
+ super(TAG);
+ }
+
+ public static EditBookmarksRestriction getInstance() {
+ synchronized (EditBookmarksRestriction.class) {
+ if (sInstance == null) {
+ sInstance = new EditBookmarksRestriction();
+ }
+ }
+ return sInstance;
+ }
+
+ @Override
+ protected void doCustomInit() {
+ targetListView = null;
+ registeredButtons = new ArrayList<>();
+ registeredDrawables = new ArrayList<>();
+ initialButtonColors = null;
+ disabledColor = Color.parseColor("grey");
+ }
+
+ public void registerView(ExpandableListView f) {
+ targetListView = f;
+ }
+
+ public void registerControl(Button v) {
+ if (initialButtonColors == null) {
+ // we assume both buttons will have the same color info
+ initialButtonColors = v.getTextColors();
+ }
+ if (!registeredButtons.contains(v)) {
+ registeredButtons.add(v);
+ }
+ updateUiElems();
+ }
+
+ public void registerControl(Drawable d) {
+ if (!registeredDrawables.contains(d)) {
+ registeredDrawables.add(d);
+ }
+ updateUiElems();
+ }
+
+ private void updateUiElems() {
+ if (null != registeredButtons) {
+ for (Button v : registeredButtons) {
+ if (null != v) {
+ if (isEnabled()) {
+ v.setTextColor(disabledColor);
+ }
+ else {
+ v.setTextColor(initialButtonColors);
+ }
+ }
+ }
+ }
+ if (null != registeredDrawables) {
+ for (Drawable d : registeredDrawables) {
+ if (null != d) {
+ d.setAlpha((isEnabled() ? 0x80 : 0xff));
+ }
+ }
+ }
+ if (targetListView != null) {
+ targetListView.invalidateViews();
+ }
+ }
+
+ /*
+ * Note reversed logic:
+ * [x] 'Restrict' true = EditBookmarksEnabled : false => disable editing in swe
+ * [ ] 'Restrict' false = EditBookmarksEnabled : true => enable editing in swe
+ */
+ @Override
+ public void enforce(Bundle restrictions) {
+ boolean bEnable = false;
+ if (restrictions.containsKey(EDIT_BOOKMARKS_RESTRICTION)) {
+ bEnable = ! restrictions.getBoolean(EDIT_BOOKMARKS_RESTRICTION);
+ }
+ Log.i(TAG, "Enforce [" + bEnable + "]");
+ enable(bEnable);
+
+ updateUiElems();
+ }
+}
diff --git a/src/src/com/android/browser/mdm/IncognitoRestriction.java b/src/src/com/android/browser/mdm/IncognitoRestriction.java
new file mode 100644
index 00000000..ee2f2e63
--- /dev/null
+++ b/src/src/com/android/browser/mdm/IncognitoRestriction.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2015, 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.mdm;
+
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.View;
+
+import java.util.ArrayList;
+
+public class IncognitoRestriction extends Restriction {
+
+ private final static String TAG = "IncognitoRestriction";
+
+ public static final String INCOGNITO_RESTRICTION_ENABLED = "IncognitoRestrictionEnabled"; // boolean
+
+ private static IncognitoRestriction sInstance;
+
+ private ArrayList<View> registeredViews;
+ private ArrayList<Drawable> registeredDrawables;
+
+ private IncognitoRestriction() {
+ super(TAG);
+ registeredViews = new ArrayList<>();
+ registeredDrawables = new ArrayList<>();
+ }
+
+ public static IncognitoRestriction getInstance() {
+ synchronized (IncognitoRestriction.class) {
+ if (sInstance == null) {
+ sInstance = new IncognitoRestriction();
+ }
+ }
+ return sInstance;
+ }
+
+ @Override
+ public void enforce(Bundle restrictions) {
+ enable(restrictions.getBoolean(INCOGNITO_RESTRICTION_ENABLED, false));
+ updateButton();
+ }
+
+ public void registerControl(View v) {
+ if (!registeredViews.contains(v)) {
+ registeredViews.add(v);
+ }
+ updateButton();
+ }
+
+ public void registerControl(Drawable d) {
+ if (!registeredDrawables.contains(d)) {
+ registeredDrawables.add(d);
+ }
+ updateButton();
+ }
+
+ private void updateButton() {
+ if (null != registeredViews) {
+ for (View v : registeredViews) {
+ if (null != v) {
+ v.setAlpha((float) (isEnabled() ? 0.5 : 1.0));
+ }
+ }
+ }
+ if (null != registeredDrawables) {
+ for (Drawable d : registeredDrawables) {
+ if (null != d) {
+ d.setAlpha((isEnabled() ? 0x80 : 0xff));
+ }
+ }
+ }
+ }
+
+ // For testing
+ public float getButtonAlpha() {
+ View v = registeredViews.get(0);
+ return v != null ? v.getAlpha() : (float) 1.0;
+ }
+}
diff --git a/src/src/com/android/browser/mdm/ManagedBookmarksRestriction.java b/src/src/com/android/browser/mdm/ManagedBookmarksRestriction.java
new file mode 100644
index 00000000..84c21a72
--- /dev/null
+++ b/src/src/com/android/browser/mdm/ManagedBookmarksRestriction.java
@@ -0,0 +1,441 @@
+/*
+ * Copyright (c) 2015, 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.mdm;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.browser.platformsupport.BrowserContract;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import static org.chromium.base.ApplicationStatus.getApplicationContext;
+
+public class ManagedBookmarksRestriction extends Restriction {
+
+ private final static String TAG = "+++MngdBookmarks_Rest";
+
+ public static final String MANAGED_BOOKMARKS = "ManagedBookmarks";
+ private static final String FOLDER_URL_KEY = "MDM";
+ private static ManagedBookmarksRestriction sInstance;
+ private String mJsonBookmarks;
+ public BookmarksDb mDb;
+ private boolean mCreatedMdmBookmarks;
+
+ private ManagedBookmarksRestriction() {
+ super(TAG);
+ }
+
+ public static ManagedBookmarksRestriction getInstance() {
+ synchronized (ManagedBookmarksRestriction.class) {
+ if (sInstance == null) {
+ sInstance = new ManagedBookmarksRestriction();
+ }
+ }
+ return sInstance;
+ }
+
+ @Override
+ protected void doCustomInit() {
+ mDb = new BookmarksDb();
+ mCreatedMdmBookmarks = false;
+ }
+
+ public class BookmarksDb {
+ private ContentResolver mCr = null;
+
+ public class CRException extends Exception {
+ public CRException(String s) {
+ super(s);
+ }
+ }
+ private ContentResolver cr() throws CRException {
+ if (mCr == null) {
+ mCr = getApplicationContext().getContentResolver();
+ }
+ if (mCr == null) {
+ throw new CRException("Null ContentResolver");
+ }
+ return mCr;
+ }
+
+ public Cursor queryById(long id, String[] projections) {
+ String where = BrowserContract.Bookmarks._ID + " = " + id;
+ Cursor c = null;
+ try {
+ c = cr().query(BrowserContract.Bookmarks.CONTENT_URI,
+ projections, // projections... the columns we want. null means all
+ where, // where clause (without the WHERE keyword)
+ null, // selectionArgs
+ null); // sortOrder
+ } catch (android.database.sqlite.SQLiteException e) {
+ Log.e(TAG, "queryById SQL Exception: [" + e.toString() + "]");
+ } catch (CRException e) {
+ Log.e(TAG, "queryById CR Exception: [" + e.toString() + "]");
+ }
+ return c;
+ }
+
+ public void deleteItemById(long id) {
+ String where = BrowserContract.Bookmarks._ID + " = " + id;
+ try {
+ cr().delete(BrowserContract.Bookmarks.CONTENT_URI, where, null);
+ }
+ catch (android.database.sqlite.SQLiteException e) {
+ Log.e(TAG, "deleteItemById Exception: [" + e.toString() + "]");
+ } catch (CRException e) {
+ Log.e(TAG, "deleteItemById CR Exception: [" + e.toString() + "]");
+ }
+ }
+
+ public long getMdmRootFolderId() {
+ long result = -1;
+ String projections[] = new String[] {BrowserContract.Bookmarks._ID};
+ String where = BrowserContract.Bookmarks.TITLE + " = 'Managed' AND " +
+ BrowserContract.Bookmarks.IS_FOLDER + " = 1 AND " +
+ BrowserContract.Bookmarks.URL + " like '" + FOLDER_URL_KEY + "%' AND " +
+ BrowserContract.Bookmarks.PARENT + " = 1";
+ try {
+ Cursor c = cr().query(BrowserContract.Bookmarks.CONTENT_URI,
+ projections, // projections... the columns we want. null means all
+ where, // where clause (without the WHERE keyword)
+ null, // selectionArgs
+ null); // sortOrder
+ if (c.getCount() != 0) {
+ c.moveToFirst();
+ result = c.getLong(0);
+ }
+ c.close();
+ } catch (android.database.sqlite.SQLiteException e) {
+ Log.e(TAG, "getMdmRootFolderId SQL Exception: [" + e.toString() + "]");
+ } catch (CRException e) {
+ Log.e(TAG, "getMdmRootFolderId CR Exception: [" + e.toString() + "]");
+ }
+ return result;
+ }
+
+ public Cursor getChildrenForMdmFolder(long id, String [] projections) {
+ String where = BrowserContract.Bookmarks.PARENT + " = " + id;
+ Cursor c = null;
+ try {
+ c = cr().query(BrowserContract.Bookmarks.CONTENT_URI,
+ projections, // projections... the columns we want. null means all
+ where, // where clause (without the WHERE keyword)
+ null, // selectionArgs
+ null); // sortOrder
+ } catch (android.database.sqlite.SQLiteException e) {
+ Log.e(TAG, "getChildrenForMdmFolder SQL Exception: [" + e.toString() + "]");
+ } catch (CRException e) {
+ Log.e(TAG, "getChildrenForMdmFolder CR Exception: [" + e.toString() + "]");
+ }
+ return c;
+ }
+
+ private void addBookmark(String title, long parent, String url) {
+ ContentValues values = new ContentValues();
+ values.put(BrowserContract.Bookmarks.PARENT, parent);
+ values.put(BrowserContract.Bookmarks.TITLE, title);
+ values.put(BrowserContract.Bookmarks.URL, url);
+
+ try {
+ Uri uri = cr().insert(BrowserContract.Bookmarks.CONTENT_URI, values);
+ if (uri == null) {
+ Log.e(TAG, "Bookmark '" + title + "' creation failed.");
+ }
+ } catch (android.database.sqlite.SQLiteException e) {
+ Log.e(TAG, "addBookmark-SQL Exception during creation: [" + e.toString() + "]");
+ } catch (CRException e) {
+ Log.e(TAG, "addBookmark CR Exception: [" + e.toString() + "]");
+ }
+ }
+
+ private boolean bookmarksAlreadyEnabled(int hash) {
+ boolean ret = false;
+ String incomingHash = String.valueOf(hash);
+ long rootId = getMdmRootFolderId();
+ if (rootId != -1) {
+ Cursor c = queryById(rootId, new String[] {BrowserContract.Bookmarks.URL});
+ if (c.getCount() == 1) {
+ c.moveToFirst();
+ String url = c.getString(0);
+ if (url.contains(incomingHash)) {
+ ret = true;
+ }
+ }
+ c.close();
+ }
+ return ret;
+ }
+
+ private void addFolder(String title, long parent, JSONArray children, int hash) {
+ ContentValues values = new ContentValues();
+ values.put(BrowserContract.Bookmarks.PARENT, parent);
+ values.put(BrowserContract.Bookmarks.TITLE, title);
+ values.put(BrowserContract.Bookmarks.IS_FOLDER, 1);
+
+ // We are using the URL field (normally not used for folders) to
+ // lock down that this folder is managed by Mdm. This is certainly
+ // non-standard, but the alternative would be to modify the database schema,
+ // which would become a maintenance headache later when google updates the schema.
+ if (hash != 0) {
+ // We add the hash of the json string to the root folder. We use this
+ // to check if we already have this bookmark set enabled.
+ values.put(BrowserContract.Bookmarks.URL, FOLDER_URL_KEY + ":" + hash);
+ }
+ else {
+ values.put(BrowserContract.Bookmarks.URL, FOLDER_URL_KEY);
+ }
+
+ try {
+ Uri uri = cr().insert(BrowserContract.Bookmarks.CONTENT_URI, values);
+ if (uri != null) {
+ long nodeId = ContentUris.parseId(uri);
+
+ if (children != null) {
+ for (int i = 0; i < children.length(); i++) {
+ JSONObject j = children.getJSONObject(i);
+
+ // if it has a URL, then it's a bookmark
+ if (j.has("url")) {
+ String t = j.getString("name");
+ String u = j.getString("url");
+ mDb.addBookmark(t, nodeId, u);
+ }
+ // if it has children, then it's a subfolder
+ else if (j.has("children")) {
+ String t = j.getString("name");
+ JSONArray ja = new JSONArray(j.getString("children"));
+ addFolder(t, nodeId, ja, 0);
+ } else {
+ Log.e(TAG, "Parse error processing children for [" + title + "]");
+ }
+ }
+ }
+ } else {
+ Log.e(TAG, "Folder creation failed.");
+ }
+ } catch (android.database.sqlite.SQLiteException e) {
+ Log.e(TAG, "addFolder-SQL Exception during creation: [" + e.toString() + "]");
+ } catch (JSONException e) {
+ Log.e(TAG, "addFolder-JSON exception during creation: [" + e.toString() + "]");
+ } catch (CRException e) {
+ Log.e(TAG, "addFolder CR Exception: [" + e.toString() + "]");
+ }
+ }
+
+ public boolean isMdmElement(long id) {
+ boolean ret = false;
+
+ String[] projections =
+ new String[]{
+ BrowserContract.Bookmarks.IS_FOLDER,
+ BrowserContract.Bookmarks.URL,
+ BrowserContract.Bookmarks.PARENT};
+
+ Cursor c = mDb.queryById(id, projections);
+ if(1 == c.getCount()) {
+ c.moveToFirst();
+ int isFolder = c.getInt(0);
+ String url = c.getString(1);
+ long parent = c.getLong(2);
+
+ if (isFolder != 0) {
+ if (url != null && url.startsWith(FOLDER_URL_KEY)) {
+ ret = true;
+ }
+ }
+ else {
+ Cursor cp = mDb.queryById(parent, projections);
+ if(1 == cp.getCount()) {
+ cp.moveToFirst();
+ String u2 = cp.getString(1);
+ if (u2 != null && u2.startsWith(FOLDER_URL_KEY)) {
+ ret = true;
+ }
+ }
+ else {
+ Log.e(TAG,"isMdmElement: Invalid parent id ["+id+"]");
+ }
+ }
+ }
+ else {
+ Log.e(TAG,"isMdmElement: Invalid id ["+id+"]");
+ }
+ c.close();
+ return ret;
+ }
+
+ private void deleteTree(long folderId) {
+ if (folderId == -1) {
+ Log.i(TAG, " deleteTree: no tree to delete.");
+ return;
+ }
+
+ String[] projections =
+ new String[]{
+ BrowserContract.Bookmarks.IS_FOLDER,
+ BrowserContract.Bookmarks._ID};
+
+ try {
+ Cursor c = getChildrenForMdmFolder(folderId, projections);
+
+ int n = c.getCount();
+ if (n != 0) {
+ c.moveToFirst();
+ for (int i = 0; i < n; i++) {
+ int isFolder = c.getInt(0);
+ int itemId = c.getInt(1);
+
+ if (isFolder == 1) {
+ deleteTree(itemId);
+ }
+ else {
+ mDb.deleteItemById(itemId);
+ }
+ c.moveToNext();
+ }
+ }
+ else {
+ Log.i(TAG,"DeleteTree: no children for id["+folderId+"]");
+ }
+ c.close();
+
+ // now that the contents have been deleted, delete this folder
+ mDb.deleteItemById(folderId);
+ } catch (android.database.sqlite.SQLiteException e) {
+ Log.e(TAG, "deleteTree SQL Exception: [" + e.toString() + "]");
+ }
+ }
+ }
+
+ /* For Debugging
+ public void dumpCursor(Cursor c) {
+ int n = c.getCount();
+ int pos = c.getPosition();
+
+ if (n != 0) {
+ String cols[] = c.getColumnNames();
+ Log.i(TAG, " ********* Dumping " + n + " records *************");
+ c.moveToFirst();
+ for (int i = 0; i < n; i++) {
+ Log.i(TAG," == Record ["+i+"]");
+ for (String col : cols) {
+ int ndx = c.getColumnIndex(col);
+ switch (c.getType(ndx)) {
+ case Cursor.FIELD_TYPE_NULL:
+ Log.i(TAG, " " + col + " = <null>");
+ break;
+ case Cursor.FIELD_TYPE_BLOB:
+ Log.i(TAG, " " + col + " = <blob>");
+ break;
+ case Cursor.FIELD_TYPE_FLOAT:
+ Log.i(TAG, " " + col + " = " +
+ c.getFloat(ndx));
+ break;
+ case Cursor.FIELD_TYPE_INTEGER:
+ Log.i(TAG, " " + col + " = " +
+ c.getInt(ndx));
+ break;
+ case Cursor.FIELD_TYPE_STRING:
+ Log.i(TAG, " " + col + " = " +
+ c.getString(ndx));
+ break;
+ }
+ }
+ c.moveToNext();
+ }
+ Log.i(TAG, " ********* END Dump *************");
+ }
+ c.moveToPosition(pos); // restore incoming position
+ } */
+
+ private void removeManagedBookmarks() {
+ mDb.deleteTree(mDb.getMdmRootFolderId());
+ }
+
+ public boolean bookmarksWereCreated() {
+ return mCreatedMdmBookmarks;
+ }
+
+ private void addManagedBookmarks() {
+ String name = "Managed";
+ int rootFolder = 1;
+ mCreatedMdmBookmarks = false;
+
+ int hash = mJsonBookmarks.hashCode();
+ if (! mDb.bookmarksAlreadyEnabled(hash)) {
+ Log.i(TAG, ">>>>>>> BOOKMARKS NOT ALREADY ENABLED <<<<<<<<<<<<");
+ removeManagedBookmarks();
+ JSONArray dict = null;
+ try {
+ dict = new JSONArray(mJsonBookmarks);
+ } catch (JSONException e) {
+ Log.w(TAG, "addManagedBookmarks: Incoming JSON didn't parse. Creating empty folder." + e.toString());
+ }
+
+ mDb.addFolder(name, rootFolder, dict, hash);
+
+ mCreatedMdmBookmarks = true;
+ }
+ else {
+ Log.w(TAG, ">>>>>>> BOOKMARKS ALREADY ENABLED <<<<<<<<<<<<");
+ mCreatedMdmBookmarks = false;
+ }
+ }
+
+ @Override
+ public void enforce(Bundle restrictions) {
+ mJsonBookmarks = restrictions.getString(MANAGED_BOOKMARKS);
+
+ // Enable if we got something in the json bookmarks string
+ enable(!(mJsonBookmarks == null || mJsonBookmarks.isEmpty()));
+
+ Log.i(TAG, "Enforcing. enabled[" + isEnabled() + "]. val[" + mJsonBookmarks + "]");
+
+ if(isEnabled()){
+ addManagedBookmarks();
+ }
+ else {
+ removeManagedBookmarks();
+ }
+ }
+
+ public String getValue() {
+ return mJsonBookmarks;
+ }
+}
diff --git a/src/src/com/android/browser/mdm/ManagedProfileManager.java b/src/src/com/android/browser/mdm/ManagedProfileManager.java
new file mode 100644
index 00000000..46fda0b2
--- /dev/null
+++ b/src/src/com/android/browser/mdm/ManagedProfileManager.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (c) 2015, 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.mdm;
+
+import android.annotation.TargetApi;
+import android.app.admin.DevicePolicyManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.UserManager;
+import android.util.Log;
+
+import com.android.browser.Browser;
+
+import org.codeaurora.swe.util.Observable;
+
+/**
+ * Restrictions manager and merger. Layers 3 levels of restrictions:
+ *
+ * 1. Device administrator policies (device administrators are for example the Email app, or
+ * traditional MDMs)
+ *
+ * 2. User restricted profile policies (different users, such as the 'guest' user can have
+ * restrictions on APPs). Note that we don't advertise the restrictions, which is a
+ * possibility allowed by the framework and which can be used by parents to restrict certain
+ * aspects of APPs to children, for example.
+ *
+ * 3. Provisioned properties, for example from the 'Android at Work' project in which
+ * strings with an understood meaning are available for the apps to be read.
+ *
+ * Persistency: The Android framework makes sure the properties will persist even in case of
+ * uninstalling the application (just for 3, 2 and 1 are app independent).
+ *
+ * Availability: Restrictions are available right after creating this class. If a delegate is
+ * passed in the constructor it will be notified of events, such as the change in
+ * provisioned policies (3).
+ *
+ */
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+public class ManagedProfileManager extends Observable {
+
+ private final static String TAG = "ManagedProfileManager";
+
+ // this string is literal because not publicly defined as of API 21
+ private final static String INTENT_ACTION_POLICY_CHANGE = "android.intent.action.APPLICATION_RESTRICTIONS_CHANGED";
+
+ /* Generic platform restriction keys. Booleans default to false, missing from bundle means unrestricted */
+ private static final String VOLUME_CHANGE_DISABLE = "VolumeChangeDisable"; // boolean
+ private static final String VPN_CHANGE_DISABLED = "VpnChangeDisabled"; // boolean
+ private static final String LOCATION_DISABLED = "LocationDisabled"; // boolean
+ private static final String CAMERA_DISABLED = "CameraDisabled"; // boolean
+ private static final String MICROPHONE_DISABLED = "MicrophoneDisabled"; // boolean
+ private static final String SCREEN_CAPTURE_DISABLED = "ScreenCaptureDisabled"; // boolean
+ private static final String WEB_DEVELOPMENT_DISABLED = "WebDevelopmentDisabled"; // boolean
+ private static final String STORAGE_ENCRYPTION_REQUIRED = "StorageEncryptionRequired"; // boolean
+ private static final String PASSWORDS_MINIMUM_LENGTH = "PasswordsMinimumLength"; // integer
+ private static final String PASSWORDS_MINIMUM_LETTERS = "PasswordsMinimumLetters"; // integer
+ private static final String PASSWORDS_MINIMUM_NON_LETTERS = "PasswordsMinimumNonLetters"; // integer
+
+ private static ManagedProfileManager sInstance = null;
+
+ private final Context mContext;
+
+ private final DevicePolicyManager mDevicePolicyManager;
+ private final UserManager mUserPolicyManager;
+
+ // cached policies; the first two won't change during execution, the third may
+ private Bundle mDeviceAdministratorRestrictions;
+ private Bundle mUserRestrictions;
+ private Bundle mMdmProvisioningRestrictions;
+
+ private BroadcastReceiver mMdmBroadcastReceiver;
+
+ public static ManagedProfileManager getInstance() {
+ if (sInstance == null)
+ sInstance = new ManagedProfileManager(Browser.getContext());
+ return sInstance;
+ }
+
+ private ManagedProfileManager(Context context) {
+ mContext = context;
+
+ mDevicePolicyManager = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
+ mUserPolicyManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+
+ // Fetch restrictions
+ getMdmPackageRestrictions(mContext.getPackageName());
+ getDeviceAdministratorRestrictions();
+ getUserRestrictions();
+
+ mergeRestrictions();
+
+ // listen for any change
+ registerMdmPolicyChangeListener();
+ }
+
+ /**
+ * Reads the restrictions which are set by the MDM Administrator
+ * @param packageName
+ */
+ private void getMdmPackageRestrictions(String packageName) {
+ try {
+ // no need to map the MDM restrictions since they're already in the right format
+ // the keys in the bundle have the values of the Restriction.* constants
+ mMdmProvisioningRestrictions =
+ mUserPolicyManager.getApplicationRestrictions(packageName);
+ } catch (SecurityException e) {
+ // Only the system can get/set restrictions on other apps
+ }
+ }
+
+ /**
+ * Reads and maps any Android User Restriction (e.g. 'user can't use the microphone')
+ */
+ private void getUserRestrictions() {
+ Bundle b;
+ try {
+ b = mUserPolicyManager.getUserRestrictions();
+ } catch (Exception e) {
+ return;
+ }
+
+ // map pertinent user restrictions to our restrictions (the list comes from the
+ // UserManager doc, and was last revised for API 21)
+ mUserRestrictions = new Bundle();
+ if (Build.VERSION.SDK_INT >= 18) {
+ if (b.getBoolean(UserManager.DISALLOW_SHARE_LOCATION, false))
+ mUserRestrictions.putBoolean(LOCATION_DISABLED, true);
+ }
+ if (Build.VERSION.SDK_INT >= 21) {
+ if (b.getBoolean(UserManager.DISALLOW_ADJUST_VOLUME, false))
+ mUserRestrictions.putBoolean(VOLUME_CHANGE_DISABLE, true);
+ if (b.getBoolean(UserManager.DISALLOW_CONFIG_VPN, false))
+ mUserRestrictions.putBoolean(VPN_CHANGE_DISABLED, true);
+ if (b.getBoolean(UserManager.DISALLOW_DEBUGGING_FEATURES, false))
+ mUserRestrictions.putBoolean(WEB_DEVELOPMENT_DISABLED, true);
+ if (b.getBoolean(UserManager.DISALLOW_UNMUTE_MICROPHONE, false))
+ mUserRestrictions.putBoolean(MICROPHONE_DISABLED, true);
+ }
+ }
+
+ /**
+ * Reads and maps any Android Device Restriction (e.g. 'this device can't access location'),
+ * which are set by legacy MDMs.
+ */
+ @SuppressWarnings("ConstantConditions")
+ private void getDeviceAdministratorRestrictions() {
+ mDeviceAdministratorRestrictions = new Bundle();
+
+ // We are checking all the administrators, not just targeting one in particular - we'll
+ // get the more restrictive set of values
+ final ComponentName n = null;
+
+ try {
+ // map pertinent administrators restrictions to our restrictions
+ if (mDevicePolicyManager.getCameraDisabled(n))
+ mDeviceAdministratorRestrictions.putBoolean(CAMERA_DISABLED, true);
+ int passwordMinimumLength = mDevicePolicyManager.getPasswordMinimumLength(n);
+ if (passwordMinimumLength > 0)
+ mDeviceAdministratorRestrictions.putInt(PASSWORDS_MINIMUM_LENGTH,
+ passwordMinimumLength);
+ int passwordMinimumLetters = mDevicePolicyManager.getPasswordMinimumLetters(n);
+ // default minimum number of letters required is 1
+ if (passwordMinimumLetters > 1)
+ mDeviceAdministratorRestrictions.putInt(PASSWORDS_MINIMUM_LETTERS,
+ passwordMinimumLetters);
+ int passwordMinimumNonletters = mDevicePolicyManager.getPasswordMinimumNonLetter(n);
+ if (passwordMinimumNonletters > 0)
+ mDeviceAdministratorRestrictions.putInt(PASSWORDS_MINIMUM_NON_LETTERS,
+ passwordMinimumNonletters);
+ // NOTE: there are more passwords requirement which haven't been parsed yet because
+ // the author deemed that superfluous
+ if (mDevicePolicyManager.getStorageEncryption(n))
+ mDeviceAdministratorRestrictions.putBoolean(STORAGE_ENCRYPTION_REQUIRED, true);
+
+ if (Build.VERSION.SDK_INT >= 21) {
+ if (mDevicePolicyManager.getScreenCaptureDisabled(n))
+ mDeviceAdministratorRestrictions.putBoolean(SCREEN_CAPTURE_DISABLED, true);
+ }
+ } catch (Exception e) {
+ // better safe than sorry
+ Log.e(TAG, "Error reading from the policy manager: " + e.getMessage());
+ }
+ }
+
+ public void onActivityDestroy() {
+ unregisterMdmPolicyChangeListener();
+ }
+
+ /* private implementation ahead */
+
+ private void mergeRestrictions() {
+ // Merge restrictions
+ Bundle restrictions = new Bundle();
+ if (mDeviceAdministratorRestrictions != null)
+ restrictions.putAll(mDeviceAdministratorRestrictions);
+ if (mUserRestrictions != null)
+ restrictions.putAll(mUserRestrictions);
+ if (mMdmProvisioningRestrictions != null)
+ restrictions.putAll(mMdmProvisioningRestrictions);
+
+ // notify observers
+ set(restrictions);
+ }
+
+ private void registerMdmPolicyChangeListener() {
+ // create a listener that acts upon receiving data
+ mMdmBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (action == INTENT_ACTION_POLICY_CHANGE) {
+ getMdmPackageRestrictions(mContext.getPackageName());
+ mergeRestrictions();
+ }
+ }
+ };
+
+ // listen in broadcast
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(INTENT_ACTION_POLICY_CHANGE);
+ mContext.registerReceiver(mMdmBroadcastReceiver, filter);
+ }
+
+ private void unregisterMdmPolicyChangeListener() {
+ if (mMdmBroadcastReceiver != null) {
+ mContext.unregisterReceiver(mMdmBroadcastReceiver);
+ mMdmBroadcastReceiver = null;
+ }
+ }
+
+ /**
+ * Added for testing
+ * @param restrictions The set of restrictions to apply.
+ */
+ public void setMdmRestrictions(Bundle restrictions) {
+ mMdmProvisioningRestrictions = restrictions;
+ mergeRestrictions();
+ }
+}
diff --git a/src/src/com/android/browser/mdm/MdmCheckBoxPreference.java b/src/src/com/android/browser/mdm/MdmCheckBoxPreference.java
new file mode 100644
index 00000000..91f9e038
--- /dev/null
+++ b/src/src/com/android/browser/mdm/MdmCheckBoxPreference.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) 2015, 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.mdm;
+
+import android.content.Context;
+import android.preference.CheckBoxPreference;
+import android.preference.Preference;
+import android.preference.SwitchPreference;
+import android.util.AttributeSet;
+//import android.util.Log;
+import android.view.View;
+import android.widget.Toast;
+
+import com.android.browser.R;
+
+public class MdmCheckBoxPreference extends SwitchPreference {
+
+ View mView = null;
+ OnPreferenceClickListener mOrigClickListener = null;
+ boolean mMdmRestrictionState;
+ private boolean mPrefEnabled = true;
+
+ public MdmCheckBoxPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public MdmCheckBoxPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public MdmCheckBoxPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public MdmCheckBoxPreference(Context context) {
+ super(context);
+ }
+
+ public void setMdmRestrictionState(boolean val) {
+ // Log.i("+++", "setMdmRestrictionState(" + val + ")");
+ mMdmRestrictionState = val;
+ }
+
+ public void disablePref() {
+ // Log.i("+++", "disablePref(): mView[" +
+ // (mView != null ? "OK" : "Null") +
+ // "] mPrefEnabled[" + mPrefEnabled + "]");
+
+ if (null != mView && mPrefEnabled == true) {
+ // Set the onClick listener that will present the toast message to the user
+ mOrigClickListener = getOnPreferenceClickListener();
+
+ // Log.i("+++", "Setting toast");
+ setOnPreferenceClickListener( new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ Toast.makeText(getContext(), R.string.mdm_managed_alert, Toast.LENGTH_SHORT).show();
+ return true;
+ }
+ });
+
+ // Prevent clicks from registering. We can't use setEnable() because
+ // we need the click to trigger the toast message.
+ setOnPreferenceChangeListener( new OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ return false; // Do Not update
+ }
+ });
+
+ // Dim the view. setEnable usually does this for us, but we can't
+ // use it here.
+ mView.setAlpha((float) 0.5);
+ mPrefEnabled = false;
+
+ }
+ }
+
+ public void enablePref () {
+ // Log.i("+++", "enablePref(): mView[" +
+ // (mView != null ? "OK" : "Null") +
+ // "] mPrefEnabled[" + mPrefEnabled + "]");
+
+ if (null != mView && mPrefEnabled == false) {
+ setOnPreferenceClickListener(mOrigClickListener);
+ setOnPreferenceChangeListener(null);
+ mView.setAlpha((float) 1.0);
+ mPrefEnabled = true;
+ }
+ }
+
+ @Override
+ protected void onBindView(View view) {
+ // Log.i("+++", "onBindView() : rs[" + mMdmRestrictionState + "]");
+ super.onBindView(view);
+
+ mView = view;
+ if (mMdmRestrictionState) {
+ disablePref();
+ }
+ }
+}
diff --git a/src/src/com/android/browser/mdm/ProxyRestriction.java b/src/src/com/android/browser/mdm/ProxyRestriction.java
new file mode 100644
index 00000000..1a4b5915
--- /dev/null
+++ b/src/src/com/android/browser/mdm/ProxyRestriction.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (c) 2015, 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.mdm;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.Log;
+
+import com.android.browser.Browser;
+import com.android.browser.PreferenceKeys;
+
+public class ProxyRestriction extends Restriction implements PreferenceKeys {
+
+ private final static String TAG = "ProxyRestriction";
+
+ // These strings are duplicated in chromium.net.ProxyChangeListener.java
+ // (duplication due to layer policy)
+ // If you need to change/add/delete, make sure they stay in sync
+ public static final String MDM_PROXY_CHANGE_ACTION = "swe.mdm.intent.action.PROXY_CHANGE";
+ public static final String PROXY_MODE = "ProxyMode";
+ public static final String MODE_DIRECT = "direct";
+ public static final String MODE_SYSTEM = "system";
+ public static final String MODE_AUTO_DETECT = "auto_detect";
+ public static final String MODE_FIXED_SERVERS = "fixed_servers";
+ public static final String MODE_PAC_SCRIPT = "pac_script";
+
+ public static final String PROXY_SERVER = "ProxyServer";
+ public static final String PROXY_PORT = "ProxyPort";
+ public static final String PROXY_PAC_URL = "ProxyPacUrl";
+ public static final String PROXY_BYPASS_LIST = "ProxyBypassList";
+
+ private static ProxyRestriction sInstance;
+
+ private ProxyRestriction() {
+ super(TAG);
+ }
+
+ public static ProxyRestriction getInstance() {
+ synchronized (ProxyRestriction.class) {
+ if (sInstance == null) {
+ sInstance = new ProxyRestriction();
+ }
+ }
+ return sInstance;
+ }
+
+ @Override
+ public void enforce(Bundle restrictions) {
+ String proxyMode = restrictions.getString(PROXY_MODE);
+
+ // Leaving ProxyMode not set lifts any managed profile proxy restrictions, allowing users to
+ // choose the proxy settings on their own.
+ if (proxyMode == null) {
+ Log.v(TAG, "enforce: proxyMode is null, disabling.");
+ saveProxyConfig(false, null, null, -1, null, null);
+ }
+
+ // If policy is to not use the proxy and always connect directly, then all other options
+ // are ignored.
+ else if (proxyMode.equals(MODE_DIRECT)) {
+ Log.v(TAG, "enforce: proxyMode is MODE_DIRECT, enabling and passing to ProxyChangeListener.");
+ saveProxyConfig(true, proxyMode, null, -1, null, null);
+ }
+
+ // If you choose to use system proxy settings or auto detect the proxy server,
+ // all other options are ignored.
+ else if (proxyMode.equals(MODE_SYSTEM) ||
+ proxyMode.equals(MODE_AUTO_DETECT)) {
+ saveProxyConfig(true, proxyMode, null, -1, null, null);
+ }
+
+ // If you choose fixed server proxy mode, you can specify further options in 'Address or URL
+ // of proxy server' and 'Comma-separated list of proxy bypass rules'.
+ else if (proxyMode.equals(MODE_FIXED_SERVERS)) {
+ String host;
+ int port;
+ try {
+ Uri proxyServerUri = Uri.parse(restrictions.getString(PROXY_SERVER));
+ host = proxyServerUri.getHost();
+ // Bail out if host is not present
+ if (host == null) {
+ Log.e(TAG, "enforce: host == null while processing MODE_FIXED_SERVERS");
+ saveProxyConfig(false, null, null, -1, null, null);
+ return;
+ }
+ port = proxyServerUri.getPort();
+ } catch (Exception e) {
+ // Bail out if ProxyServer string is missing
+ Log.e(TAG,"enforce: Exception caught while processing MODE_FIXED_SERVERS");
+ saveProxyConfig(false, null, null, -1, null, null);
+ return;
+ }
+ String proxyBypassList = restrictions.getString(PROXY_BYPASS_LIST);
+ Log.v(TAG,"enforce: saving MODE_FIXED_SERVERS proxy config: ");
+ Log.v(TAG," - host : " + host);
+ Log.v(TAG," - port : " + port);
+ Log.v(TAG," - bypassList : " + (proxyBypassList != null ? proxyBypassList : "NULL"));
+
+ saveProxyConfig(true, proxyMode, host, port, null, proxyBypassList);
+ }
+
+ // This policy only takes effect if you have selected manual proxy settings at 'Choose how
+ // to specify proxy server settings'. You should leave this policy not set if you have
+ // selected any other mode for setting proxy policies.
+ else if (proxyMode.equals(MODE_PAC_SCRIPT)) {
+ String proxyPacUrl = restrictions.getString(PROXY_PAC_URL);
+ // Bail out if ProxyPacUrl string is missing
+ if (proxyPacUrl == null) {
+ Log.v(TAG, "enforce: MODE_PAC_SCRIPT. proxyPacUrl is null. disabling");
+ saveProxyConfig(false, null, null, -1, null, null);
+
+ } else {
+ Log.v(TAG, "enforce: MODE_PAC_SCRIPT. proxyPacUrl ["+ proxyPacUrl +
+ "]. sending and enabling");
+ saveProxyConfig(true, proxyMode, null, -1, proxyPacUrl, null);
+ }
+ }
+ }
+
+ private void saveProxyConfig(boolean isEnabled, String proxyMode, String host,
+ int port, String proxyPacUrl, String proxyBypassList) {
+
+ enable(isEnabled);
+
+ Intent proxySignal = new Intent(MDM_PROXY_CHANGE_ACTION);
+
+ proxySignal.putExtra(PROXY_MODE, isEnabled ? proxyMode : null);
+
+ if (isEnabled) {
+ proxySignal.putExtra(PROXY_SERVER, host);
+ proxySignal.putExtra(PROXY_PORT, port);
+ proxySignal.putExtra(PROXY_PAC_URL, proxyPacUrl);
+ proxySignal.putExtra(PROXY_BYPASS_LIST, proxyBypassList);
+ }
+
+ // Send the broadcast. We use LocalBroadcastManager because it is more secure
+ // and it provides a handy synchronous call.
+ LocalBroadcastManager.getInstance(Browser.getContext()).sendBroadcastSync(proxySignal);
+ }
+}
diff --git a/src/src/com/android/browser/mdm/Restriction.java b/src/src/com/android/browser/mdm/Restriction.java
new file mode 100644
index 00000000..a9e4285f
--- /dev/null
+++ b/src/src/com/android/browser/mdm/Restriction.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2015, 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.mdm;
+
+import android.os.Bundle;
+import android.util.Log;
+
+import org.codeaurora.swe.util.Activator;
+import org.codeaurora.swe.util.Observable;
+
+/**
+ * Abstract implementation of a restriction set by a Mobile Device Management (MDM) agent on browser
+ * instances running in a managed profile. A subclass must implement the abstract method enforce()
+ * to set the restriction.
+ */
+public abstract class Restriction {
+
+ private boolean mEnabled = false;
+
+ public Restriction(String s) {
+ // Register observer for restrictions
+ Log.i("+++", "["+ s + "] is registering it's observer");
+ doCustomInit();
+ Activator.activate(new Observable.Observer() {
+ @Override
+ public void onChange(Object... params) {
+ if (params[0] != null) {
+ enforce((Bundle) params[0]);
+ }
+ }
+ }, ManagedProfileManager.getInstance());
+ }
+
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ public void enable(boolean enable) {
+ mEnabled = enable;
+ }
+
+ abstract public void enforce(Bundle restrictions);
+
+ protected void doCustomInit() {}
+}
+
diff --git a/src/src/com/android/browser/mdm/SearchEngineRestriction.java b/src/src/com/android/browser/mdm/SearchEngineRestriction.java
new file mode 100644
index 00000000..3f0eef87
--- /dev/null
+++ b/src/src/com/android/browser/mdm/SearchEngineRestriction.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2015, 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.mdm;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+
+import com.android.browser.Browser;
+import com.android.browser.BrowserSettings;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+import com.android.browser.search.SearchEngineInfo;
+import com.android.browser.search.SearchEngines;
+
+public class SearchEngineRestriction extends Restriction implements PreferenceKeys {
+
+ private final static String TAG = "SearchEngineRestriction";
+
+ private static final String DEFAULT_SEARCH_PROVIDER_ENABLED = "DefaultSearchProviderEnabled"; // boolean
+ private static final String SEARCH_PROVIDER_NAME = "SearchProviderName"; // String
+
+ private static SearchEngineRestriction sInstance;
+
+ private SearchEngineInfo mSearchEngineInfo;
+
+ private SearchEngineRestriction() {
+ super(TAG);
+ }
+
+ public static SearchEngineRestriction getInstance() {
+ synchronized (SearchEngineRestriction.class) {
+ if (sInstance == null) {
+ sInstance = new SearchEngineRestriction();
+ }
+ }
+ return sInstance;
+ }
+
+ @Override
+ public void enforce(Bundle restrictions) {
+ SharedPreferences.Editor editor = BrowserSettings.getInstance().getPreferences().edit();
+ String searchEngineName = restrictions.getString(SEARCH_PROVIDER_NAME);
+ SearchEngineInfo searchEngineInfo = null;
+ Context context = Browser.getContext();
+
+ if (searchEngineName != null)
+ searchEngineInfo = SearchEngines.getSearchEngineInfo(context, searchEngineName);
+
+ if (restrictions.getBoolean(DEFAULT_SEARCH_PROVIDER_ENABLED, false) &&
+ searchEngineInfo != null) {
+ mSearchEngineInfo = searchEngineInfo;
+ // Set search engine
+ editor.putString(PREF_SEARCH_ENGINE, searchEngineName);
+ editor.apply();
+ enable(true);
+ } else if (isEnabled()) {
+ mSearchEngineInfo = null;
+ enable(false);
+ // Restore default search engine
+ editor.putString(PREF_SEARCH_ENGINE,
+ context.getString(R.string.default_search_engine_value));
+ editor.apply();
+ }
+ }
+
+ public SearchEngineInfo getSearchEngineInfo() {
+ return mSearchEngineInfo;
+ }
+}
diff --git a/src/src/com/android/browser/mdm/ThirdPartyCookiesRestriction.java b/src/src/com/android/browser/mdm/ThirdPartyCookiesRestriction.java
new file mode 100644
index 00000000..43bf6bd1
--- /dev/null
+++ b/src/src/com/android/browser/mdm/ThirdPartyCookiesRestriction.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2015, 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.mdm;
+
+import android.os.Bundle;
+import android.preference.Preference;
+import android.util.Log;
+
+import org.codeaurora.swe.MdmManager;
+
+public class ThirdPartyCookiesRestriction extends Restriction {
+
+ private final static String TAG = "TPC_Restriction";
+
+ public static final String TPC_ENABLED = "ThirdPartyCookiesRestrictionEnabled"; // boolean
+ public static final String TPC_ALLOWED = "AllowThirdPartyCookies"; // boolean
+
+ private static ThirdPartyCookiesRestriction sInstance;
+ private boolean mAllowTpc;
+ private MdmCheckBoxPreference mPref = null;
+
+ private ThirdPartyCookiesRestriction() {
+ super(TAG);
+ }
+
+ public static ThirdPartyCookiesRestriction getInstance() {
+ synchronized (ThirdPartyCookiesRestriction.class) {
+ if (sInstance == null) {
+ sInstance = new ThirdPartyCookiesRestriction();
+ }
+ }
+ return sInstance;
+ }
+
+ public void registerPreference (Preference pref) {
+ mPref = (MdmCheckBoxPreference) pref;
+ updatePref();
+ }
+
+ private void updatePref() {
+ if (null != mPref) {
+ if (isEnabled()) {
+ mPref.setChecked(getValue());
+ mPref.disablePref();
+ }
+ else {
+ mPref.enablePref();
+ }
+ mPref.setMdmRestrictionState(isEnabled());
+ }
+ }
+
+ @Override
+ public void enforce(Bundle restrictions) {
+ enable(restrictions.getBoolean(TPC_ENABLED,false));
+ mAllowTpc = restrictions.getBoolean(TPC_ALLOWED, true);
+ Log.d(TAG, "Enforcing. enabled[" + isEnabled() + "]. tpc allowed[" + mAllowTpc + "]");
+
+ // Real time update of the Preference if it is registered
+ updatePref();
+
+ if (isEnabled()) {
+ // Logic in native is "should we block?", so we need to
+ // reverse the logic here.
+ MdmManager.updateMdmThirdPartyCookies(!mAllowTpc);
+ }
+ }
+
+ public boolean getValue() {
+ return mAllowTpc;
+ }
+}
diff --git a/src/src/com/android/browser/mdm/URLFilterRestriction.java b/src/src/com/android/browser/mdm/URLFilterRestriction.java
new file mode 100644
index 00000000..3141247a
--- /dev/null
+++ b/src/src/com/android/browser/mdm/URLFilterRestriction.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2015, 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.mdm;
+
+import android.os.Bundle;
+
+import org.codeaurora.swe.MdmManager;
+
+public class URLFilterRestriction extends Restriction {
+ private final static String TAG = "URLFilterRestriction";
+ public static final String URL_BLACK_LIST = "URLBlackList";
+ public static final String URL_WHITE_LIST = "URLWhiteList";
+ private static URLFilterRestriction sInstance;
+
+ private URLFilterRestriction() {
+ super(TAG);
+ }
+
+ public static URLFilterRestriction getInstance() {
+ synchronized (URLFilterRestriction.class) {
+ if (sInstance == null) {
+ sInstance = new URLFilterRestriction();
+ }
+ }
+ return sInstance;
+ }
+
+ @Override
+ public void enforce(Bundle restrictions) {
+ String urlBlackList = restrictions.getString(URL_BLACK_LIST);
+ String urlWhiteList = restrictions.getString(URL_WHITE_LIST);
+
+ MdmManager.updateMdmUrlFilters(urlBlackList, urlWhiteList);
+ }
+}
diff --git a/src/src/com/android/browser/mdm/tests/AutoFillRestrictionsTest.java b/src/src/com/android/browser/mdm/tests/AutoFillRestrictionsTest.java
new file mode 100644
index 00000000..fc1f35a9
--- /dev/null
+++ b/src/src/com/android/browser/mdm/tests/AutoFillRestrictionsTest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2015, 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.mdm.tests;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
+
+import com.android.browser.BrowserActivity;
+import com.android.browser.mdm.AutoFillRestriction;
+import com.android.browser.mdm.ManagedProfileManager;
+
+public class AutoFillRestrictionsTest extends ActivityInstrumentationTestCase2<BrowserActivity> {
+
+ private final static String TAG = "+++AutoFillRestTest";
+
+ private Instrumentation mInstrumentation;
+ private BrowserActivity mActivity;
+ private AutoFillRestriction autoFillRestriction;
+
+ public AutoFillRestrictionsTest() {
+ super(BrowserActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mInstrumentation = getInstrumentation();
+ mActivity = getActivity();
+ autoFillRestriction = AutoFillRestriction.getInstance();
+ }
+
+ public void test_EditBookmarksRestriction() throws Throwable {
+ Log.i(TAG, "*** Starting AutoFillTest ***");
+ clearAutoFillRestrictions();
+ assertFalse(autoFillRestriction.isEnabled());
+
+ setAutoFillRestrictions(true, true);
+ assertTrue(autoFillRestriction.isEnabled());
+ assertTrue(autoFillRestriction.getValue());
+
+ setAutoFillRestrictions(true, false);
+ assertTrue(autoFillRestriction.isEnabled());
+ assertFalse(autoFillRestriction.getValue());
+
+ setAutoFillRestrictions(false, true);
+ assertFalse(autoFillRestriction.isEnabled());
+
+ setAutoFillRestrictions(false, false);
+ assertFalse(autoFillRestriction.isEnabled());
+ }
+
+ /**
+ * Activate EditBookmarks restriction
+ * @param clear if true, sends an empty bundle (which is interpreted as "allow editing"
+ * @param restrictionEnabled Enables the restriction
+ * @param allowed Required. true : allow editing
+ * or false : disallow editing
+ *
+ */
+ private void setAutoFillRestrictions(boolean clear, boolean restrictionEnabled, boolean allowed) {
+ final Bundle restrictions = new Bundle();
+
+ if (!clear) {
+ restrictions.putBoolean(AutoFillRestriction.AUTO_FILL_RESTRICTION_ENABLED, restrictionEnabled);
+ restrictions.putBoolean(AutoFillRestriction.AUTO_FILL_ALLOWED, allowed);
+ }
+
+ // Deliver restriction on UI thread
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ManagedProfileManager.getInstance().setMdmRestrictions(restrictions);
+ }
+ });
+
+ // Wait to ensure restriction is set
+ mInstrumentation.waitForIdleSync();
+ }
+
+ private void clearAutoFillRestrictions() {
+ setAutoFillRestrictions(true, false, false);
+ }
+
+ private void setAutoFillRestrictions(boolean enabled, boolean allowed) {
+ setAutoFillRestrictions(false, enabled, allowed);
+ }
+}
diff --git a/src/src/com/android/browser/mdm/tests/DNTRestrictionsTest.java b/src/src/com/android/browser/mdm/tests/DNTRestrictionsTest.java
new file mode 100644
index 00000000..5778fe38
--- /dev/null
+++ b/src/src/com/android/browser/mdm/tests/DNTRestrictionsTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) 2015, 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.mdm.tests;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
+
+import com.android.browser.BrowserActivity;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.mdm.DoNotTrackRestriction;
+import com.android.browser.mdm.ManagedProfileManager;
+
+public class DNTRestrictionsTest extends ActivityInstrumentationTestCase2<BrowserActivity>
+ implements PreferenceKeys {
+
+ private final static String TAG = "DNTRestrictionsTest";
+
+ private Instrumentation mInstrumentation;
+ private BrowserActivity mActivity;
+ private DoNotTrackRestriction mDNTRestriction;
+
+ public DNTRestrictionsTest() {
+ super(BrowserActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mInstrumentation = getInstrumentation();
+ mActivity = getActivity();
+ mDNTRestriction = DoNotTrackRestriction.getInstance();
+ }
+
+ // Possible states
+ // DNT_enabled DNT_value | menu-item-enabled check-box-value
+ // -----------------------------------------------------------------
+ // not set x | Yes curr-sys-value
+ // 0 x | Yes curr-sys-value
+ // 1 0 | No 0
+ // 1 1 | No 1
+ public void test_DNT() throws Throwable {
+ Log.i(TAG,"!!! ******** Starting DNT Tests *************");
+
+ clearDNTRestrictions();
+ assertFalse(mDNTRestriction.isEnabled());
+
+ setDNTRestrictions(false, true);
+ assertFalse(mDNTRestriction.isEnabled());
+
+ setDNTRestrictions(true, false);
+ assertTrue(mDNTRestriction.isEnabled());
+ assertFalse(mDNTRestriction.getValue());
+
+ setDNTRestrictions(true, true);
+ assertTrue(mDNTRestriction.isEnabled());
+ assertTrue(mDNTRestriction.getValue());
+ }
+
+ /**
+ * Activate DoNotTrack restriction
+ * @param clear boolean. if true, clears the restriction by sending an empty bundle. In
+ * this case, the other args are ignored.
+ *
+ * @param enable boolean. Set the state of the restriction.
+ *
+ * @param value boolean. Set the state of Do Not Track if enabled is set to true.
+ * we still bundle it, but it should be ignored by the handler.
+ */
+ private void setDNTRestrictions(boolean clear, boolean enable, boolean value) {
+ // Construct restriction bundle
+ final Bundle restrictions = new Bundle();
+
+ if(!clear) {
+ restrictions.putBoolean(DoNotTrackRestriction.DO_NOT_TRACK_ENABLED,enable);
+ restrictions.putBoolean(DoNotTrackRestriction.DO_NOT_TRACK_VALUE, value);
+ }
+
+ // Deliver restriction on UI thread
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ManagedProfileManager.getInstance().setMdmRestrictions(restrictions);
+ }
+ });
+
+ // Wait to ensure restriction is set
+ mInstrumentation.waitForIdleSync();
+ }
+
+ private void setDNTRestrictions (boolean enable, boolean value) {
+ setDNTRestrictions(false, enable, value);
+ }
+
+ private void clearDNTRestrictions() {
+ setDNTRestrictions(true, false, false);
+ }
+}
diff --git a/src/src/com/android/browser/mdm/tests/DevToolsRestrictionsTest.java b/src/src/com/android/browser/mdm/tests/DevToolsRestrictionsTest.java
new file mode 100644
index 00000000..06a41177
--- /dev/null
+++ b/src/src/com/android/browser/mdm/tests/DevToolsRestrictionsTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2015, 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.mdm.tests;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
+
+import com.android.browser.BrowserActivity;
+import com.android.browser.mdm.DevToolsRestriction;
+import com.android.browser.mdm.ManagedProfileManager;
+
+public class DevToolsRestrictionsTest extends ActivityInstrumentationTestCase2<BrowserActivity> {
+
+ private final static String TAG = "+++DevToolsRestTest";
+
+ private Instrumentation mInstrumentation;
+ private BrowserActivity mActivity;
+ private DevToolsRestriction devToolsRestriction;
+
+ public DevToolsRestrictionsTest() {
+ super(BrowserActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mInstrumentation = getInstrumentation();
+ mActivity = getActivity();
+ devToolsRestriction = DevToolsRestriction.getInstance();
+ }
+
+ public void test_EditBookmarksRestriction() throws Throwable {
+ Log.i(TAG, "*** Starting DevTools Test ***");
+ clearDevToolsRestrictions();
+ assertFalse(devToolsRestriction.isEnabled());
+
+ setDevToolsRestrictions(true);
+ assertTrue(devToolsRestriction.isEnabled());
+
+ setDevToolsRestrictions(false);
+ assertFalse(devToolsRestriction.isEnabled());
+ }
+
+ /**
+ * Activate DevTools restriction
+ * @param clear if true, sends an empty bundle (which is interpreted as "allow editing"
+ * @param enabled Required. true (disallow editing: restriction enforced)
+ * or false (allow editing: restriction lifted)
+ *
+ */
+ private void setDevToolsRestrictions(boolean clear, boolean enabled) {
+ final Bundle restrictions = new Bundle();
+
+ if (!clear) {
+ // note reversed logic. This is setting 'DevToolsEnabled'
+ // if enabled is true, we want it set to false and vice cersa
+ restrictions.putBoolean(DevToolsRestriction.DEV_TOOLS_RESTRICTION, ! enabled);
+ }
+
+ // Deliver restriction on UI thread
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ManagedProfileManager.getInstance().setMdmRestrictions(restrictions);
+ }
+ });
+
+ // Wait to ensure restriction is set
+ mInstrumentation.waitForIdleSync();
+ }
+
+ private void clearDevToolsRestrictions() {
+ setDevToolsRestrictions(true, false);
+ }
+
+ private void setDevToolsRestrictions(boolean enabled) {
+ setDevToolsRestrictions(false, enabled);
+ }
+}
diff --git a/src/src/com/android/browser/mdm/tests/DownloadDirRestrictionsTest.java b/src/src/com/android/browser/mdm/tests/DownloadDirRestrictionsTest.java
new file mode 100644
index 00000000..743f0178
--- /dev/null
+++ b/src/src/com/android/browser/mdm/tests/DownloadDirRestrictionsTest.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2015, 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.mdm.tests;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.test.ActivityInstrumentationTestCase2;
+
+import com.android.browser.BrowserActivity;
+import com.android.browser.mdm.DownloadDirRestriction;
+import com.android.browser.mdm.ManagedProfileManager;
+
+public class DownloadDirRestrictionsTest extends ActivityInstrumentationTestCase2<BrowserActivity> {
+
+ // private final static String TAG = "IncognitoRestrictionsTest";
+
+ private Instrumentation mInstrumentation;
+ private BrowserActivity mActivity;
+ private DownloadDirRestriction mDDirRestriction;
+
+ public DownloadDirRestrictionsTest() {
+ super(BrowserActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mInstrumentation = getInstrumentation();
+ mActivity = getActivity();
+ mDDirRestriction = DownloadDirRestriction.getInstance();
+ }
+
+ public void test_DownloadDirRestriction() throws Throwable {
+ // Possible states
+ // Enabled Downloads Download | allowed dir
+ // Allowed Dir |
+ // -----------------------------+-------------------
+ // not set n default | y default
+ // not set n /fubar | y default
+ // not set y default | y default
+ // not set y /fubar | y default
+ // No n default | y default
+ // No n /fubar | y default
+ // No y default | y default
+ // No y /fubar | y default
+ // Yes n default | n default (Dir wouldn't be noticed since the dialog won't be seen)
+ // Yes n /fubar | n /fubar (Dir wouldn't be noticed since the dialog won't be seen)
+ // Yes y default | y default
+ // Yes y /fubar | y /fubar
+
+
+ // Initial conditions: no restrictions at all
+ clearAllRestrictions();
+ assertFalse(mDDirRestriction.isEnabled());
+ assertEquals(DownloadDirRestriction.defaultDownloadsAllowed, mDDirRestriction.downloadsAllowed());
+ assertEquals(DownloadDirRestriction.defaultDownloadDir, mDDirRestriction.getDownloadDirectory());
+
+ // Enable not set
+ setDDirRestrictions("NotSet", false, DownloadDirRestriction.defaultDownloadDir);
+ assertFalse(mDDirRestriction.isEnabled());
+ assertEquals(DownloadDirRestriction.defaultDownloadsAllowed, mDDirRestriction.downloadsAllowed());
+ assertEquals(DownloadDirRestriction.defaultDownloadDir, mDDirRestriction.getDownloadDirectory());
+
+ setDDirRestrictions("NotSet", false, "/fubar");
+ assertFalse(mDDirRestriction.isEnabled());
+ assertEquals(DownloadDirRestriction.defaultDownloadsAllowed, mDDirRestriction.downloadsAllowed());
+ assertEquals(DownloadDirRestriction.defaultDownloadDir, mDDirRestriction.getDownloadDirectory());
+
+ setDDirRestrictions("NotSet", true, DownloadDirRestriction.defaultDownloadDir);
+ assertFalse(mDDirRestriction.isEnabled());
+ assertEquals(DownloadDirRestriction.defaultDownloadsAllowed, mDDirRestriction.downloadsAllowed());
+ assertEquals(DownloadDirRestriction.defaultDownloadDir, mDDirRestriction.getDownloadDirectory());
+
+ setDDirRestrictions("NotSet", true, "/fubar");
+ assertFalse(mDDirRestriction.isEnabled());
+ assertEquals(DownloadDirRestriction.defaultDownloadsAllowed, mDDirRestriction.downloadsAllowed());
+ assertEquals(DownloadDirRestriction.defaultDownloadDir, mDDirRestriction.getDownloadDirectory());
+
+ // Enable is false
+ setDDirRestrictions("false", false, DownloadDirRestriction.defaultDownloadDir);
+ assertFalse(mDDirRestriction.isEnabled());
+ assertEquals(DownloadDirRestriction.defaultDownloadsAllowed, mDDirRestriction.downloadsAllowed());
+ assertEquals(DownloadDirRestriction.defaultDownloadDir, mDDirRestriction.getDownloadDirectory());
+
+ setDDirRestrictions("false", false, "/fubar");
+ assertFalse(mDDirRestriction.isEnabled());
+ assertEquals(DownloadDirRestriction.defaultDownloadsAllowed, mDDirRestriction.downloadsAllowed());
+ assertEquals(DownloadDirRestriction.defaultDownloadDir, mDDirRestriction.getDownloadDirectory());
+
+ setDDirRestrictions("false", true, DownloadDirRestriction.defaultDownloadDir);
+ assertFalse(mDDirRestriction.isEnabled());
+ assertEquals(DownloadDirRestriction.defaultDownloadsAllowed, mDDirRestriction.downloadsAllowed());
+ assertEquals(DownloadDirRestriction.defaultDownloadDir, mDDirRestriction.getDownloadDirectory());
+
+ setDDirRestrictions("false", true, "/fubar");
+ assertFalse(mDDirRestriction.isEnabled());
+ assertEquals(DownloadDirRestriction.defaultDownloadsAllowed, mDDirRestriction.downloadsAllowed());
+ assertEquals(DownloadDirRestriction.defaultDownloadDir, mDDirRestriction.getDownloadDirectory());
+
+ // Enable is True
+ setDDirRestrictions("true", false, DownloadDirRestriction.defaultDownloadDir);
+ assertTrue(mDDirRestriction.isEnabled());
+ assertFalse(mDDirRestriction.downloadsAllowed());
+ assertEquals(DownloadDirRestriction.defaultDownloadDir, mDDirRestriction.getDownloadDirectory());
+
+ setDDirRestrictions("true", false, "/fubar");
+ assertTrue(mDDirRestriction.isEnabled());
+ assertFalse(mDDirRestriction.downloadsAllowed());
+ assertEquals("/fubar", mDDirRestriction.getDownloadDirectory());
+
+ setDDirRestrictions("true", true, DownloadDirRestriction.defaultDownloadDir);
+ assertTrue(mDDirRestriction.isEnabled());
+ assertTrue(mDDirRestriction.downloadsAllowed());
+ assertEquals(DownloadDirRestriction.defaultDownloadDir, mDDirRestriction.getDownloadDirectory());
+
+ setDDirRestrictions("true", true, "/fubar");
+ assertTrue(mDDirRestriction.isEnabled());
+ assertTrue(mDDirRestriction.downloadsAllowed());
+ assertEquals("/fubar", mDDirRestriction.getDownloadDirectory());
+ }
+
+ /**
+ * Activate Download Directory restriction
+ * @param enabled Required. "NotSet" | "true" | "false"
+ * @param allow determines if downloads are allowed
+ * @param dir override download dir
+ *
+ */
+ private void setDDirRestrictions(String enabled, boolean allow, String dir) {
+ final Bundle restrictions = new Bundle();
+
+ if (!enabled.equals("NotSet")) {
+ if (enabled.equals("true")) {
+ restrictions.putBoolean(DownloadDirRestriction.RESTRICTION_ENABLED, true);
+ } else if (enabled.equals("false")) {
+ restrictions.putBoolean(DownloadDirRestriction.RESTRICTION_ENABLED, false);
+ }
+ }
+
+ restrictions.putBoolean(DownloadDirRestriction.DOWNLOADS_ALLOWED, allow);
+ restrictions.putString(DownloadDirRestriction.DOWNLOADS_DIR, dir);
+
+ sendRestriction(restrictions);
+ }
+
+ private void clearAllRestrictions() {
+ sendRestriction(new Bundle());
+ }
+
+ private void sendRestriction(final Bundle restrictions) {
+ // Deliver restriction on UI thread
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ManagedProfileManager.getInstance().setMdmRestrictions(restrictions);
+ }
+ });
+
+ // Wait to ensure restriction is set
+ mInstrumentation.waitForIdleSync();
+ }
+}
diff --git a/src/src/com/android/browser/mdm/tests/EditBookmarkRestrictionsTest.java b/src/src/com/android/browser/mdm/tests/EditBookmarkRestrictionsTest.java
new file mode 100644
index 00000000..edc77cbc
--- /dev/null
+++ b/src/src/com/android/browser/mdm/tests/EditBookmarkRestrictionsTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2015, 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.mdm.tests;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
+
+import com.android.browser.BrowserActivity;
+import com.android.browser.mdm.EditBookmarksRestriction;
+import com.android.browser.mdm.ManagedProfileManager;
+
+public class EditBookmarkRestrictionsTest extends ActivityInstrumentationTestCase2<BrowserActivity> {
+
+ private final static String TAG = "+++EdBookmarkRestTest";
+
+ private Instrumentation mInstrumentation;
+ private BrowserActivity mActivity;
+ private EditBookmarksRestriction editBookmarksRestriction;
+
+ public EditBookmarkRestrictionsTest() {
+ super(BrowserActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mInstrumentation = getInstrumentation();
+ mActivity = getActivity();
+ editBookmarksRestriction = EditBookmarksRestriction.getInstance();
+ }
+
+ public void test_EditBookmarksRestriction() throws Throwable {
+ Log.i(TAG, "*** Starting EditBookmarksTest ***");
+ clearBookmarksRestrictions();
+ assertFalse(editBookmarksRestriction.isEnabled());
+
+ setBookmarksRestrictions(true);
+ assertTrue(editBookmarksRestriction.isEnabled());
+
+ setBookmarksRestrictions(false);
+ assertFalse(editBookmarksRestriction.isEnabled());
+ }
+
+ /**
+ * Activate EditBookmarks restriction
+ * @param clear if true, sends an empty bundle (which is interpreted as "allow editing"
+ * @param enabled Required. true (disallow editing: restriction enforced)
+ * or false (allow editing: restriction lifted)
+ *
+ */
+ private void setBookmarksRestrictions(boolean clear, boolean enabled) {
+ final Bundle restrictions = new Bundle();
+
+ if (!clear) {
+ // note reversed logic. This is setting 'EditBookmarksEnabled'
+ // if enabled is true, we want it set to false and vice cersa
+ restrictions.putBoolean(EditBookmarksRestriction.EDIT_BOOKMARKS_RESTRICTION, ! enabled);
+ }
+
+ // Deliver restriction on UI thread
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ManagedProfileManager.getInstance().setMdmRestrictions(restrictions);
+ }
+ });
+
+ // Wait to ensure restriction is set
+ mInstrumentation.waitForIdleSync();
+ }
+
+ private void clearBookmarksRestrictions() {
+ setBookmarksRestrictions(true, false);
+ }
+
+ private void setBookmarksRestrictions(boolean enabled) {
+ setBookmarksRestrictions(false, enabled);
+ }
+}
diff --git a/src/src/com/android/browser/mdm/tests/IncognitoRestrictionsTest.java b/src/src/com/android/browser/mdm/tests/IncognitoRestrictionsTest.java
new file mode 100644
index 00000000..e58d64cd
--- /dev/null
+++ b/src/src/com/android/browser/mdm/tests/IncognitoRestrictionsTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2015, 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.mdm.tests;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.test.ActivityInstrumentationTestCase2;
+
+import com.android.browser.BrowserActivity;
+import com.android.browser.mdm.IncognitoRestriction;
+import com.android.browser.mdm.ManagedProfileManager;
+
+public class IncognitoRestrictionsTest extends ActivityInstrumentationTestCase2<BrowserActivity> {
+
+ private final static String TAG = "IncognitoRestrictionsTest";
+
+ private Instrumentation mInstrumentation;
+ private BrowserActivity mActivity;
+ private IncognitoRestriction mIncognitoRestriction;
+
+ public IncognitoRestrictionsTest() {
+ super(BrowserActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mInstrumentation = getInstrumentation();
+ mActivity = getActivity();
+ mIncognitoRestriction = IncognitoRestriction.getInstance();
+ }
+
+ public void test_IncognitoRestriction() throws Throwable {
+ // Possible states
+ // enabled | button enabled
+ // ---------------------------------
+ // not set | Yes
+ // true | No
+ // false | Yes
+
+ clearIncognitoRestrictions();
+ assertFalse(mIncognitoRestriction.isEnabled());
+ assertEquals((float) 1.0, mIncognitoRestriction.getButtonAlpha());
+
+ setIncognitoRestrictions(true);
+ assertTrue(mIncognitoRestriction.isEnabled());
+ assertEquals((float) 0.5, mIncognitoRestriction.getButtonAlpha());
+
+ setIncognitoRestrictions(false);
+ assertFalse(mIncognitoRestriction.isEnabled());
+ assertEquals((float) 1.0, mIncognitoRestriction.getButtonAlpha());
+ }
+
+ /**
+ * Activate Incognito restriction
+ * @param clear if true, sends an empty bundle
+ * @param enabled Required. true or false
+ *
+ */
+ private void setIncognitoRestrictions(boolean clear, boolean enabled) {
+ final Bundle restrictions = new Bundle();
+
+ if (!clear) {
+ restrictions.putBoolean(IncognitoRestriction.INCOGNITO_RESTRICTION_ENABLED, enabled);
+ }
+
+ // Deliver restriction on UI thread
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ManagedProfileManager.getInstance().setMdmRestrictions(restrictions);
+ }
+ });
+
+ // Wait to ensure restriction is set
+ mInstrumentation.waitForIdleSync();
+ }
+
+ private void clearIncognitoRestrictions() {
+ setIncognitoRestrictions(true, false);
+ }
+
+ private void setIncognitoRestrictions(boolean enabled) {
+ setIncognitoRestrictions(false, enabled);
+ }
+
+}
diff --git a/src/src/com/android/browser/mdm/tests/ManagedBookmarksRestrictionsTest.java b/src/src/com/android/browser/mdm/tests/ManagedBookmarksRestrictionsTest.java
new file mode 100644
index 00000000..37676633
--- /dev/null
+++ b/src/src/com/android/browser/mdm/tests/ManagedBookmarksRestrictionsTest.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (c) 2015, 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.mdm.tests;
+
+import android.app.Instrumentation;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
+
+import com.android.browser.BrowserActivity;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.mdm.ManagedBookmarksRestriction;
+import com.android.browser.mdm.ManagedProfileManager;
+import com.android.browser.platformsupport.BrowserContract;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+public class ManagedBookmarksRestrictionsTest extends ActivityInstrumentationTestCase2<BrowserActivity>
+ implements PreferenceKeys {
+
+ private final static String TAG = "BkmrksRestTest";
+
+ private Instrumentation mInstrumentation;
+ private BrowserActivity mActivity;
+ private ManagedBookmarksRestriction managedBookmarksRestriction;
+ private ManagedBookmarksRestriction.BookmarksDb mDb;
+
+ private ArrayList<bmTuple> mBookmarks;
+ private String mSubDirName;
+
+ public ManagedBookmarksRestrictionsTest() {
+ super(BrowserActivity.class);
+ }
+
+ private class bmTuple {
+ String name;
+ String url;
+ public bmTuple(String n, String u) {
+ name = n;
+ url = u;
+ }
+ public String getName(){ return name;}
+ public String getUrl() { return url;}
+ }
+
+ void initializeTestData() {
+ mBookmarks = new ArrayList<>();
+ // Level 0
+ mBookmarks.add(new bmTuple("Chromium for Snapdragon", "www.codeaurora.org/forums/chromium-snapdragon"));
+ mBookmarks.add(new bmTuple("Chromium Browser for Snapdragon", "www.codeaurora.org/xwiki/bin/Chromium+for+Snapdragon"));
+ mSubDirName = "Repos And Patches";
+
+ // Level 1
+ mBookmarks.add(new bmTuple("Code Aurora git repositories", "www.codeaurora.org/cgit/quic/chrome4sdp"));
+ mBookmarks.add(new bmTuple("Patches", "www.codeaurora.org/patches/quic/chrome4snapdragon"));
+ }
+
+ private String getBookmarksDict(int indent) {
+ JSONArray dict = new JSONArray();
+
+ JSONObject bm_0_0 = new JSONObject();
+ JSONObject bm_0_1 = new JSONObject();
+
+ JSONObject level_1_Folder = new JSONObject();
+ JSONArray level_1_Children = new JSONArray();
+ JSONObject bm_1_0 = new JSONObject();
+ JSONObject bm_1_1 = new JSONObject();
+ try {
+ bm_0_0.put("name", mBookmarks.get(0).getName());
+ bm_0_0.put("url", mBookmarks.get(0).getUrl());
+
+ bm_0_1.put("name", mBookmarks.get(1).getName());
+ bm_0_1.put("url", mBookmarks.get(1).getUrl());
+
+ bm_1_0.put("name", mBookmarks.get(2).getName());
+ bm_1_0.put("url", mBookmarks.get(2).getUrl());
+
+ bm_1_1.put("name", mBookmarks.get(3).getName());
+ bm_1_1.put("url", mBookmarks.get(3).getUrl());
+
+ level_1_Children.put(bm_1_0);
+ level_1_Children.put(bm_1_1);
+
+ level_1_Folder.put("name", mSubDirName);
+ level_1_Folder.put("children", level_1_Children);
+
+ dict.put(bm_0_0);
+ dict.put(bm_0_1);
+ dict.put(level_1_Folder);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
+ String ret = null;
+ try {
+ ret = (indent != 0 ? dict.toString(indent): dict.toString());
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
+ return ret;
+ }
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mInstrumentation = getInstrumentation();
+ mActivity = getActivity();
+ managedBookmarksRestriction = ManagedBookmarksRestriction.getInstance();
+ mDb = managedBookmarksRestriction.mDb;
+ initializeTestData();
+ }
+
+ public void test_MB() throws Throwable {
+ Log.i(TAG,"!!! ******** Starting Managed Bookmark Tests *************");
+
+ clearMBRestrictions();
+ assertFalse(managedBookmarksRestriction.isEnabled());
+ assertNull(managedBookmarksRestriction.getValue());
+
+ setMBRestrictions(true);
+ assertTrue(managedBookmarksRestriction.isEnabled());
+ assertEquals(getBookmarksDict(0), managedBookmarksRestriction.getValue());
+
+ // Now check the DB for our bookmark records
+ assertTrue(managedBookmarksRestriction.bookmarksWereCreated());
+ long rootId = mDb.getMdmRootFolderId();
+ assertFalse(rootId == -1);
+ assertTrue(mDb.isMdmElement(rootId));
+
+ String[] projections = new String[] {
+ BrowserContract.Bookmarks.URL,
+ BrowserContract.Bookmarks.IS_FOLDER,
+ BrowserContract.Bookmarks.TITLE,
+ BrowserContract.Bookmarks._ID,
+ };
+
+ long chromeLinksId = -1; // we need the folder id for the 2nd level checks
+
+ //
+ // Check Level 0
+ //
+ Cursor c = mDb.getChildrenForMdmFolder(rootId, projections);
+ int n = c.getCount();
+ assertTrue(n == 3);
+
+ for (c.moveToFirst(); !c.isAfterLast(); c.moveToNext()) {
+ String url = c.getString(0);
+ int isFolder = c.getInt(1);
+ String title = c.getString(2);
+ long id = c.getLong(3);
+
+ assertTrue(mDb.isMdmElement(id));
+
+ boolean found = false;
+ for (bmTuple t : mBookmarks){
+ if (t.getName().equals(title)) {
+ found = true;
+ assertEquals(t.getUrl(), url);
+ assertEquals(0, isFolder);
+ }
+ }
+
+ if (title.equals(mSubDirName)) {
+ assertEquals("MDM", url);
+ assertEquals(1, isFolder);
+ chromeLinksId = id;
+ found = true;
+ }
+ if (!found) {
+ assertFalse("Unexpected entry ["+title+"] found", true);
+ }
+ }
+ //
+ // Check Level 1
+ //
+ c = mDb.getChildrenForMdmFolder(chromeLinksId, projections);
+ n = c.getCount();
+ assertTrue(n == 2);
+
+ for (c.moveToFirst(); !c.isAfterLast(); c.moveToNext()) {
+ String url = c.getString(0);
+ int isFolder = c.getInt(1);
+ String title = c.getString(2);
+ long id = c.getLong(3);
+
+ assertTrue(mDb.isMdmElement(id));
+
+ boolean found = false;
+ for (bmTuple t : mBookmarks){
+ if (t.getName().equals(title)) {
+ found = true;
+ assertEquals(t.getUrl(), url);
+ assertEquals(0, isFolder);
+ }
+ }
+ if (!found) {
+ assertFalse("Unexpected entry ["+title+"] found", true);
+ }
+ }
+
+ //
+ // attempt to add bookmarks again. Should fail silently.
+ // catt bookmarksWereCreated() to see if they were actually created or not
+ //
+ setMBRestrictions(true);
+ assertFalse(managedBookmarksRestriction.bookmarksWereCreated());
+
+ //
+ // Now, clear the restriction and check that there is no root mdm folder in the DB
+ //
+ clearMBRestrictions();
+ assertFalse(managedBookmarksRestriction.isEnabled());
+ assertNull(managedBookmarksRestriction.getValue());
+ rootId = mDb.getMdmRootFolderId();
+ assertTrue(rootId == -1);
+ }
+
+ /**
+ * Activate ManagedBookmarks restriction
+ * @param enable boolean. Set the state of the restriction.
+ */
+ private void setMBRestrictions(boolean enable) {
+ // Construct restriction bundle
+ final Bundle restrictions = new Bundle();
+
+ if(enable) {
+ restrictions.putString(ManagedBookmarksRestriction.MANAGED_BOOKMARKS, getBookmarksDict(0));
+ }
+
+ // Deliver restriction on UI thread
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ManagedProfileManager.getInstance().setMdmRestrictions(restrictions);
+ }
+ });
+
+ // Wait to ensure restriction is set
+ mInstrumentation.waitForIdleSync();
+ }
+
+ private void clearMBRestrictions() {
+ setMBRestrictions(false);
+ }
+}
diff --git a/src/src/com/android/browser/mdm/tests/ProxyRestrictionsTest.java b/src/src/com/android/browser/mdm/tests/ProxyRestrictionsTest.java
new file mode 100644
index 00000000..5448c293
--- /dev/null
+++ b/src/src/com/android/browser/mdm/tests/ProxyRestrictionsTest.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (c) 2015, 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.mdm.tests;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.os.Bundle;
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
+
+import com.android.browser.BrowserActivity;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.mdm.ManagedProfileManager;
+import com.android.browser.mdm.ProxyRestriction;
+
+import org.codeaurora.swe.MdmManager;
+
+public class ProxyRestrictionsTest extends ActivityInstrumentationTestCase2<BrowserActivity>
+ implements PreferenceKeys {
+
+ private final static String TAG = "ProxyRestrictionsTest";
+
+ private Instrumentation mInstrumentation;
+ private BrowserActivity mActivity;
+ private Context mContext;
+
+ public ProxyRestrictionsTest() {
+ super(BrowserActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mInstrumentation = getInstrumentation();
+ mActivity = getActivity();
+ mContext = getInstrumentation().getTargetContext();
+ }
+
+ public void checkValue (String key, String expected) {
+ if (expected == null) {
+ assertNull(MdmManager.getProxyProperty(key));
+ // native gives us empty strings, not nulls
+ assertEquals(MdmManager.getNativeProxyProperty(mContext,key), "");
+ }
+ else {
+ assertEquals(MdmManager.getProxyProperty(key), expected);
+ assertEquals(MdmManager.getNativeProxyProperty(mContext,key), expected);
+ }
+ }
+
+ // Ensure we start mode and proxyConfig as null
+ public void testPreconditions() throws Throwable {
+ Log.v(TAG, "== Init Conditions ==");
+
+ // check configured proxy mode
+ assertNull("Init: mode should be null", MdmManager.getMdmProxyMode());
+
+ // get the proxy config from ProxyChangeListener
+ assertFalse("Init: proxyConfig should be null", MdmManager.isMdmProxyCfgValid());
+
+ checkValue("http.proxyHost", null);
+ checkValue("http.proxyPort", null);
+ checkValue("http.nonProxyHosts", null);
+ }
+
+ // If you choose to never use a proxy server and always connect directly,
+ // all other options are ignored.
+ public void testProxy_ModeDirect() throws Throwable {
+ String mode = ProxyRestriction.MODE_DIRECT;
+ Log.v(TAG, "== Testing " + mode + " ==");
+
+ // set the restrictions
+ setProxyRestrictions(mode, null, null, null);
+
+ // check configured proxy mode
+ String configuredMode = MdmManager.getMdmProxyMode();
+ assertEquals(mode + ": configuration", mode, configuredMode);
+
+ // get the proxy config from ProxyChangeListener
+ boolean valid = MdmManager.isMdmProxyCfgValid();
+ assertFalse(mode +": proxyConfig should be null", valid);
+
+ checkValue("http.proxyHost", "");
+ checkValue("http.proxyPort", "0");
+ checkValue("http.nonProxyHosts", "");
+ }
+
+ // If you choose to use system proxy settings or auto detect the proxy server,
+ // all other options are ignored.
+ public void testProxy_ModeSystem() throws Throwable {
+ String mode = ProxyRestriction.MODE_SYSTEM;
+ Log.v(TAG, "== Testing " + mode + " ==");
+
+ // Clear any restrictions
+ setProxyRestrictions(null, null, null, null);
+
+ // set the restrictions
+ setProxyRestrictions(mode, null, null, null);
+
+ // check configured proxy mode
+ String configuredMode = MdmManager.getMdmProxyMode();
+ assertNotNull(configuredMode);
+ assertEquals(mode + ": configuration",mode,configuredMode);
+
+ // get the proxy config from ProxyChangeListener
+ boolean valid = MdmManager.isMdmProxyCfgValid();
+ assertFalse(mode +": proxyConfig should be null", valid);
+
+ checkValue("http.proxyHost", null);
+ checkValue("http.proxyPort", null);
+ checkValue("http.nonProxyHosts", null);
+ }
+
+ // If you choose to use system proxy settings or auto detect the proxy server,
+ // all other options are ignored.
+ public void testProxy_ModeAutoDetect() throws Throwable {
+ String mode = ProxyRestriction.MODE_AUTO_DETECT;
+ Log.v(TAG, "== Testing " + mode + " ==");
+
+ // Clear any restrictions
+ setProxyRestrictions(null, null, null, null);
+
+ // set the restrictions
+ setProxyRestrictions(mode, null, null, null);
+
+ // check configured proxy mode
+ String configuredMode = MdmManager.getMdmProxyMode();
+ assertNotNull(configuredMode);
+ assertEquals(mode + ": configuration",mode,configuredMode);
+
+ // get the proxy config from ProxyChangeListener
+ boolean valid = MdmManager.isMdmProxyCfgValid();
+ assertFalse(mode +": proxyConfig should be null", valid);
+
+ checkValue("http.proxyHost", null);
+ checkValue("http.proxyPort", null);
+ checkValue("http.nonProxyHosts", null);
+ }
+
+ // If you choose fixed server proxy mode, you can specify further options in
+ // 'Address or URL of proxy server' and 'Comma-separated list of proxy bypass rules'.
+ public void testProxy_ModeFixedServers() throws Throwable {
+ String mode = ProxyRestriction.MODE_FIXED_SERVERS;
+ Log.v(TAG, "== Testing " + mode + " ==");
+
+ String proxyHost = "192.241.207.220";
+ String proxyPort = "9090";
+ String proxyServer = "http://" + proxyHost + ":" + proxyPort;
+ String configuredMode;
+
+ // Clear any restrictions
+ setProxyRestrictions(null, null, null, null);
+
+ // Test that mode didn't get set if no proxy server is set
+ setProxyRestrictions(mode, null, null, null);
+ configuredMode = MdmManager.getMdmProxyMode();
+ assertNull(configuredMode);
+
+ //
+ // set the restrictions without Exclusion List
+ //
+ setProxyRestrictions(mode, proxyServer, null, null);
+
+ // check configured proxy mode
+ configuredMode = MdmManager.getMdmProxyMode();
+ assertNotNull(configuredMode);
+ assertEquals(mode + ": configuration",mode,configuredMode);
+
+ // check proxy values
+ checkValue("http.proxyHost", proxyHost);
+ checkValue("http.proxyPort", proxyPort);
+ checkValue("http.nonProxyHosts", null);
+
+ //
+ // set the restrictions with Exclusion list
+ //
+ setProxyRestrictions(mode, proxyServer, "*.google.com, *foo.com, 127.0.0.1:8080", null);
+
+ // check configured proxy mode
+ configuredMode = MdmManager.getMdmProxyMode();
+ assertNotNull(configuredMode);
+ assertEquals(mode + ": configuration",mode,configuredMode);
+
+ // check properties
+ checkValue("http.proxyHost", proxyHost);
+ checkValue("http.proxyPort", proxyPort);
+
+ String expected = "*.google.com|*foo.com|127.0.0.1:8080";
+ checkValue("http.nonProxyHosts", expected);
+ }
+
+ // If you choose to use a .pac proxy script, you must specify the URL to the
+ // script in 'URL to a proxy .pac file'.
+ public void testProxy_ModePacScript() throws Throwable {
+ String mode = ProxyRestriction.MODE_PAC_SCRIPT;
+ Log.v(TAG, "== Testing " + mode + " ==");
+
+ // Clear any restrictions
+ setProxyRestrictions(null, null, null, null);
+
+ // set the restrictions without pac url
+ setProxyRestrictions(mode, null, null, null);
+ assertNull(MdmManager.getMdmProxyMode()); // registered mode should be null
+
+ // set the restrictions
+ String pacUrl = "http://internal.site:8888/example.pac";
+ setProxyRestrictions(mode, null, null, pacUrl);
+
+ // check configured proxy mode
+ String configuredMode = MdmManager.getMdmProxyMode();
+ assertNotNull(configuredMode);
+ assertEquals(mode + ": configuration",mode,configuredMode);
+
+ checkValue(ProxyRestriction.PROXY_PAC_URL, pacUrl);
+ }
+
+ public void testProxy_SwitchModesWithoutClear() throws Throwable {
+ String mode;
+ Log.v(TAG, "== Testing Proxy Switch ==");
+
+ String proxyHost = "192.241.207.220";
+ String proxyPort = "9090";
+ String proxyServer = "http://" + proxyHost + ":" + proxyPort;
+ String configuredMode;
+
+ // Clear any restrictions
+ setProxyRestrictions(null, null, null, null);
+
+ //
+ // set to Fixed Servers with exclusion list
+ //
+ mode = ProxyRestriction.MODE_FIXED_SERVERS;
+ Log.v(TAG, "-- Setting mode " + mode + " ==");
+
+ setProxyRestrictions(mode, proxyServer, "*.google.com, *foo.com, 127.0.0.1:8080", null);
+
+ // check configured proxy mode
+ configuredMode = MdmManager.getMdmProxyMode();
+ assertNotNull(configuredMode);
+ assertEquals(mode + ": configuration",mode,configuredMode);
+
+ // check properties
+ checkValue("http.proxyHost", proxyHost);
+ checkValue("http.proxyPort", proxyPort);
+
+ String expected = "*.google.com|*foo.com|127.0.0.1:8080";
+ checkValue("http.nonProxyHosts", expected);
+
+ //
+ // Now set to direct mode
+ //
+ mode = ProxyRestriction.MODE_DIRECT;
+ Log.v(TAG, "-- Setting mode " + mode + " ==");
+
+ // set the restrictions
+ setProxyRestrictions(mode, null, null, null);
+
+ // check configured proxy mode
+ configuredMode = MdmManager.getMdmProxyMode();
+ assertEquals(mode + ": configuration", mode, configuredMode);
+
+ // get the proxy config from ProxyChangeListener
+ boolean valid = MdmManager.isMdmProxyCfgValid();
+ assertFalse(mode +": proxyConfig should be null", valid);
+
+ checkValue("http.proxyHost", "");
+ checkValue("http.proxyPort", "0");
+ checkValue("http.nonProxyHosts", "");
+ }
+
+
+ /**
+ * Activate Proxy restriction
+ * @param mode Required. The Proxy mode we are to configure.
+ * @param proxyServer Required for MODE_FIXED_SERVERS, otherwise optional.
+ * @param nonProxyList Optional for MODE_FIXED_SERVERS, otherwise optional.
+ * This is a comma separated list of host patterns..
+ * @param pacScriptUri Required for MODE_PAC_SCRIPT, otherwise optional.
+ */
+ private void setProxyRestrictions(String mode, String proxyServer,
+ String nonProxyList, String pacScriptUri) {
+ // Construct restriction bundle
+ final Bundle restrictions = new Bundle();
+ restrictions.putString(ProxyRestriction.PROXY_MODE, mode);
+
+ if (proxyServer != null) {
+ restrictions.putString(ProxyRestriction.PROXY_SERVER, proxyServer);
+ }
+ if (nonProxyList != null) {
+ restrictions.putString(ProxyRestriction.PROXY_BYPASS_LIST, nonProxyList);
+ }
+ if (pacScriptUri != null) {
+ restrictions.putString(ProxyRestriction.PROXY_PAC_URL, pacScriptUri);
+ }
+
+ // Deliver restriction on UI thread
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ManagedProfileManager.getInstance().setMdmRestrictions(restrictions);
+ }
+ });
+
+ // Wait to ensure restriction is set
+ mInstrumentation.waitForIdleSync();
+ }
+}
diff --git a/src/src/com/android/browser/mdm/tests/SearchRestrictionsTest.java b/src/src/com/android/browser/mdm/tests/SearchRestrictionsTest.java
new file mode 100644
index 00000000..7a80c5ad
--- /dev/null
+++ b/src/src/com/android/browser/mdm/tests/SearchRestrictionsTest.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (c) 2015, 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.mdm.tests;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.os.Bundle;
+import android.test.ActivityInstrumentationTestCase2;
+
+import com.android.browser.BrowserActivity;
+import com.android.browser.BrowserSettings;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+import com.android.browser.mdm.ManagedProfileManager;
+import com.android.browser.mdm.SearchEngineRestriction;
+
+public class SearchRestrictionsTest extends ActivityInstrumentationTestCase2<BrowserActivity>
+ implements PreferenceKeys {
+
+ private final static String TAG = "RestrictionsTest";
+ private final static String VALID_SEARCH_ENGINE_NAME_1 = "netsprint";
+ private final static String VALID_SEARCH_ENGINE_NAME_2 = "naver";
+ //Search engine name that does not match an entry in res/values/all_search_engines.xml
+ private final static String INVALID_SEARCH_ENGINE_NAME = "foo";
+
+ private Instrumentation mInstrumentation;
+ private Context mContext;
+ private BrowserActivity mActivity;
+ private SearchEngineRestriction mSearchEngineRestriction;
+ String mDefaultSearchEngineName;
+
+ public SearchRestrictionsTest() {
+ super(BrowserActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mInstrumentation = getInstrumentation();
+ mContext = getInstrumentation().getTargetContext();
+ mActivity = getActivity();
+ mSearchEngineRestriction = SearchEngineRestriction.getInstance();
+ mDefaultSearchEngineName = mActivity.getApplicationContext()
+ .getString(R.string.default_search_engine_value);
+ }
+
+ /*
+ * Search Engine Restrictions Tests
+ */
+
+ // Ensure we start with the default search engine and no restriction
+ public void testSR_initConditions() throws Throwable {
+ assertFalse("Search engine restriction", mSearchEngineRestriction.isEnabled());
+ assertEquals("Search provider", mDefaultSearchEngineName,
+ BrowserSettings.getInstance().getSearchEngineName());
+ }
+
+ // Restriction is not set when Enabled is null or false
+ public void testSR_NotSetWhenNotEnabled() throws Throwable {
+ setDefaultSearchProvider(null, VALID_SEARCH_ENGINE_NAME_1);
+ assertFalse("Search engine restriction", mSearchEngineRestriction.isEnabled());
+ assertEquals("Search provider", mDefaultSearchEngineName,
+ BrowserSettings.getInstance().getSearchEngineName());
+
+ setDefaultSearchProvider(false, VALID_SEARCH_ENGINE_NAME_1);
+ assertFalse("Search engine restriction", mSearchEngineRestriction.isEnabled());
+ assertEquals("Search provider", mDefaultSearchEngineName,
+ BrowserSettings.getInstance().getSearchEngineName());
+ }
+
+ // Restriction is not set when DefaultSearchProviderName is null or invalid
+ public void testSR_NotSetWhenNameNullOrInvalid() throws Throwable {
+ setDefaultSearchProvider(true, null);
+ assertFalse("Search engine restriction", mSearchEngineRestriction.isEnabled());
+ assertEquals("Search provider", mDefaultSearchEngineName,
+ BrowserSettings.getInstance().getSearchEngineName());
+
+ setDefaultSearchProvider(true, INVALID_SEARCH_ENGINE_NAME);
+ assertFalse("Search engine restriction", mSearchEngineRestriction.isEnabled());
+ assertEquals("Search provider", mDefaultSearchEngineName,
+ BrowserSettings.getInstance().getSearchEngineName());
+ }
+
+ // Restriction is set when Enabled is TRUE and Name is VALID
+ public void testSR_SetWhenEnabledAndNameValid() throws Throwable {
+ setDefaultSearchProvider(true, VALID_SEARCH_ENGINE_NAME_1);
+ assertTrue("Search engine restriction", mSearchEngineRestriction.isEnabled());
+ assertEquals("Search provider", VALID_SEARCH_ENGINE_NAME_1,
+ BrowserSettings.getInstance().getSearchEngineName());
+
+ setDefaultSearchProvider(true, VALID_SEARCH_ENGINE_NAME_2);
+ assertTrue("Search engine restriction", mSearchEngineRestriction.isEnabled());
+ assertEquals("Search provider", VALID_SEARCH_ENGINE_NAME_2,
+ BrowserSettings.getInstance().getSearchEngineName());
+
+ // Restriction is lifted when neither Enabled nor Name are present
+ setDefaultSearchProvider(null, null);
+ assertFalse("Search engine restriction", mSearchEngineRestriction.isEnabled());
+ assertEquals("Search provider", mDefaultSearchEngineName,
+ BrowserSettings.getInstance().getSearchEngineName());
+ }
+
+ // Restriction is lifted when DefaultSearchProviderEnabled is FALSE or null
+ public void testSR_LiftedWhenDisabledOrNull() throws Throwable {
+ // set a valid search engine restriction
+ setDefaultSearchProvider(true, VALID_SEARCH_ENGINE_NAME_1);
+ assertTrue("Search engine restriction", mSearchEngineRestriction.isEnabled());
+ assertEquals("Search provider", VALID_SEARCH_ENGINE_NAME_1,
+ BrowserSettings.getInstance().getSearchEngineName());
+ // then lift the restriction (false)
+ setDefaultSearchProvider(false, VALID_SEARCH_ENGINE_NAME_2);
+ assertFalse("Search engine restriction", mSearchEngineRestriction.isEnabled());
+ assertEquals("Search provider", mDefaultSearchEngineName,
+ BrowserSettings.getInstance().getSearchEngineName());
+
+ // set a valid search engine restriction
+ setDefaultSearchProvider(true, VALID_SEARCH_ENGINE_NAME_1);
+ assertTrue("Search engine restriction", mSearchEngineRestriction.isEnabled());
+ assertEquals("Search provider", VALID_SEARCH_ENGINE_NAME_1,
+ BrowserSettings.getInstance().getSearchEngineName());
+ // then lift the restriction (null)
+ setDefaultSearchProvider(null, VALID_SEARCH_ENGINE_NAME_2);
+ assertFalse("Search engine restriction", mSearchEngineRestriction.isEnabled());
+ assertEquals("Search provider", mDefaultSearchEngineName,
+ BrowserSettings.getInstance().getSearchEngineName());
+ }
+
+ // Restriction is lifted when Enabled is TRUE and Name is null or INVALID
+ public void testSR_LiftedWhenNameIsNullOrInvalid() throws Throwable {
+ // set a valid search engine restriction
+ setDefaultSearchProvider(true, VALID_SEARCH_ENGINE_NAME_1);
+ assertTrue("Search engine restriction", mSearchEngineRestriction.isEnabled());
+ assertEquals("Search provider", VALID_SEARCH_ENGINE_NAME_1,
+ BrowserSettings.getInstance().getSearchEngineName());
+ // then lift the restriction with invalid name
+ setDefaultSearchProvider(true, INVALID_SEARCH_ENGINE_NAME);
+ assertFalse("Search engine restriction", mSearchEngineRestriction.isEnabled());
+ assertEquals("Search provider", mDefaultSearchEngineName,
+ BrowserSettings.getInstance().getSearchEngineName());
+
+ // set a valid search engine restriction
+ setDefaultSearchProvider(true, VALID_SEARCH_ENGINE_NAME_1);
+ assertTrue("Search engine restriction", mSearchEngineRestriction.isEnabled());
+ assertEquals("Search provider", VALID_SEARCH_ENGINE_NAME_1,
+ BrowserSettings.getInstance().getSearchEngineName());
+ // then lift the restriction with a null
+ setDefaultSearchProvider(true, null);
+ assertFalse("Search engine restriction", mSearchEngineRestriction.isEnabled());
+ assertEquals("Search provider", mDefaultSearchEngineName,
+ BrowserSettings.getInstance().getSearchEngineName());
+ }
+
+ /**
+ * Activate search engine restriction
+ * @param defaultSearchProviderEnabled must be true to activate restriction
+ * @param defaultSearchProviderName must be an entry in res/values/all_search_engines.xml,
+ * otherwise restriction is not set
+ */
+ private void setDefaultSearchProvider(Boolean defaultSearchProviderEnabled,
+ String defaultSearchProviderName) {
+ // Construct restriction bundle
+ final Bundle restrictions = new Bundle();
+ if (defaultSearchProviderEnabled != null)
+ restrictions.putBoolean("DefaultSearchProviderEnabled", defaultSearchProviderEnabled);
+ if (defaultSearchProviderName != null)
+ restrictions.putString("SearchProviderName", defaultSearchProviderName);
+
+ // Deliver restriction on UI thread
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ManagedProfileManager.getInstance().setMdmRestrictions(restrictions);
+ }
+ });
+
+ // Wait to ensure restriction is set
+ mInstrumentation.waitForIdleSync();
+ }
+
+}
diff --git a/src/src/com/android/browser/mdm/tests/ThirdPartyCookiesRestrictionsTest.java b/src/src/com/android/browser/mdm/tests/ThirdPartyCookiesRestrictionsTest.java
new file mode 100644
index 00000000..a77e94ae
--- /dev/null
+++ b/src/src/com/android/browser/mdm/tests/ThirdPartyCookiesRestrictionsTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (c) 2015, 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.mdm.tests;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
+
+import com.android.browser.BrowserActivity;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.mdm.ManagedProfileManager;
+import com.android.browser.mdm.ThirdPartyCookiesRestriction;
+
+public class ThirdPartyCookiesRestrictionsTest extends ActivityInstrumentationTestCase2<BrowserActivity>
+ implements PreferenceKeys {
+
+ private final static String TAG = "TPCRestrictionsTest";
+
+ private Instrumentation mInstrumentation;
+ private BrowserActivity mActivity;
+ private ThirdPartyCookiesRestriction mTBCRestriction;
+
+ public ThirdPartyCookiesRestrictionsTest() {
+ super(BrowserActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mInstrumentation = getInstrumentation();
+ mActivity = getActivity();
+ mTBCRestriction = ThirdPartyCookiesRestriction.getInstance();
+ }
+
+ public void test_DNT() throws Throwable {
+ Log.i(TAG,"!!! ******** Starting TPC Tests *************");
+
+ clearTPCRestrictions();
+ assertFalse(mTBCRestriction.isEnabled());
+ assertTrue(mTBCRestriction.getValue()); // default is 'allowed'
+
+ setTPCRestrictions(false, true);
+ assertFalse(mTBCRestriction.isEnabled());
+ assertTrue(mTBCRestriction.getValue());
+
+ setTPCRestrictions(true, false);
+ assertTrue(mTBCRestriction.isEnabled());
+ assertFalse(mTBCRestriction.getValue());
+
+ setTPCRestrictions(true, true);
+ assertTrue(mTBCRestriction.isEnabled());
+ assertTrue(mTBCRestriction.getValue());
+ }
+
+ /**
+ * Activate ThirdPartyCookies restriction
+ * @param clear boolean. if true, clears the restriction by sending an empty bundle. In
+ * this case, the other args are ignored.
+ *
+ * @param enable boolean. Set the state of the restriction.
+ *
+ * @param value boolean. Set the state of TPC. true == allowed. If enabled is set to true.
+ * we still bundle it, but it should be ignored by the handler.
+ */
+ private void setTPCRestrictions(boolean clear, boolean enable, boolean value) {
+ // Construct restriction bundle
+ final Bundle restrictions = new Bundle();
+
+ if(!clear) {
+ restrictions.putBoolean(ThirdPartyCookiesRestriction.TPC_ENABLED,enable);
+ restrictions.putBoolean(ThirdPartyCookiesRestriction.TPC_ALLOWED, value);
+ }
+
+ // Deliver restriction on UI thread
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ManagedProfileManager.getInstance().setMdmRestrictions(restrictions);
+ }
+ });
+
+ // Wait to ensure restriction is set
+ mInstrumentation.waitForIdleSync();
+ }
+
+ private void setTPCRestrictions(boolean enable, boolean value) {
+ setTPCRestrictions(false, enable, value);
+ }
+
+ private void clearTPCRestrictions() {
+ setTPCRestrictions(true, false, false);
+ }
+}
diff --git a/src/src/com/android/browser/mdm/tests/URLRestrictionsTest.java b/src/src/com/android/browser/mdm/tests/URLRestrictionsTest.java
new file mode 100644
index 00000000..ea921c62
--- /dev/null
+++ b/src/src/com/android/browser/mdm/tests/URLRestrictionsTest.java
@@ -0,0 +1,536 @@
+/*
+ * Copyright (c) 2015, 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.mdm.tests;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
+
+import com.android.browser.BrowserActivity;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.mdm.ManagedProfileManager;
+import com.android.browser.mdm.URLFilterRestriction;
+
+import org.codeaurora.swe.MdmManager;
+
+public class URLRestrictionsTest extends ActivityInstrumentationTestCase2<BrowserActivity>
+ implements PreferenceKeys {
+
+ private final static String TAG = "URLRestrictionsTest";
+
+ private Instrumentation mInstrumentation;
+ private BrowserActivity mActivity;
+ private URLFilterRestriction mUrlRestriction;
+
+ public URLRestrictionsTest() {
+ super(BrowserActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mInstrumentation = getInstrumentation();
+ mActivity = getActivity();
+ mUrlRestriction = URLFilterRestriction.getInstance();
+ }
+
+ private boolean isBlocked (final String url) {
+ // Query native for blocked status for this url
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ MdmManager.isMdmUrlBlocked(url);
+ }
+ });
+ mInstrumentation.waitForIdleSync();
+
+ // Wait for native to post the result
+ while(!MdmManager.isMdmUrlBlockedResultReady()) {
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ // Retrieve the result
+ return MdmManager.getMdmUrlBlockedResult();
+ }
+
+ private boolean isBlocked (final String url, boolean expected) {
+ boolean lastBlockedResult = isBlocked(url);
+
+ if (lastBlockedResult != expected) {
+ Log.e(TAG, "[" + url + "] should" + (expected ? " " : " NOT ") + "have been blocked");
+ }
+ else {
+ //Log.i(TAG, "[" + url + "] was " + (expected ? " " : " not ") + "blocked as expected");
+ }
+ return expected;
+ }
+
+ public void testBl_FileBased() throws Throwable {
+ Log.i(TAG,"!!! ******** Starting File Based Tests *************");
+
+ clearURLRestrictions();
+ assertFalse(isBlocked("file:///data/local/tmp/bl-test/1.html"));
+ setUrlBlacklist("file:///data/local/tmp/bl-test/1.html");
+ assertTrue(isBlocked("file:///data/local/tmp/bl-test/1.html"));
+ clearURLRestrictions();
+ assertFalse(isBlocked("file:///data/local/tmp/bl-test/1.html"));
+
+ setUrlBlacklist("file:///data/local/tmp/bl-test");
+ assertTrue(isBlocked("file:///data/local/tmp/bl-test/2.html"));
+ clearURLRestrictions();
+ assertFalse(isBlocked("file:///data/local/tmp/bl-test/2.html"));
+
+ setUrlBlacklist("file:///data/local/tmp");
+ assertTrue(isBlocked("file:///data/local/tmp/bl-test/3.html"));
+ clearURLRestrictions();
+ assertFalse(isBlocked("file:///data/local/tmp/bl-test/3.html"));
+
+ setUrlBlacklist("file://*");
+ assertTrue(isBlocked("file:///data/local/tmp/bl-test/4.html"));
+ assertFalse(isBlocked("http://www.google.com"));
+ clearURLRestrictions();
+ assertFalse(isBlocked("file:///data/local/tmp/bl-test/4.html"));
+
+ setUrlBlacklist("*");
+ assertTrue(isBlocked("file:///data/local/tmp/bl-test/5.html"));
+
+ clearURLRestrictions();
+ assertFalse(isBlocked("file:///data/local/tmp/bl-test/5.html"));
+ }
+
+ public void testBl_ServerBased() throws Throwable {
+ Log.i(TAG,"!!! ******** Starting Server Based *************");
+ // only restricts http traffic on port 8080 that includes given path prefix on this server
+ setUrlBlacklist("http://server:8080/path");
+ assertTrue(isBlocked( "http://server:8080/path"));
+ assertFalse(isBlocked("https://server:8080/path"));
+ assertFalse(isBlocked("http://server:8080"));
+ assertFalse(isBlocked("http://server:9090/path"));
+ assertFalse(isBlocked("http://server/path"));
+ assertFalse(isBlocked("http://server:80/path"));
+ assertFalse(isBlocked("http://server"));
+
+ // only restricts http traffic on port 8080 (any path) on given server
+ setUrlBlacklist("http://server:8080");
+ assertTrue(isBlocked("http://server:8080/path"));
+ assertTrue(isBlocked("http://server:8080"));
+ assertFalse(isBlocked("https://server:8080/path"));
+ assertFalse(isBlocked("http://server:9090/path"));
+ assertFalse(isBlocked("http://server/path"));
+ assertFalse(isBlocked("http://server:80/path"));
+ assertFalse(isBlocked("http://server"));
+
+ // restricts all http traffic at given server on all ports
+ setUrlBlacklist("http://server");
+ assertTrue(isBlocked("http://server:8080/path"));
+ assertTrue(isBlocked("http://server:8080"));
+ assertFalse(isBlocked("https://server:8080/path"));
+ assertFalse(isBlocked("ftp://server"));
+ assertTrue(isBlocked("http://server:9090/path"));
+ assertTrue(isBlocked("http://server/path"));
+ assertTrue(isBlocked("http://server:80/path"));
+ assertTrue(isBlocked("http://server"));
+ }
+
+ private void exampleDotComChecks() {
+ assertFalse(isBlocked("http://www.google.com"));
+ assertFalse(isBlocked("http://www.yahoo.com"));
+
+ assertTrue(isBlocked("http://example.com"));
+ assertTrue(isBlocked("http://example.com:80"));
+ assertTrue(isBlocked("http://example.com:8080"));
+ assertTrue(isBlocked("http://example.com:8080/path"));
+ assertTrue(isBlocked("http://foo.example.com"));
+ assertTrue(isBlocked("http://foo.example.com:80"));
+ assertTrue(isBlocked("http://foo.example.com:8080"));
+ assertTrue(isBlocked("http://foo.example.com:8080/path"));
+
+ assertTrue(isBlocked("https://example.com"));
+ assertTrue(isBlocked("https://example.com:80"));
+ assertTrue(isBlocked("https://example.com:8080"));
+ assertTrue(isBlocked("https://example.com:8080/path" ));
+ assertTrue(isBlocked("https://foo.example.com"));
+ assertTrue(isBlocked("https://foo.example.com:80"));
+ assertTrue(isBlocked("https://foo.example.com:8080"));
+ assertTrue(isBlocked("https://foo.example.com:8080/path"));
+
+ assertTrue(isBlocked("ftp://example.com"));
+ assertTrue(isBlocked("ftp://example.com:80"));
+ assertTrue(isBlocked("ftp://example.com:8080"));
+ assertTrue(isBlocked("ftp://example.com:8080/path" ));
+ assertTrue(isBlocked("ftp://foo.example.com"));
+ assertTrue(isBlocked("ftp://foo.example.com:80"));
+ assertTrue(isBlocked("ftp://foo.example.com:8080"));
+ assertTrue(isBlocked("ftp://foo.example.com:8080/path"));
+ }
+
+ public void testBl_URL_DomainOnly() throws Throwable {
+ Log.i(TAG,"!!! ******** Starting domain only *************");
+ // restricts all (i.e. http, https, ftp, ...) traffic on any port at given domain and any subdomains
+ setUrlBlacklist("example.com");
+ exampleDotComChecks();
+ }
+
+ public void testBl_URL_IPOnly() throws Throwable {
+ Log.i(TAG,"!!! ******** Starting IP only *************");
+ setUrlBlacklist("192.168.0.123");
+ assertFalse(isBlocked("http://www.google.com"));
+ assertFalse(isBlocked("http://www.yahoo.com"));
+
+ assertTrue(isBlocked("http://192.168.0.123"));
+ assertTrue(isBlocked("http://192.168.0.123:80"));
+ assertTrue(isBlocked("http://192.168.0.123:8080"));
+ assertTrue(isBlocked("http://192.168.0.123:8080/path"));
+
+ assertTrue(isBlocked("https://192.168.0.123"));
+ assertTrue(isBlocked("https://192.168.0.123:80"));
+ assertTrue(isBlocked("https://192.168.0.123:8080"));
+ assertTrue(isBlocked("https://192.168.0.123:8080/path"));
+
+ assertTrue(isBlocked("ftp://192.168.0.123"));
+ assertTrue(isBlocked("ftp://192.168.0.123:80"));
+ assertTrue(isBlocked("ftp://192.168.0.123:8080"));
+ assertTrue(isBlocked("ftp://192.168.0.123:8080/path"));
+ }
+
+ private void sslServerDotComChecks() {
+ assertFalse(isBlocked("http://www.google.com"));
+ assertFalse(isBlocked("http://www.yahoo.com"));
+
+ assertTrue(isBlocked("https://ssl.server.com"));
+ assertTrue(isBlocked("https://ssl.server.com:80"));
+ assertTrue(isBlocked("https://ssl.server.com:8080"));
+ assertTrue(isBlocked("https://ssl.server.com:8080/path"));
+ assertTrue(isBlocked("https://foo.ssl.server.com"));
+ assertTrue(isBlocked("https://foo.ssl.server.com:80"));
+ assertTrue(isBlocked("https://foo.ssl.server.com:8080"));
+ assertTrue(isBlocked("https://foo.ssl.server.com:8080/path"));
+
+ assertFalse(isBlocked("http://ssl.server.com"));
+ assertFalse(isBlocked("http://ssl.server.com:80"));
+ assertFalse(isBlocked("http://ssl.server.com:8080"));
+ assertFalse(isBlocked("http://ssl.server.com:8080/path"));
+ assertFalse(isBlocked("http://foo.ssl.server.com"));
+ assertFalse(isBlocked("http://foo.ssl.server.com:80"));
+ assertFalse(isBlocked("http://foo.ssl.server.com:8080"));
+ assertFalse(isBlocked("http://foo.ssl.server.com:8080/path"));
+
+ assertFalse(isBlocked("ftp://ssl.server.com"));
+ assertFalse(isBlocked("ftp://ssl.server.com:80"));
+ assertFalse(isBlocked("ftp://ssl.server.com:8080"));
+ assertFalse(isBlocked("ftp://ssl.server.com:8080/path"));
+ assertFalse(isBlocked("ftp://foo.ssl.server.com"));
+ assertFalse(isBlocked("ftp://foo.ssl.server.com:80"));
+ assertFalse(isBlocked("ftp://foo.ssl.server.com:8080"));
+ assertFalse(isBlocked("ftp://foo.ssl.server.com:8080/path"));
+ }
+
+ public void testBl_URL_Https() throws Throwable {
+ Log.i(TAG,"!!! ******** Starting https tests *************");
+ // restricts https traffic on any port at given domain and any subdomains
+ setUrlBlacklist("https://ssl.server.com");
+ sslServerDotComChecks();
+ }
+
+ private void hostingDotComChecks() {
+ assertFalse(isBlocked("http://www.google.com"));
+ assertFalse(isBlocked("http://www.yahoo.com"));
+
+ assertFalse(isBlocked("http://hosting.com"));
+ assertFalse(isBlocked("http://hosting.com:80"));
+ assertFalse(isBlocked("http://hosting.com:8080"));
+ assertTrue(isBlocked("http://hosting.com:8080/bad_path"));
+ assertFalse(isBlocked("http://foo.hosting.com"));
+ assertFalse(isBlocked("http://foo.hosting.com:80"));
+ assertFalse(isBlocked("http://foo.hosting.com:8080"));
+ assertTrue(isBlocked("http://foo.hosting.com:8080/bad_path"));
+
+ assertFalse(isBlocked("https://hosting.com"));
+ assertFalse(isBlocked("https://hosting.com:80"));
+ assertFalse(isBlocked("https://hosting.com:8080"));
+ assertTrue(isBlocked("https://hosting.com:8080/bad_path"));
+ assertFalse(isBlocked("https://foo.hosting.com"));
+ assertFalse(isBlocked("https://foo.hosting.com:80"));
+ assertFalse(isBlocked("https://foo.hosting.com:8080"));
+ assertTrue(isBlocked("https://foo.hosting.com:8080/bad_path"));
+
+ assertFalse(isBlocked("ftp://hosting.com" ));
+ assertFalse(isBlocked("ftp://hosting.com:80"));
+ assertFalse(isBlocked("ftp://hosting.com:8080"));
+ assertTrue(isBlocked("ftp://hosting.com:8080/bad_path"));
+ assertFalse(isBlocked("ftp://foo.hosting.com"));
+ assertFalse(isBlocked("ftp://foo.hosting.com:80"));
+ assertFalse(isBlocked("ftp://foo.hosting.com:8080"));
+ assertTrue(isBlocked("ftp://foo.hosting.com:8080/bad_path"));
+ }
+
+ public void testBl_URL_DomainPath() throws Throwable {
+ Log.i(TAG,"!!! ******** Starting Domain Path tests *************");
+ //restricts all traffic on any port at given domain (and any subdomains) that includes given path prefix
+ setUrlBlacklist("hosting.com/bad_path");
+ hostingDotComChecks();
+ }
+
+ public void testBl_URL_SubDomains() throws Throwable {
+ Log.i(TAG,"!!! ******** Starting SubDomain tests *************");
+ // restricts all traffic on any port from domain 'exact.hostname.com' but allows traffic from subdomains
+ // like "foobar.exact.hostname.com"
+ setUrlBlacklist(".exact.hostname.com");
+ assertFalse(isBlocked("http://www.google.com"));
+ assertFalse(isBlocked("http://www.yahoo.com"));
+
+ assertFalse(isBlocked("http://www.google.com"));
+ assertFalse(isBlocked("http://www.yahoo.com"));
+
+ assertTrue(isBlocked("http://exact.hostname.com"));
+ assertTrue(isBlocked("http://exact.hostname.com:8080"));
+ assertTrue(isBlocked("http://exact.hostname.com:8080/path"));
+
+ assertTrue(isBlocked("https://exact.hostname.com"));
+ assertTrue(isBlocked("https://exact.hostname.com:8080"));
+ assertTrue(isBlocked("https://exact.hostname.com:8080/path"));
+
+ assertTrue(isBlocked("ftp://exact.hostname.com"));
+ assertTrue(isBlocked("ftp://exact.hostname.com:8080"));
+ assertTrue(isBlocked("ftp://exact.hostname.com:8080/path"));
+
+ assertFalse(isBlocked("http://foo.exact.hostname.com"));
+ assertFalse(isBlocked("http://foo.exact.hostname.com:8080"));
+ assertFalse(isBlocked("http://foo.exact.hostname.com:8080/path"));
+
+ assertFalse(isBlocked("https://foo.exact.hostname.com"));
+ assertFalse(isBlocked("https://foo.exact.hostname.com:8080"));
+ assertFalse(isBlocked("https://foo.exact.hostname.com:8080/path"));
+
+ assertFalse(isBlocked("ftp://foo.exact.hostname.com"));
+ assertFalse(isBlocked("ftp://foo.exact.hostname.com:8080"));
+ assertFalse(isBlocked("ftp://foo.exact.hostname.com:8080/path"));
+ }
+
+ public void testBl_URL_Universal() throws Throwable {
+ Log.i(TAG,"!!! ******** Starting Universal Blacklist tests *************");
+ //restricts everything. No URLS will get through.
+ setUrlBlacklist("*");
+ assertTrue(isBlocked("http://www.google.com"));
+ assertTrue(isBlocked("http://www.yahoo.com"));
+
+ assertTrue(isBlocked("http://hosting.com"));
+ assertTrue(isBlocked("ftp://hosting.com:8080/bad_path"));
+ assertTrue(isBlocked("ftp://hosting.com:8080"));
+ assertTrue(isBlocked("ftp://hosting.com:80"));
+ assertTrue(isBlocked("https://ssl.server.com"));
+ assertTrue(isBlocked("https://ssl.server.com:80"));
+ assertTrue(isBlocked("https://ssl.server.com:8080"));
+ assertTrue(isBlocked("https://ssl.server.com:8080/path"));
+ assertTrue(isBlocked("https://foo.ssl.server.com"));
+ assertTrue(isBlocked("https://foo.ssl.server.com:80"));
+ assertTrue(isBlocked("https://foo.ssl.server.com:8080"));
+ assertTrue(isBlocked("https://foo.ssl.server.com:8080/path"));
+ assertTrue(isBlocked("http://192.168.0.123"));
+ assertTrue(isBlocked("http://192.168.0.123:80"));
+ assertTrue(isBlocked("http://192.168.0.123:8080"));
+ assertTrue(isBlocked("http://192.168.0.123:8080/path"));
+ assertTrue(isBlocked("http://server:8080/path"));
+ assertTrue(isBlocked("https://server:8080/path"));
+ assertTrue(isBlocked("http://server:8080"));
+ assertTrue(isBlocked("http://server:9090/path"));
+ assertTrue(isBlocked("http://server/path"));
+ assertTrue(isBlocked("http://server:80/path"));
+ assertTrue(isBlocked("http://server"));
+ }
+
+ public void testBl_Multiple() throws Throwable {
+ Log.i(TAG,"!!! ******** Starting Multiple Blacklist tests *************");
+
+ // Multiple black list specs, comma separated
+ setUrlBlacklist("hosting.com/bad_path,https://ssl.server.com,example.com");
+ hostingDotComChecks();
+ sslServerDotComChecks();
+ exampleDotComChecks();
+
+ // test extra whitespace
+ setUrlBlacklist(" hosting.com/bad_path , https://ssl.server.com\t, example.com ");
+ hostingDotComChecks();
+ sslServerDotComChecks();
+ exampleDotComChecks();
+ }
+
+ // Basic Whitelist Test
+ public void testWl_Basic() throws Throwable {
+ Log.i(TAG,"!!! ******** Starting Whitelist tests *************");
+ // basic whitelist. First we set universal black list, then exempt specific
+ // urls.
+ setURLRestrictions("*","google.com, https://yahoo.com," +
+ " http://foo.com:80, http://bar.com:80/path, .fubar.com");
+
+ // Check that sites unrelated to whitelist are blocked
+ assertTrue(isBlocked("http://facebook.com"));
+ assertTrue(isBlocked("http://twitter.com"));
+
+
+ // google.com:
+ // types: all
+ // ports: all
+ // paths: all
+ // subdomains: all
+ assertFalse(isBlocked("http://google.com"));
+ assertFalse(isBlocked("https://google.com"));
+ assertFalse(isBlocked("ftp://google.com"));
+
+ assertFalse(isBlocked("http://google.com:80"));
+ assertFalse(isBlocked("http://google.com:8080"));
+
+ assertFalse(isBlocked("http://google.com/path1"));
+ assertFalse(isBlocked("http://google.com/path2"));
+
+ assertFalse(isBlocked("http://fr.google.com"));
+ assertFalse(isBlocked("http://us.google.com"));
+
+ // yahoo.com:
+ // types: https only
+ // ports: all
+ // paths: all
+ // subdomains: all
+ assertTrue(isBlocked("http://yahoo.com"));
+ assertFalse(isBlocked("https://yahoo.com"));
+ assertTrue(isBlocked("ftp://yahoo.com"));
+
+ assertFalse(isBlocked("https://yahoo.com:80"));
+ assertFalse(isBlocked("https://yahoo.com:8080"));
+
+ assertFalse(isBlocked("https://yahoo.com/path1"));
+ assertFalse(isBlocked("https://yahoo.com/path2"));
+
+ assertFalse(isBlocked("https://fr.yahoo.com"));
+ assertFalse(isBlocked("https://us.yahoo.com"));
+
+ // foo.com
+ // types: http only
+ // ports: 80 only
+ // paths: all
+ // subdomains: all
+ assertFalse(isBlocked("http://foo.com:80"));
+ assertTrue(isBlocked("https://foo.com:80"));
+ assertTrue(isBlocked("ftp://foo.com:80"));
+
+ assertFalse(isBlocked("http://foo.com:80"));
+ assertTrue(isBlocked("http://foo.com:8080"));
+
+ assertFalse(isBlocked("http://foo.com:80/path1"));
+ assertFalse(isBlocked("http://foo.com:80/path2"));
+
+ assertFalse(isBlocked("http://fr.foo.com:80"));
+ assertFalse(isBlocked("http://us.foo.com:80"));
+
+ // bar.com
+ // types: http only
+ // ports: 80 only
+ // paths: 'path' and all subs
+ // subdomains: all
+ assertFalse(isBlocked("http://bar.com:80/path"));
+ assertTrue(isBlocked("https://bar.com:80/path"));
+ assertTrue(isBlocked("ftp://bar.com:80/path"));
+
+ assertFalse(isBlocked("http://bar.com:80/path"));
+ assertTrue(isBlocked("http://bar.com:8080/path"));
+
+ assertFalse(isBlocked("http://bar.com:80/path"));
+ assertFalse(isBlocked("http://bar.com:80/path/sub1"));
+ assertFalse(isBlocked("http://bar.com:80/path/sbu2/sub3"));
+
+ assertTrue(isBlocked("http://bar.com:80/anotherpath"));
+
+ assertFalse(isBlocked("http://fr.bar.com:80/path"));
+ assertFalse(isBlocked("http://us.bar.com:80/path"));
+
+ // fubar.com
+ // types: all
+ // ports: all
+ // paths: all
+ // subdomains: none
+ assertFalse(isBlocked("http://fubar.com"));
+ assertFalse(isBlocked("https://fubar.com"));
+ assertFalse(isBlocked("ftp://fubar.com"));
+
+ assertFalse(isBlocked("http://fubar.com:80"));
+ assertFalse(isBlocked("http://fubar.com:8080"));
+
+ assertFalse(isBlocked("http://fubar.com/path1"));
+ assertFalse(isBlocked("http://fubar.com/path2"));
+
+ assertTrue(isBlocked("http://fr.fubar.com"));
+ assertTrue(isBlocked("http://us.fubar.com"));
+ }
+
+ /**
+ * Activate URL restriction
+ * @param blackList Required. comma separated list of URL restrictions. If null,
+ * no restrictions are set.
+ * @param whiteList Optional exceptions to blacklist.
+ *
+ * Note: we don't enforce blackList/whiteList requirements here, this will be done
+ * in the URLRestrictions.enforce() method.
+ */
+ private void setURLRestrictions(String blackList, String whiteList) {
+ // Construct restriction bundle
+ final Bundle restrictions = new Bundle();
+
+ if (blackList != null)
+ restrictions.putString(URLFilterRestriction.URL_BLACK_LIST, blackList);
+
+ if (whiteList != null)
+ restrictions.putString(URLFilterRestriction.URL_WHITE_LIST, whiteList);
+
+ // Deliver restriction on UI thread
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ManagedProfileManager.getInstance().setMdmRestrictions(restrictions);
+ }
+ });
+
+ // Wait to ensure restriction is set
+ mInstrumentation.waitForIdleSync();
+ }
+
+ private void clearURLRestrictions() {
+ setURLRestrictions(null, null);
+ }
+
+ private void setUrlBlacklist(String bl) {
+ setURLRestrictions(bl, null);
+ }
+}
diff --git a/src/src/com/android/browser/mynavigation/AddMyNavigationPage.java b/src/src/com/android/browser/mynavigation/AddMyNavigationPage.java
new file mode 100755
index 00000000..cc42d96a
--- /dev/null
+++ b/src/src/com/android/browser/mynavigation/AddMyNavigationPage.java
@@ -0,0 +1,272 @@
+/*
+ * 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) {
+ save();
+ }
+ };
+
+ 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);
+ AddMyNavigationPage.this.setResult(Activity.RESULT_OK,
+ (new Intent()).putExtra("need_refresh", true));
+ } else {
+ Log.e(LOGTAG, "this item does not exist!");
+ }
+ } catch (IllegalStateException e) {
+ Log.e(LOGTAG, "SaveMyNavigationRunnable", e);
+ } finally {
+ if (null != cursor) {
+ cursor.close();
+ AddMyNavigationPage.this.finish();
+ }
+ }
+ }
+ }
+
+ 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/src/com/android/browser/mynavigation/MyNavigationRequestHandler.java b/src/src/com/android/browser/mynavigation/MyNavigationRequestHandler.java
new file mode 100755
index 00000000..d7b6864d
--- /dev/null
+++ b/src/src/com/android/browser/mynavigation/MyNavigationRequestHandler.java
@@ -0,0 +1,177 @@
+/*
+ * 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();
+ int id = res.getIdentifier(fileName, null, mContext.getPackageName());
+ if (id == 0) {
+ String packageName = R.class.getPackage().getName();
+ 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/src/com/android/browser/mynavigation/MyNavigationTemplate.java b/src/src/com/android/browser/mynavigation/MyNavigationTemplate.java
new file mode 100755
index 00000000..efaa3b29
--- /dev/null
+++ b/src/src/com/android/browser/mynavigation/MyNavigationTemplate.java
@@ -0,0 +1,307 @@
+/*
+ * 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();
+ 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 {
+ final String packageName = R.class.getPackage().getName();
+ int id = res.getIdentifier(name, null, packageName);
+ if(id == 0) {
+ id = res.getIdentifier(name, null, context.getPackageName());
+ }
+ 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/src/com/android/browser/mynavigation/MyNavigationUtil.java b/src/src/com/android/browser/mynavigation/MyNavigationUtil.java
new file mode 100755
index 00000000..1874c3f6
--- /dev/null
+++ b/src/src/com/android/browser/mynavigation/MyNavigationUtil.java
@@ -0,0 +1,124 @@
+/*
+ * 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;
+
+import com.android.browser.BrowserConfig;
+
+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 = BrowserConfig.AUTHORITY + ".mynavigation";
+ public static final String MY_NAVIGATION = "content://" + AUTHORITY + "/" + "websites";
+ public static final Uri MY_NAVIGATION_URI = Uri.parse(MY_NAVIGATION);
+ 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/src/com/android/browser/platformsupport/Browser.java b/src/src/com/android/browser/platformsupport/Browser.java
new file mode 100644
index 00000000..a6128c40
--- /dev/null
+++ b/src/src/com/android/browser/platformsupport/Browser.java
@@ -0,0 +1,660 @@
+/*
+ * 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 android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.util.Log;
+
+import com.android.browser.AddBookmarkPage;
+import com.android.browser.R;
+import com.android.browser.platformsupport.BrowserContract.Bookmarks;
+import com.android.browser.platformsupport.BrowserContract.Combined;
+import com.android.browser.platformsupport.BrowserContract.History;
+import com.android.browser.platformsupport.BrowserContract.Searches;
+
+public class Browser {
+ private static final String LOGTAG = "browser";
+
+ /**
+ * A table containing both bookmarks and history items. The columns of the table are defined in
+ * {@link BookmarkColumns}. Reading this table requires the
+ * {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} permission and writing to it
+ * requires the {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} permission.
+ */
+ public static final Uri BOOKMARKS_URI = Uri.parse("content://browser/bookmarks");
+
+ /**
+ * The name of extra data when starting Browser with ACTION_VIEW or
+ * ACTION_SEARCH intent.
+ * <p>
+ * The value should be an integer between 0 and 1000. If not set or set to
+ * 0, the Browser will use default. If set to 100, the Browser will start
+ * with 100%.
+ */
+ public static final String INITIAL_ZOOM_LEVEL = "browser.initialZoomLevel";
+
+ /**
+ * The name of the extra data when starting the Browser from another
+ * application.
+ * <p>
+ * The value is a unique identification string that will be used to
+ * identify the calling application. The Browser will attempt to reuse the
+ * same window each time the application launches the Browser with the same
+ * identifier.
+ */
+ public static final String EXTRA_APPLICATION_ID = "com.android.browser.application_id";
+
+ /**
+ * The name of the extra data in the VIEW intent. The data are key/value
+ * pairs in the format of Bundle. They will be sent in the HTTP request
+ * headers for the provided url. The keys can't be the standard HTTP headers
+ * as they are set by the WebView. The url's schema must be http(s).
+ * <p>
+ */
+ public static final String EXTRA_HEADERS = "com.android.browser.headers";
+
+ /* if you change column order you must also change indices
+ below */
+ public static final String[] HISTORY_PROJECTION = new String[] {
+ BookmarkColumns._ID, // 0
+ BookmarkColumns.URL, // 1
+ BookmarkColumns.VISITS, // 2
+ BookmarkColumns.DATE, // 3
+ BookmarkColumns.BOOKMARK, // 4
+ BookmarkColumns.TITLE, // 5
+ BookmarkColumns.FAVICON, // 6
+ BookmarkColumns.THUMBNAIL, // 7
+ BookmarkColumns.TOUCH_ICON, // 8
+ BookmarkColumns.USER_ENTERED, // 9
+ };
+
+ /* these indices dependent on HISTORY_PROJECTION */
+ public static final int HISTORY_PROJECTION_ID_INDEX = 0;
+ public static final int HISTORY_PROJECTION_URL_INDEX = 1;
+ public static final int HISTORY_PROJECTION_VISITS_INDEX = 2;
+ public static final int HISTORY_PROJECTION_DATE_INDEX = 3;
+ public static final int HISTORY_PROJECTION_BOOKMARK_INDEX = 4;
+ public static final int HISTORY_PROJECTION_TITLE_INDEX = 5;
+ public static final int HISTORY_PROJECTION_FAVICON_INDEX = 6;
+ /**
+ * @hide
+ */
+ public static final int HISTORY_PROJECTION_THUMBNAIL_INDEX = 7;
+ /**
+ * @hide
+ */
+ public static final int HISTORY_PROJECTION_TOUCH_ICON_INDEX = 8;
+
+ /* columns needed to determine whether to truncate history */
+ public static final String[] TRUNCATE_HISTORY_PROJECTION = new String[] {
+ BookmarkColumns._ID,
+ BookmarkColumns.DATE,
+ };
+
+ public static final int TRUNCATE_HISTORY_PROJECTION_ID_INDEX = 0;
+
+ /* truncate this many history items at a time */
+ public static final int TRUNCATE_N_OLDEST = 5;
+
+ /**
+ * A table containing a log of browser searches. The columns of the table are defined in
+ * {@link SearchColumns}. Reading this table requires the
+ * {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} permission and writing to it
+ * requires the {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} permission.
+ */
+ public static final Uri SEARCHES_URI = Uri.parse("content://browser/searches");
+
+ /**
+ * A projection of {@link #SEARCHES_URI} that contains {@link SearchColumns#_ID},
+ * {@link SearchColumns#SEARCH}, and {@link SearchColumns#DATE}.
+ */
+ public static final String[] SEARCHES_PROJECTION = new String[] {
+ // if you change column order you must also change indices below
+ SearchColumns._ID, // 0
+ SearchColumns.SEARCH, // 1
+ SearchColumns.DATE, // 2
+ };
+
+ /* these indices dependent on SEARCHES_PROJECTION */
+ public static final int SEARCHES_PROJECTION_SEARCH_INDEX = 1;
+ public static final int SEARCHES_PROJECTION_DATE_INDEX = 2;
+
+ /* Set a cap on the count of history items in the history/bookmark
+ table, to prevent db and layout operations from dragging to a
+ crawl. Revisit this cap when/if db/layout performance
+ improvements are made. Note: this does not affect bookmark
+ entries -- if the user wants more bookmarks than the cap, they
+ get them. */
+ private static final int MAX_HISTORY_COUNT = 250;
+
+ /**
+ * Open an activity to save a bookmark. Launch with a title
+ * and/or a url, both of which can be edited by the user before saving.
+ *
+ * @param c Context used to launch the activity to add a bookmark.
+ * @param title Title for the bookmark. Can be null or empty string.
+ * @param url Url for the bookmark. Can be null or empty string.
+ */
+ public static final void saveBookmark(Context c,
+ String title,
+ String url) {
+ Intent intent = new Intent(c, AddBookmarkPage.class);
+ intent.putExtra(BrowserContract.Bookmarks.URL, url);
+ intent.putExtra(BrowserContract.Bookmarks.TITLE, title);
+ c.startActivity(intent);
+ }
+
+ /**
+ * Boolean extra passed along with an Intent to a browser, specifying that
+ * a new tab be created. Overrides EXTRA_APPLICATION_ID; if both are set,
+ * a new tab will be used, rather than using the same one.
+ */
+ public static final String EXTRA_CREATE_NEW_TAB = "create_new_tab";
+
+ /**
+ * Stores a Bitmap extra in an {@link Intent} representing the screenshot of
+ * a page to share. When receiving an {@link Intent#ACTION_SEND} from the
+ * Browser, use this to access the screenshot.
+ * @hide
+ */
+ public final static String EXTRA_SHARE_SCREENSHOT = "share_screenshot";
+
+ /**
+ * Stores a Bitmap extra in an {@link Intent} representing the favicon of a
+ * page to share. When receiving an {@link Intent#ACTION_SEND} from the
+ * Browser, use this to access the favicon.
+ * @hide
+ */
+ public final static String EXTRA_SHARE_FAVICON = "share_favicon";
+
+ /**
+ * Sends the given string using an Intent with {@link Intent#ACTION_SEND} and a mime type
+ * of text/plain. The string is put into {@link Intent#EXTRA_TEXT}.
+ *
+ * @param context the context used to start the activity
+ * @param string the string to send
+ */
+ public static final void sendString(Context context, String string) {
+ sendString(context, string, context.getString(R.string.sendText));
+ }
+
+ /**
+ * Find an application to handle the given string and, if found, invoke
+ * it with the given string as a parameter.
+ * @param c Context used to launch the new activity.
+ * @param stringToSend The string to be handled.
+ * @param chooserDialogTitle The title of the dialog that allows the user
+ * to select between multiple applications that are all capable of handling
+ * the string.
+ * @hide pending API council approval
+ */
+ public static final void sendString(Context c,
+ String stringToSend,
+ String chooserDialogTitle) {
+ Intent send = new Intent(Intent.ACTION_SEND);
+ send.setType("text/plain");
+ send.putExtra(Intent.EXTRA_TEXT, stringToSend);
+
+ try {
+ Intent i = Intent.createChooser(send, chooserDialogTitle);
+ // In case this is called from outside an Activity
+ i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ c.startActivity(i);
+ } catch(android.content.ActivityNotFoundException ex) {
+ // if no app handles it, do nothing
+ }
+ }
+
+ /**
+ * Return a cursor pointing to a list of all the bookmarks. The cursor will have a single
+ * column, {@link BookmarkColumns#URL}.
+ * <p>
+ * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS}
+ *
+ * @param cr The ContentResolver used to access the database.
+ */
+ public static final Cursor getAllBookmarks(ContentResolver cr) throws
+ IllegalStateException {
+ return cr.query(Bookmarks.CONTENT_URI,
+ new String[] { Bookmarks.URL },
+ Bookmarks.IS_FOLDER + " = 0", null, null);
+ }
+
+ /**
+ * Return a cursor pointing to a list of all visited site urls. The cursor will
+ * have a single column, {@link BookmarkColumns#URL}.
+ * <p>
+ * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS}
+ *
+ * @param cr The ContentResolver used to access the database.
+ */
+ public static final Cursor getAllVisitedUrls(ContentResolver cr) throws
+ IllegalStateException {
+ return cr.query(Combined.CONTENT_URI,
+ new String[] { Combined.URL }, null, null,
+ Combined.DATE_CREATED + " ASC");
+ }
+
+ private static final void addOrUrlEquals(StringBuilder sb) {
+ sb.append(" OR " + BookmarkColumns.URL + " = ");
+ }
+
+ private static final Cursor getVisitedLike(ContentResolver cr, String url) {
+ boolean secure = false;
+ String compareString = url;
+ if (compareString.startsWith("http://")) {
+ compareString = compareString.substring(7);
+ } else if (compareString.startsWith("https://")) {
+ compareString = compareString.substring(8);
+ secure = true;
+ }
+ if (compareString.startsWith("www.")) {
+ compareString = compareString.substring(4);
+ }
+ StringBuilder whereClause = null;
+ if (secure) {
+ whereClause = new StringBuilder(Bookmarks.URL + " = ");
+ DatabaseUtils.appendEscapedSQLString(whereClause,
+ "https://" + compareString);
+ addOrUrlEquals(whereClause);
+ DatabaseUtils.appendEscapedSQLString(whereClause,
+ "https://www." + compareString);
+ } else {
+ whereClause = new StringBuilder(Bookmarks.URL + " = ");
+ DatabaseUtils.appendEscapedSQLString(whereClause,
+ compareString);
+ addOrUrlEquals(whereClause);
+ String wwwString = "www." + compareString;
+ DatabaseUtils.appendEscapedSQLString(whereClause,
+ wwwString);
+ addOrUrlEquals(whereClause);
+ DatabaseUtils.appendEscapedSQLString(whereClause,
+ "http://" + compareString);
+ addOrUrlEquals(whereClause);
+ DatabaseUtils.appendEscapedSQLString(whereClause,
+ "http://" + wwwString);
+ }
+ return cr.query(History.CONTENT_URI, new String[] { History._ID, History.VISITS },
+ whereClause.toString(), null, null);
+ }
+
+ /**
+ * Update the visited history to acknowledge that a site has been
+ * visited.
+ * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS}
+ * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS}
+ * @param cr The ContentResolver used to access the database.
+ * @param url The site being visited.
+ * @param real If true, this is an actual visit, and should add to the
+ * number of visits. If false, the user entered it manually.
+ */
+ public static final void updateVisitedHistory(ContentResolver cr,
+ String url, boolean real) {
+ long now = System.currentTimeMillis();
+ Cursor c = null;
+ try {
+ c = getVisitedLike(cr, url);
+ /* We should only get one answer that is exactly the same. */
+ if (c.moveToFirst()) {
+ ContentValues values = new ContentValues();
+ if (real) {
+ values.put(History.VISITS, c.getInt(1) + 1);
+ } else {
+ values.put(History.USER_ENTERED, 1);
+ }
+ values.put(History.DATE_LAST_VISITED, now);
+ cr.update(ContentUris.withAppendedId(History.CONTENT_URI, c.getLong(0)),
+ values, null, null);
+ } else {
+ truncateHistory(cr);
+ ContentValues values = new ContentValues();
+ int visits;
+ int user_entered;
+ if (real) {
+ visits = 1;
+ user_entered = 0;
+ } else {
+ visits = 0;
+ user_entered = 1;
+ }
+ values.put(History.URL, url);
+ values.put(History.VISITS, visits);
+ values.put(History.DATE_LAST_VISITED, now);
+ values.put(History.TITLE, url);
+ values.put(History.DATE_CREATED, 0);
+ values.put(History.USER_ENTERED, user_entered);
+ cr.insert(History.CONTENT_URI, values);
+ }
+ } catch (IllegalStateException e) {
+ Log.e(LOGTAG, "updateVisitedHistory", e);
+ } finally {
+ if (c != null) c.close();
+ }
+ }
+
+ /**
+ * Returns all the URLs in the history.
+ * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS}
+ * @param cr The ContentResolver used to access the database.
+ * @hide pending API council approval
+ */
+ public static final String[] getVisitedHistory(ContentResolver cr) {
+ Cursor c = null;
+ String[] str = null;
+ try {
+ String[] projection = new String[] {
+ History.URL,
+ };
+ c = cr.query(History.CONTENT_URI, projection, History.VISITS + " > 0", null, null);
+ if (c == null) return new String[0];
+ str = new String[c.getCount()];
+ int i = 0;
+ while (c.moveToNext()) {
+ str[i] = c.getString(0);
+ i++;
+ }
+ } catch (IllegalStateException e) {
+ Log.e(LOGTAG, "getVisitedHistory", e);
+ str = new String[0];
+ } finally {
+ if (c != null) c.close();
+ }
+ return str;
+ }
+
+ /**
+ * If there are more than MAX_HISTORY_COUNT non-bookmark history
+ * items in the bookmark/history table, delete TRUNCATE_N_OLDEST
+ * of them. This is used to keep our history table to a
+ * reasonable size. Note: it does not prune bookmarks. If the
+ * user wants 1000 bookmarks, the user gets 1000 bookmarks.
+ * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS}
+ * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS}
+ *
+ * @param cr The ContentResolver used to access the database.
+ */
+ public static final void truncateHistory(ContentResolver cr) {
+ // TODO make a single request to the provider to do this in a single transaction
+ Cursor cursor = null;
+ try {
+
+ // Select non-bookmark history, ordered by date
+ cursor = cr.query(History.CONTENT_URI,
+ new String[] { History._ID, History.URL, History.DATE_LAST_VISITED },
+ null, null, History.DATE_LAST_VISITED + " ASC");
+
+ if (cursor.moveToFirst() && cursor.getCount() >= MAX_HISTORY_COUNT) {
+ /* eliminate oldest history items */
+ for (int i = 0; i < TRUNCATE_N_OLDEST; i++) {
+ cr.delete(ContentUris.withAppendedId(History.CONTENT_URI, cursor.getLong(0)),
+ null, null);
+ if (!cursor.moveToNext()) break;
+ }
+ }
+ } catch (IllegalStateException e) {
+ Log.e(LOGTAG, "truncateHistory", e);
+ } finally {
+ if (cursor != null) cursor.close();
+ }
+ }
+
+ /**
+ * Returns whether there is any history to clear.
+ * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS}
+ * @param cr The ContentResolver used to access the database.
+ * @return boolean True if the history can be cleared.
+ */
+ public static final boolean canClearHistory(ContentResolver cr) {
+ Cursor cursor = null;
+ boolean ret = false;
+ try {
+ cursor = cr.query(History.CONTENT_URI,
+ new String [] { History._ID, History.VISITS },
+ null, null, null);
+ ret = cursor.getCount() > 0;
+ } catch (IllegalStateException e) {
+ Log.e(LOGTAG, "canClearHistory", e);
+ } finally {
+ if (cursor != null) cursor.close();
+ }
+ return ret;
+ }
+
+ /**
+ * Delete all entries from the bookmarks/history table which are
+ * not bookmarks. Also set all visited bookmarks to unvisited.
+ * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS}
+ * @param cr The ContentResolver used to access the database.
+ */
+ public static final void clearHistory(ContentResolver cr) {
+ deleteHistoryWhere(cr, null);
+ }
+
+ /**
+ * Helper function to delete all history items and release the icons for them in the
+ * {@link WebIconDatabase}.
+ *
+ * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS}
+ * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS}
+ *
+ * @param cr The ContentResolver used to access the database.
+ * @param whereClause String to limit the items affected.
+ * null means all items.
+ */
+ private static final void deleteHistoryWhere(ContentResolver cr, String whereClause) {
+ Cursor cursor = null;
+ try {
+ cursor = cr.query(History.CONTENT_URI, new String[] { History.URL }, whereClause,
+ null, null);
+ if (cursor.moveToFirst()) {
+ cr.delete(History.CONTENT_URI, whereClause, null);
+ }
+ } catch (IllegalStateException e) {
+ Log.e(LOGTAG, "deleteHistoryWhere", e);
+ return;
+ } finally {
+ if (cursor != null) cursor.close();
+ }
+ }
+
+ /**
+ * Delete all history items from begin to end.
+ * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS}
+ * @param cr The ContentResolver used to access the database.
+ * @param begin First date to remove. If -1, all dates before end.
+ * Inclusive.
+ * @param end Last date to remove. If -1, all dates after begin.
+ * Non-inclusive.
+ */
+ public static final void deleteHistoryTimeFrame(ContentResolver cr,
+ long begin, long end) {
+ String whereClause;
+ String date = BookmarkColumns.DATE;
+ if (-1 == begin) {
+ if (-1 == end) {
+ clearHistory(cr);
+ return;
+ }
+ whereClause = date + " < " + Long.toString(end);
+ } else if (-1 == end) {
+ whereClause = date + " >= " + Long.toString(begin);
+ } else {
+ whereClause = date + " >= " + Long.toString(begin) + " AND " + date
+ + " < " + Long.toString(end);
+ }
+ deleteHistoryWhere(cr, whereClause);
+ }
+
+ /**
+ * Remove a specific url from the history database.
+ * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS}
+ * @param cr The ContentResolver used to access the database.
+ * @param url url to remove.
+ */
+ public static final void deleteFromHistory(ContentResolver cr,
+ String url) {
+ cr.delete(History.CONTENT_URI, History.URL + "=?", new String[] { url });
+ }
+
+ /**
+ * Add a search string to the searches database.
+ * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS}
+ * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS}
+ * @param cr The ContentResolver used to access the database.
+ * @param search The string to add to the searches database.
+ */
+ public static final void addSearchUrl(ContentResolver cr, String search) {
+ // The content provider will take care of updating existing searches instead of duplicating
+ ContentValues values = new ContentValues();
+ values.put(Searches.SEARCH, search);
+ values.put(Searches.DATE, System.currentTimeMillis());
+ cr.insert(Searches.CONTENT_URI, values);
+ }
+
+ /**
+ * Remove all searches from the search database.
+ * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS}
+ * @param cr The ContentResolver used to access the database.
+ */
+ public static final void clearSearches(ContentResolver cr) {
+ // FIXME: Should this clear the urls to which these searches lead?
+ // (i.e. remove google.com/query= blah blah blah)
+ try {
+ cr.delete(Searches.CONTENT_URI, null, null);
+ } catch (IllegalStateException e) {
+ Log.e(LOGTAG, "clearSearches", e);
+ }
+ }
+
+ /**
+ * Column definitions for the mixed bookmark and history items available
+ * at {@link #BOOKMARKS_URI}.
+ */
+ public static class BookmarkColumns{
+ /**
+ * The unique ID for a row.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String _ID = "_id";
+
+ /**
+ * The count of rows in a directory.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String _COUNT = "_count";
+
+ /**
+ * 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";
+ }
+
+ /**
+ * Column definitions for the search history table, available at {@link #SEARCHES_URI}.
+ */
+ public static class SearchColumns{
+ /**
+ * The unique ID for a row.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String _ID = "_id";
+
+ /**
+ * The count of rows in a directory.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String _COUNT = "_count";
+
+ /**
+ * @deprecated Not used.
+ */
+ @Deprecated
+ public static final String URL = "url";
+
+ /**
+ * 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";
+ }
+}
diff --git a/src/src/com/android/browser/platformsupport/BrowserContract.java b/src/src/com/android/browser/platformsupport/BrowserContract.java
new file mode 100644
index 00000000..79bdfb87
--- /dev/null
+++ b/src/src/com/android/browser/platformsupport/BrowserContract.java
@@ -0,0 +1,746 @@
+/*
+ * 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;
+
+import com.android.browser.BrowserConfig;
+/**
+ * <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 = BrowserConfig.AUTHORITY;
+
+ /** 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/src/com/android/browser/platformsupport/Process.java b/src/src/com/android/browser/platformsupport/Process.java
new file mode 100644
index 00000000..5731b274
--- /dev/null
+++ b/src/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/src/com/android/browser/platformsupport/SeekBarPreference.java b/src/src/com/android/browser/platformsupport/SeekBarPreference.java
new file mode 100644
index 00000000..41b79153
--- /dev/null
+++ b/src/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/src/com/android/browser/platformsupport/SyncStateContentProviderHelper.java b/src/src/com/android/browser/platformsupport/SyncStateContentProviderHelper.java
new file mode 100644
index 00000000..d6a40c7e
--- /dev/null
+++ b/src/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/src/com/android/browser/platformsupport/WebAddress.java b/src/src/com/android/browser/platformsupport/WebAddress.java
new file mode 100644
index 00000000..10fac153
--- /dev/null
+++ b/src/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/src/com/android/browser/preferences/AboutPreferencesFragment.java b/src/src/com/android/browser/preferences/AboutPreferencesFragment.java
new file mode 100644
index 00000000..7d143205
--- /dev/null
+++ b/src/src/com/android/browser/preferences/AboutPreferencesFragment.java
@@ -0,0 +1,228 @@
+/*
+ * 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.app.ActionBar;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+import android.provider.Browser;
+
+import com.android.browser.BrowserActivity;
+import com.android.browser.BrowserPreferencesPage;
+import com.android.browser.BrowserSwitches;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+import com.android.browser.UpdateNotificationService;
+
+import org.codeaurora.swe.BrowserCommandLine;
+
+public class AboutPreferencesFragment extends PreferenceFragment
+ implements OnPreferenceClickListener {
+
+ final String ABOUT_TEXT_VERSION_KEY = "Version:";
+ final String ABOUT_TEXT_BUILT_KEY = "Built:";
+ final String ABOUT_TEXT_HASH_KEY = "Hash:";
+
+ String mFeedbackRecipient = "";
+ String mHelpURL = "";
+ String mVersion = "";
+ String mBuilt = "";
+ String mHash = "";
+ String mTabTitle = "";
+ String mTabURL = "";
+
+ String mAboutText = "";
+ PreferenceScreen mHeadPref = null;
+
+ private String findValueFromAboutText(String aboutKey) {
+ int start = mAboutText.indexOf(aboutKey);
+ int end = mAboutText.indexOf("\n", start);
+ String value = "";
+
+ if (start != -1 && end != -1) {
+ start += aboutKey.length();
+ value = mAboutText.substring(start, end);
+ }
+ return value;
+ }
+
+ private void setPreference(String prefKey, String value) {
+ Preference pref = findPreference(prefKey);
+ if (pref == null) {
+ return;
+ }
+
+ if (value.isEmpty()) {
+ if (mHeadPref != null)
+ mHeadPref.removePreference(pref);
+ } else {
+ pref.setSummary(value);
+ }
+ }
+
+ private void setOnClickListener(String prefKey, boolean set) {
+ Preference pref = findPreference(prefKey);
+ if (pref == null) {
+ return;
+ }
+
+ if (set) {
+ pref.setOnPreferenceClickListener(this);
+ } else {
+ if (mHeadPref != null)
+ mHeadPref.removePreference(pref);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ ActionBar bar = getActivity().getActionBar();
+ if (bar != null) {
+ bar.setTitle(R.string.about);
+ }
+
+ mAboutText = getString(R.string.about_text);
+
+ addPreferencesFromResource(R.xml.about_preferences);
+ mHeadPref = (PreferenceScreen) findPreference(PreferenceKeys.PREF_ABOUT);
+
+ mVersion = findValueFromAboutText(ABOUT_TEXT_VERSION_KEY);
+ setPreference(PreferenceKeys.PREF_VERSION, mVersion);
+
+ mBuilt = findValueFromAboutText(ABOUT_TEXT_BUILT_KEY);
+ setPreference(PreferenceKeys.PREF_BUILD_DATE, mBuilt);
+
+ mHash = findValueFromAboutText(ABOUT_TEXT_HASH_KEY);
+ setPreference(PreferenceKeys.PREF_BUILD_HASH, mHash);
+
+ final Bundle arguments = getArguments();
+ String user_agent = "";
+ if (arguments != null) {
+ user_agent = (String) arguments.getCharSequence("UA", "");
+ mTabTitle = (String) arguments.getCharSequence("TabTitle", "");
+ mTabURL = (String) arguments.getCharSequence("TabURL", "");
+ }
+
+ setPreference(PreferenceKeys.PREF_USER_AGENT, user_agent);
+
+ if (BrowserCommandLine.hasSwitch(BrowserSwitches.CMD_LINE_SWITCH_HELPURL)) {
+ mHelpURL = BrowserCommandLine.getSwitchValue(
+ BrowserSwitches.CMD_LINE_SWITCH_HELPURL);
+ }
+
+ setOnClickListener(PreferenceKeys.PREF_HELP, !mHelpURL.isEmpty());
+
+ if (BrowserCommandLine.hasSwitch(BrowserSwitches.CMD_LINE_SWITCH_FEEDBACK)) {
+ mFeedbackRecipient = BrowserCommandLine.getSwitchValue(
+ BrowserSwitches.CMD_LINE_SWITCH_FEEDBACK);
+ }
+
+ setOnClickListener(PreferenceKeys.PREF_FEEDBACK, !mFeedbackRecipient.isEmpty());
+
+ setOnClickListener(PreferenceKeys.PREF_LEGAL, true);
+ if (BrowserCommandLine.hasSwitch(BrowserSwitches.AUTO_UPDATE_SERVER_CMD)) {
+ setPreference(PreferenceKeys.PREF_AUTO_UPDATE,
+ UpdateNotificationService.getLatestVersion(getActivity()));
+ setOnClickListener(PreferenceKeys.PREF_AUTO_UPDATE,
+ UpdateNotificationService.getCurrentVersionCode(getActivity()) <
+ UpdateNotificationService.getLatestVersionCode(getActivity()));
+ } else {
+ Preference pref = findPreference(PreferenceKeys.PREF_AUTO_UPDATE);
+ if (mHeadPref != null)
+ mHeadPref.removePreference(pref);
+ }
+
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ if (preference.getKey().equals(PreferenceKeys.PREF_HELP)) {
+ Intent intent = new Intent(getActivity(), BrowserActivity.class);
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(mHelpURL));
+ getActivity().startActivity(intent);
+ return true;
+ } else if(preference.getKey().equals(PreferenceKeys.PREF_LEGAL)) {
+ Bundle bundle = new Bundle();
+ BrowserPreferencesPage.startPreferenceFragmentExtraForResult(getActivity(),
+ LegalPreferencesFragment.class.getName(), bundle, 0);
+ return true;
+ } else if (preference.getKey().equals(PreferenceKeys.PREF_FEEDBACK)) {
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType("message/rfc822");
+ intent.putExtra(Intent.EXTRA_EMAIL, new String[]{mFeedbackRecipient});
+ intent.putExtra(Intent.EXTRA_SUBJECT,"Browser Feedback");
+
+ String message = "";
+ if (!mVersion.isEmpty()) {
+ message += "Version: " + mVersion + "\n";
+ }
+
+ if (!mBuilt.isEmpty()) {
+ message += "Build Date: " + mBuilt + "\n";
+ }
+
+ if (!mHash.isEmpty()) {
+ message += "Build Hash: " + mHash + "\n";
+ }
+
+ if (!mTabTitle.isEmpty()) {
+ message += "Tab Title: " + mTabTitle + "\n";
+ }
+
+ if (!mTabURL.isEmpty()) {
+ message += "Tab URL: " + mTabURL + "\n";
+ }
+
+ message += "\nEnter your feedback here...";
+
+ intent.putExtra(Intent.EXTRA_TEXT, message);
+ startActivity(Intent.createChooser(intent, "Select email application"));
+ return true;
+ } else if (preference.getKey().equals(PreferenceKeys.PREF_AUTO_UPDATE)) {
+ Intent intent = new Intent(getActivity(), BrowserActivity.class);
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.putExtra(Browser.EXTRA_APPLICATION_ID, getActivity().getPackageName());
+ intent.putExtra(Browser.EXTRA_CREATE_NEW_TAB, true);
+ intent.setData(Uri.parse(
+ UpdateNotificationService.getLatestDownloadUrl(getActivity())));
+ getActivity().startActivity(intent);
+ }
+ return false;
+ }
+}
diff --git a/src/src/com/android/browser/preferences/AccessibilityPreferencesFragment.java b/src/src/com/android/browser/preferences/AccessibilityPreferencesFragment.java
new file mode 100644
index 00000000..a505836b
--- /dev/null
+++ b/src/src/com/android/browser/preferences/AccessibilityPreferencesFragment.java
@@ -0,0 +1,128 @@
+/*
+ * 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.app.ActionBar;
+import android.content.Context;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.util.Log;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+
+import java.text.NumberFormat;
+
+public class AccessibilityPreferencesFragment extends SWEPreferenceFragment
+ implements Preference.OnPreferenceChangeListener {
+
+ NumberFormat mFormat;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ 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());
+
+ /* SWE: Comment out double tap zoom feature
+ 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();
+ ActionBar bar = getActivity().getActionBar();
+ if (bar != null) {
+ bar.setTitle(R.string.pref_accessibility_title);
+ bar.setDisplayHomeAsUpEnabled(false);
+ bar.setHomeButtonEnabled(false);
+ }
+ }
+
+ 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.
+ Log.d("AccessibilityPref", "Activity is null");
+ return false;
+ }
+ Log.d("AccessibilityPref", "User clicked on " + pref.getKey());
+
+ 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/src/com/android/browser/preferences/AdvancedPreferencesFragment.java b/src/src/com/android/browser/preferences/AdvancedPreferencesFragment.java
new file mode 100644
index 00000000..3d988947
--- /dev/null
+++ b/src/src/com/android/browser/preferences/AdvancedPreferencesFragment.java
@@ -0,0 +1,225 @@
+/*
+ * 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.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Intent;
+import android.content.SharedPreferences.Editor;
+import android.net.Uri;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+import android.util.Log;
+
+import com.android.browser.BaseUi;
+import com.android.browser.BrowserActivity;
+import com.android.browser.BrowserSettings;
+import com.android.browser.BrowserYesNoPreference;
+import com.android.browser.DownloadHandler;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+
+public class AdvancedPreferencesFragment
+ implements Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener {
+
+ PreferenceFragment mFragment = null;
+
+ AdvancedPreferencesFragment(PreferenceFragment fragment) {
+ mFragment = fragment;
+
+ Preference e = mFragment.findPreference(PreferenceKeys.PREF_RESET_DEFAULT_PREFERENCES);
+ e.setOnPreferenceChangeListener(this);
+
+ e = mFragment.findPreference(PreferenceKeys.PREF_SEARCH_ENGINE);
+ e.setOnPreferenceChangeListener(this);
+ updateListPreferenceSummary((ListPreference) e);
+
+ e = mFragment.findPreference("privacy_security");
+ e.setOnPreferenceClickListener(this);
+
+ e = mFragment.findPreference(PreferenceKeys.PREF_DEBUG_MENU);
+ if (!BrowserSettings.getInstance().isDebugEnabled()) {
+ PreferenceCategory category = (PreferenceCategory) mFragment.findPreference("advanced");
+ category.removePreference(e);
+ } else {
+ e.setOnPreferenceClickListener(this);
+ }
+
+ e = mFragment.findPreference("accessibility_menu");
+ e.setOnPreferenceClickListener(this);
+
+ // Below are preferences for carrier specific features
+ PreferenceScreen contentSettingsPrefScreen =
+ (PreferenceScreen) mFragment.findPreference("content_settings");
+ contentSettingsPrefScreen.setOnPreferenceClickListener(this);
+
+ ListPreference edgeSwipePref =
+ (ListPreference) mFragment.findPreference("edge_swiping_action");
+ edgeSwipePref.setOnPreferenceChangeListener(this);
+
+ if (BaseUi.isUiLowPowerMode()) {
+ edgeSwipePref.setEnabled(false);
+ } else {
+ String[] options = mFragment.getResources().getStringArray(
+ R.array.pref_edge_swiping_values);
+
+ String value = BrowserSettings.getInstance().getEdgeSwipeAction();
+
+ if (value.equals(mFragment.getString(R.string.value_unknown_edge_swipe))) {
+ edgeSwipePref.setSummary(mFragment.getString(R.string.pref_edge_swipe_unknown));
+ } else {
+ for (int i = 0; i < options.length; i++) {
+ if (value.equals(options[i])) {
+ edgeSwipePref.setValueIndex(i);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ if (requestCode == ContentPreferencesFragment.DOWNLOAD_PATH_RESULT_CODE) {
+ if ( resultCode == Activity.RESULT_OK && intent != null) {
+ final String result_dir_sel =
+ mFragment.getResources().getString(R.string.def_file_manager_result_dir);
+ String downloadPath = intent.getStringExtra(result_dir_sel);
+ // Fallback logic to stock browser
+ if (downloadPath == null) {
+ Uri uri = intent.getData();
+ if(uri != null)
+ downloadPath = uri.getPath();
+ }
+ if (downloadPath != null) {
+ PreferenceScreen downloadPathPreset =
+ (PreferenceScreen) mFragment.findPreference(
+ PreferenceKeys.PREF_DOWNLOAD_PATH);
+ Editor editor = downloadPathPreset.getEditor();
+ editor.putString(PreferenceKeys.PREF_DOWNLOAD_PATH, downloadPath);
+ editor.apply();
+ String downloadPathForUser = DownloadHandler.getDownloadPathForUser(
+ mFragment.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.
+ */
+ public void onResume() {
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference pref, Object objValue) {
+ if (mFragment.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("edge_swiping_action")){
+ ListPreference lp = (ListPreference) pref;
+ lp.setValue((String) objValue);
+ updateListPreferenceSummary(lp);
+ return true;
+ }
+
+ else if (pref.getKey().equals(PreferenceKeys.PREF_RESET_DEFAULT_PREFERENCES)) {
+ Integer value = (Integer) objValue;
+ if (value.intValue() != BrowserYesNoPreference.CANCEL_BTN) {
+ BrowserSettings settings = BrowserSettings.getInstance();
+ if (value.intValue() == BrowserYesNoPreference.OTHER_BTN) {
+ settings.clearCache();
+ settings.clearDatabases();
+ settings.clearCookies();
+ settings.clearHistory();
+ settings.clearFormData();
+ settings.clearPasswords();
+ settings.clearLocationAccess();
+ }
+
+ settings.resetDefaultPreferences();
+ mFragment.startActivity(new Intent(BrowserActivity.ACTION_RESTART, null,
+ mFragment.getActivity(), BrowserActivity.class));
+ return true;
+ }
+
+ } else if (pref.getKey().equals(PreferenceKeys.PREF_SEARCH_ENGINE)) {
+ ListPreference lp = (ListPreference) pref;
+ lp.setValue((String) objValue);
+ updateListPreferenceSummary(lp);
+ return false;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ FragmentManager fragmentManager = mFragment.getFragmentManager();
+
+ if (preference.getKey().equals(PreferenceKeys.PREF_DEBUG_MENU)) {
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+
+ Fragment newFragment = new DebugPreferencesFragment();
+ fragmentTransaction.replace(mFragment.getId(), newFragment);
+ fragmentTransaction.addToBackStack(null);
+ fragmentTransaction.commit();
+ return true;
+ } else if (preference.getKey().equals("accessibility_menu")) {
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+
+ Fragment newFragment = new AccessibilityPreferencesFragment();
+ fragmentTransaction.replace(mFragment.getId(), newFragment);
+ fragmentTransaction.addToBackStack(null);
+ fragmentTransaction.commit();
+ return true;
+ } else if (preference.getKey().equals("privacy_security")) {
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+
+ Fragment newFragment = new PrivacySecurityPreferencesFragment();
+ fragmentTransaction.replace(mFragment.getId(), newFragment);
+ fragmentTransaction.addToBackStack(null);
+ fragmentTransaction.commit();
+ return true;
+ } else if (preference.getKey().equals("content_settings")) {
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+
+ Fragment newFragment = new ContentPreferencesFragment();
+ fragmentTransaction.replace(mFragment.getId(), newFragment);
+ fragmentTransaction.addToBackStack(null);
+ fragmentTransaction.commit();
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/src/com/android/browser/preferences/BandwidthPreferencesFragment.java b/src/src/com/android/browser/preferences/BandwidthPreferencesFragment.java
new file mode 100644
index 00000000..0cb064ab
--- /dev/null
+++ b/src/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/src/com/android/browser/preferences/ContentPreferencesFragment.java b/src/src/com/android/browser/preferences/ContentPreferencesFragment.java
new file mode 100644
index 00000000..63bae5b9
--- /dev/null
+++ b/src/src/com/android/browser/preferences/ContentPreferencesFragment.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2015, 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.app.ActionBar;
+import android.content.Intent;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceScreen;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.android.browser.BrowserConfig;
+import com.android.browser.BrowserSettings;
+import com.android.browser.DownloadHandler;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+
+public class ContentPreferencesFragment extends SWEPreferenceFragment {
+ public static final int DOWNLOAD_PATH_RESULT_CODE = 1;
+ private final static String LOGTAG = "ContentPreferences";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the XML preferences file
+ addPreferencesFromResource(R.xml.content_preferences);
+
+ PreferenceScreen screen = (PreferenceScreen) findPreference("content_settings");
+
+ if (!BrowserConfig.getInstance(getActivity().getApplicationContext())
+ .hasFeature(BrowserConfig.Feature.CUSTOM_DOWNLOAD_PATH)) {
+ screen.removePreference(findPreference(PreferenceKeys.PREF_DOWNLOAD_PATH));
+ } else {
+ 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(getActivity(),
+ downloadPath);
+ downloadPathPreset.setSummary(downloadPathForUser);
+ }
+ }
+
+ private Preference.OnPreferenceClickListener onClickDownloadPathSettings() {
+ return new Preference.OnPreferenceClickListener() {
+ public boolean onPreferenceClick(Preference preference) {
+ final String filemanagerIntent =
+ getResources().getString(R.string.def_intent_file_manager);
+ if (!TextUtils.isEmpty(filemanagerIntent)) {
+ try {
+ Intent i = new Intent(filemanagerIntent);
+ startActivityForResult(i,
+ DOWNLOAD_PATH_RESULT_CODE);
+ } catch (Exception e) {
+ String err_msg = getResources().getString(
+ R.string.activity_not_found,
+ filemanagerIntent);
+ Toast.makeText(getActivity(), err_msg, Toast.LENGTH_LONG).show();
+ }
+ return true;
+ } else {
+ Log.e(LOGTAG, "File Manager intent not defined !!");
+ return true;
+ }
+ }
+ };
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ ActionBar bar = getActivity().getActionBar();
+ if (bar != null) {
+ bar.setTitle(R.string.pref_content_title);
+ bar.setDisplayHomeAsUpEnabled(false);
+ bar.setHomeButtonEnabled(false);
+ }
+ }
+}
diff --git a/src/src/com/android/browser/preferences/DebugPreferencesFragment.java b/src/src/com/android/browser/preferences/DebugPreferencesFragment.java
new file mode 100644
index 00000000..2283fb6a
--- /dev/null
+++ b/src/src/com/android/browser/preferences/DebugPreferencesFragment.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.preferences;
+
+import android.app.ActionBar;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.SwitchPreference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.Preference.OnPreferenceChangeListener;
+
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+
+import org.codeaurora.swe.PermissionsServiceFactory;
+
+public class DebugPreferencesFragment extends SWEPreferenceFragment
+ implements OnPreferenceClickListener, OnPreferenceChangeListener {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the XML preferences file
+ addPreferencesFromResource(R.xml.debug_preferences);
+
+ SwitchPreference pref = (SwitchPreference) findPreference(PreferenceKeys.PREF_DISABLE_PERF);
+ pref.setOnPreferenceChangeListener(this);
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference pref, Object objValue) {
+ if (getActivity() == null) {
+ return false;
+ }
+
+ if (pref.getKey().equals(PreferenceKeys.PREF_DISABLE_PERF)) {
+ PermissionsServiceFactory.setDefaultPermissions(
+ PermissionsServiceFactory.PermissionType.WEBREFINER, (Boolean)objValue);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ return false;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ ActionBar bar = getActivity().getActionBar();
+ if (bar != null) {
+ bar.setTitle(R.string.pref_development_title);
+ bar.setDisplayHomeAsUpEnabled(false);
+ bar.setHomeButtonEnabled(false);
+ }
+ }
+}
diff --git a/src/src/com/android/browser/preferences/FontSizePreview.java b/src/src/com/android/browser/preferences/FontSizePreview.java
new file mode 100644
index 00000000..67d0bd7b
--- /dev/null
+++ b/src/src/com/android/browser/preferences/FontSizePreview.java
@@ -0,0 +1,56 @@
+/*
+ * 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.util.AttributeSet;
+import com.android.browser.BrowserSettings;
+import com.android.browser.R;
+
+import org.codeaurora.swe.WebSettings;
+
+public class FontSizePreview extends WebViewPreview {
+
+ //default size for normal sized preview text
+ static final int DEFAULT_FONT_PREVIEW_SIZE = 13;
+
+ 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 updatePreview(boolean forceReload) {
+ if (mWebView == null || mTextView == null)
+ return;
+
+ WebSettings ws = mWebView.getSettings();
+ BrowserSettings bs = BrowserSettings.getInstance();
+ ws.setMinimumFontSize(bs.getMinimumFontSize());
+ ws.setTextZoom(bs.getTextZoom());
+ mTextView.setText(R.string.pref_sample_font_size);
+ mTextView.setTextSize(DEFAULT_FONT_PREVIEW_SIZE * bs.getTextZoom() / 100);
+ }
+}
diff --git a/src/src/com/android/browser/preferences/GeneralPreferencesFragment.java b/src/src/com/android/browser/preferences/GeneralPreferencesFragment.java
new file mode 100644
index 00000000..9f610617
--- /dev/null
+++ b/src/src/com/android/browser/preferences/GeneralPreferencesFragment.java
@@ -0,0 +1,393 @@
+/*
+ * 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.ActionBar;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceScreen;
+import android.preference.SwitchPreference;
+import android.text.InputType;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Gravity;
+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 android.widget.Toast;
+
+import com.android.browser.AutoFillSettingsFragment;
+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;
+import com.android.browser.mdm.AutoFillRestriction;
+import com.android.browser.mdm.SearchEngineRestriction;
+
+import org.codeaurora.swe.PermissionsServiceFactory;
+
+public class GeneralPreferencesFragment extends SWEPreferenceFragment
+ implements Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener {
+
+ static final String TAG = "GeneralPreferencesFragment";
+
+ public static final String EXTRA_CURRENT_PAGE = "currentPage";
+
+ 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";
+ static final String PREF_POWERSAVE = "powersave_enabled";
+
+ String[] mChoices, mValues;
+ String mCurrentPage;
+
+ AdvancedPreferencesFragment mAdvFrag = null;
+ PrivacySecurityPreferencesFragment mPrivFrag = null;
+
+ @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(EXTRA_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);
+
+ PreferenceScreen autofill = (PreferenceScreen) findPreference(
+ PreferenceKeys.PREF_AUTOFILL_PROFILE);
+ autofill.setOnPreferenceClickListener(this);
+
+ SwitchPreference powersave = (SwitchPreference) findPreference(PREF_POWERSAVE);
+ powersave.setOnPreferenceChangeListener(this);
+
+ SwitchPreference nightmode = (SwitchPreference) findPreference(
+ PreferenceKeys.PREF_NIGHTMODE_ENABLED);
+ nightmode.setOnPreferenceChangeListener(this);
+
+ final Bundle arguments = getArguments();
+ if (arguments != null && arguments.getBoolean("LowPower")) {
+ LowPowerDialogFragment fragment = LowPowerDialogFragment.newInstance();
+ fragment.show(getActivity().getFragmentManager(), "setPowersave dialog");
+ }
+
+ //Disable set search engine preference if SEARCH_ENGINE restriction is enabled
+ if (SearchEngineRestriction.getInstance().isEnabled()) {
+ findPreference("search_engine").setEnabled(false);
+ }
+
+ // Register Preference objects with their MDM restriction handlers
+ AutoFillRestriction.getInstance().
+ registerPreference(findPreference(PreferenceKeys.PREF_AUTOFILL_ENABLED));
+
+ mAdvFrag = new AdvancedPreferencesFragment(this);
+ //mPrivFrag = new PrivacySecurityPreferencesFragment(this);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ // Un-register Preference objects from their MDM restriction handlers
+ AutoFillRestriction.getInstance().registerPreference(null);
+ }
+
+ @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;
+ }
+
+ if (pref.getKey().equals(PREF_POWERSAVE)) {
+ BrowserSettings settings = BrowserSettings.getInstance();
+ settings.setPowerSaveModeEnabled((Boolean)objValue);
+ PermissionsServiceFactory.setDefaultPermissions(
+ PermissionsServiceFactory.PermissionType.WEBREFINER, !(Boolean)objValue);
+ showPowerSaveInfo((Boolean) objValue);
+ BrowserPreferencesPage.sResultExtra = PreferenceKeys.ACTION_RELOAD_PAGE;
+ }
+
+ if (pref.getKey().equals(PreferenceKeys.PREF_NIGHTMODE_ENABLED)) {
+ BrowserPreferencesPage.sResultExtra = PreferenceKeys.ACTION_RELOAD_PAGE;
+ }
+ 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();
+
+ mAdvFrag.onResume();
+ refreshUi();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ mAdvFrag.onActivityResult(requestCode,resultCode, data);
+ }
+
+ void refreshUi() {
+ ActionBar bar = getActivity().getActionBar();
+ if (bar != null) {
+ bar.setTitle(R.string.menu_preferences);
+ bar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ PreferenceScreen autoFillSettings =
+ (PreferenceScreen)findPreference(PreferenceKeys.PREF_AUTOFILL_PROFILE);
+ autoFillSettings.setDependency(PreferenceKeys.PREF_AUTOFILL_ENABLED);
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ if (preference.getKey().equals(PreferenceKeys.PREF_AUTOFILL_PROFILE)) {
+ FragmentManager fragmentManager = getFragmentManager();
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+
+ Fragment newFragment = new AutoFillSettingsFragment();
+ fragmentTransaction.replace(getId(), newFragment);
+ fragmentTransaction.addToBackStack(null);
+ fragmentTransaction.commit();
+ return true;
+ }
+ return false;
+ }
+
+ void showPowerSaveInfo(boolean toggle) {
+ String toastInfo;
+ if (toggle)
+ toastInfo = getActivity().getResources().getString(R.string.powersave_dialog_on);
+ else
+ toastInfo = getActivity().getResources().getString(R.string.powersave_dialog_off);
+
+ Toast toast = Toast.makeText(getActivity(), toastInfo, Toast.LENGTH_SHORT);
+ toast.setGravity(Gravity.CENTER, 0, 0);
+ toast.show();
+ }
+
+ /*
+ Add this class to manage AlertDialog lifecycle.
+ */
+ public static class MyAlertDialogFragment extends DialogFragment {
+ private final String HOME_PAGE = "homepage";
+ private EditText editText = null;
+ public static MyAlertDialogFragment newInstance() {
+ MyAlertDialogFragment frag = new MyAlertDialogFragment();
+ return frag;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final BrowserSettings settings = BrowserSettings.getInstance();
+ editText = new EditText(getActivity());
+ String homePage = savedInstanceState != null ?
+ savedInstanceState.getString(HOME_PAGE): settings.getHomePage();
+ editText.setInputType(InputType.TYPE_CLASS_TEXT
+ | InputType.TYPE_TEXT_VARIATION_URI);
+ editText.setText(homePage);
+ 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;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState){
+ super.onSaveInstanceState(outState);
+ outState.putString(HOME_PAGE, editText.getText().toString().trim());
+ }
+ }
+ public static class LowPowerDialogFragment extends DialogFragment {
+ public static LowPowerDialogFragment newInstance() {
+ LowPowerDialogFragment frag = new LowPowerDialogFragment();
+ return frag;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final BrowserSettings settings = BrowserSettings.getInstance();
+ final AlertDialog dialog = new AlertDialog.Builder(getActivity())
+ .setPositiveButton(android.R.string.ok, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ settings.setPowerSaveModeEnabled(true);
+ getActivity().finish();
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ getActivity().finish();
+ }
+ })
+ .setTitle(R.string.pref_powersave_enabled_summary)
+ .create();
+
+ dialog.getWindow().setSoftInputMode(
+ WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+
+ return dialog;
+ }
+ }
+}
diff --git a/src/src/com/android/browser/preferences/InvertedContrastPreview.java b/src/src/com/android/browser/preferences/InvertedContrastPreview.java
new file mode 100644
index 00000000..690a529a
--- /dev/null
+++ b/src/src/com/android/browser/preferences/InvertedContrastPreview.java
@@ -0,0 +1,99 @@
+/*
+ * 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("\" />&nbsp;");
+ }
+ builder.append("</body></html>");
+ mHtml = builder.toString();
+ }
+
+ @Override
+ protected void updatePreview(boolean forceReload) {
+ if (mWebView == null) return;
+
+ /* SWE: This class extends WebViewPreview, however, WebViewPreview has
+ * been modified to use the system webview. Commenting out code for now
+ * which implements the preview as this class requires refactoring
+ * regardless & is currently not in use.
+
+ 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/src/com/android/browser/preferences/LegalPreferencesFragment.java b/src/src/com/android/browser/preferences/LegalPreferencesFragment.java
new file mode 100644
index 00000000..ea3fb17b
--- /dev/null
+++ b/src/src/com/android/browser/preferences/LegalPreferencesFragment.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2015 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.app.ActionBar;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+
+import com.android.browser.BrowserSwitches;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+
+import org.codeaurora.swe.BrowserCommandLine;
+
+public class LegalPreferencesFragment extends PreferenceFragment
+ implements OnPreferenceClickListener {
+
+ private static final String creditsUrl = "chrome://credits";
+ PreferenceScreen mHeadPref = null;
+ String mEulaUrl = "";
+ String mPrivacyPolicyUrl = "";
+
+ private void setOnClickListener(String prefKey, boolean set) {
+ Preference pref = findPreference(prefKey);
+ if (pref == null) {
+ return;
+ }
+
+ if (set) {
+ pref.setOnPreferenceClickListener(this);
+ } else {
+ if (mHeadPref != null)
+ mHeadPref.removePreference(pref);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ActionBar bar = getActivity().getActionBar();
+ if (bar != null) {
+ bar.setTitle(R.string.swe_legal);
+ }
+ addPreferencesFromResource(R.xml.legal_preferences);
+ mHeadPref = (PreferenceScreen) findPreference(PreferenceKeys.PREF_LEGAL);
+
+
+ setOnClickListener(PreferenceKeys.PREF_LEGAL_CREDITS, true);
+
+ if(BrowserCommandLine.hasSwitch(BrowserSwitches.CMD_LINE_SWITCH_EULA_URL)) {
+ mEulaUrl = BrowserCommandLine.getSwitchValue(BrowserSwitches.CMD_LINE_SWITCH_EULA_URL);
+ } else {
+ mEulaUrl = getResources().getString(R.string.swe_eula_url);
+ }
+ setOnClickListener(PreferenceKeys.PREF_LEGAL_EULA, !mEulaUrl.isEmpty());
+
+
+ if(BrowserCommandLine.hasSwitch(BrowserSwitches.CMD_LINE_SWITCH_PRIVACY_POLICY_URL)) {
+ mPrivacyPolicyUrl = BrowserCommandLine.getSwitchValue(
+ BrowserSwitches.CMD_LINE_SWITCH_PRIVACY_POLICY_URL);
+ } else {
+ mPrivacyPolicyUrl = getResources().getString(R.string.swe_privacy_policy_url);
+ }
+ setOnClickListener(PreferenceKeys.PREF_LEGAL_PRIVACY_POLICY, !mPrivacyPolicyUrl.isEmpty());
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ Bundle b = new Bundle();
+ if(preference.getKey().equals(PreferenceKeys.PREF_LEGAL_CREDITS)) {
+ Intent i = new Intent(getActivity(), LegalPreviewActivity.class);
+ i.putExtra(LegalPreviewActivity.URL_INTENT_EXTRA, creditsUrl);
+ startActivity(i);
+ return true;
+ } else if(preference.getKey().equals(PreferenceKeys.PREF_LEGAL_EULA)) {
+ Intent i = new Intent(getActivity(), LegalPreviewActivity.class);
+ i.putExtra(LegalPreviewActivity.URL_INTENT_EXTRA, mEulaUrl);
+ startActivity(i);
+ return true;
+ } else if(preference.getKey().equals(PreferenceKeys.PREF_LEGAL_PRIVACY_POLICY)) {
+ Intent i = new Intent(getActivity(), LegalPreviewActivity.class);
+ i.putExtra(LegalPreviewActivity.URL_INTENT_EXTRA, mPrivacyPolicyUrl);
+ startActivity(i);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/src/com/android/browser/preferences/LegalPreviewActivity.java b/src/src/com/android/browser/preferences/LegalPreviewActivity.java
new file mode 100644
index 00000000..eeaf5589
--- /dev/null
+++ b/src/src/com/android/browser/preferences/LegalPreviewActivity.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2015 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.app.ActionBar;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
+import android.view.KeyEvent;
+import android.view.MenuItem;
+
+import com.android.browser.R;
+
+public class LegalPreviewActivity extends FragmentActivity {
+ LegalPreviewFragment mLegalPreviewFragment;
+ protected static final String URL_INTENT_EXTRA = "url";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.credits_tab);
+ ActionBar bar = getActionBar();
+ if (bar != null) {
+ bar.setTitle(R.string.swe_open_source_licenses);
+ bar.setDisplayHomeAsUpEnabled(true);
+ }
+ FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
+ mLegalPreviewFragment = new LegalPreviewFragment();
+ Bundle args = new Bundle();
+ args.putString(URL_INTENT_EXTRA, getIntent().getExtras()
+ .getString(URL_INTENT_EXTRA));
+ mLegalPreviewFragment.setArguments(args);
+ fragmentTransaction.add(R.id.license_layout, mLegalPreviewFragment,
+ "LegalPreviewFragmentTag");
+ fragmentTransaction.addToBackStack(null);
+ fragmentTransaction.commit();
+ }
+
+ private boolean back() {
+ if(!mLegalPreviewFragment.onBackPressed()) {
+ onBackPressed();
+ return true;
+ } else {
+ return false;
+ }
+ }
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ switch(keyCode) {
+ case KeyEvent.KEYCODE_BACK:
+ if (event.isTracking() && !event.isCanceled()) {
+ return back();
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ return back();
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/src/src/com/android/browser/preferences/LegalPreviewFragment.java b/src/src/com/android/browser/preferences/LegalPreviewFragment.java
new file mode 100644
index 00000000..ec60e10c
--- /dev/null
+++ b/src/src/com/android/browser/preferences/LegalPreviewFragment.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2015 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.app.Fragment;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.android.browser.R;
+
+import org.codeaurora.swe.WebView;
+
+public class LegalPreviewFragment extends Fragment {
+
+ private WebView mWebView;
+ private String mUrl;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Bundle args = getArguments();
+ mUrl = args.getString(LegalPreviewActivity.URL_INTENT_EXTRA);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ ViewGroup contentContainer = (ViewGroup) inflater.inflate(
+ R.layout.webview_wrapper, container, false);
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.MATCH_PARENT);
+ mWebView = new WebView(getActivity());
+ contentContainer.addView(mWebView.getView(), params);
+ return contentContainer;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle b) {
+ super.onActivityCreated(b);
+ if (mWebView == null) return;
+ mWebView.loadUrl(mUrl);
+ }
+
+ public boolean onBackPressed() {
+ if(mWebView != null && mWebView.canGoBack()) {
+ mWebView.goBack();
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/src/com/android/browser/preferences/NonformattingListPreference.java b/src/src/com/android/browser/preferences/NonformattingListPreference.java
new file mode 100644
index 00000000..51b3231e
--- /dev/null
+++ b/src/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/src/com/android/browser/preferences/PrivacySecurityPreferencesFragment.java b/src/src/com/android/browser/preferences/PrivacySecurityPreferencesFragment.java
new file mode 100644
index 00000000..d038163f
--- /dev/null
+++ b/src/src/com/android/browser/preferences/PrivacySecurityPreferencesFragment.java
@@ -0,0 +1,250 @@
+/*
+ * 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.BrowserLocationSwitchPreference;
+import com.android.browser.BrowserPreferencesPage;
+import com.android.browser.BrowserSettings;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+import com.android.browser.mdm.DoNotTrackRestriction;
+import com.android.browser.mdm.ThirdPartyCookiesRestriction;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceScreen;
+import android.preference.TwoStatePreference;
+
+import org.codeaurora.swe.PermissionsServiceFactory;
+import org.codeaurora.swe.WebRefiner;
+
+public class PrivacySecurityPreferencesFragment extends SWEPreferenceFragment
+ implements Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener {
+
+ private Preference mClearPref;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.privacy_and_security_preferences);
+
+ PreferenceScreen websiteSettings = (PreferenceScreen) findPreference(
+ PreferenceKeys.PREF_WEBSITE_SETTINGS);
+ websiteSettings.setFragment(WebsiteSettingsFragment.class.getName());
+ websiteSettings.setOnPreferenceClickListener(this);
+
+ mClearPref = findPreference(PreferenceKeys.PREF_CLEAR_SELECTED_DATA);
+ mClearPref.setOnPreferenceChangeListener(this);
+
+ readAndShowPermission("enable_geolocation",
+ PermissionsServiceFactory.PermissionType.GEOLOCATION);
+
+ readAndShowPermission("microphone", PermissionsServiceFactory.PermissionType.VOICE);
+
+ readAndShowPermission("camera", PermissionsServiceFactory.PermissionType.VIDEO);
+
+ Preference pref = findPreference("distracting_contents");
+ if (!BrowserSettings.getInstance().getPreferences()
+ .getBoolean(PreferenceKeys.PREF_WEB_REFINER, false)) {
+ PreferenceCategory category =
+ (PreferenceCategory) findPreference("default_site_settings");
+ if (category != null) {
+ category.removePreference(pref);
+ }
+ } else {
+ // since webrefiner and distracting_contents are paradoxes
+ // the value needs to be flipped
+ pref.setOnPreferenceChangeListener(this);
+ showPermission(pref,
+ !PermissionsServiceFactory.getDefaultPermissions(
+ PermissionsServiceFactory.PermissionType.WEBREFINER));
+ }
+
+ readAndShowPermission("popup_windows", PermissionsServiceFactory.PermissionType.POPUP);
+
+ readAndShowPermission("accept_cookies", PermissionsServiceFactory.PermissionType.COOKIE);
+
+ readAndShowPermission("accept_third_cookies",
+ PermissionsServiceFactory.PermissionType.THIRDPARTYCOOKIES);
+
+ // Register Preference objects with their MDM restriction handlers
+ DoNotTrackRestriction.getInstance().
+ registerPreference(findPreference(PreferenceKeys.PREF_DO_NOT_TRACK));
+ ThirdPartyCookiesRestriction.getInstance().
+ registerPreference(findPreference("accept_third_cookies"));
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ // Un-register Preference objects from their MDM restriction handlers
+ DoNotTrackRestriction.getInstance().registerPreference(null);
+ ThirdPartyCookiesRestriction.getInstance().registerPreference(null);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ PermissionsServiceFactory.flushPendingSettings();
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ FragmentManager fragmentManager = getFragmentManager();
+
+ if (preference.getKey().equals(PreferenceKeys.PREF_WEBSITE_SETTINGS)) {
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+
+ Fragment newFragment = new WebsiteSettingsFragment();
+ fragmentTransaction.replace(getId(), newFragment);
+ fragmentTransaction.addToBackStack(null);
+ fragmentTransaction.commit();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference pref, Object objValue) {
+ boolean flag = true;
+ if (pref == mClearPref) {
+ Integer value = (Integer) objValue;
+ if (value == 0) {
+ return false;
+ }
+ } else {
+ Boolean bFlag = (Boolean) objValue;
+ flag = bFlag.booleanValue();
+ }
+
+ if (pref.getKey().equals(PreferenceKeys.PREF_CLEAR_SELECTED_DATA)) {
+ if (pref.getPreferenceManager().getDefaultSharedPreferences(
+ (Context) getActivity()).getBoolean(
+ PreferenceKeys.PREF_PRIVACY_CLEAR_HISTORY, false)) {
+ // 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 by default for all preferences
+ return true;
+ }
+
+ if (pref.getKey().toString().equalsIgnoreCase("enable_geolocation")) {
+ PermissionsServiceFactory.setDefaultPermissions(
+ PermissionsServiceFactory.PermissionType.GEOLOCATION, flag);
+ BrowserPreferencesPage.sResultExtra = PreferenceKeys.ACTION_RELOAD_PAGE;
+ return true;
+ }
+
+ if (pref.getKey().toString().equalsIgnoreCase("microphone")) {
+ PermissionsServiceFactory.setDefaultPermissions(
+ PermissionsServiceFactory.PermissionType.VOICE, flag);
+ BrowserPreferencesPage.sResultExtra = PreferenceKeys.ACTION_RELOAD_PAGE;
+ return true;
+ }
+
+ if (pref.getKey().toString().equalsIgnoreCase("camera")) {
+ PermissionsServiceFactory.setDefaultPermissions(
+ PermissionsServiceFactory.PermissionType.VIDEO, flag);
+ BrowserPreferencesPage.sResultExtra = PreferenceKeys.ACTION_RELOAD_PAGE;
+ return true;
+ }
+
+ if (pref.getKey().toString().equalsIgnoreCase("distracting_contents")) {
+ PermissionsServiceFactory.setDefaultPermissions(
+ PermissionsServiceFactory.PermissionType.WEBREFINER, !flag);
+ BrowserPreferencesPage.sResultExtra = PreferenceKeys.ACTION_RELOAD_PAGE;
+ return true;
+ }
+
+ if (pref.getKey().toString().equalsIgnoreCase("popup_windows")) {
+ PermissionsServiceFactory.setDefaultPermissions(
+ PermissionsServiceFactory.PermissionType.POPUP, flag);
+ BrowserPreferencesPage.sResultExtra = PreferenceKeys.ACTION_RELOAD_PAGE;
+ return true;
+ }
+
+ if (pref.getKey().toString().equalsIgnoreCase("accept_cookies")) {
+ PermissionsServiceFactory.setDefaultPermissions(
+ PermissionsServiceFactory.PermissionType.COOKIE, flag);
+
+ if (!flag) {
+ // Disable third party cookies as well
+ PermissionsServiceFactory.setDefaultPermissions(
+ PermissionsServiceFactory.PermissionType.THIRDPARTYCOOKIES, flag);
+ showPermission(findPreference("accept_third_cookies"), flag);
+ }
+ BrowserPreferencesPage.sResultExtra = PreferenceKeys.ACTION_RELOAD_PAGE;
+ return true;
+ }
+
+ if (pref.getKey().toString().equalsIgnoreCase("accept_third_cookies")) {
+ PermissionsServiceFactory.setDefaultPermissions(
+ PermissionsServiceFactory.PermissionType.THIRDPARTYCOOKIES, flag);
+ BrowserPreferencesPage.sResultExtra = PreferenceKeys.ACTION_RELOAD_PAGE;
+ return true;
+ }
+
+ return false;
+ }
+
+ private void readAndShowPermission(CharSequence key,
+ PermissionsServiceFactory.PermissionType type) {
+ Preference pref = findPreference(key);
+ pref.setOnPreferenceChangeListener(this);
+ showPermission(pref, PermissionsServiceFactory.getDefaultPermissions(type));
+ }
+
+ private void showPermission(Preference pref, boolean perm) {
+ if (pref instanceof TwoStatePreference) {
+ TwoStatePreference twoStatePreference = (TwoStatePreference) pref;
+ if (twoStatePreference.isChecked() != perm) {
+ twoStatePreference.setChecked(perm);
+ }
+ } else {
+ if (!perm) {
+ pref.setSummary(R.string.pref_security_not_allowed);
+ } else {
+ pref.setSummary(R.string.pref_security_ask_before_using);
+ }
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ ActionBar bar = getActivity().getActionBar();
+ BrowserLocationSwitchPreference pref =
+ (BrowserLocationSwitchPreference) findPreference(PreferenceKeys.PREF_ENABLE_GEOLOCATION);
+ if (bar != null) {
+ bar.setTitle(R.string.pref_privacy_security_title);
+ bar.setDisplayHomeAsUpEnabled(false);
+ bar.setHomeButtonEnabled(false);
+ }
+ if ( pref != null) pref.setEnabled(PermissionsServiceFactory.isSystemLocationEnabled());
+ }
+}
diff --git a/src/src/com/android/browser/preferences/SWEPreferenceFragment.java b/src/src/com/android/browser/preferences/SWEPreferenceFragment.java
new file mode 100644
index 00000000..7bfd0d1e
--- /dev/null
+++ b/src/src/com/android/browser/preferences/SWEPreferenceFragment.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2015, 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.app.ActionBar;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.Switch;
+
+import com.android.browser.R;
+
+public abstract class SWEPreferenceFragment extends PreferenceFragment {
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ View view = super.onCreateView(inflater, container, bundle);
+
+ ListView list = (ListView) view.findViewById(android.R.id.list);
+
+ if (list == null) {
+ return view;
+ }
+
+ ViewGroup.LayoutParams params = list.getLayoutParams();
+ params.width = ViewGroup.LayoutParams.MATCH_PARENT;
+ list.setLayoutParams(params);
+ list.setPadding(0, list.getPaddingTop(), 0, list.getPaddingBottom());
+ list.setDivider(null);
+ list.setDividerHeight(0);
+
+ list.setOnHierarchyChangeListener(
+ new ViewGroup.OnHierarchyChangeListener() {
+ @Override
+ public void onChildViewAdded(View parent, View child) {
+ onChildViewAddedToHierarchy(parent, child);
+ findAndResizeSwitchPreferenceWidget(child);
+ }
+
+ @Override
+ public void onChildViewRemoved(View parent, View child) {
+ onChildViewRemovedFromHierarchy(parent, child);
+ }
+ }
+ );
+
+ return view;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ /*ActionBar bar = getActivity().getActionBar();
+ if (bar != null) {
+ bar.setBackgroundDrawable(new ColorDrawable(getResources().getColor(R.color.accent)));
+ }*/
+ }
+
+ private final void findAndResizeSwitchPreferenceWidget(View parent) {
+ LinearLayout layout = (LinearLayout) parent.findViewById(android.R.id.widget_frame);
+ if (layout != null) {
+ for (int i = 0; i < layout.getChildCount(); i++) {
+ View view = layout.getChildAt(i);
+ if (view instanceof Switch) {
+ Switch switchView = (Switch) view;
+ switchView.setThumbTextPadding(0);
+ int width = switchView.getSwitchMinWidth();
+ switchView.setSwitchMinWidth(width/2);
+ }
+ }
+ }
+ }
+
+ public void onChildViewAddedToHierarchy(View parent, View child) {
+
+ }
+
+ public void onChildViewRemovedFromHierarchy(View parent, View child) {
+
+ }
+}
diff --git a/src/src/com/android/browser/preferences/SeekBarSummaryPreference.java b/src/src/com/android/browser/preferences/SeekBarSummaryPreference.java
new file mode 100644
index 00000000..5cb8ae67
--- /dev/null
+++ b/src/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/src/com/android/browser/preferences/SiteSpecificPreferencesFragment.java b/src/src/com/android/browser/preferences/SiteSpecificPreferencesFragment.java
new file mode 100644
index 00000000..b5d748df
--- /dev/null
+++ b/src/src/com/android/browser/preferences/SiteSpecificPreferencesFragment.java
@@ -0,0 +1,816 @@
+/*
+ * Copyright (c) 2015, 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.app.ActionBar;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.http.SslCertificate;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceScreen;
+import android.preference.TwoStatePreference;
+import android.text.Html;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.webkit.ValueCallback;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.browser.BrowserLocationListPreference;
+import com.android.browser.BrowserPreferencesPage;
+import com.android.browser.BrowserSettings;
+import com.android.browser.NavigationBarBase;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+import com.android.browser.reflect.ReflectHelper;
+
+import org.codeaurora.swe.PermissionsServiceFactory;
+import org.codeaurora.swe.WebRefiner;
+import org.codeaurora.swe.util.ColorUtils;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.Formatter;
+import java.util.List;
+import java.util.Map;
+
+public class SiteSpecificPreferencesFragment extends SWEPreferenceFragment
+ implements Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener,
+ View.OnClickListener {
+ public static final String EXTRA_SITE = "website";
+ public static final String EXTRA_ORIGIN = "website_origin";
+ public static final String EXTRA_FAVICON = "website_favicon";
+ public static final String EXTRA_SITE_TITLE = "website_title";
+ public static final String EXTRA_WEB_REFINER_ADS_INFO = "website_refiner_ads_info";
+ public static final String EXTRA_WEB_REFINER_TRACKER_INFO = "website_refiner_tracker_info";
+ public static final String EXTRA_WEB_REFINER_MALWARE_INFO = "website_refiner_malware_info";
+ public static final String EXTRA_SECURITY_CERT = "website_security_cert";
+ public static final String EXTRA_SECURITY_CERT_BAD = "website_security_cert_bad";
+ public static final String EXTRA_SECURITY_CERT_MIXED = "website_security_cert_mixed";
+
+ private PermissionsServiceFactory.PermissionsService.OriginInfo mOriginInfo;
+ private PermissionsServiceFactory.PermissionsService mPermServ;
+ private ActionBar mBar;
+ private List<String> mLocationValues;
+
+ private Preference mSecurityInfoPrefs;
+
+ private boolean mUsingDefaultSettings = true;
+ private int mOriginalActionBarOptions;
+ private int mIconColor = 0;
+
+ private SslCertificate mSslCert;
+ private int mSslState;
+
+ private static class SiteSecurityViewFactory {
+ private class SiteSecurityView {
+ private TextView mTextView;
+ private View mContainer;
+ private String mDisplayText;
+
+ public SiteSecurityView(View parent, int resId, String text) {
+ mContainer = parent.findViewById(resId);
+ mTextView = (TextView) mContainer.findViewById(R.id.security_view_text);
+ mTextView.setText(text);
+ mDisplayText = text;
+ updateVisibility();
+ }
+
+ private void updateVisibility() {
+ if (TextUtils.isEmpty(mDisplayText)) {
+ mContainer.setVisibility(View.GONE);
+ } else {
+ mContainer.setVisibility(View.VISIBLE);
+ }
+ }
+
+ public void setText(String text) {
+ mDisplayText = text;
+ mTextView.setText(mDisplayText);
+ updateVisibility();
+ }
+
+ public void clearText() {
+ mDisplayText = null;
+ updateVisibility();
+ }
+ }
+
+ public enum ViewType{
+ ERROR,
+ WARNING,
+ INFO
+ };
+
+ private Map<ViewType, SiteSecurityView> mViews =
+ new EnumMap<ViewType, SiteSecurityView>(ViewType.class);
+ private Map<ViewType, String> mTexts = new EnumMap<ViewType, String>(ViewType.class);
+
+ private boolean mbEmpty = true;
+
+ public void setText(ViewType type, String text) {
+ mTexts.put(type, text);
+
+ SiteSecurityView view = mViews.get(type);
+ if (view != null) {
+ view.setText(text);
+ }
+
+ mbEmpty = false;
+ }
+
+ public void appendText(ViewType type, String text) {
+ String new_text = mTexts.get(type);
+ if (new_text != null)
+ new_text += text;
+ else
+ new_text = text;
+
+ mTexts.put(type, new_text);
+
+ SiteSecurityView view = mViews.get(type);
+ if (view != null) {
+ view.setText(new_text);
+ }
+
+ mbEmpty = false;
+ }
+
+ public void clearText(ViewType type) {
+ mTexts.remove(type);
+
+ SiteSecurityView view = mViews.get(type);
+ if (view != null) {
+ view.clearText();
+ }
+
+ boolean empty = true;
+ for (Map.Entry<ViewType, String> entry: mTexts.entrySet()) {
+ if (!entry.getValue().isEmpty()) {
+ empty = false;
+ }
+ }
+ mbEmpty = empty;
+ }
+
+ public void setResource(ViewType type, View parent, int resId) {
+ String text = mTexts.get(type);
+ mViews.remove(type);
+ mViews.put(type, new SiteSecurityView(parent, resId, text));
+ }
+ }
+
+ private SiteSecurityViewFactory mSecurityViews;
+
+ private String mOriginText;
+ private String mSiteTitle;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.site_specific_preferences);
+
+ mBar = getActivity().getActionBar();
+
+ mLocationValues = Arrays.asList(
+ getResources().getStringArray(R.array.geolocation_settings_choices));
+
+ mSecurityViews = new SiteSecurityViewFactory();
+
+ Bundle args = getArguments();
+ if (args != null) {
+ mOriginText = args.getString(EXTRA_ORIGIN, null);
+ mSiteTitle = args.getString(EXTRA_SITE_TITLE, null);
+
+ if (mOriginText == null) {
+ mOriginText = args.getString(EXTRA_SITE);
+ }
+ }
+
+ mIconColor = NavigationBarBase.getSiteIconColor(mOriginText);
+
+ PermissionsServiceFactory.getPermissionsService(
+ new ValueCallback<PermissionsServiceFactory.PermissionsService>() {
+ @Override
+ public void onReceiveValue(PermissionsServiceFactory.PermissionsService value) {
+ mPermServ = value;
+ Preference pref = findPreference("site_name");
+
+ pref.setTitle((mSiteTitle != null) ?
+ mSiteTitle :
+ mOriginText);
+
+ try {
+ URL url = new URL(mOriginText);
+ pref.setSummary((mSiteTitle != null) ?
+ mOriginText :
+ "(" + url.getHost() + ")");
+ } catch (MalformedURLException e) {
+ }
+ mOriginInfo = mPermServ.getOriginInfo(mOriginText);
+ setActionBarTitle(PermissionsServiceFactory.getPrettyUrl(mOriginText));
+ updatePreferenceInfo();
+ }
+ }
+ );
+
+ if (!BrowserSettings.getInstance().getPreferences()
+ .getBoolean(PreferenceKeys.PREF_WEB_REFINER, false)) {
+ PreferenceCategory category = (PreferenceCategory) findPreference("site_pref_list");
+ if (category != null) {
+ Preference pref = findPreference("distracting_contents");
+ category.removePreference(pref);
+ }
+ }
+
+ int ads = args.getInt(EXTRA_WEB_REFINER_ADS_INFO, 0);
+ String[] strings = new String[3];
+ int index = 0;
+
+ if (ads > 0) {
+ strings[index++] = getResources().getQuantityString(
+ R.plurals.pref_web_refiner_advertisements, ads, ads);
+ }
+
+ int trackers = args.getInt(EXTRA_WEB_REFINER_TRACKER_INFO, 0);
+ if (trackers > 0) {
+ strings[index++] = getResources().getQuantityString(
+ R.plurals.pref_web_refiner_trackers, trackers, trackers);
+
+ }
+
+ int malware = args.getInt(EXTRA_WEB_REFINER_MALWARE_INFO, 0);
+ if (malware > 0) {
+ strings[index++] = getResources().getQuantityString(
+ R.plurals.pref_web_refiner_malware, malware, malware);
+ }
+
+ if (index > 0) {
+ String[] formats = getResources().getStringArray(R.array.pref_web_refiner_message);
+ Formatter formatter = new Formatter();
+ formatter.format(formats[index - 1], strings[0], strings[1], strings[2]);
+ mSecurityViews.appendText(SiteSecurityViewFactory.ViewType.INFO, formatter.toString());
+ }
+
+ Bundle parcel = args.getParcelable(EXTRA_SECURITY_CERT);
+ mSslCert = (parcel != null) ? SslCertificate.restoreState(parcel) : null;
+
+ if (mSslCert != null) {
+ Preference pref = findPreference("site_security_info");
+ if (pref != null) {
+ pref.setSelectable(true);
+ }
+
+ boolean certBad = args.getBoolean(EXTRA_SECURITY_CERT_BAD, false);
+ boolean certMix = args.getBoolean(EXTRA_SECURITY_CERT_MIXED, false);
+ if (!certBad && !certMix) {
+ final String string = getString(R.string.pref_valid_cert);
+ mSecurityViews.appendText(SiteSecurityViewFactory.ViewType.INFO,
+ string);
+ mSslState = 0;
+ } else if (certMix) {
+ mSecurityViews.appendText(SiteSecurityViewFactory.ViewType.WARNING,
+ getString(R.string.pref_warning_cert));
+ mSslState = 1;
+ } else {
+ mSecurityViews.appendText(SiteSecurityViewFactory.ViewType.ERROR,
+ getString(R.string.pref_invalid_cert));
+ mSslState = 2;
+ }
+ }
+
+ updateSecurityViewVisibility();
+ }
+
+ private AlertDialog.Builder createSslCertificateDialog(Context ctx,
+ SslCertificate certificate) {
+ Object[] params = {ctx};
+ Class[] type = new Class[] {Context.class};
+ View certificateView = (View) ReflectHelper.invokeMethod(certificate,
+ "inflateCertificateView", type, params);
+ Resources res = Resources.getSystem();
+ // load 'android.R.placeholder' via introspection, since it's not a public resource ID
+ int placeholder_id = res.getIdentifier("placeholder", "id", "android");
+ final LinearLayout placeholder =
+ (LinearLayout)certificateView.findViewById(placeholder_id);
+
+ LayoutInflater factory = LayoutInflater.from(ctx);
+ int iconId = R.drawable.ic_cert_trusted;
+ TextView textView;
+
+ switch (mSslState) {
+ case 0:
+ iconId = R.drawable.ic_cert_trusted;
+ LinearLayout table = (LinearLayout)factory.inflate(R.layout.ssl_success, placeholder);
+ textView = (TextView)table.findViewById(R.id.success);
+ textView.setText(R.string.ssl_certificate_is_valid);
+ break;
+ case 1:
+ iconId = R.drawable.ic_cert_untrusted;
+ textView = (TextView) factory.inflate(R.layout.ssl_warning, placeholder, false);
+ textView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_sp_level_warning,
+ 0, 0, 0);
+ textView.setText(R.string.ssl_unknown);
+ placeholder.addView(textView);
+ break;
+ case 2:
+ iconId = R.drawable.ic_cert_avoid;
+ textView = (TextView) factory.inflate(R.layout.ssl_warning, placeholder, false);
+ textView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_sp_level_severe,
+ 0, 0, 0);
+ textView.setText(R.string.ssl_invalid);
+ placeholder.addView(textView);
+ break;
+ }
+
+ return new AlertDialog.Builder(ctx)
+ .setTitle(R.string.ssl_certificate)
+ .setIcon(iconId)
+ .setView(certificateView);
+ }
+
+ private void setActionBarTitle(String url) {
+ if (mBar != null) {
+ mBar.setTitle(" " + url);
+ }
+ }
+
+ private String getStorage() {
+ if (mOriginInfo == null) {
+ return new String("");
+ }
+
+ long value = mOriginInfo.getStoredData();
+ if (value == 0) {
+ return "Empty";
+ }
+
+ if (value < (1 << 10)) {
+ return value + "B";
+ } else if (value < (1 << 20)) {
+ return (value >> 10) + "KB";
+ } else if (value < (1 << 30)) {
+ return (value >> 20) + "MB";
+ }
+
+ return (value >> 30) + "GB";
+ }
+
+ private long showPermission(CharSequence key, PermissionsServiceFactory.PermissionType type,
+ int defaultOnSummary, int defaultOffSummary) {
+ Preference pref = findPreference(key);
+ long permission = (mOriginInfo != null) ? mOriginInfo.getPermission(type) :
+ PermissionsServiceFactory.Permission.NOTSET;
+
+ pref.setOnPreferenceChangeListener(this);
+
+ if (permission == PermissionsServiceFactory.Permission.ALLOW) {
+ if (pref instanceof TwoStatePreference) {
+ ((TwoStatePreference) pref).setChecked(true);
+ ((TwoStatePreference) pref).setSummaryOn(R.string.pref_security_allowed);
+ } else {
+ pref.setSummary(R.string.pref_security_allowed);
+ }
+ mUsingDefaultSettings = false;
+ } else if (permission == PermissionsServiceFactory.Permission.BLOCK) {
+ if (pref instanceof TwoStatePreference) {
+ ((TwoStatePreference) pref).setChecked(false);
+ } else {
+ pref.setSummary(R.string.pref_security_not_allowed);
+ }
+ mUsingDefaultSettings = false;
+ } else if (permission == PermissionsServiceFactory.Permission.ASK) {
+ if (pref instanceof TwoStatePreference) {
+ ((TwoStatePreference) pref).setChecked(true);
+ ((TwoStatePreference) pref).setSummaryOn(R.string.pref_security_ask_before_using);
+ } else {
+ pref.setSummary(R.string.pref_security_ask_before_using);
+ }
+ mUsingDefaultSettings = false;
+ } else if (permission == PermissionsServiceFactory.Permission.NOTSET) {
+ boolean defaultPerm = PermissionsServiceFactory.getDefaultPermissions(type);
+ if (pref instanceof TwoStatePreference) {
+ if (!defaultPerm) {
+ ((TwoStatePreference) pref).setChecked(false);
+ ((TwoStatePreference) pref).setSummaryOff(defaultOffSummary);
+ return PermissionsServiceFactory.Permission.BLOCK;
+ } else {
+ ((TwoStatePreference) pref).setChecked(true);
+ ((TwoStatePreference) pref).setSummaryOn(defaultOnSummary);
+ return PermissionsServiceFactory.Permission.ASK;
+ }
+ } else {
+ if (!defaultPerm) {
+ pref.setSummary(defaultOffSummary);
+ return PermissionsServiceFactory.Permission.BLOCK;
+ } else {
+ pref.setSummary(defaultOnSummary);
+ return PermissionsServiceFactory.Permission.ASK;
+ }
+ }
+ }
+ return permission;
+ }
+
+ private void updateStorageInfo(Preference pref) {
+ if (mOriginInfo != null) {
+ pref.setTitle(R.string.webstorage_clear_data_title);
+ pref.setSummary("(" + getStorage() + ")");
+ }
+ }
+
+ private void updatePreferenceInfo() {
+ Preference pref = findPreference("clear_data");
+ updateStorageInfo(pref);
+ pref.setOnPreferenceClickListener(this);
+ String warningText = (mSslState == 1) ? getString(R.string.pref_warning_cert) + " " :
+ new String("");
+ boolean setting_warnings = false;
+
+ long permission = showPermission("select_geolocation",
+ PermissionsServiceFactory.PermissionType.GEOLOCATION,
+ R.string.pref_security_ask_before_using, R.string.pref_security_not_allowed);
+
+ if (PermissionsServiceFactory.Permission.ALLOW == permission) {
+ warningText += getString(R.string.pref_privacy_enable_geolocation);
+ setting_warnings = true;
+ }
+
+ ListPreference geolocation_pref = (ListPreference) findPreference("select_geolocation");
+ geolocation_pref.setValueIndex(0);
+ if (permission == PermissionsServiceFactory.Permission.CUSTOM) {
+ pref = findPreference("select_geolocation");
+ long custom = mOriginInfo.getPermissionCustomValue(
+ PermissionsServiceFactory.PermissionType.GEOLOCATION);
+ float customHrs = ((float) custom) / (60 * 60);
+ String customSummary = "Allowed for " + String.format("%.02f", customHrs) + " hours";
+ if (pref instanceof TwoStatePreference) {
+ ((TwoStatePreference) pref).setChecked(true);
+ ((TwoStatePreference) pref).setSummaryOn(customSummary);
+ } else {
+ pref.setSummary(customSummary);
+ }
+ mUsingDefaultSettings = false;
+ warningText += getString(R.string.pref_privacy_enable_geolocation);
+ setting_warnings = true;
+ geolocation_pref.setValueIndex(1);
+ } else if (permission == PermissionsServiceFactory.Permission.ALLOW) {
+ geolocation_pref.setValueIndex(2);
+ }
+
+ permission = showPermission("microphone", PermissionsServiceFactory.PermissionType.VOICE,
+ R.string.pref_security_ask_before_using, R.string.pref_security_not_allowed);
+
+ if (PermissionsServiceFactory.Permission.ALLOW == permission) {
+ if (!warningText.isEmpty() && setting_warnings) {
+ warningText += ", ";
+ }
+ warningText += getString(R.string.pref_security_allow_mic);
+ setting_warnings = true;
+ }
+
+ permission = showPermission("camera", PermissionsServiceFactory.PermissionType.VIDEO,
+ R.string.pref_security_ask_before_using, R.string.pref_security_not_allowed);
+ if (PermissionsServiceFactory.Permission.ALLOW == permission) {
+ if (!warningText.isEmpty() && setting_warnings) {
+ warningText += ", ";
+ }
+ warningText += getString(R.string.pref_security_allow_camera);
+ setting_warnings = true;
+ }
+
+ if (!warningText.isEmpty()) {
+ if (setting_warnings) {
+ warningText += " ";
+ warningText += getResources().getString(R.string.pref_security_access_is_allowed);
+ }
+ mSecurityViews.setText(SiteSecurityViewFactory.ViewType.WARNING, warningText);
+ } else {
+ mSecurityViews.clearText(SiteSecurityViewFactory.ViewType.WARNING);
+ }
+
+ pref = findPreference("distracting_contents");
+ if (pref != null) {
+ permission = showPermission("distracting_contents",
+ PermissionsServiceFactory.PermissionType.WEBREFINER,
+ R.string.pref_security_allowed, R.string.pref_security_not_allowed);
+ if (permission == PermissionsServiceFactory.Permission.BLOCK) {
+ ((TwoStatePreference) pref).setChecked(true);
+ } else {
+ ((TwoStatePreference) pref).setChecked(false);
+ }
+ }
+
+ showPermission("popup_windows", PermissionsServiceFactory.PermissionType.POPUP,
+ R.string.pref_security_allowed, R.string.pref_security_not_allowed);
+
+ showPermission("accept_cookies", PermissionsServiceFactory.PermissionType.COOKIE,
+ R.string.pref_security_allowed, R.string.pref_security_not_allowed);
+
+ if (!mUsingDefaultSettings && mBar != null) {
+ mBar.getCustomView().setVisibility(View.VISIBLE);
+ }
+
+ updateSecurityViewVisibility();
+ }
+
+ private void updateSecurityViewVisibility() {
+ if (mSecurityViews.mbEmpty) {
+ PreferenceScreen screen = (PreferenceScreen)
+ findPreference("site_specific_prefs");
+
+ if (mSecurityInfoPrefs == null) {
+ mSecurityInfoPrefs = findPreference("site_security_info_title");
+ }
+
+ if (mSecurityInfoPrefs != null && screen != null) {
+ screen.removePreference(mSecurityInfoPrefs);
+ }
+ } else {
+ PreferenceScreen screen = (PreferenceScreen)
+ findPreference("site_specific_prefs");
+
+ Preference pref = findPreference("site_security_info_title");
+ if (pref == null && mSecurityInfoPrefs != null) {
+ screen.addPreference(mSecurityInfoPrefs);
+ }
+ }
+
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ BrowserLocationListPreference pref =
+ (BrowserLocationListPreference) findPreference("select_geolocation");
+ if ( pref != null) pref.setEnabled(PermissionsServiceFactory.isSystemLocationEnabled());
+ if (mBar != null) {
+ mOriginalActionBarOptions = mBar.getDisplayOptions();
+ mBar.setDisplayHomeAsUpEnabled(false);
+ mBar.setHomeButtonEnabled(false);
+
+ assignResetButton();
+
+ Bundle args = getArguments();
+ if (args != null) {
+ byte[] data = args.getByteArray(EXTRA_FAVICON);
+ if (data != null) {
+ Bitmap bm = BitmapFactory.decodeByteArray(data, 0, data.length);
+ if (bm != null) {
+ Bitmap bitmap = Bitmap.createScaledBitmap(bm, 150, 150, true);
+ int color = ColorUtils.getDominantColorForBitmap(bitmap);
+
+ appendActionBarDisplayOptions(ActionBar.DISPLAY_SHOW_HOME |
+ ActionBar.DISPLAY_SHOW_TITLE);
+ mBar.setHomeButtonEnabled(true);
+ mBar.setIcon(new BitmapDrawable(getResources(), bitmap));
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (mBar != null) {
+ mBar.setDisplayOptions(mOriginalActionBarOptions);
+ }
+
+ // flush all the settings in pause to assure that writes happen
+ // as soon the user leaves the activity
+ PermissionsServiceFactory.flushPendingSettings();
+
+ }
+
+ private void appendActionBarDisplayOptions(int extraOptions) {
+ int options = mBar.getDisplayOptions();
+ options |= extraOptions;
+ mBar.setDisplayOptions(options);
+ }
+
+ private void assignResetButton() {
+ appendActionBarDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
+ mBar.setCustomView(R.layout.swe_preference_custom_actionbar);
+ //mBar.getCustomView().setVisibility(View.GONE);
+ Button btn = (Button) mBar.getCustomView().findViewById(R.id.reset);
+ if (btn == null) {
+ return;
+ }
+
+ btn.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ new AlertDialog.Builder(getActivity())
+ .setMessage(R.string.pref_extras_reset_default_dlg)
+ .setPositiveButton(
+ R.string.ok,
+ new AlertDialog.OnClickListener() {
+ public void onClick(DialogInterface dlg, int which) {
+ if (mOriginInfo != null) {
+ mOriginInfo.resetSitePermission();
+ Preference e = findPreference("clear_data");
+ e.setSummary("(Empty)");
+ updatePreferenceInfo();
+
+ WebRefiner refiner = WebRefiner.getInstance();
+ if (refiner != null) {
+ String[] origins = new String[1];
+ origins[0] = mOriginInfo.getOrigin();
+ refiner.useDefaultPermissionForOrigins(origins);
+ }
+
+ BrowserPreferencesPage.sResultExtra =
+ PreferenceKeys.ACTION_RELOAD_PAGE;
+ BrowserPreferencesPage.onUrlNeedsReload(mOriginText);
+ finish();
+ }
+ }
+ })
+ .setNegativeButton(
+ R.string.cancel, null)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .show();
+ }
+ }
+ );
+ }
+
+ private void finish() {
+ Activity activity = getActivity();
+ if (activity != null) {
+ getActivity().getFragmentManager().popBackStack();
+ }
+ }
+
+ @Override
+ public void onChildViewAddedToHierarchy(View parent, View child) {
+ if (child.getId() == R.id.site_security_info) {
+ mSecurityViews.setResource(SiteSecurityViewFactory.ViewType.ERROR,
+ child, R.id.site_security_error);
+ mSecurityViews.setResource(SiteSecurityViewFactory.ViewType.WARNING,
+ child, R.id.site_security_warning);
+ mSecurityViews.setResource(SiteSecurityViewFactory.ViewType.INFO,
+ child, R.id.site_security_verbose);
+
+ if (mSslCert != null) {
+ child.setOnClickListener(this);
+ }
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.site_security_info) {
+ createSslCertificateDialog(getActivity(), mSslCert)
+ .setPositiveButton(R.string.ok, null)
+ .show();
+ }
+ }
+
+ private void updateTwoStatePreference(Preference pref,
+ PermissionsServiceFactory.PermissionType type,
+ boolean state) {
+ if (state) {
+ mOriginInfo.setPermission(type, PermissionsServiceFactory.Permission.ALLOW);
+ ((TwoStatePreference)pref).setSummaryOn(R.string.pref_security_allowed);
+ } else {
+ mOriginInfo.setPermission(type, PermissionsServiceFactory.Permission.BLOCK);
+ }
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference pref, Object objValue) {
+ if (mOriginInfo == null) {
+ if (mOriginText != null) {
+ mOriginInfo = mPermServ.addOriginInfo(mOriginText);
+ if (mOriginInfo == null) {
+ mOriginInfo = mPermServ.getOriginInfo(mOriginText);
+ if (mOriginInfo == null) {
+ return false;
+ }
+ }
+ } else {
+ return false;
+ }
+ }
+ if (pref.getKey().toString().equalsIgnoreCase("select_geolocation")) {
+ int index = mLocationValues.indexOf(objValue.toString());
+ switch (index) {
+ case 0:
+ mOriginInfo.setPermission(PermissionsServiceFactory.PermissionType.GEOLOCATION,
+ PermissionsServiceFactory.Permission.BLOCK);
+ pref.setSummary(R.string.pref_security_not_allowed);
+ break;
+ case 1:
+ mOriginInfo.setPermission(PermissionsServiceFactory.PermissionType.GEOLOCATION,
+ PermissionsServiceFactory.Permission.CUSTOM);
+ pref.setSummary(R.string.geolocation_permissions_prompt_share_for_limited_time);
+ break;
+ case 2:
+ mOriginInfo.setPermission(PermissionsServiceFactory.PermissionType.GEOLOCATION,
+ PermissionsServiceFactory.Permission.ALLOW);
+ pref.setSummary(R.string.pref_security_allowed);
+ break;
+ default:
+ break;
+ }
+ } else if (pref.getKey().toString().equalsIgnoreCase("microphone")) {
+ updateTwoStatePreference(pref,
+ PermissionsServiceFactory.PermissionType.VOICE, (boolean)objValue);
+ } else if (pref.getKey().toString().equalsIgnoreCase("camera")) {
+ updateTwoStatePreference(pref,
+ PermissionsServiceFactory.PermissionType.VIDEO, (boolean)objValue);
+ } else if (pref.getKey().toString().equalsIgnoreCase("distracting_contents")) {
+ WebRefiner refiner = WebRefiner.getInstance();
+ if (refiner != null) {
+ boolean disable = (boolean) objValue;
+ String[] origins = new String[1];
+ origins[0] = mOriginInfo.getOrigin();
+ refiner.setPermissionForOrigins(origins, !disable);
+ }
+ // Distracting contents and web refiner complimentary of each other
+ updateTwoStatePreference(pref,
+ PermissionsServiceFactory.PermissionType.WEBREFINER, !(boolean)objValue);
+ } else if (pref.getKey().toString().equalsIgnoreCase("popup_windows")) {
+ updateTwoStatePreference(pref,
+ PermissionsServiceFactory.PermissionType.POPUP, (boolean)objValue);
+ } else if (pref.getKey().toString().equalsIgnoreCase("accept_cookies")) {
+ updateTwoStatePreference(pref,
+ PermissionsServiceFactory.PermissionType.COOKIE, (boolean)objValue);
+ }
+ BrowserPreferencesPage.sResultExtra = PreferenceKeys.ACTION_RELOAD_PAGE;
+ BrowserPreferencesPage.onUrlNeedsReload(mOriginText);
+ updatePreferenceInfo();
+ return true;
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference pref) {
+ if (pref.getKey().toString().equalsIgnoreCase("clear_data")) {
+ new AlertDialog.Builder(getActivity())
+ .setMessage(R.string.website_settings_clear_all_dialog_message)
+ .setPositiveButton(R.string.ok,
+ new AlertDialog.OnClickListener() {
+ public void onClick(DialogInterface dlg, int which) {
+ if (mOriginInfo != null) {
+ mOriginInfo.clearAllStoredData();
+ Preference e = findPreference("clear_data");
+ e.setSummary("(Empty)");
+ BrowserPreferencesPage.sResultExtra =
+ PreferenceKeys.ACTION_RELOAD_PAGE;
+ BrowserPreferencesPage.onUrlNeedsReload(mOriginText);
+ }
+ }
+ })
+ .setNegativeButton(R.string.cancel, null)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .show();
+ }
+ return true;
+ }
+
+}
diff --git a/src/src/com/android/browser/preferences/WebViewPreview.java b/src/src/com/android/browser/preferences/WebViewPreview.java
new file mode 100644
index 00000000..03ffcb24
--- /dev/null
+++ b/src/src/com/android/browser/preferences/WebViewPreview.java
@@ -0,0 +1,93 @@
+/*
+ * 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 android.widget.TextView;
+import com.android.browser.BrowserSettings;
+import com.android.browser.R;
+import org.codeaurora.swe.WebView;
+
+public abstract class WebViewPreview extends Preference
+ implements OnSharedPreferenceChangeListener {
+
+ protected TextView mTextView;
+ 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);
+ BrowserSettings bs = BrowserSettings.getInstance();
+ mWebView = bs.getTopWebView();
+ }
+
+ protected abstract void updatePreview(boolean forceReload);
+
+ @Override
+ protected void onBindView(View view) {
+ super.onBindView(view);
+ mTextView = (TextView) view.findViewById(R.id.text_size_preview);
+ // Ignore all touch events & don't show scrollbars
+ mTextView.setFocusable(false);
+ mTextView.setFocusableInTouchMode(false);
+ mTextView.setClickable(false);
+ mTextView.setLongClickable(false);
+ mTextView.setHorizontalScrollBarEnabled(false);
+ mTextView.setVerticalScrollBarEnabled(false);
+ 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/src/com/android/browser/preferences/WebsiteSettingsFragment.java b/src/src/com/android/browser/preferences/WebsiteSettingsFragment.java
new file mode 100644
index 00000000..c9dd04d3
--- /dev/null
+++ b/src/src/com/android/browser/preferences/WebsiteSettingsFragment.java
@@ -0,0 +1,388 @@
+/*
+ * 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.ActionBar;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.app.ListFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.ColorDrawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.TextUtils;
+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.EditText;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.browser.R;
+import com.android.browser.SiteTileView;
+import com.android.browser.WebStorageSizeManager;
+
+import java.io.ByteArrayOutputStream;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import org.codeaurora.swe.GeolocationPermissions;
+import org.codeaurora.swe.PermissionsServiceFactory;
+import org.codeaurora.swe.WebRefiner;
+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 SiteAdapter mAdapter = null;
+
+ private class Site implements OnClickListener {
+ private String mOrigin;
+ private String mTitle;
+ private Bitmap mIcon;
+ private View mView;
+ private Bitmap mDefaultIcon = mAdapter.mDefaultIcon;
+
+ public Site(String origin) {
+ mOrigin = origin;
+ mTitle = null;
+ mIcon = null;
+ fetchFavicon();
+ }
+
+ private void fetchFavicon() {
+ // Fetch favicon and set it
+ PermissionsServiceFactory.getFavicon(mOrigin, getActivity(),
+ new ValueCallback<Bitmap>() {
+ @Override
+ public void onReceiveValue(Bitmap value) {
+ setIcon(value);
+
+ }
+ });
+ }
+
+ public void updateView(View view){
+ mView = view;
+ fetchFavicon();
+ }
+
+ public String getOrigin() {
+ return mOrigin;
+ }
+
+ public void setTitle(String title) {
+ mTitle = title;
+ }
+
+ public void setIcon(Bitmap image) {
+ mIcon = image;
+ if (mView != null) {
+ SiteTileView icon = (SiteTileView) mView.findViewById(R.id.icon);
+ icon.replaceFavicon((image == null) ? mDefaultIcon : image);
+ icon.setVisibility(View.VISIBLE);
+ icon.setOnClickListener(this);
+ }
+ }
+
+ 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) {
+ if (str == null)
+ return null;
+ Uri uri = Uri.parse(str);
+ return "http".equals(uri.getScheme()) ? str.substring(7) : str;
+ }
+
+ @Override
+ public void onClick(View v) {
+ clickHandler(this);
+ }
+ }
+
+ class SiteAdapter extends ArrayAdapter<Site>
+ implements AdapterView.OnItemClickListener {
+ private int mResource;
+ private LayoutInflater mInflater;
+ private Bitmap mDefaultIcon;
+ private PermissionsServiceFactory.PermissionsService mPermServ;
+ private boolean mReady;
+
+ public SiteAdapter(Context context, int rsc) {
+ super(context, rsc);
+ mResource = rsc;
+ mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mDefaultIcon = BitmapFactory.decodeResource(getResources(),
+ R.drawable.ic_deco_favicon_normal);
+ mReady = false;
+ askForOrigins();
+ }
+
+ public void askForOrigins() {
+ if (mPermServ == null) {
+ PermissionsServiceFactory.getPermissionsService(
+ new ValueCallback<PermissionsServiceFactory.PermissionsService>() {
+ @Override
+ public void onReceiveValue(
+ PermissionsServiceFactory.PermissionsService value) {
+ mPermServ = value;
+ Map<String, Site> sites = new HashMap<>();
+
+ Set<String> origins = mPermServ.getOrigins();
+ for (String origin : origins) {
+ if (!TextUtils.isEmpty(origin))
+ sites.put(origin, new Site(origin));
+ }
+
+ // 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(ctx, sites).execute();
+
+ populateOrigins(sites);
+ mReady = true;
+ }
+ }
+ );
+ }
+ }
+
+ public void deleteAllOrigins() {
+ if (mPermServ != null) {
+ Set<String> origins = mPermServ.getOrigins();
+ String[] originArray = origins.toArray(new String[origins.size()]);
+
+ for (String origin : originArray) {
+ PermissionsServiceFactory.PermissionsService.OriginInfo info =
+ mPermServ.getOriginInfo(origin);
+ if (info != null) {
+ info.clearAllStoredData();
+ }
+ }
+ // purge the permissionservice since its not needed
+ mPermServ.purge();
+ mPermServ = null;
+
+ // reset all site settings
+ PermissionsServiceFactory.resetSiteSettings();
+
+ WebRefiner refiner = WebRefiner.getInstance();
+ if (refiner != null) {
+ refiner.useDefaultPermissionForOrigins(originArray);
+ }
+ }
+ }
+
+ private 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();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view;
+ final TextView title;
+ final TextView subtitle;
+
+ 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);
+
+ Site site = getItem(position);
+ site.updateView(view);
+ 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);
+ }
+ // We set the site as the view's tag,
+ // so that we can get it in onItemClick()
+ view.setTag(site);
+
+ return view;
+ }
+
+ public void onItemClick(AdapterView<?> parent,
+ View view,
+ int position,
+ long id) {
+ clickHandler((Site) view.getTag());
+ }
+ }
+
+ private void clickHandler(Site site) {
+ Activity activity = getActivity();
+ if (activity != null) {
+ Bundle args = new Bundle();
+ args.putString(SiteSpecificPreferencesFragment.EXTRA_ORIGIN, site.getOrigin());
+ if(site.getIcon() != null) {
+ ByteArrayOutputStream favicon = new ByteArrayOutputStream();
+ site.getIcon().compress(Bitmap.CompressFormat.PNG, 100, favicon);
+ args.putByteArray(SiteSpecificPreferencesFragment.EXTRA_FAVICON,
+ favicon.toByteArray());
+ }
+ FragmentManager fragmentManager = activity.getFragmentManager();
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+
+ Fragment newFragment = new SiteSpecificPreferencesFragment();
+ newFragment.setArguments(args);
+ fragmentTransaction.replace(getId(), newFragment);
+ fragmentTransaction.addToBackStack(null);
+ fragmentTransaction.commit();
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.swe_website_settings, container, false);
+ View new_site = view.findViewById(R.id.add_new_site);
+ new_site.setVisibility(View.VISIBLE);
+ new_site.setOnClickListener(this);
+ 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);
+ mAdapter = new SiteAdapter(getActivity(), R.layout.website_settings_row);
+ getListView().setAdapter(mAdapter);
+ getListView().setOnItemClickListener(mAdapter);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mAdapter.askForOrigins();
+ ActionBar bar = getActivity().getActionBar();
+ if (bar != null) {
+ bar.setTitle(R.string.pref_extras_website_settings);
+ bar.setDisplayHomeAsUpEnabled(false);
+ bar.setHomeButtonEnabled(false);
+ }
+ }
+
+ private void finish() {
+ Activity activity = getActivity();
+ if (activity != null) {
+ getActivity().getFragmentManager().popBackStack();
+ }
+ }
+
+ @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.ok,
+ new AlertDialog.OnClickListener() {
+ public void onClick(DialogInterface dlg, int which) {
+ mAdapter.deleteAllOrigins();
+ if (GeolocationPermissions.isIncognitoCreated()) {
+ GeolocationPermissions.getIncognitoInstance().clearAll();
+ }
+ WebStorageSizeManager.resetLastOutOfSpaceNotificationTime();
+ mAdapter.askForOrigins();
+ finish();
+ }
+ })
+ .setNegativeButton(R.string.cancel, null)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .show();
+ break;
+ case R.id.add_new_site:
+ final EditText input = new EditText(getActivity());
+ new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.website_settings_add_origin)
+ .setMessage(R.string.pref_security_origin_name)
+ .setView(input)
+ .setPositiveButton(R.string.pref_security_add,
+ new AlertDialog.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ String origin = input.getText().toString();
+ Bundle args = new Bundle();
+ args.putString(SiteSpecificPreferencesFragment.EXTRA_SITE,
+ origin);
+
+ FragmentTransaction fragmentTransaction =
+ getActivity().getFragmentManager().beginTransaction();
+
+ Fragment newFragment = new SiteSpecificPreferencesFragment();
+ newFragment.setArguments(args);
+ fragmentTransaction.replace(getId(), newFragment);
+ fragmentTransaction.addToBackStack(null);
+ fragmentTransaction.commit();
+ }})
+ .setNegativeButton(R.string.cancel, null)
+ .show();
+
+ break;
+ }
+ }
+}
diff --git a/src/src/com/android/browser/provider/BrowserProvider.java b/src/src/com/android/browser/provider/BrowserProvider.java
new file mode 100644
index 00000000..c86ae844
--- /dev/null
+++ b/src/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.text.TextUtils;
+import android.util.Log;
+import android.util.Patterns;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.R;
+import com.android.browser.platformsupport.Browser;
+import com.android.browser.platformsupport.Browser.BookmarkColumns;
+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_suggest_bookmark_normal)
+ .toString();
+ } else {
+ return Integer.valueOf(
+ R.drawable.ic_suggest_history_normal)
+ .toString();
+ }
+ } else {
+ return Integer.valueOf(
+ R.drawable.ic_suggest_search_normal)
+ .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/src/com/android/browser/provider/BrowserProvider2.java b/src/src/com/android/browser/provider/BrowserProvider2.java
new file mode 100644
index 00000000..53f567f8
--- /dev/null
+++ b/src/src/com/android/browser/provider/BrowserProvider2.java
@@ -0,0 +1,2290 @@
+/*
+ * 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.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.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.Browser;
+import com.android.browser.platformsupport.Browser.BookmarkColumns;
+import com.android.browser.platformsupport.BrowserContract;
+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.platformsupport.SyncStateContentProviderHelper;
+import com.android.browser.widget.BookmarkThumbnailWidgetProvider;
+import org.chromium.base.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_action_bookmark),
+ Integer.toString(R.drawable.ic_suggest_history_normal)),
+ 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 the default bookmarks
+ addDefaultBookmarks(db, FIXED_ID_ROOT);
+ }
+
+ private void addDefaultBookmarks(SQLiteDatabase db, long parentId) {
+ Resources res = getContext().getResources();
+ final CharSequence[] bookmarks = res.getTextArray(R.array.bookmarks);
+ int size = bookmarks.length;
+ String[] preloads = res.getStringArray(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 +
+ ");");
+
+ String faviconFileName = preloads[i];
+ String thumbFileName = preloads[i+1];
+
+ int favIconId = res.getIdentifier(faviconFileName, "raw",
+ R.class.getPackage().getName());
+ if(favIconId == 0) {
+ favIconId = res.getIdentifier(faviconFileName, "raw",
+ getContext().getPackageName());
+ }
+
+ int thumbId = res.getIdentifier(thumbFileName, "raw",
+ R.class.getPackage().getName());
+ if(thumbId == 0) {
+ thumbId = res.getIdentifier(thumbFileName, "raw",
+ getContext().getPackageName());
+ }
+
+ byte[] thumb = null, favicon = null;
+
+ try {
+ thumb = readRaw(res, thumbId);
+ } catch (IOException e) {
+ }
+ try {
+ favicon = readRaw(res, favIconId);
+ } catch (IOException e) {
+ }
+
+ if (thumb != null || favicon != null) {
+ ContentValues imageValues = new ContentValues();
+ imageValues.put(Images.URL, bookmarkDestination.toString());
+ if (favicon != null) {
+ imageValues.put(Images.FAVICON, favicon);
+ }
+ if (thumb != null) {
+ imageValues.put(Images.THUMBNAIL, thumb);
+ }
+ db.insert(TABLE_IMAGES, Images.FAVICON, imageValues);
+ }
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ }
+ }
+
+ private byte[] readRaw(Resources res, int id) throws IOException {
+ 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;
+ /* do not allow the id with val =1
+ * since all the columns at id 1 are null
+ * and do not represent any information to the user
+ */
+ selection = DatabaseUtils.concatenateWhere(
+ selection, "_id > 1");
+ }
+ 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: {
+ if (BrowserSettings.getInstance() == null) {
+ BrowserSettings.initialize(getContext());
+ }
+ 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 || android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.KITKAT) {
+ 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;
+ }
+ }
+ }
+ } catch (Exception e) {
+ return false;
+ } 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/src/com/android/browser/provider/MyNavigationProvider.java b/src/src/com/android/browser/provider/MyNavigationProvider.java
new file mode 100755
index 00000000..61d574b3
--- /dev/null
+++ b/src/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, "");
+ 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/src/com/android/browser/provider/SQLiteContentProvider.java b/src/src/com/android/browser/provider/SQLiteContentProvider.java
new file mode 100644
index 00000000..75e298e5
--- /dev/null
+++ b/src/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/src/com/android/browser/provider/SnapshotProvider.java b/src/src/com/android/browser/provider/SnapshotProvider.java
new file mode 100644
index 00000000..923613cb
--- /dev/null
+++ b/src/src/com/android/browser/provider/SnapshotProvider.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.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.BrowserConfig;
+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 = BrowserConfig.AUTHORITY + ".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/src/com/android/browser/reflect/ReflectHelper.java b/src/src/com/android/browser/reflect/ReflectHelper.java
new file mode 100644
index 00000000..06ac8d02
--- /dev/null
+++ b/src/src/com/android/browser/reflect/ReflectHelper.java
@@ -0,0 +1,168 @@
+/*
+ * 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, Class[] argTypes, Object[] args) {
+ Object obj = null;
+
+ try{
+ Class clazz = Class.forName(className);
+
+ if (args == null || args.length == 0) {
+ obj = clazz.newInstance();
+ } else {
+ 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) {
+ boolean modifiedAccessibility = false;
+ Object result = null;
+ Method m = null;
+
+ if (obj == null || method == null)
+ return null;
+
+ try {
+ if (obj instanceof String){
+ //Process call as a static method call
+ String className = (String)obj;
+ obj = null;
+ Class clazz = Class.forName(className);
+ m = clazz.getDeclaredMethod(method, argTypes);
+ } else {
+ //Process call on instance of obj
+ m = obj.getClass().getDeclaredMethod(method, argTypes);
+ }
+
+ if(m != null) {
+ 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 null;
+ }
+ return result;
+ }
+
+ public static Object invokeProxyMethod(String proxyClassName, String method, Object obj,
+ Class[] proxyArgTypes, Object[] args) {
+ Object result = null;
+ boolean modifiedAccessibility = false;
+ if (proxyClassName == null || method == null) {
+ throw new IllegalArgumentException("Object and Method must be supplied.");
+ }
+ try {
+ Class clazz = Class.forName(proxyClassName);
+ Method m = clazz.getDeclaredMethod(method, proxyArgTypes);
+ 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 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/src/com/android/browser/search/DefaultSearchEngine.java b/src/src/com/android/browser/search/DefaultSearchEngine.java
new file mode 100644
index 00000000..41bd238c
--- /dev/null
+++ b/src/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.text.TextUtils;
+import android.util.Log;
+
+import com.android.browser.platformsupport.Browser;
+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/src/com/android/browser/search/OpenSearchSearchEngine.java b/src/src/com/android/browser/search/OpenSearchSearchEngine.java
new file mode 100644
index 00000000..eb7c97ed
--- /dev/null
+++ b/src/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.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.text.TextUtils;
+import android.util.Log;
+
+import com.android.browser.platformsupport.Browser;
+
+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.ic_action_search_normal);
+ }
+ }
+ 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/src/com/android/browser/search/SearchEngine.java b/src/src/com/android/browser/search/SearchEngine.java
new file mode 100644
index 00000000..8f2d58db
--- /dev/null
+++ b/src/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/src/com/android/browser/search/SearchEngineInfo.java b/src/src/com/android/browser/search/SearchEngineInfo.java
new file mode 100644
index 00000000..f0b478bd
--- /dev/null
+++ b/src/src/com/android/browser/search/SearchEngineInfo.java
@@ -0,0 +1,179 @@
+/*
+ * 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;
+
+ final Resources res = context.getResources();
+ String packageName = R.class.getPackage().getName();
+ int id_data = res.getIdentifier(name, "array", packageName);
+ if(id_data == 0) {
+ id_data = res.getIdentifier(name, "array", context.getPackageName());
+ }
+ 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/src/com/android/browser/search/SearchEnginePreference.java b/src/src/com/android/browser/search/SearchEnginePreference.java
new file mode 100644
index 00000000..a129ef13
--- /dev/null
+++ b/src/src/com/android/browser/search/SearchEnginePreference.java
@@ -0,0 +1,89 @@
+/*
+ * 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.mdm.SearchEngineRestriction;
+
+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 android.view.View;
+import android.widget.Toast;
+
+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());
+ }
+
+ SearchEngineInfo managedSearchEngineInfo = SearchEngineRestriction.getInstance()
+ .getSearchEngineInfo();
+
+ if (managedSearchEngineInfo != null) {
+ // Add managed searched engine to the list if SEARCH_ENGINE restriction is enabled
+ entryValues.add(managedSearchEngineInfo.getName());
+ entries.add(managedSearchEngineInfo.getLabel());
+ } else {
+ for (SearchEngineInfo searchEngineInfo : SearchEngines.getSearchEngineInfos(context)) {
+ String name = searchEngineInfo.getName();
+ // Skip entry if name is same as the default or the managed
+ 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()]));
+ }
+
+ @Override
+ protected void onBindView(View view) {
+ super.onBindView(view);
+
+ if (!isEnabled()) {
+ view.setEnabled(true);
+ view.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Toast.makeText(getContext(), R.string.mdm_managed_alert, Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+ }
+}
diff --git a/src/src/com/android/browser/search/SearchEngines.java b/src/src/com/android/browser/search/SearchEngines.java
new file mode 100644
index 00000000..64497c36
--- /dev/null
+++ b/src/src/com/android/browser/search/SearchEngines.java
@@ -0,0 +1,68 @@
+/*
+ * 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.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>();
+ String[] searchEngines = context.getResources().getStringArray(R.array.search_engines);
+ for (int i = 0; i < searchEngines.length; i++) {
+ String name = searchEngines[i];
+ 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/src/com/android/browser/stub/NullController.java b/src/src/com/android/browser/stub/NullController.java
new file mode 100644
index 00000000..d513b7d8
--- /dev/null
+++ b/src/src/com/android/browser/stub/NullController.java
@@ -0,0 +1,162 @@
+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 void onStop() {
+ }
+
+ @Override
+ public void onStart() {
+ }
+
+ @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;
+ }
+
+ public void invalidateOptionsMenu() {}
+
+}
diff --git a/src/src/com/android/browser/util/ThreadedCursorAdapter.java b/src/src/com/android/browser/util/ThreadedCursorAdapter.java
new file mode 100644
index 00000000..5a0407b0
--- /dev/null
+++ b/src/src/com/android/browser/util/ThreadedCursorAdapter.java
@@ -0,0 +1,221 @@
+/*
+ * 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 HandlerThread mThread;
+
+ 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();
+ mThread = new HandlerThread("threaded_adapter_" + this,
+ Process.THREAD_PRIORITY_BACKGROUND);
+ mThread.start();
+ mLoadHandler = new Handler(mThread.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 void quitThread() {
+ if (mThread != null) {
+ HandlerThread thread = mThread;
+ mThread = null;
+ thread.quit();
+ }
+ }
+
+ 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);
+}
diff --git a/src/src/com/android/browser/view/BookmarkContainer.java b/src/src/com/android/browser/view/BookmarkContainer.java
new file mode 100644
index 00000000..b36dde2f
--- /dev/null
+++ b/src/src/com/android/browser/view/BookmarkContainer.java
@@ -0,0 +1,201 @@
+/*
+ * 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.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.StateListDrawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.browser.FolderTileView;
+import com.android.browser.R;
+import com.android.browser.SiteTileView;
+
+public class BookmarkContainer extends LinearLayout implements OnClickListener {
+
+ private OnClickListener mClickListener;
+ private boolean mIgnoreRequestLayout = false;
+
+ private FrameLayout mTileContainer;
+ private FolderTileView mFolderTile;
+ private SiteTileView mSiteTile;
+ private ImageView mOverlayBadge;
+
+ 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);
+
+ if (mSiteTile == null) {
+ mSiteTile = new SiteTileView(getContext(), (Bitmap)null);
+ }
+
+ if (mFolderTile == null) {
+ mFolderTile = new FolderTileView(getContext(), null, null);
+ mFolderTile.setClickable(true);
+ }
+ super.setOnClickListener(this);
+ }
+
+ public void reConfigureAsFolder(String title, String numItems) {
+ // hide elements that may have been already created
+ mSiteTile.setVisibility(View.GONE);
+ if (mOverlayBadge != null)
+ mOverlayBadge.setVisibility(View.GONE);
+
+ // reconfigure the existing Folder
+ mFolderTile.setVisibility(View.VISIBLE);
+ mFolderTile.setText(title);
+ mFolderTile.setLabel(numItems);
+ addTileToContainer(mFolderTile);
+ }
+
+ public void reConfigureAsSite(Bitmap favicon) {
+ // hide elements that may have been already created
+ mFolderTile.setVisibility(View.GONE);
+ if (mOverlayBadge != null)
+ mOverlayBadge.setVisibility(View.GONE);
+
+ // reconfigure the existing Site
+ mSiteTile.setVisibility(View.VISIBLE);
+ mSiteTile.replaceFavicon(favicon);
+ addTileToContainer(mSiteTile);
+ }
+
+ public void setBottomLabelText(String bottomLabel) {
+ ((TextView) findViewById(R.id.label)).setText(bottomLabel);
+ }
+
+ public void setOverlayBadge(int imgResId) {
+ // remove the badge if already existing
+ if (imgResId == 0) {
+ if (mOverlayBadge != null) {
+ mOverlayBadge.setVisibility(View.GONE);
+ mOverlayBadge.setImageDrawable(null);
+ }
+ return;
+ }
+
+ // create the badge if needed and not present
+ if (mOverlayBadge == null) {
+ mOverlayBadge = new ImageView(getContext());
+ FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
+ );
+ lp.gravity = Gravity.BOTTOM | Gravity.END;
+ FrameLayout frameLayout = (FrameLayout) findViewById(R.id.container);
+ frameLayout.addView(mOverlayBadge, lp);
+ }
+ mOverlayBadge.setVisibility(View.VISIBLE);
+ mOverlayBadge.bringToFront();
+ mOverlayBadge.setImageResource(imgResId);
+ }
+
+
+ private void addTileToContainer(View view) {
+ if (view.getParent() != null) {
+ return;
+ }
+
+ // insert the view in the container, filling it
+ FrameLayout frameLayout = (FrameLayout) findViewById(R.id.container);
+ frameLayout.addView(view, 0, new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
+ ));
+ // common customizations for folders or sites
+ view.setLongClickable(true);
+ }
+
+
+ @Override
+ public void setOnClickListener(OnClickListener l) {
+ mClickListener = l;
+ mSiteTile.setOnClickListener(l);
+ mFolderTile.setOnClickListener(l);
+ }
+
+ @Override
+ public void setTag(int key, final Object tag) {
+ super.setTag(key, tag);
+ mSiteTile.setTag(key, tag);
+ mFolderTile.setTag(key, tag);
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ updateTransitionDrawable(isPressed());
+ }
+
+ void updateTransitionDrawable(boolean pressed) {
+ Drawable selector = getBackground();
+ if (selector != null && selector instanceof StateListDrawable) {
+ Drawable d = ((StateListDrawable)selector).getCurrent();
+ if (d != null && d instanceof TransitionDrawable) {
+ if (pressed && isLongClickable()) {
+ final int longPressTimeout = ViewConfiguration.getLongPressTimeout();
+ ((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/src/com/android/browser/view/BookmarkExpandableView.java b/src/src/com/android/browser/view/BookmarkExpandableView.java
new file mode 100644
index 00000000..3ed000c3
--- /dev/null
+++ b/src/src/com/android/browser/view/BookmarkExpandableView.java
@@ -0,0 +1,478 @@
+/*
+ * 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.platformsupport.BrowserContract;
+import com.android.browser.reflect.ReflectHelper;
+import com.android.browser.R;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+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);
+ if (mAdapter.getGroupCount() < 2) {
+ setGroupIndicator(null);
+ }
+ 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);
+ int margin = getResources().getDimensionPixelSize(R.dimen.combo_bookmark_thumbnail_margin);
+ mColumnWidth = v.getMeasuredWidth() + (margin * 2);
+ }
+
+ 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) {
+ Object[] params = {menuInfo};
+ Class[] proxyType = new Class[] {ContextMenuInfo.class};
+ ReflectHelper.invokeProxyMethod("com.android.internal.view.menu.MenuBuilder",
+ "setCurrentMenuInfo", menu, proxyType, params);
+ }
+
+ @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) {
+ if (mAdapter.getGroupCount() < 2) {
+ return;
+ }
+
+ 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.setEnabled(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);
+ }
+ crumbs.setVisibility(VISIBLE);
+ crumbHolder.addView(crumbs);
+
+ TextView overflowView = (TextView) view.findViewById(R.id.crumb_overflow);
+ crumbs.addOverflowLabel(overflowView);
+/*
+ TextView name = (TextView) view.findViewById(R.id.group_name);
+ String groupName = mGroups.get(groupPosition);
+ if (groupName == null) {
+ groupName = mContext.getString(R.string.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(2);
+ 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/src/com/android/browser/view/BookmarkThumbImageView.java b/src/src/com/android/browser/view/BookmarkThumbImageView.java
new file mode 100644
index 00000000..50b35447
--- /dev/null
+++ b/src/src/com/android/browser/view/BookmarkThumbImageView.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2015, 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.view;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+public class BookmarkThumbImageView extends ImageView {
+
+ public BookmarkThumbImageView(Context context) {
+ this(context, null);
+ }
+
+ public BookmarkThumbImageView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public BookmarkThumbImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public void setImageDrawable(Drawable drawable) {
+ int drawableWidth = drawable.getIntrinsicWidth();
+ int drawableHeight = drawable.getIntrinsicHeight();
+ int containerWidth = getWidth() - getPaddingLeft() - getPaddingRight();
+ int containerHeight = getHeight() - getPaddingTop() - getPaddingBottom();
+
+ float scale;
+ Matrix m = new Matrix();
+ if ( (drawableWidth * containerHeight) > (containerWidth * drawableHeight)) {
+ scale = (float) containerHeight / (float) drawableHeight;
+ } else {
+ scale = (float) containerWidth / (float) drawableWidth;
+ float translateY = (containerHeight - drawableHeight * scale) / 2;
+ if (translateY < 0) {
+ translateY = 0;
+ }
+ m.postTranslate(0, translateY + 0.5f);
+ }
+ m.setScale(scale, scale);
+
+ this.setScaleType(ScaleType.MATRIX);
+ this.setImageMatrix(m);
+ super.setImageDrawable(drawable);
+ }
+}
diff --git a/src/src/com/android/browser/view/EventRedirectingFrameLayout.java b/src/src/com/android/browser/view/EventRedirectingFrameLayout.java
new file mode 100644
index 00000000..901b0217
--- /dev/null
+++ b/src/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/src/com/android/browser/view/ScrollerView.java b/src/src/com/android/browser/view/ScrollerView.java
new file mode 100644
index 00000000..7e5a4c85
--- /dev/null
+++ b/src/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/src/com/android/browser/view/SnapshotGridView.java b/src/src/com/android/browser/view/SnapshotGridView.java
new file mode 100644
index 00000000..ab12060b
--- /dev/null
+++ b/src/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/src/com/android/browser/widget/BookmarkThumbnailWidgetProvider.java b/src/src/com/android/browser/widget/BookmarkThumbnailWidgetProvider.java
new file mode 100644
index 00000000..4b3ae96b
--- /dev/null
+++ b/src/src/com/android/browser/widget/BookmarkThumbnailWidgetProvider.java
@@ -0,0 +1,119 @@
+/*
+ * 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.BrowserConfig;
+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 =
+ BrowserConfig.AUTHORITY +".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/src/com/android/browser/widget/BookmarkThumbnailWidgetService.java b/src/src/com/android/browser/widget/BookmarkThumbnailWidgetService.java
new file mode 100644
index 00000000..c8181917
--- /dev/null
+++ b/src/src/com/android/browser/widget/BookmarkThumbnailWidgetService.java
@@ -0,0 +1,379 @@
+/*
+ * 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.BrowserConfig;
+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 = BrowserConfig.AUTHORITY+ ".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_deco_bookmarks_normal);
+ // 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.ic_deco_favicon_normal);
+ }
+ }
+ 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/src/com/android/browser/widget/BookmarkWidgetConfigure.java b/src/src/com/android/browser/widget/BookmarkWidgetConfigure.java
new file mode 100644
index 00000000..2dee9894
--- /dev/null
+++ b/src/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/src/com/android/browser/widget/BookmarkWidgetProxy.java b/src/src/com/android/browser/widget/BookmarkWidgetProxy.java
new file mode 100644
index 00000000..8ab57fc3
--- /dev/null
+++ b/src/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);
+ }
+ }
+}