summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2019-04-23 15:32:23 -0400
committerMatt A. Tobin <email@mattatobin.com>2019-04-23 15:32:23 -0400
commitabe80cc31d5a40ebed743085011fbcda0c1a9a10 (patch)
treefb3762f06b84745b182af281abb107b95a9fcf01 /mobile/android/base/java
parent63295d0087eb58a6eb34cad324c4c53d1b220491 (diff)
downloadUXP-abe80cc31d5a40ebed743085011fbcda0c1a9a10.tar
UXP-abe80cc31d5a40ebed743085011fbcda0c1a9a10.tar.gz
UXP-abe80cc31d5a40ebed743085011fbcda0c1a9a10.tar.lz
UXP-abe80cc31d5a40ebed743085011fbcda0c1a9a10.tar.xz
UXP-abe80cc31d5a40ebed743085011fbcda0c1a9a10.zip
Issue #1053 - Drop support Android and remove Fennec - Part 1a: Remove mobile/android
Diffstat (limited to 'mobile/android/base/java')
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/ANRReporter.java596
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/AboutPages.java117
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/AccountsHelper.java318
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java256
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/ActionModeCompat.java135
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/ActionModeCompatView.java202
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/ActivityHandlerHelper.java61
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/BootReceiver.java27
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/BrowserApp.java4261
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/BrowserLocaleManager.java439
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/ChromeCastDisplay.java112
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/ChromeCastPlayer.java509
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/CrashReporter.java480
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/CustomEditText.java89
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/DataReportingNotification.java133
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/DevToolsAuthHelper.java52
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/DoorHangerPopup.java361
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/DownloadsIntegration.java235
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/DynamicToolbar.java218
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/EditBookmarkDialog.java252
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/Experiments.java119
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/FilePicker.java227
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/FilePickerResultHandler.java282
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/FindInPageBar.java256
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java459
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/GeckoActivity.java100
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/GeckoActivityStatus.java10
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/GeckoApp.java2878
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java314
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/GeckoJavaSampler.java211
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/GeckoMediaPlayer.java27
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/GeckoMessageReceiver.java19
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/GeckoPresentationDisplay.java22
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/GeckoProfilesProvider.java149
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/GeckoService.java236
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/GeckoUpdateReceiver.java25
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/GlobalHistory.java178
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/GlobalPageMetadata.java182
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/GuestSession.java51
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/IntentHelper.java599
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/LauncherActivity.java110
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/LocaleManager.java42
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/Locales.java136
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/MediaCastingBar.java131
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java323
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/MemoryMonitor.java279
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/MotionEventInterceptor.java13
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/PackageReplacedReceiver.java38
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/PresentationMediaPlayerManager.java149
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/PresentationView.java27
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/PrintHelper.java124
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/PrivateTab.java28
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/RemoteClientsDialogFragment.java133
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/RemotePresentationService.java150
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/Restarter.java50
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/ScreenManagerHelper.java43
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/ScreenshotObserver.java146
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/SessionParser.java140
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/SharedPreferencesHelper.java311
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/SiteIdentity.java249
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/SnackbarBuilder.java257
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/SuggestClient.java142
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/Tab.java843
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/Tabs.java1021
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/Telemetry.java246
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/TelemetryContract.java307
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/ThumbnailHelper.java246
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/ZoomedView.java838
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/activitystream/ActivityStream.java149
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/adjust/AdjustBrowserAppDelegate.java52
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/adjust/AdjustHelper.java75
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/adjust/AdjustHelperInterface.java22
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/adjust/AttributionHelperListener.java17
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/adjust/StubAdjustHelper.java31
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/animation/AnimationUtils.java21
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/animation/HeightChangeAnimation.java27
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/animation/PropertyAnimator.java342
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/animation/Rotate3DAnimation.java97
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/animation/ViewHelper.java109
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/cleanup/FileCleanupController.java81
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/cleanup/FileCleanupService.java80
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java177
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/customtabs/GeckoCustomTabsService.java65
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/AbstractPerProfileDatabaseProvider.java79
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/AbstractTransactionalProvider.java328
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/BaseTable.java64
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java785
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java205
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java2237
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java2340
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/DBUtils.java450
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/FormHistoryProvider.java166
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/HomeProvider.java194
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java1938
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/LocalSearches.java28
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/LocalTabsAccessor.java320
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/LocalURLMetadata.java240
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/LocalUrlAnnotations.java253
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/LoginsProvider.java520
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/PasswordsProvider.java348
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/PerProfileDatabaseProvider.java55
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/PerProfileDatabases.java94
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/RemoteClient.java69
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/RemoteTab.java90
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/SQLiteBridgeContentProvider.java471
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/SearchHistoryProvider.java127
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/Searches.java12
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/SharedBrowserDatabaseProvider.java128
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/SuggestedSites.java629
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/Table.java47
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/TabsAccessor.java28
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/TabsProvider.java361
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/URLMetadata.java25
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/URLMetadataTable.java92
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/db/UrlAnnotations.java51
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/delegates/BookmarkStateChangeDelegate.java237
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/delegates/BrowserAppDelegate.java78
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/delegates/BrowserAppDelegateWithReference.java29
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/delegates/OfflineTabStatusDelegate.java119
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/delegates/ScreenshotDelegate.java80
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/delegates/TabsTrayVisibilityAwareDelegate.java38
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/distribution/Distribution.java1046
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/distribution/DistributionStoreCallback.java61
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBookmarksProviderProxy.java322
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBrowserCustomizationsClient.java43
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerDescriptor.java64
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerReceiver.java107
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/dlc/BaseAction.java166
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/dlc/CleanupAction.java49
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/dlc/DownloadAction.java325
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/dlc/DownloadContentService.java144
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/dlc/StudyAction.java81
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/dlc/SyncAction.java263
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/dlc/VerifyAction.java63
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContent.java189
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentBootstrap.java161
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentBuilder.java238
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentCatalog.java303
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/feeds/ContentNotificationsDelegate.java89
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/feeds/FeedAlarmReceiver.java31
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/feeds/FeedFetcher.java110
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/feeds/FeedService.java168
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/feeds/action/CheckForUpdatesAction.java281
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/feeds/action/EnrollSubscriptionsAction.java101
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/feeds/action/FeedAction.java58
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/feeds/action/SetupAlarmsAction.java146
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/feeds/action/SubscribeToFeedAction.java79
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/feeds/action/WithdrawSubscriptionsAction.java109
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSite.java38
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteBlogger.java29
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteMedium.java29
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteTumblr.java33
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteWordpress.java26
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/feeds/parser/Feed.java70
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/feeds/parser/Item.java49
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/feeds/parser/SimpleFeedParser.java367
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/feeds/subscriptions/FeedSubscription.java130
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/firstrun/DataPanel.java47
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunAnimationContainer.java94
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPager.java174
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPagerConfig.java107
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPanel.java80
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/firstrun/RestrictedWelcomePanel.java61
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/firstrun/SyncPanel.java61
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/firstrun/TabQueuePanel.java92
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/gcm/GcmInstanceIDListenerService.java35
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/gcm/GcmMessageListenerService.java38
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/gcm/GcmTokenClient.java131
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/health/HealthRecorder.java40
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/health/SessionInformation.java138
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/health/StubbedHealthRecorder.java53
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/BookmarkFolderView.java147
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/BookmarkScreenshotRow.java67
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/BookmarksListAdapter.java352
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/BookmarksListView.java218
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/BookmarksPanel.java316
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/BrowserSearch.java1316
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/ClientsAdapter.java373
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java433
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java127
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java697
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryRecyclerView.java145
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/DynamicPanel.java393
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/FramePanelLayout.java52
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/HistorySectionsHelper.java80
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/HomeAdapter.java224
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/HomeBanner.java315
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java1694
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/HomeConfigLoader.java83
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java663
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/HomeContextMenuInfo.java82
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/HomeExpandableListView.java68
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java498
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/HomeListView.java138
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/HomePager.java564
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/HomePanelsManager.java368
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/HomeScreen.java57
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/ImageLoader.java164
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/MultiTypeCursorAdapter.java100
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/PanelAuthCache.java82
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/PanelAuthLayout.java63
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/PanelBackItemView.java48
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/PanelHeaderView.java28
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/PanelInfoManager.java162
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/PanelItemView.java136
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/PanelLayout.java747
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/PanelListView.java83
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerView.java178
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerViewAdapter.java137
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/PanelRefreshLayout.java90
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/PanelViewAdapter.java113
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/PanelViewItemHandler.java59
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/PinSiteDialog.java256
-rwxr-xr-xmobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java454
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/RemoteTabsExpandableListState.java163
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/SearchEngine.java102
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/SearchEngineAdapter.java122
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/SearchEngineBar.java148
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java494
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/SearchLoader.java114
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/SimpleCursorLoader.java147
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/SpacingDecoration.java20
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/TabMenuStrip.java127
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/TabMenuStripLayout.java246
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridItemView.java312
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridView.java169
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/TopSitesPanel.java968
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/TopSitesThumbnailView.java102
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java324
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStream.java145
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeFragment.java39
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeScreen.java73
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamItem.java196
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamRecyclerAdapter.java135
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/ActivityStreamContextMenu.java239
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/BottomSheetContextMenu.java102
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/PopupContextMenu.java76
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/CirclePageIndicator.java568
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesCard.java105
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPage.java38
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPageAdapter.java117
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPagerAdapter.java124
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/IconCallback.java13
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/IconDescriptor.java96
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/IconDescriptorComparator.java67
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/IconRequest.java181
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/IconRequestBuilder.java143
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/IconRequestExecutor.java152
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/IconResponse.java167
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/IconTask.java222
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/Icons.java35
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/IconsHelper.java140
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/decoders/FaviconDecoder.java197
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/decoders/ICODecoder.java396
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/decoders/IconDirectoryEntry.java212
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/decoders/LoadFaviconResult.java133
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/loader/ContentProviderLoader.java96
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/loader/DataUriLoader.java36
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/loader/DiskLoader.java27
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/loader/IconDownloader.java219
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/loader/IconGenerator.java168
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/loader/IconLoader.java23
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/loader/JarLoader.java45
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/loader/LegacyLoader.java74
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/loader/MemoryLoader.java31
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/preparation/AboutPagesPreparer.java39
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/preparation/AddDefaultIconUrl.java39
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterKnownFailureUrls.java29
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterMimeTypes.java39
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterPrivilegedUrls.java30
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/preparation/LookupIconUrl.java56
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/preparation/Preparer.java19
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/processing/ColorProcessor.java61
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/processing/DiskProcessor.java36
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/processing/MemoryProcessor.java38
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/processing/Processor.java21
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/processing/ResizingProcessor.java68
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/storage/DiskStorage.java293
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/storage/FailureCache.java70
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/icons/storage/MemoryStorage.java112
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManager.java195
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManagerV1.java260
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/lwt/LightweightTheme.java455
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/lwt/LightweightThemeDrawable.java133
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/mdns/MulticastDNSManager.java535
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/media/AsyncCodec.java34
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/media/AsyncCodecFactory.java14
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/media/AudioFocusAgent.java135
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/media/Codec.java366
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/media/CodecProxy.java191
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/media/FormatParam.java133
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrm.java35
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV21.java627
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV23.java44
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/media/JellyBeanAsyncCodec.java405
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/media/LocalMediaDrmBridge.java162
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java431
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/media/MediaDrmProxy.java307
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/media/MediaManager.java44
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/media/RemoteManager.java224
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridge.java152
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridgeStub.java247
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/media/Sample.java264
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/media/SamplePool.java115
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/media/SessionKeyInfo.java51
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/media/VideoPlayer.java204
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenu.java928
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenuInflater.java163
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenuItem.java472
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/menu/GeckoSubMenu.java81
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/menu/MenuItemActionBar.java64
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/menu/MenuItemDefault.java152
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/menu/MenuItemSwitcherLayout.java188
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/menu/MenuPanel.java36
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/menu/MenuPopup.java76
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/mozglue/SharedMemBuffer.java81
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/mozglue/SharedMemory.java171
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/notifications/NotificationClient.java324
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/notifications/NotificationHelper.java366
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/notifications/NotificationReceiver.java106
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/notifications/NotificationService.java37
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/notifications/WhatsNewReceiver.java99
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/OverlayConstants.java68
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/service/OverlayActionService.java126
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/service/ShareData.java48
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/AddBookmark.java30
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/SendTab.java296
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/ShareMethod.java82
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/ui/OverlayDialogButton.java128
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabDeviceListArrayAdapter.java185
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabList.java150
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabTargetSelectedListener.java25
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/overlays/ui/ShareDialog.java493
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/AlignRightLinkPreference.java24
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/AndroidImport.java230
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/AndroidImportPreference.java112
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/AppCompatPreferenceActivity.java115
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/ClearOnShutdownPref.java37
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/CustomCheckBoxPreference.java44
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/CustomListCategory.java72
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/CustomListPreference.java182
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/DistroSharedPrefsImport.java61
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/FontSizePreference.java192
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferenceFragment.java296
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java1514
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/LinkPreference.java35
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/ListCheckboxPreference.java58
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/LocaleListPreference.java316
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/ModifiableHintPreference.java67
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/MultiChoicePreference.java271
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/MultiPrefMultiChoicePreference.java116
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/PanelsPreference.java255
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/PanelsPreferenceCategory.java261
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/PrivateDataPreference.java67
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/SearchEnginePreference.java183
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/SearchPreferenceCategory.java145
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/SetHomepagePreference.java124
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/preferences/SyncPreference.java103
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/promotion/AddToHomeScreenPromotion.java237
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/promotion/HomeScreenPrompt.java237
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/promotion/ReaderViewBookmarkPromotion.java103
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/promotion/SimpleHelperUI.java194
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/prompts/ColorPickerInput.java59
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/prompts/IconGridInput.java171
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/prompts/IntentChooserPrompt.java158
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/prompts/IntentHandler.java12
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/prompts/Prompt.java586
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/prompts/PromptInput.java398
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/prompts/PromptListAdapter.java281
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/prompts/PromptListItem.java128
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/prompts/PromptService.java72
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/prompts/TabInput.java107
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/push/Fetched.java71
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/push/PushClient.java110
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/push/PushManager.java354
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/push/PushRegistration.java126
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/push/PushService.java440
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/push/PushState.java137
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/push/PushSubscription.java81
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/reader/ReaderModeUtils.java72
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/reader/ReadingListHelper.java154
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/reader/SavedReaderViewHelper.java247
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/restrictions/DefaultConfiguration.java34
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/restrictions/GuestProfileConfiguration.java83
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictable.java112
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.java129
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionCache.java99
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionConfiguration.java31
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionProvider.java84
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictions.java127
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/search/SearchEngine.java304
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/search/SearchEngineManager.java764
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueueHelper.java357
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueuePrompt.java215
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueueService.java342
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabqueue/TabReceivedService.java130
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/PrivateTabsPanel.java63
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabCurve.java70
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryController.java87
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryFragment.java172
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryItemRow.java69
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryPage.java60
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabPanelBackButton.java55
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabStrip.java170
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabStripAdapter.java98
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabStripItemView.java254
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabStripView.java449
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java712
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java216
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutAdapter.java100
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutItemView.java172
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutRecyclerAdapter.java124
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayout.java118
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayoutAnimator.java65
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanel.java456
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanelThumbnailView.java52
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabsTouchHelperCallback.java69
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java16
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryCorePingDelegate.java188
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java118
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java34
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPreferences.java73
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java347
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/CampaignIdMeasurements.java37
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SearchCountMeasurements.java100
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SessionMeasurements.java99
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryCorePingBuilder.java247
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryPingBuilder.java87
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadAllPingsImmediatelyScheduler.java32
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadScheduler.java26
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryJSONFilePingStore.java301
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryPingStore.java66
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/text/FloatingActionModeCallback.java69
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/text/FloatingToolbarTextSelection.java206
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/text/TextAction.java68
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/text/TextSelection.java13
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/AutocompleteHandler.java10
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/BackButton.java26
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbar.java960
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarPhone.java128
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarPhoneBase.java219
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarTablet.java211
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarTabletBase.java182
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/CanvasDelegate.java62
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/ForwardButton.java23
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/NavButton.java85
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/PageActionLayout.java371
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/PhoneTabsButton.java29
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/ShapedButton.java109
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/ShapedButtonFrameLayout.java74
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/SiteIdentityPopup.java571
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/TabCounter.java154
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarDisplayLayout.java530
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditLayout.java348
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditText.java630
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarPrefs.java78
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarProgressView.java195
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/trackingprotection/TrackingProtectionPrompt.java131
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/updater/PostUpdateHandler.java120
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/updater/UpdateService.java795
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/updater/UpdateServiceHelper.java213
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/util/ColorUtil.java44
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/util/DrawableUtil.java66
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/util/ResourceDrawableUtils.java136
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/util/TouchTargetUtil.java48
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/util/UnusedResourcesUtil.java128
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/util/ViewUtil.java33
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/ActivityChooserModel.java1359
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/AllCapsTextView.java21
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/AnchoredPopup.java130
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/AnimatedHeightLayout.java77
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/BasicColorPicker.java140
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/CheckableLinearLayout.java52
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/ClickableWhenDisabledEditText.java25
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/ContentSecurityDoorHanger.java127
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/CropImageView.java143
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/DateTimePicker.java665
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/DefaultDoorHanger.java190
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/DefaultItemAnimatorBase.java685
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/DoorHanger.java220
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/DoorhangerConfig.java127
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/EllipsisTextView.java65
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/ExternalIntentDuringPrivateBrowsingPromptFragment.java106
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/FadedMultiColorTextView.java108
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/FadedSingleColorTextView.java74
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/FadedTextView.java48
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/FaviconView.java268
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/FilledCardView.java39
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/FlowLayout.java91
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/GeckoActionProvider.java360
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/GeckoPopupMenu.java189
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/HistoryDividerItemDecoration.java66
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/IconTabWidget.java111
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/LoginDoorHanger.java228
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/RecyclerViewClickSupport.java105
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/ResizablePathDrawable.java117
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/RoundedCornerLayout.java79
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/SiteLogins.java16
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/SquaredImageView.java21
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/SquaredRelativeLayout.java33
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/SwipeDismissListViewTouchListener.java356
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/TabThumbnailWrapper.java38
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/ThumbnailView.java86
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/TouchDelegateWithReset.java134
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/TwoWayView.java7191
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedEditText.java172
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedFrameLayout.java172
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedImageButton.java200
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedImageView.java199
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedLinearLayout.java167
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedRelativeLayout.java172
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedTextSwitcher.java167
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedTextView.java172
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedView.java172
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedView.java.frag211
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/themed/generate_themed_views.py72
516 files changed, 0 insertions, 116043 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/ANRReporter.java b/mobile/android/base/java/org/mozilla/gecko/ANRReporter.java
deleted file mode 100644
index 3c29edef3..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/ANRReporter.java
+++ /dev/null
@@ -1,596 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import java.io.BufferedOutputStream;
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.FileReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.Reader;
-import java.util.Locale;
-import java.util.UUID;
-import java.util.regex.Pattern;
-
-import org.json.JSONObject;
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.Log;
-
-public final class ANRReporter extends BroadcastReceiver
-{
- private static final boolean DEBUG = false;
- private static final String LOGTAG = "GeckoANRReporter";
-
- private static final String ANR_ACTION = "android.intent.action.ANR";
- // Number of lines to search traces.txt to decide whether it's a Gecko ANR
- private static final int LINES_TO_IDENTIFY_TRACES = 10;
- // ANRs may happen because of memory pressure,
- // so don't use up too much memory here
- // Size of buffer to hold one line of text
- private static final int TRACES_LINE_SIZE = 100;
- // Size of block to use when processing traces.txt
- private static final int TRACES_BLOCK_SIZE = 2000;
- private static final String TRACES_CHARSET = "utf-8";
- private static final String PING_CHARSET = "utf-8";
-
- private static final ANRReporter sInstance = new ANRReporter();
- private static int sRegisteredCount;
- private Handler mHandler;
- private volatile boolean mPendingANR;
-
- @WrapForJNI
- private static native boolean requestNativeStack(boolean unwind);
- @WrapForJNI
- private static native String getNativeStack();
- @WrapForJNI
- private static native void releaseNativeStack();
-
- public static void register(Context context) {
- if (sRegisteredCount++ != 0) {
- // Already registered
- return;
- }
- sInstance.start(context);
- }
-
- public static void unregister() {
- if (sRegisteredCount == 0) {
- Log.w(LOGTAG, "register/unregister mismatch");
- return;
- }
- if (--sRegisteredCount != 0) {
- // Should still be registered
- return;
- }
- sInstance.stop();
- }
-
- private void start(final Context context) {
-
- Thread receiverThread = new Thread(new Runnable() {
- @Override
- public void run() {
- Looper.prepare();
- synchronized (ANRReporter.this) {
- mHandler = new Handler();
- ANRReporter.this.notify();
- }
- if (DEBUG) {
- Log.d(LOGTAG, "registering receiver");
- }
- context.registerReceiver(ANRReporter.this,
- new IntentFilter(ANR_ACTION),
- null,
- mHandler);
- Looper.loop();
-
- if (DEBUG) {
- Log.d(LOGTAG, "unregistering receiver");
- }
- context.unregisterReceiver(ANRReporter.this);
- mHandler = null;
- }
- }, LOGTAG);
-
- receiverThread.setDaemon(true);
- receiverThread.start();
- }
-
- private void stop() {
- synchronized (this) {
- while (mHandler == null) {
- try {
- wait(1000);
- if (mHandler == null) {
- // We timed out; just give up. The process is probably
- // quitting anyways, so we let the OS do the clean up
- Log.w(LOGTAG, "timed out waiting for handler");
- return;
- }
- } catch (InterruptedException e) {
- }
- }
- }
- Looper looper = mHandler.getLooper();
- looper.quit();
- try {
- looper.getThread().join();
- } catch (InterruptedException e) {
- }
- }
-
- private ANRReporter() {
- }
-
- // Return the "traces.txt" file, or null if there is no such file
- private static File getTracesFile() {
- // Check most common location first.
- File tracesFile = new File("/data/anr/traces.txt");
- if (tracesFile.isFile() && tracesFile.canRead()) {
- return tracesFile;
- }
-
- // Find the traces file name if we can.
- try {
- // getprop [prop-name [default-value]]
- Process propProc = (new ProcessBuilder())
- .command("/system/bin/getprop", "dalvik.vm.stack-trace-file")
- .redirectErrorStream(true)
- .start();
- try {
- BufferedReader buf = new BufferedReader(
- new InputStreamReader(propProc.getInputStream()), TRACES_LINE_SIZE);
- String propVal = buf.readLine();
- if (DEBUG) {
- Log.d(LOGTAG, "getprop returned " + String.valueOf(propVal));
- }
- // getprop can return empty string when the prop value is empty
- // or prop is undefined, treat both cases the same way
- if (propVal != null && propVal.length() != 0) {
- tracesFile = new File(propVal);
- if (tracesFile.isFile() && tracesFile.canRead()) {
- return tracesFile;
- } else if (DEBUG) {
- Log.d(LOGTAG, "cannot access traces file");
- }
- } else if (DEBUG) {
- Log.d(LOGTAG, "empty getprop result");
- }
- } finally {
- propProc.destroy();
- }
- } catch (IOException e) {
- Log.w(LOGTAG, e);
- } catch (ClassCastException e) {
- Log.w(LOGTAG, e); // Bug 975436
- }
- return null;
- }
-
- private static File getPingFile() {
- if (GeckoAppShell.getContext() == null) {
- return null;
- }
- GeckoProfile profile = GeckoAppShell.getGeckoInterface().getProfile();
- if (profile == null) {
- return null;
- }
- File profDir = profile.getDir();
- if (profDir == null) {
- return null;
- }
- File pingDir = new File(profDir, "saved-telemetry-pings");
- pingDir.mkdirs();
- if (!(pingDir.exists() && pingDir.isDirectory())) {
- return null;
- }
- return new File(pingDir, UUID.randomUUID().toString());
- }
-
- // Return true if the traces file corresponds to a Gecko ANR
- private static boolean isGeckoTraces(String pkgName, File tracesFile) {
- try {
- final String END_OF_PACKAGE_NAME = "([^a-zA-Z0-9_]|$)";
- // Regex for finding our package name in the traces file
- Pattern pkgPattern = Pattern.compile(Pattern.quote(pkgName) + END_OF_PACKAGE_NAME);
- Pattern mangledPattern = null;
- if (!AppConstants.MANGLED_ANDROID_PACKAGE_NAME.equals(pkgName)) {
- mangledPattern = Pattern.compile(Pattern.quote(
- AppConstants.MANGLED_ANDROID_PACKAGE_NAME) + END_OF_PACKAGE_NAME);
- }
- if (DEBUG) {
- Log.d(LOGTAG, "trying to match package: " + pkgName);
- }
- BufferedReader traces = new BufferedReader(
- new FileReader(tracesFile), TRACES_BLOCK_SIZE);
- try {
- for (int count = 0; count < LINES_TO_IDENTIFY_TRACES; count++) {
- String line = traces.readLine();
- if (DEBUG) {
- Log.d(LOGTAG, "identifying line: " + String.valueOf(line));
- }
- if (line == null) {
- if (DEBUG) {
- Log.d(LOGTAG, "reached end of traces file");
- }
- return false;
- }
- if (pkgPattern.matcher(line).find()) {
- // traces.txt file contains our package
- return true;
- }
- if (mangledPattern != null && mangledPattern.matcher(line).find()) {
- // traces.txt file contains our alternate package
- return true;
- }
- }
- } finally {
- traces.close();
- }
- } catch (IOException e) {
- // meh, can't even read from it right. just return false
- }
- return false;
- }
-
- private static long getUptimeMins() {
-
- long uptimeMins = (new File("/proc/self/stat")).lastModified();
- if (uptimeMins != 0L) {
- uptimeMins = (System.currentTimeMillis() - uptimeMins) / 1000L / 60L;
- if (DEBUG) {
- Log.d(LOGTAG, "uptime " + String.valueOf(uptimeMins));
- }
- return uptimeMins;
- }
- if (DEBUG) {
- Log.d(LOGTAG, "could not get uptime");
- }
- return 0L;
- }
-
- /*
- a saved telemetry ping file consists of JSON in the following format,
- {
- "reason": "android-anr-report",
- "slug": "<uuid-string>",
- "payload": <json-object>
- }
- for Android ANR, our JSON payload should look like,
- {
- "ver": 1,
- "simpleMeasurements": {
- "uptime": <uptime>
- },
- "info": {
- "reason": "android-anr-report",
- "OS": "Android",
- ...
- },
- "androidANR": "...",
- "androidLogcat": "..."
- }
- */
-
- private static int writePingPayload(OutputStream ping,
- String payload) throws IOException {
- byte [] data = payload.getBytes(PING_CHARSET);
- ping.write(data);
- return data.length;
- }
-
- private static void fillPingHeader(OutputStream ping, String slug)
- throws IOException {
-
- // ping file header
- byte [] data = ("{" +
- "\"reason\":\"android-anr-report\"," +
- "\"slug\":" + JSONObject.quote(slug) + "," +
- "\"payload\":").getBytes(PING_CHARSET);
- ping.write(data);
- if (DEBUG) {
- Log.d(LOGTAG, "wrote ping header, size = " + String.valueOf(data.length));
- }
-
- // payload start
- int size = writePingPayload(ping, ("{" +
- "\"ver\":1," +
- "\"simpleMeasurements\":{" +
- "\"uptime\":" + String.valueOf(getUptimeMins()) +
- "}," +
- "\"info\":{" +
- "\"reason\":\"android-anr-report\"," +
- "\"OS\":" + JSONObject.quote(SysInfo.getName()) + "," +
- "\"version\":\"" + String.valueOf(SysInfo.getVersion()) + "\"," +
- "\"appID\":" + JSONObject.quote(AppConstants.MOZ_APP_ID) + "," +
- "\"appVersion\":" + JSONObject.quote(AppConstants.MOZ_APP_VERSION) + "," +
- "\"appName\":" + JSONObject.quote(AppConstants.MOZ_APP_BASENAME) + "," +
- "\"appBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," +
- "\"appUpdateChannel\":" + JSONObject.quote(AppConstants.MOZ_UPDATE_CHANNEL) + "," +
- // Technically the platform build ID may be different, but we'll never know
- "\"platformBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," +
- "\"locale\":" + JSONObject.quote(Locales.getLanguageTag(Locale.getDefault())) + "," +
- "\"cpucount\":" + String.valueOf(SysInfo.getCPUCount()) + "," +
- "\"memsize\":" + String.valueOf(SysInfo.getMemSize()) + "," +
- "\"arch\":" + JSONObject.quote(SysInfo.getArchABI()) + "," +
- "\"kernel_version\":" + JSONObject.quote(SysInfo.getKernelVersion()) + "," +
- "\"device\":" + JSONObject.quote(SysInfo.getDevice()) + "," +
- "\"manufacturer\":" + JSONObject.quote(SysInfo.getManufacturer()) + "," +
- "\"hardware\":" + JSONObject.quote(SysInfo.getHardware()) +
- "}," +
- "\"androidANR\":\""));
- if (DEBUG) {
- Log.d(LOGTAG, "wrote metadata, size = " + String.valueOf(size));
- }
-
- // We are at the start of ANR data
- }
-
- // Block is a section of the larger input stream, and we want to find pattern within
- // the stream. This is straightforward if the entire pattern is within one block;
- // however, if the pattern spans across two blocks, we have to match both the start of
- // the pattern in the first block and the end of the pattern in the second block.
- // * If pattern is found in block, this method returns the index at the end of the
- // found pattern, which must always be > 0.
- // * If pattern is not found, it returns 0.
- // * If the start of the pattern matches the end of the block, it returns a number
- // < 0, which equals the negated value of how many characters in pattern are already
- // matched; when processing the next block, this number is passed in through
- // prevIndex, and the rest of the characters in pattern are matched against the
- // start of this second block. The method returns value > 0 if the rest of the
- // characters match, or 0 if they do not.
- private static int getEndPatternIndex(String block, String pattern, int prevIndex) {
- if (pattern == null || block.length() < pattern.length()) {
- // Nothing to do
- return 0;
- }
- if (prevIndex < 0) {
- // Last block ended with a partial start; now match start of block to rest of pattern
- if (block.startsWith(pattern.substring(-prevIndex, pattern.length()))) {
- // Rest of pattern matches; return index at end of pattern
- return pattern.length() + prevIndex;
- }
- // Not a match; continue with normal search
- }
- // Did not find pattern in last block; see if entire pattern is inside this block
- int index = block.indexOf(pattern);
- if (index >= 0) {
- // Found pattern; return index at end of the pattern
- return index + pattern.length();
- }
- // Block does not contain the entire pattern, but see if the end of the block
- // contains the start of pattern. To do that, we see if block ends with the
- // first n-1 characters of pattern, the first n-2 characters of pattern, etc.
- for (index = block.length() - pattern.length() + 1; index < block.length(); index++) {
- // Using index as a start, see if the rest of block contains the start of pattern
- if (block.charAt(index) == pattern.charAt(0) &&
- block.endsWith(pattern.substring(0, block.length() - index))) {
- // Found partial match; return -(number of characters matched),
- // i.e. -1 for 1 character matched, -2 for 2 characters matched, etc.
- return index - block.length();
- }
- }
- return 0;
- }
-
- // Copy the content of reader to ping;
- // copying stops when endPattern is found in the input stream
- private static int fillPingBlock(OutputStream ping,
- Reader reader, String endPattern)
- throws IOException {
-
- int total = 0;
- int endIndex = 0;
- char [] block = new char[TRACES_BLOCK_SIZE];
- for (int size = reader.read(block); size >= 0; size = reader.read(block)) {
- String stringBlock = new String(block, 0, size);
- endIndex = getEndPatternIndex(stringBlock, endPattern, endIndex);
- if (endIndex > 0) {
- // Found end pattern; clip the string
- stringBlock = stringBlock.substring(0, endIndex);
- }
- String quoted = JSONObject.quote(stringBlock);
- total += writePingPayload(ping, quoted.substring(1, quoted.length() - 1));
- if (endIndex > 0) {
- // End pattern already found; return now
- break;
- }
- }
- return total;
- }
-
- private static void fillLogcat(final OutputStream ping) {
- if (Versions.preJB) {
- // Logcat retrieval is not supported on pre-JB devices.
- return;
- }
-
- try {
- // get the last 200 lines of logcat
- Process proc = (new ProcessBuilder())
- .command("/system/bin/logcat", "-v", "threadtime", "-t", "200", "-d", "*:D")
- .redirectErrorStream(true)
- .start();
- try {
- Reader procOut = new InputStreamReader(proc.getInputStream(), TRACES_CHARSET);
- int size = fillPingBlock(ping, procOut, null);
- if (DEBUG) {
- Log.d(LOGTAG, "wrote logcat, size = " + String.valueOf(size));
- }
- } finally {
- proc.destroy();
- }
- } catch (IOException e) {
- // ignore because logcat is not essential
- Log.w(LOGTAG, e);
- }
- }
-
- private static void fillPingFooter(OutputStream ping,
- boolean haveNativeStack)
- throws IOException {
-
- // We are at the end of ANR data
-
- int total = writePingPayload(ping, ("\"," +
- "\"androidLogcat\":\""));
- fillLogcat(ping);
-
- if (haveNativeStack) {
- total += writePingPayload(ping, ("\"," +
- "\"androidNativeStack\":"));
-
- String nativeStack = String.valueOf(getNativeStack());
- int size = writePingPayload(ping, nativeStack);
- if (DEBUG) {
- Log.d(LOGTAG, "wrote native stack, size = " + String.valueOf(size));
- }
- total += size + writePingPayload(ping, "}");
- } else {
- total += writePingPayload(ping, "\"}");
- }
-
- byte [] data = (
- "}").getBytes(PING_CHARSET);
- ping.write(data);
- if (DEBUG) {
- Log.d(LOGTAG, "wrote ping footer, size = " + String.valueOf(data.length + total));
- }
- }
-
- private static void processTraces(Reader traces, File pingFile) {
-
- // Only get native stack if Gecko is running.
- // Also, unwinding is memory intensive, so only unwind if we have enough memory.
- final boolean haveNativeStack =
- GeckoThread.isRunning() ?
- requestNativeStack(/* unwind */ SysInfo.getMemSize() >= 640) : false;
-
- try {
- OutputStream ping = new BufferedOutputStream(
- new FileOutputStream(pingFile), TRACES_BLOCK_SIZE);
- try {
- fillPingHeader(ping, pingFile.getName());
- // Traces file has the format
- // ----- pid xxx at xxx -----
- // Cmd line: org.mozilla.xxx
- // * stack trace *
- // ----- end xxx -----
- // ----- pid xxx at xxx -----
- // Cmd line: com.android.xxx
- // * stack trace *
- // ...
- // If we end the stack dump at the first end marker,
- // only Fennec stacks will be dumped
- int size = fillPingBlock(ping, traces, "\n----- end");
- if (DEBUG) {
- Log.d(LOGTAG, "wrote traces, size = " + String.valueOf(size));
- }
- fillPingFooter(ping, haveNativeStack);
- if (DEBUG) {
- Log.d(LOGTAG, "finished creating ping file");
- }
- return;
- } finally {
- ping.close();
- if (haveNativeStack) {
- releaseNativeStack();
- }
- }
- } catch (IOException e) {
- Log.w(LOGTAG, e);
- }
- // exception; delete ping file
- if (pingFile.exists()) {
- pingFile.delete();
- }
- }
-
- private static void processTraces(File tracesFile, File pingFile) {
- try {
- Reader traces = new InputStreamReader(
- new FileInputStream(tracesFile), TRACES_CHARSET);
- try {
- processTraces(traces, pingFile);
- } finally {
- traces.close();
- }
- } catch (IOException e) {
- Log.w(LOGTAG, e);
- }
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (mPendingANR) {
- // we already processed an ANR without getting unstuck; skip this one
- if (DEBUG) {
- Log.d(LOGTAG, "skipping duplicate ANR");
- }
- return;
- }
- if (ThreadUtils.getUiHandler() != null) {
- mPendingANR = true;
- // detect when the main thread gets unstuck
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- // okay to reset mPendingANR on main thread
- mPendingANR = false;
- if (DEBUG) {
- Log.d(LOGTAG, "yay we got unstuck!");
- }
- }
- });
- }
- if (DEBUG) {
- Log.d(LOGTAG, "receiving " + String.valueOf(intent));
- }
- if (!ANR_ACTION.equals(intent.getAction())) {
- return;
- }
-
- // make sure we have a good save location first
- File pingFile = getPingFile();
- if (DEBUG) {
- Log.d(LOGTAG, "using ping file: " + String.valueOf(pingFile));
- }
- if (pingFile == null) {
- return;
- }
-
- File tracesFile = getTracesFile();
- if (DEBUG) {
- Log.d(LOGTAG, "using traces file: " + String.valueOf(tracesFile));
- }
- if (tracesFile == null) {
- return;
- }
-
- // We get ANR intents from all ANRs in the system, but we only want Gecko ANRs
- if (!isGeckoTraces(context.getPackageName(), tracesFile)) {
- if (DEBUG) {
- Log.d(LOGTAG, "traces is not Gecko ANR");
- }
- return;
- }
- Log.i(LOGTAG, "processing Gecko ANR");
- processTraces(tracesFile, pingFile);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/AboutPages.java b/mobile/android/base/java/org/mozilla/gecko/AboutPages.java
deleted file mode 100644
index 705d700af..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/AboutPages.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.home.HomeConfig;
-import org.mozilla.gecko.home.HomeConfig.PanelType;
-import org.mozilla.gecko.util.StringUtils;
-
-public class AboutPages {
- // All of our special pages.
- public static final String ACCOUNTS = "about:accounts";
- public static final String ADDONS = "about:addons";
- public static final String CONFIG = "about:config";
- public static final String DOWNLOADS = "about:downloads";
- public static final String FIREFOX = "about:firefox";
- public static final String HEALTHREPORT = "about:healthreport";
- public static final String HOME = "about:home";
- public static final String LOGINS = "about:logins";
- public static final String PRIVATEBROWSING = "about:privatebrowsing";
- public static final String READER = "about:reader";
- public static final String UPDATER = "about:";
-
- public static final String URL_FILTER = "about:%";
-
- public static final String PANEL_PARAM = "panel";
-
- public static final boolean isAboutPage(final String url) {
- return url != null && url.startsWith("about:");
- }
-
- public static final boolean isTitlelessAboutPage(final String url) {
- return isAboutHome(url) ||
- PRIVATEBROWSING.equals(url);
- }
-
- public static final boolean isAboutHome(final String url) {
- if (url == null || !url.startsWith(HOME)) {
- return false;
- }
- // We sometimes append a parameter to "about:home" to specify which page to
- // show when we open the home pager. Discard this parameter when checking
- // whether or not this URL is "about:home".
- return HOME.equals(url.split("\\?")[0]);
- }
-
- public static final String getPanelIdFromAboutHomeUrl(String aboutHomeUrl) {
- return StringUtils.getQueryParameter(aboutHomeUrl, PANEL_PARAM);
- }
-
- public static boolean isAboutReader(final String url) {
- return isAboutPage(READER, url);
- }
-
- public static boolean isAboutConfig(final String url) {
- return isAboutPage(CONFIG, url);
- }
-
- public static boolean isAboutAddons(final String url) {
- return isAboutPage(ADDONS, url);
- }
-
- public static boolean isAboutPrivateBrowsing(final String url) {
- return isAboutPage(PRIVATEBROWSING, url);
- }
-
- public static boolean isAboutPage(String page, String url) {
- return url != null && url.toLowerCase().startsWith(page);
-
- }
-
- public static final String[] DEFAULT_ICON_PAGES = new String[] {
- HOME,
- ACCOUNTS,
- ADDONS,
- CONFIG,
- DOWNLOADS,
- FIREFOX,
- HEALTHREPORT,
- UPDATER
- };
-
- public static boolean isBuiltinIconPage(final String url) {
- if (url == null ||
- !url.startsWith("about:")) {
- return false;
- }
-
- // about:home uses a separate search built-in icon.
- if (isAboutHome(url)) {
- return true;
- }
-
- // TODO: it'd be quicker to not compare the "about:" part every time.
- for (int i = 0; i < DEFAULT_ICON_PAGES.length; ++i) {
- if (DEFAULT_ICON_PAGES[i].equals(url)) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Get a URL that navigates to the specified built-in Home Panel.
- *
- * @param panelType to navigate to.
- * @return URL.
- * @throws IllegalArgumentException if the built-in panel type is not a built-in panel.
- */
- @RobocopTarget
- public static String getURLForBuiltinPanelType(PanelType panelType) throws IllegalArgumentException {
- return HOME + "?panel=" + HomeConfig.getIdForBuiltinPanelType(panelType);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/AccountsHelper.java b/mobile/android/base/java/org/mozilla/gecko/AccountsHelper.java
deleted file mode 100644
index 5892c16b6..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/AccountsHelper.java
+++ /dev/null
@@ -1,318 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.accounts.Account;
-import android.accounts.AccountManager;
-import android.accounts.AccountManagerCallback;
-import android.accounts.AccountManagerFuture;
-import android.accounts.AuthenticatorException;
-import android.accounts.OperationCanceledException;
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.background.fxa.FxAccountUtils;
-import org.mozilla.gecko.fxa.FirefoxAccounts;
-import org.mozilla.gecko.fxa.FxAccountConstants;
-import org.mozilla.gecko.fxa.FxAccountDeviceRegistrator;
-import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
-import org.mozilla.gecko.fxa.login.Engaged;
-import org.mozilla.gecko.fxa.login.State;
-import org.mozilla.gecko.restrictions.Restrictable;
-import org.mozilla.gecko.restrictions.Restrictions;
-import org.mozilla.gecko.sync.SyncConfiguration;
-import org.mozilla.gecko.sync.Utils;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.net.URISyntaxException;
-import java.security.GeneralSecurityException;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Helper class to manage Android Accounts corresponding to Firefox Accounts.
- */
-public class AccountsHelper implements NativeEventListener {
- public static final String LOGTAG = "GeckoAccounts";
-
- protected final Context mContext;
- protected final GeckoProfile mProfile;
-
- public AccountsHelper(Context context, GeckoProfile profile) {
- mContext = context;
- mProfile = profile;
-
- EventDispatcher dispatcher = GeckoApp.getEventDispatcher();
- if (dispatcher == null) {
- Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException());
- return;
- }
- dispatcher.registerGeckoThreadListener(this,
- "Accounts:CreateFirefoxAccountFromJSON",
- "Accounts:UpdateFirefoxAccountFromJSON",
- "Accounts:Create",
- "Accounts:DeleteFirefoxAccount",
- "Accounts:Exist",
- "Accounts:ProfileUpdated",
- "Accounts:ShowSyncPreferences");
- }
-
- public synchronized void uninit() {
- EventDispatcher dispatcher = GeckoApp.getEventDispatcher();
- if (dispatcher == null) {
- Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException());
- return;
- }
- dispatcher.unregisterGeckoThreadListener(this,
- "Accounts:CreateFirefoxAccountFromJSON",
- "Accounts:UpdateFirefoxAccountFromJSON",
- "Accounts:Create",
- "Accounts:DeleteFirefoxAccount",
- "Accounts:Exist",
- "Accounts:ProfileUpdated",
- "Accounts:ShowSyncPreferences");
- }
-
- @Override
- public void handleMessage(String event, NativeJSObject message, final EventCallback callback) {
- if (!Restrictions.isAllowed(mContext, Restrictable.MODIFY_ACCOUNTS)) {
- // We register for messages in all contexts; we drop, with a log and an error to JavaScript,
- // when the profile is restricted. It's better to return errors than silently ignore messages.
- Log.e(LOGTAG, "Profile is not allowed to modify accounts! Ignoring event: " + event);
- if (callback != null) {
- callback.sendError("Profile is not allowed to modify accounts!");
- }
- return;
- }
-
- if ("Accounts:CreateFirefoxAccountFromJSON".equals(event)) {
- // As we are about to create a new account, let's ensure our in-memory accounts cache
- // is empty so that there are no undesired side-effects.
- AndroidFxAccount.invalidateCaches();
-
- AndroidFxAccount fxAccount = null;
- try {
- final NativeJSObject json = message.getObject("json");
- final String email = json.getString("email");
- final String uid = json.getString("uid");
- final boolean verified = json.optBoolean("verified", false);
- final byte[] unwrapkB = Utils.hex2Byte(json.getString("unwrapBKey"));
- final byte[] sessionToken = Utils.hex2Byte(json.getString("sessionToken"));
- final byte[] keyFetchToken = Utils.hex2Byte(json.getString("keyFetchToken"));
- final String authServerEndpoint =
- json.optString("authServerEndpoint", FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT);
- final String tokenServerEndpoint =
- json.optString("tokenServerEndpoint", FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT);
- final String profileServerEndpoint =
- json.optString("profileServerEndpoint", FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT);
- // TODO: handle choose what to Sync.
- State state = new Engaged(email, uid, verified, unwrapkB, sessionToken, keyFetchToken);
- fxAccount = AndroidFxAccount.addAndroidAccount(mContext,
- email,
- mProfile.getName(),
- authServerEndpoint,
- tokenServerEndpoint,
- profileServerEndpoint,
- state,
- AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP);
-
- final String[] declinedSyncEngines = json.optStringArray("declinedSyncEngines", null);
- if (declinedSyncEngines != null) {
- Log.i(LOGTAG, "User has selected engines; storing to prefs.");
- final Map<String, Boolean> selectedEngines = new HashMap<String, Boolean>();
- for (String enabledSyncEngine : SyncConfiguration.validEngineNames()) {
- selectedEngines.put(enabledSyncEngine, true);
- }
- for (String declinedSyncEngine : declinedSyncEngines) {
- selectedEngines.put(declinedSyncEngine, false);
- }
- // The "forms" engine has the same state as the "history" engine.
- selectedEngines.put("forms", selectedEngines.get("history"));
- FxAccountUtils.pii(LOGTAG, "User selected engines: " + selectedEngines.toString());
- try {
- SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), selectedEngines);
- } catch (UnsupportedEncodingException | GeneralSecurityException e) {
- Log.e(LOGTAG, "Got exception storing selected engines; ignoring.", e);
- }
- }
- } catch (URISyntaxException | GeneralSecurityException | UnsupportedEncodingException e) {
- Log.w(LOGTAG, "Got exception creating Firefox Account from JSON; ignoring.", e);
- if (callback != null) {
- callback.sendError("Could not create Firefox Account from JSON: " + e.toString());
- return;
- }
- }
- if (callback != null) {
- callback.sendSuccess(fxAccount != null);
- }
-
- } else if ("Accounts:UpdateFirefoxAccountFromJSON".equals(event)) {
- // We might be significantly changing state of the account; let's ensure our in-memory
- // accounts cache is empty so that there are no undesired side-effects.
- AndroidFxAccount.invalidateCaches();
-
- try {
- final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
- if (account == null) {
- if (callback != null) {
- callback.sendError("Could not update Firefox Account since none exists");
- }
- return;
- }
-
- final NativeJSObject json = message.getObject("json");
- final String email = json.getString("email");
- final String uid = json.getString("uid");
-
- // Protect against cross-connecting accounts.
- if (account.name == null || !account.name.equals(email)) {
- final String errorMessage = "Cannot update Firefox Account from JSON: datum has different email address!";
- Log.e(LOGTAG, errorMessage);
- if (callback != null) {
- callback.sendError(errorMessage);
- }
- return;
- }
-
- final boolean verified = json.optBoolean("verified", false);
- final byte[] unwrapkB = Utils.hex2Byte(json.getString("unwrapBKey"));
- final byte[] sessionToken = Utils.hex2Byte(json.getString("sessionToken"));
- final byte[] keyFetchToken = Utils.hex2Byte(json.getString("keyFetchToken"));
- final State state = new Engaged(email, uid, verified, unwrapkB, sessionToken, keyFetchToken);
-
- final AndroidFxAccount fxAccount = new AndroidFxAccount(mContext, account);
- fxAccount.setState(state);
-
- if (callback != null) {
- callback.sendSuccess(true);
- }
- } catch (NativeJSObject.InvalidPropertyException e) {
- Log.w(LOGTAG, "Got exception updating Firefox Account from JSON; ignoring.", e);
- if (callback != null) {
- callback.sendError("Could not update Firefox Account from JSON: " + e.toString());
- return;
- }
- }
-
- } else if ("Accounts:Create".equals(event)) {
- // Do exactly the same thing as if you tapped 'Sync' in Settings.
- final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- final NativeJSObject extras = message.optObject("extras", null);
- if (extras != null) {
- intent.putExtra("extras", extras.toString());
- }
- mContext.startActivity(intent);
-
- } else if ("Accounts:DeleteFirefoxAccount".equals(event)) {
- try {
- final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
- if (account == null) {
- Log.w(LOGTAG, "Could not delete Firefox Account since none exists!");
- if (callback != null) {
- callback.sendError("Could not delete Firefox Account since none exists");
- }
- return;
- }
-
- final AccountManagerCallback<Boolean> accountManagerCallback = new AccountManagerCallback<Boolean>() {
- @Override
- public void run(AccountManagerFuture<Boolean> future) {
- try {
- final boolean result = future.getResult();
- Log.i(LOGTAG, "Account named like " + Utils.obfuscateEmail(account.name) + " removed: " + result);
- if (callback != null) {
- callback.sendSuccess(result);
- }
- } catch (OperationCanceledException | IOException | AuthenticatorException e) {
- if (callback != null) {
- callback.sendError("Could not delete Firefox Account: " + e.toString());
- }
- }
- }
- };
-
- AccountManager.get(mContext).removeAccount(account, accountManagerCallback, null);
- } catch (Exception e) {
- Log.w(LOGTAG, "Got exception updating Firefox Account from JSON; ignoring.", e);
- if (callback != null) {
- callback.sendError("Could not update Firefox Account from JSON: " + e.toString());
- return;
- }
- }
-
- } else if ("Accounts:Exist".equals(event)) {
- if (callback == null) {
- Log.w(LOGTAG, "Accounts:Exist requires a callback");
- return;
- }
-
- final String kind = message.optString("kind", null);
- final JSONObject response = new JSONObject();
-
- try {
- if ("any".equals(kind)) {
- response.put("exists", FirefoxAccounts.firefoxAccountsExist(mContext));
- callback.sendSuccess(response);
- } else if ("fxa".equals(kind)) {
- final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
- response.put("exists", account != null);
- if (account != null) {
- response.put("email", account.name);
- // We should always be able to extract the server endpoints.
- final AndroidFxAccount fxAccount = new AndroidFxAccount(mContext, account);
- response.put("authServerEndpoint", fxAccount.getAccountServerURI());
- response.put("profileServerEndpoint", fxAccount.getProfileServerURI());
- response.put("tokenServerEndpoint", fxAccount.getTokenServerURI());
- try {
- // It is possible for the state fetch to fail and us to not be able to provide a UID.
- // Long term, the UID (and verification flag) will be attached to the Android account
- // user data and not the internal state representation.
- final State state = fxAccount.getState();
- response.put("uid", state.uid);
- } catch (Exception e) {
- Log.w(LOGTAG, "Got exception extracting account UID; ignoring.", e);
- }
- }
-
- callback.sendSuccess(response);
- } else {
- callback.sendError("Could not query account existence: unknown kind.");
- }
- } catch (JSONException e) {
- Log.w(LOGTAG, "Got exception querying account existence; ignoring.", e);
- callback.sendError("Could not query account existence: " + e.toString());
- return;
- }
- } else if ("Accounts:ProfileUpdated".equals(event)) {
- final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
- if (account == null) {
- Log.w(LOGTAG, "Can't change profile of non-existent Firefox Account!; ignored");
- return;
- }
- final AndroidFxAccount androidFxAccount = new AndroidFxAccount(mContext, account);
- androidFxAccount.fetchProfileJSON();
- } else if ("Accounts:ShowSyncPreferences".equals(event)) {
- final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
- if (account == null) {
- Log.w(LOGTAG, "Can't change show Sync preferences of non-existent Firefox Account!; ignored");
- return;
- }
- // We don't necessarily have an Activity context here, so we always start in a new task.
- final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_STATUS);
- intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- mContext.startActivity(intent);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java b/mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java
deleted file mode 100644
index 7f2eb219e..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java
+++ /dev/null
@@ -1,256 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.menu.GeckoMenu;
-import org.mozilla.gecko.menu.GeckoMenuItem;
-import org.mozilla.gecko.util.ResourceDrawableUtils;
-import org.mozilla.gecko.text.TextSelection;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.ActionModeCompat.Callback;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.view.MenuItem;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.Timer;
-import java.util.TimerTask;
-
-import android.util.Log;
-
-class ActionBarTextSelection implements TextSelection, GeckoEventListener {
- private static final String LOGTAG = "GeckoTextSelection";
- private static final int SHUTDOWN_DELAY_MS = 250;
-
- private final Context context;
-
- private boolean mDraggingHandles;
-
- private String selectionID; // Unique ID provided for each selection action.
-
- private String mCurrentItems;
-
- private TextSelectionActionModeCallback mCallback;
-
- // These timers are used to avoid flicker caused by selection handles showing/hiding quickly.
- // For instance when moving between single handle caret mode and two handle selection mode.
- private final Timer mActionModeTimer = new Timer("actionMode");
- private class ActionModeTimerTask extends TimerTask {
- @Override
- public void run() {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- endActionMode();
- }
- });
- }
- };
- private ActionModeTimerTask mActionModeTimerTask;
-
- ActionBarTextSelection(Context context) {
- this.context = context;
- }
-
- @Override
- public void create() {
- // Only register listeners if we have valid start/middle/end handles
- if (context == null) {
- Log.e(LOGTAG, "Failed to initialize text selection because at least one context is null");
- } else {
- GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
- "TextSelection:ActionbarInit",
- "TextSelection:ActionbarStatus",
- "TextSelection:ActionbarUninit",
- "TextSelection:Update");
- }
- }
-
- @Override
- public boolean dismiss() {
- // We do not call endActionMode() here because this is already handled by the activity.
- return false;
- }
-
- @Override
- public void destroy() {
- if (context == null) {
- Log.e(LOGTAG, "Do not unregister TextSelection:* listeners since context is null");
- } else {
- GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
- "TextSelection:ActionbarInit",
- "TextSelection:ActionbarStatus",
- "TextSelection:ActionbarUninit",
- "TextSelection:Update");
- }
- }
-
- @Override
- public void handleMessage(final String event, final JSONObject message) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- try {
- if (event.equals("TextSelection:Update")) {
- if (mActionModeTimerTask != null)
- mActionModeTimerTask.cancel();
- showActionMode(message.getJSONArray("actions"));
- } else if (event.equals("TextSelection:ActionbarInit")) {
- // Init / Open the action bar. Note the current selectionID,
- // cancel any pending actionBar close.
- Telemetry.sendUIEvent(TelemetryContract.Event.SHOW,
- TelemetryContract.Method.CONTENT, "text_selection");
-
- selectionID = message.getString("selectionID");
- mCurrentItems = null;
- if (mActionModeTimerTask != null) {
- mActionModeTimerTask.cancel();
- }
-
- } else if (event.equals("TextSelection:ActionbarStatus")) {
- // Ensure async updates from SearchService for example are valid.
- if (selectionID != message.optString("selectionID")) {
- return;
- }
-
- // Update the actionBar actions as provided by Gecko.
- showActionMode(message.getJSONArray("actions"));
-
- } else if (event.equals("TextSelection:ActionbarUninit")) {
- // Uninit the actionbar. Schedule a cancellable close
- // action to avoid UI jank. (During SelectionAll for ex).
- mCurrentItems = null;
- mActionModeTimerTask = new ActionModeTimerTask();
- mActionModeTimer.schedule(mActionModeTimerTask, SHUTDOWN_DELAY_MS);
- }
-
- } catch (JSONException e) {
- Log.e(LOGTAG, "JSON exception", e);
- }
- }
- });
- }
-
- private void showActionMode(final JSONArray items) {
- String itemsString = items.toString();
- if (itemsString.equals(mCurrentItems)) {
- return;
- }
- mCurrentItems = itemsString;
-
- if (mCallback != null) {
- mCallback.updateItems(items);
- return;
- }
-
- if (context instanceof ActionModeCompat.Presenter) {
- final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context;
- mCallback = new TextSelectionActionModeCallback(items);
- presenter.startActionModeCompat(mCallback);
- mCallback.animateIn();
- }
- }
-
- private void endActionMode() {
- if (context instanceof ActionModeCompat.Presenter) {
- final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context;
- presenter.endActionModeCompat();
- }
- mCurrentItems = null;
- }
-
- private class TextSelectionActionModeCallback implements Callback {
- private JSONArray mItems;
- private ActionModeCompat mActionMode;
-
- public TextSelectionActionModeCallback(JSONArray items) {
- mItems = items;
- }
-
- public void updateItems(JSONArray items) {
- mItems = items;
- if (mActionMode != null) {
- mActionMode.invalidate();
- }
- }
-
- public void animateIn() {
- if (mActionMode != null) {
- mActionMode.animateIn();
- }
- }
-
- @Override
- public boolean onPrepareActionMode(final ActionModeCompat mode, final GeckoMenu menu) {
- // Android would normally expect us to only update the state of menu items here
- // To make the js-java interaction a bit simpler, we just wipe out the menu here and recreate all
- // the javascript menu items in onPrepare instead. This will be called any time invalidate() is called on the
- // action mode.
- menu.clear();
-
- int length = mItems.length();
- for (int i = 0; i < length; i++) {
- try {
- final JSONObject obj = mItems.getJSONObject(i);
- final GeckoMenuItem menuitem = (GeckoMenuItem) menu.add(0, i, 0, obj.optString("label"));
- final int actionEnum = obj.optBoolean("showAsAction") ? GeckoMenuItem.SHOW_AS_ACTION_ALWAYS : GeckoMenuItem.SHOW_AS_ACTION_NEVER;
- menuitem.setShowAsAction(actionEnum, R.attr.menuItemActionModeStyle);
-
- final String iconString = obj.optString("icon");
- ResourceDrawableUtils.getDrawable(context, iconString, new ResourceDrawableUtils.BitmapLoader() {
- @Override
- public void onBitmapFound(Drawable d) {
- if (d != null) {
- menuitem.setIcon(d);
- }
- }
- });
- } catch (Exception ex) {
- Log.i(LOGTAG, "Exception building menu", ex);
- }
- }
- return true;
- }
-
- @Override
- public boolean onCreateActionMode(ActionModeCompat mode, GeckoMenu unused) {
- mActionMode = mode;
- return true;
- }
-
- @Override
- public boolean onActionItemClicked(ActionModeCompat mode, MenuItem item) {
- try {
- final JSONObject obj = mItems.getJSONObject(item.getItemId());
- GeckoAppShell.notifyObservers("TextSelection:Action", obj.optString("id"));
- return true;
- } catch (Exception ex) {
- Log.i(LOGTAG, "Exception calling action", ex);
- }
- return false;
- }
-
- // Called when the user exits the action mode
- @Override
- public void onDestroyActionMode(ActionModeCompat mode) {
- mActionMode = null;
- mCallback = null;
- final JSONObject args = new JSONObject();
- try {
- args.put("selectionID", selectionID);
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error building JSON arguments for TextSelection:End", e);
- return;
- }
-
- GeckoAppShell.notifyObservers("TextSelection:End", args.toString());
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/ActionModeCompat.java b/mobile/android/base/java/org/mozilla/gecko/ActionModeCompat.java
deleted file mode 100644
index 709c0056f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/ActionModeCompat.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.menu.GeckoMenu;
-import org.mozilla.gecko.menu.GeckoMenuItem;
-import org.mozilla.gecko.widget.GeckoPopupMenu;
-
-import android.view.Gravity;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.widget.Toast;
-
-class ActionModeCompat implements GeckoPopupMenu.OnMenuItemClickListener,
- GeckoPopupMenu.OnMenuItemLongClickListener,
- View.OnClickListener {
- private final String LOGTAG = "GeckoActionModeCompat";
-
- private final Callback mCallback;
- private final ActionModeCompatView mView;
- private final Presenter mPresenter;
-
- /* A set of callbacks to be called during this ActionMode's lifecycle. These will control the
- * creation, interaction with, and destruction of menuitems for the view */
- public static interface Callback {
- /* Called when action mode is first created. Implementors should use this to inflate menu resources. */
- public boolean onCreateActionMode(ActionModeCompat mode, GeckoMenu menu);
-
- /* Called to refresh an action mode's action menu. Called whenever the mode is invalidated. Implementors
- * should use this to enable/disable/show/hide menu items. */
- public boolean onPrepareActionMode(ActionModeCompat mode, GeckoMenu menu);
-
- /* Called to report a user click on an action button. */
- public boolean onActionItemClicked(ActionModeCompat mode, MenuItem item);
-
- /* Called when an action mode is about to be exited and destroyed. */
- public void onDestroyActionMode(ActionModeCompat mode);
- }
-
- /* Presenters handle the actual showing/hiding of the action mode UI in the app. Its their responsibility
- * to create an action mode, and assign it Callbacks and ActionModeCompatView's. */
- public static interface Presenter {
- /* Called when an action mode should be shown */
- public void startActionModeCompat(final Callback callback);
-
- /* Called when whatever action mode is showing should be hidden */
- public void endActionModeCompat();
- }
-
- public ActionModeCompat(Presenter presenter, Callback callback, ActionModeCompatView view) {
- mPresenter = presenter;
- mCallback = callback;
-
- mView = view;
- mView.initForMode(this);
- }
-
- public void finish() {
- // Clearing the menu will also clear the ActionItemBar
- final GeckoMenu menu = mView.getMenu();
- menu.clear();
- menu.close();
-
- if (mCallback != null) {
- mCallback.onDestroyActionMode(this);
- }
- }
-
- public CharSequence getTitle() {
- return mView.getTitle();
- }
-
- public void setTitle(CharSequence title) {
- mView.setTitle(title);
- }
-
- public void setTitle(int resId) {
- mView.setTitle(resId);
- }
-
- public GeckoMenu getMenu() {
- return mView.getMenu();
- }
-
- public void invalidate() {
- if (mCallback != null) {
- mCallback.onPrepareActionMode(this, mView.getMenu());
- }
- mView.invalidate();
- }
-
- public void animateIn() {
- mView.animateIn();
- }
-
- /* GeckoPopupMenu.OnMenuItemClickListener */
- @Override
- public boolean onMenuItemClick(MenuItem item) {
- if (mCallback != null) {
- return mCallback.onActionItemClicked(this, item);
- }
- return false;
- }
-
- /* GeckoPopupMenu.onMenuItemLongClickListener */
- @Override
- public boolean onMenuItemLongClick(MenuItem item) {
- showTooltip((GeckoMenuItem) item);
- return true;
- }
-
- /* View.OnClickListener*/
- @Override
- public void onClick(View v) {
- mPresenter.endActionModeCompat();
- }
-
- private void showTooltip(GeckoMenuItem item) {
- // Computes the tooltip toast screen position (shown when long-tapping the menu item) with regards to the
- // menu item's position (i.e below the item and slightly to the left)
- int[] location = new int[2];
- final View view = item.getActionView();
- view.getLocationOnScreen(location);
-
- int xOffset = location[0] - view.getWidth();
- int yOffset = location[1] + view.getHeight() / 2;
-
- Toast toast = Toast.makeText(view.getContext(), item.getTitle(), Toast.LENGTH_SHORT);
- toast.setGravity(Gravity.TOP | Gravity.LEFT, xOffset, yOffset);
- toast.show();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/ActionModeCompatView.java b/mobile/android/base/java/org/mozilla/gecko/ActionModeCompatView.java
deleted file mode 100644
index c9021b710..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/ActionModeCompatView.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import org.mozilla.gecko.animation.AnimationUtils;
-import org.mozilla.gecko.menu.GeckoMenu;
-import org.mozilla.gecko.widget.GeckoPopupMenu;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.animation.Animation;
-import android.view.animation.ScaleAnimation;
-import android.view.animation.TranslateAnimation;
-import android.widget.Button;
-import android.widget.ImageButton;
-import android.widget.LinearLayout;
-
-class ActionModeCompatView extends LinearLayout implements GeckoMenu.ActionItemBarPresenter {
- private final String LOGTAG = "GeckoActionModeCompatPresenter";
-
- private static final int SPEC = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
-
- private Button mTitleView;
- private ImageButton mMenuButton;
- private ViewGroup mActionButtonBar;
- private GeckoPopupMenu mPopupMenu;
-
- // Maximum number of items to show as actions
- private static final int MAX_ACTION_ITEMS = 4;
-
- private int mActionButtonsWidth;
-
- private Paint mBottomDividerPaint;
- private int mBottomDividerOffset;
-
- public ActionModeCompatView(Context context, AttributeSet attrs) {
- super(context, attrs);
- init(context, attrs, 0);
- }
-
- public ActionModeCompatView(Context context, AttributeSet attrs, int style) {
- super(context, attrs, style);
- init(context, attrs, style);
- }
-
- public void init(final Context context, final AttributeSet attrs, final int defStyle) {
- LayoutInflater.from(context).inflate(R.layout.actionbar, this);
-
- mTitleView = (Button) findViewById(R.id.actionmode_title);
- mMenuButton = (ImageButton) findViewById(R.id.actionbar_menu);
- mActionButtonBar = (ViewGroup) findViewById(R.id.actionbar_buttons);
-
- mPopupMenu = new GeckoPopupMenu(getContext(), mMenuButton);
- mPopupMenu.getMenu().setActionItemBarPresenter(this);
-
- mMenuButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- openMenu();
- }
- });
-
- // The built-in action bar uses colorAccent for the divider so we duplicate that here.
- final TypedArray arr = context.obtainStyledAttributes(attrs, new int[] { R.attr.colorAccent }, defStyle, 0);
- final int bottomDividerColor = arr.getColor(0, 0);
- arr.recycle();
-
- mBottomDividerPaint = new Paint();
- mBottomDividerPaint.setColor(bottomDividerColor);
- mBottomDividerOffset = getResources().getDimensionPixelSize(R.dimen.action_bar_divider_height);
- }
-
- public void initForMode(final ActionModeCompat mode) {
- mTitleView.setOnClickListener(mode);
- mPopupMenu.setOnMenuItemClickListener(mode);
- mPopupMenu.setOnMenuItemLongClickListener(mode);
- }
-
- public CharSequence getTitle() {
- return mTitleView.getText();
- }
-
- public void setTitle(CharSequence title) {
- mTitleView.setText(title);
- }
-
- public void setTitle(int resId) {
- mTitleView.setText(resId);
- }
-
- public GeckoMenu getMenu() {
- return mPopupMenu.getMenu();
- }
-
- @Override
- public void invalidate() {
- // onFinishInflate may not have been called yet on some versions of Android
- if (mPopupMenu != null && mMenuButton != null) {
- mMenuButton.setVisibility(mPopupMenu.getMenu().hasVisibleItems() ? View.VISIBLE : View.GONE);
- }
- super.invalidate();
- }
-
- /* GeckoMenu.ActionItemBarPresenter */
- @Override
- public boolean addActionItem(View actionItem) {
- final int count = mActionButtonBar.getChildCount();
- if (count >= MAX_ACTION_ITEMS) {
- return false;
- }
-
- int maxWidth = mActionButtonBar.getMeasuredWidth();
- if (maxWidth == 0) {
- mActionButtonBar.measure(SPEC, SPEC);
- maxWidth = mActionButtonBar.getMeasuredWidth();
- }
-
- // If the menu button is already visible, no need to account for it
- if (mMenuButton.getVisibility() == View.GONE) {
- // Since we don't know how many items will be added, we always reserve space for the overflow menu
- mMenuButton.measure(SPEC, SPEC);
- maxWidth -= mMenuButton.getMeasuredWidth();
- }
-
- if (mActionButtonsWidth <= 0) {
- mActionButtonsWidth = 0;
-
- // Loop over child views, measure them, and add their width to the taken width
- for (int i = 0; i < count; i++) {
- View v = mActionButtonBar.getChildAt(i);
- v.measure(SPEC, SPEC);
- mActionButtonsWidth += v.getMeasuredWidth();
- }
- }
-
- actionItem.measure(SPEC, SPEC);
- int w = actionItem.getMeasuredWidth();
- if (mActionButtonsWidth + w < maxWidth) {
- // We cache the new width of our children.
- mActionButtonsWidth += w;
- mActionButtonBar.addView(actionItem);
- return true;
- }
-
- return false;
- }
-
- /* GeckoMenu.ActionItemBarPresenter */
- @Override
- public void removeActionItem(View actionItem) {
- actionItem.measure(SPEC, SPEC);
- mActionButtonsWidth -= actionItem.getMeasuredWidth();
- mActionButtonBar.removeView(actionItem);
- }
-
- public void openMenu() {
- mPopupMenu.openMenu();
- }
-
- public void closeMenu() {
- mPopupMenu.dismiss();
- }
-
- public void animateIn() {
- long duration = AnimationUtils.getShortDuration(getContext());
- TranslateAnimation t = new TranslateAnimation(Animation.RELATIVE_TO_SELF, -0.5f, Animation.RELATIVE_TO_SELF, 0f,
- Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f);
- t.setDuration(duration);
-
- ScaleAnimation s = new ScaleAnimation(1f, 1f, 0f, 1f,
- Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
- s.setDuration((long) (duration * 1.5f));
-
- mTitleView.startAnimation(t);
- mActionButtonBar.startAnimation(s);
-
- if ((mMenuButton.getVisibility() == View.VISIBLE) &&
- (mPopupMenu.getMenu().size() > 0)) {
- mMenuButton.startAnimation(s);
- }
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
-
- // Draw the divider at the bottom of the screen. We could do this with a layer-list
- // but then we'd have overdraw (http://stackoverflow.com/a/13509472).
- final int bottom = getHeight();
- final int top = bottom - mBottomDividerOffset;
- canvas.drawRect(0, top, getWidth(), bottom, mBottomDividerPaint);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/ActivityHandlerHelper.java b/mobile/android/base/java/org/mozilla/gecko/ActivityHandlerHelper.java
deleted file mode 100644
index 7174c6580..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/ActivityHandlerHelper.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.util.ActivityResultHandler;
-import org.mozilla.gecko.util.ActivityResultHandlerMap;
-
-import android.app.Activity;
-import android.content.ActivityNotFoundException;
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-
-public class ActivityHandlerHelper {
- private static final String LOGTAG = "GeckoActivityHandlerHelper";
- private static final ActivityResultHandlerMap mActivityResultHandlerMap = new ActivityResultHandlerMap();
-
- private static int makeRequestCode(ActivityResultHandler aHandler) {
- return mActivityResultHandlerMap.put(aHandler);
- }
-
- public static void startIntent(Intent intent, ActivityResultHandler activityResultHandler) {
- startIntentForActivity(GeckoAppShell.getGeckoInterface().getActivity(), intent, activityResultHandler);
- }
-
- /**
- * Starts the Activity, catching & logging if the Activity fails to start.
- *
- * We catch to prevent callers from passing in invalid Intents and crashing the browser.
- *
- * @return true if the Activity is successfully started, false otherwise.
- */
- public static boolean startIntentAndCatch(final String logtag, final Context context, final Intent intent) {
- try {
- context.startActivity(intent);
- return true;
- } catch (final ActivityNotFoundException e) {
- Log.w(logtag, "Activity not found.", e);
- return false;
- } catch (final SecurityException e) {
- Log.w(logtag, "Forbidden to launch activity.", e);
- return false;
- }
- }
-
- public static void startIntentForActivity(Activity activity, Intent intent, ActivityResultHandler activityResultHandler) {
- activity.startActivityForResult(intent, mActivityResultHandlerMap.put(activityResultHandler));
- }
-
-
- public static boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
- ActivityResultHandler handler = mActivityResultHandlerMap.getAndRemove(requestCode);
- if (handler != null) {
- handler.onActivityResult(resultCode, data);
- return true;
- }
- return false;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/BootReceiver.java b/mobile/android/base/java/org/mozilla/gecko/BootReceiver.java
deleted file mode 100644
index 39ca25b67..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/BootReceiver.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-
-import org.mozilla.gecko.feeds.FeedService;
-
-/**
- * This broadcast receiver receives ACTION_BOOT_COMPLETED broadcasts and starts components that should
- * run after the device has booted.
- */
-public class BootReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent == null || !intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
- return; // This is not the broadcast you are looking for.
- }
-
- FeedService.setup(context);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
deleted file mode 100644
index 5eddca3cf..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ /dev/null
@@ -1,4261 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.Manifest;
-import android.annotation.TargetApi;
-import android.app.DownloadManager;
-import android.content.ContentProviderClient;
-import android.os.Environment;
-import android.os.Process;
-import android.support.annotation.NonNull;
-
-import android.graphics.Rect;
-
-import org.json.JSONArray;
-import org.mozilla.gecko.activitystream.ActivityStream;
-import org.mozilla.gecko.adjust.AdjustBrowserAppDelegate;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
-import org.mozilla.gecko.Tabs.TabEvents;
-import org.mozilla.gecko.animation.PropertyAnimator;
-import org.mozilla.gecko.animation.ViewHelper;
-import org.mozilla.gecko.cleanup.FileCleanupController;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.db.SuggestedSites;
-import org.mozilla.gecko.delegates.BrowserAppDelegate;
-import org.mozilla.gecko.delegates.OfflineTabStatusDelegate;
-import org.mozilla.gecko.delegates.ScreenshotDelegate;
-import org.mozilla.gecko.distribution.Distribution;
-import org.mozilla.gecko.distribution.DistributionStoreCallback;
-import org.mozilla.gecko.distribution.PartnerBrowserCustomizationsClient;
-import org.mozilla.gecko.dlc.DownloadContentService;
-import org.mozilla.gecko.icons.decoders.IconDirectoryEntry;
-import org.mozilla.gecko.feeds.ContentNotificationsDelegate;
-import org.mozilla.gecko.feeds.FeedService;
-import org.mozilla.gecko.firstrun.FirstrunAnimationContainer;
-import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
-import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
-import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
-import org.mozilla.gecko.gfx.LayerView;
-import org.mozilla.gecko.home.BrowserSearch;
-import org.mozilla.gecko.home.HomeBanner;
-import org.mozilla.gecko.home.HomeConfig;
-import org.mozilla.gecko.home.HomeConfig.PanelType;
-import org.mozilla.gecko.home.HomeConfigPrefsBackend;
-import org.mozilla.gecko.home.HomeFragment;
-import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener;
-import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
-import org.mozilla.gecko.home.HomePanelsManager;
-import org.mozilla.gecko.home.HomeScreen;
-import org.mozilla.gecko.home.SearchEngine;
-import org.mozilla.gecko.icons.IconCallback;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.icons.Icons;
-import org.mozilla.gecko.javaaddons.JavaAddonManager;
-import org.mozilla.gecko.media.VideoPlayer;
-import org.mozilla.gecko.menu.GeckoMenu;
-import org.mozilla.gecko.menu.GeckoMenuItem;
-import org.mozilla.gecko.mozglue.SafeIntent;
-import org.mozilla.gecko.notifications.NotificationHelper;
-import org.mozilla.gecko.overlays.ui.ShareDialog;
-import org.mozilla.gecko.permissions.Permissions;
-import org.mozilla.gecko.preferences.ClearOnShutdownPref;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-import org.mozilla.gecko.promotion.AddToHomeScreenPromotion;
-import org.mozilla.gecko.delegates.BookmarkStateChangeDelegate;
-import org.mozilla.gecko.promotion.ReaderViewBookmarkPromotion;
-import org.mozilla.gecko.prompts.Prompt;
-import org.mozilla.gecko.reader.SavedReaderViewHelper;
-import org.mozilla.gecko.reader.ReaderModeUtils;
-import org.mozilla.gecko.reader.ReadingListHelper;
-import org.mozilla.gecko.restrictions.Restrictable;
-import org.mozilla.gecko.restrictions.RestrictedProfileConfiguration;
-import org.mozilla.gecko.restrictions.Restrictions;
-import org.mozilla.gecko.search.SearchEngineManager;
-import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository;
-import org.mozilla.gecko.tabqueue.TabQueueHelper;
-import org.mozilla.gecko.tabqueue.TabQueuePrompt;
-import org.mozilla.gecko.tabs.TabHistoryController;
-import org.mozilla.gecko.tabs.TabHistoryController.OnShowTabHistory;
-import org.mozilla.gecko.tabs.TabHistoryFragment;
-import org.mozilla.gecko.tabs.TabHistoryPage;
-import org.mozilla.gecko.tabs.TabsPanel;
-import org.mozilla.gecko.telemetry.TelemetryUploadService;
-import org.mozilla.gecko.telemetry.TelemetryCorePingDelegate;
-import org.mozilla.gecko.telemetry.measurements.SearchCountMeasurements;
-import org.mozilla.gecko.toolbar.AutocompleteHandler;
-import org.mozilla.gecko.toolbar.BrowserToolbar;
-import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
-import org.mozilla.gecko.toolbar.ToolbarProgressView;
-import org.mozilla.gecko.trackingprotection.TrackingProtectionPrompt;
-import org.mozilla.gecko.updater.PostUpdateHandler;
-import org.mozilla.gecko.updater.UpdateServiceHelper;
-import org.mozilla.gecko.util.ActivityUtils;
-import org.mozilla.gecko.util.Clipboard;
-import org.mozilla.gecko.util.ContextUtils;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.FloatUtils;
-import org.mozilla.gecko.util.GamepadUtils;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.HardwareUtils;
-import org.mozilla.gecko.util.IntentUtils;
-import org.mozilla.gecko.util.MenuUtils;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.PrefUtils;
-import org.mozilla.gecko.util.StringUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.widget.AnchoredPopup;
-
-import org.mozilla.gecko.widget.GeckoActionProvider;
-
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.net.Uri;
-import android.nfc.NdefMessage;
-import android.nfc.NdefRecord;
-import android.nfc.NfcAdapter;
-import android.nfc.NfcEvent;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.StrictMode;
-import android.support.design.widget.Snackbar;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.NotificationCompat;
-import android.support.v4.view.MenuItemCompat;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.util.Base64;
-import android.util.Base64OutputStream;
-import android.util.Log;
-import android.view.InputDevice;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.MotionEvent;
-import android.view.SubMenu;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.view.ViewGroup;
-import android.view.ViewStub;
-import android.view.ViewTreeObserver;
-import android.view.Window;
-import android.view.animation.Interpolator;
-import android.widget.Button;
-import android.widget.ListView;
-import android.widget.RelativeLayout;
-import android.widget.ViewFlipper;
-import com.keepsafe.switchboard.AsyncConfigLoader;
-import com.keepsafe.switchboard.SwitchBoard;
-import android.animation.Animator;
-import android.animation.ObjectAnimator;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.lang.reflect.Method;
-import java.net.URLEncoder;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Vector;
-import java.util.regex.Pattern;
-
-public class BrowserApp extends GeckoApp
- implements TabsPanel.TabsLayoutChangeListener,
- PropertyAnimator.PropertyAnimationListener,
- View.OnKeyListener,
- LayerView.DynamicToolbarListener,
- BrowserSearch.OnSearchListener,
- BrowserSearch.OnEditSuggestionListener,
- OnUrlOpenListener,
- OnUrlOpenInBackgroundListener,
- AnchoredPopup.OnVisibilityChangeListener,
- ActionModeCompat.Presenter,
- LayoutInflater.Factory {
- private static final String LOGTAG = "GeckoBrowserApp";
-
- private static final int TABS_ANIMATION_DURATION = 450;
-
- // Intent String extras used to specify custom Switchboard configurations.
- private static final String INTENT_KEY_SWITCHBOARD_SERVER = "switchboard-server";
-
- // TODO: Replace with kinto endpoint.
- private static final String SWITCHBOARD_SERVER = "https://firefox.settings.services.mozilla.com/v1/buckets/fennec/collections/experiments/records";
-
- private static final String STATE_ABOUT_HOME_TOP_PADDING = "abouthome_top_padding";
-
- private static final String BROWSER_SEARCH_TAG = "browser_search";
-
- // Request ID for startActivityForResult.
- private static final int ACTIVITY_REQUEST_PREFERENCES = 1001;
- private static final int ACTIVITY_REQUEST_TAB_QUEUE = 2001;
- public static final int ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK = 3001;
- public static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS = 3002;
- public static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE = 3003;
- public static final int ACTIVITY_REQUEST_TRIPLE_READERVIEW = 4001;
- public static final int ACTIVITY_RESULT_TRIPLE_READERVIEW_ADD_BOOKMARK = 4002;
- public static final int ACTIVITY_RESULT_TRIPLE_READERVIEW_IGNORE = 4003;
-
- public static final String ACTION_VIEW_MULTIPLE = AppConstants.ANDROID_PACKAGE_NAME + ".action.VIEW_MULTIPLE";
-
- @RobocopTarget
- public static final String EXTRA_SKIP_STARTPANE = "skipstartpane";
- private static final String EOL_NOTIFIED = "eol_notified";
-
- private BrowserSearch mBrowserSearch;
- private View mBrowserSearchContainer;
-
- public ViewGroup mBrowserChrome;
- public ViewFlipper mActionBarFlipper;
- public ActionModeCompatView mActionBar;
- private VideoPlayer mVideoPlayer;
- private BrowserToolbar mBrowserToolbar;
- private View mDoorhangerOverlay;
- // We can't name the TabStrip class because it's not included on API 9.
- private TabStripInterface mTabStrip;
- private ToolbarProgressView mProgressView;
- private FirstrunAnimationContainer mFirstrunAnimationContainer;
- private HomeScreen mHomeScreen;
- private TabsPanel mTabsPanel;
- /**
- * Container for the home screen implementation. This will be populated with any valid
- * home screen implementation (currently that is just the HomePager, but that will be extended
- * to permit further experimental replacement panels such as the activity-stream panel).
- */
- private ViewGroup mHomeScreenContainer;
- private int mCachedRecentTabsCount;
- private ActionModeCompat mActionMode;
- private TabHistoryController tabHistoryController;
- private ZoomedView mZoomedView;
-
- private static final int GECKO_TOOLS_MENU = -1;
- private static final int ADDON_MENU_OFFSET = 1000;
- public static final String TAB_HISTORY_FRAGMENT_TAG = "tabHistoryFragment";
-
- private static class MenuItemInfo {
- public int id;
- public String label;
- public boolean checkable;
- public boolean checked;
- public boolean enabled = true;
- public boolean visible = true;
- public int parent;
- public boolean added; // So we can re-add after a locale change.
- }
-
- // The types of guest mode dialogs we show.
- public static enum GuestModeDialog {
- ENTERING,
- LEAVING
- }
-
- private Vector<MenuItemInfo> mAddonMenuItemsCache;
- private PropertyAnimator mMainLayoutAnimator;
-
- private static final Interpolator sTabsInterpolator = new Interpolator() {
- @Override
- public float getInterpolation(float t) {
- t -= 1.0f;
- return t * t * t * t * t + 1.0f;
- }
- };
-
- private FindInPageBar mFindInPageBar;
- private MediaCastingBar mMediaCastingBar;
-
- // We'll ask for feedback after the user launches the app this many times.
- private static final int FEEDBACK_LAUNCH_COUNT = 15;
-
- // Stored value of the toolbar height, so we know when it's changed.
- private int mToolbarHeight;
-
- private SharedPreferencesHelper mSharedPreferencesHelper;
-
- private ReadingListHelper mReadingListHelper;
-
- private AccountsHelper mAccountsHelper;
-
- // The tab to be selected on editing mode exit.
- private Integer mTargetTabForEditingMode;
-
- private final TabEditingState mLastTabEditingState = new TabEditingState();
-
- // The animator used to toggle HomePager visibility has a race where if the HomePager is shown
- // (starting the animation), the HomePager is hidden, and the HomePager animation completes,
- // both the web content and the HomePager will be hidden. This flag is used to prevent the
- // race by determining if the web content should be hidden at the animation's end.
- private boolean mHideWebContentOnAnimationEnd;
-
- private final DynamicToolbar mDynamicToolbar = new DynamicToolbar();
-
- private final TelemetryCorePingDelegate mTelemetryCorePingDelegate = new TelemetryCorePingDelegate();
-
- private final List<BrowserAppDelegate> delegates = Collections.unmodifiableList(Arrays.asList(
- new AddToHomeScreenPromotion(),
- new ScreenshotDelegate(),
- new BookmarkStateChangeDelegate(),
- new ReaderViewBookmarkPromotion(),
- new ContentNotificationsDelegate(),
- new PostUpdateHandler(),
- mTelemetryCorePingDelegate,
- new OfflineTabStatusDelegate(),
- new AdjustBrowserAppDelegate(mTelemetryCorePingDelegate)
- ));
-
- @NonNull
- private SearchEngineManager mSearchEngineManager; // Contains reference to Context - DO NOT LEAK!
-
- private boolean mHasResumed;
-
- @Override
- public View onCreateView(final String name, final Context context, final AttributeSet attrs) {
- final View view;
- if (BrowserToolbar.class.getName().equals(name)) {
- view = BrowserToolbar.create(context, attrs);
- } else if (TabsPanel.TabsLayout.class.getName().equals(name)) {
- view = TabsPanel.createTabsLayout(context, attrs);
- } else {
- view = super.onCreateView(name, context, attrs);
- }
- return view;
- }
-
- @Override
- public void onTabChanged(Tab tab, TabEvents msg, String data) {
- if (tab == null) {
- // Only RESTORED is allowed a null tab: it's the only event that
- // isn't tied to a specific tab.
- if (msg != Tabs.TabEvents.RESTORED) {
- throw new IllegalArgumentException("onTabChanged:" + msg + " must specify a tab.");
- }
-
- final Tab selectedTab = Tabs.getInstance().getSelectedTab();
- if (selectedTab != null) {
- // After restoring the tabs we want to update the home pager immediately. Otherwise we
- // might wait for an event coming from Gecko and this can take several seconds. (Bug 1283627)
- updateHomePagerForTab(selectedTab);
- }
-
- return;
- }
-
- Log.d(LOGTAG, "BrowserApp.onTabChanged: " + tab.getId() + ": " + msg);
- switch (msg) {
- case SELECTED:
- if (mVideoPlayer.isPlaying()) {
- mVideoPlayer.stop();
- }
-
- if (Tabs.getInstance().isSelectedTab(tab) && mDynamicToolbar.isEnabled()) {
- final VisibilityTransition transition = (tab.getShouldShowToolbarWithoutAnimationOnFirstSelection()) ?
- VisibilityTransition.IMMEDIATE : VisibilityTransition.ANIMATE;
- mDynamicToolbar.setVisible(true, transition);
-
- // The first selection has happened - reset the state.
- tab.setShouldShowToolbarWithoutAnimationOnFirstSelection(false);
- }
- // fall through
- case LOCATION_CHANGE:
- if (mZoomedView != null) {
- mZoomedView.stopZoomDisplay(false);
- }
- if (Tabs.getInstance().isSelectedTab(tab)) {
- updateHomePagerForTab(tab);
- }
-
- mDynamicToolbar.persistTemporaryVisibility();
- break;
- case START:
- if (Tabs.getInstance().isSelectedTab(tab)) {
- invalidateOptionsMenu();
-
- if (mDynamicToolbar.isEnabled()) {
- mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
- }
- }
- break;
- case LOAD_ERROR:
- case STOP:
- case MENU_UPDATED:
- if (Tabs.getInstance().isSelectedTab(tab)) {
- invalidateOptionsMenu();
- }
- break;
- case PAGE_SHOW:
- tab.loadFavicon();
- break;
-
- case UNSELECTED:
- // We receive UNSELECTED immediately after the SELECTED listeners run
- // so we are ensured that the unselectedTabEditingText has not changed.
- if (tab.isEditing()) {
- // Copy to avoid constructing new objects.
- tab.getEditingState().copyFrom(mLastTabEditingState);
- }
- break;
- }
-
- if (HardwareUtils.isTablet() && msg == TabEvents.SELECTED) {
- updateEditingModeForTab(tab);
- }
-
- super.onTabChanged(tab, msg, data);
- }
-
- private void updateEditingModeForTab(final Tab selectedTab) {
- // (bug 1086983 comment 11) Because the tab may be selected from the gecko thread and we're
- // running this code on the UI thread, the selected tab argument may not still refer to the
- // selected tab. However, that means this code should be run again and the initial state
- // changes will be overridden. As an optimization, we can skip this update, but it may have
- // unknown side-effects so we don't.
- if (!Tabs.getInstance().isSelectedTab(selectedTab)) {
- Log.w(LOGTAG, "updateEditingModeForTab: Given tab is expected to be selected tab");
- }
-
- saveTabEditingState(mLastTabEditingState);
-
- if (selectedTab.isEditing()) {
- enterEditingMode();
- restoreTabEditingState(selectedTab.getEditingState());
- } else {
- mBrowserToolbar.cancelEdit();
- }
- }
-
- private void saveTabEditingState(final TabEditingState editingState) {
- mBrowserToolbar.saveTabEditingState(editingState);
- editingState.setIsBrowserSearchShown(mBrowserSearch.getUserVisibleHint());
- }
-
- private void restoreTabEditingState(final TabEditingState editingState) {
- mBrowserToolbar.restoreTabEditingState(editingState);
-
- // Since changing the editing text will show/hide browser search, this
- // must be called after we restore the editing state in the edit text View.
- if (editingState.isBrowserSearchShown()) {
- showBrowserSearch();
- } else {
- hideBrowserSearch();
- }
- }
-
- @Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- if (AndroidGamepadManager.handleKeyEvent(event)) {
- return true;
- }
-
- // Global onKey handler. This is called if the focused UI doesn't
- // handle the key event, and before Gecko swallows the events.
- if (event.getAction() != KeyEvent.ACTION_DOWN) {
- return false;
- }
-
- if ((event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_BUTTON_Y:
- // Toggle/focus the address bar on gamepad-y button.
- if (mBrowserChrome.getVisibility() == View.VISIBLE) {
- if (mDynamicToolbar.isEnabled() && !isHomePagerVisible()) {
- mDynamicToolbar.setVisible(false, VisibilityTransition.ANIMATE);
- if (mLayerView != null) {
- mLayerView.requestFocus();
- }
- } else {
- // Just focus the address bar when about:home is visible
- // or when the dynamic toolbar isn't enabled.
- mBrowserToolbar.requestFocusFromTouch();
- }
- } else {
- mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
- mBrowserToolbar.requestFocusFromTouch();
- }
- return true;
- case KeyEvent.KEYCODE_BUTTON_L1:
- // Go back on L1
- Tabs.getInstance().getSelectedTab().doBack();
- return true;
- case KeyEvent.KEYCODE_BUTTON_R1:
- // Go forward on R1
- Tabs.getInstance().getSelectedTab().doForward();
- return true;
- }
- }
-
- // Check if this was a shortcut. Meta keys exists only on 11+.
- final Tab tab = Tabs.getInstance().getSelectedTab();
- if (tab != null && event.isCtrlPressed()) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_LEFT_BRACKET:
- tab.doBack();
- return true;
-
- case KeyEvent.KEYCODE_RIGHT_BRACKET:
- tab.doForward();
- return true;
-
- case KeyEvent.KEYCODE_R:
- tab.doReload(false);
- return true;
-
- case KeyEvent.KEYCODE_PERIOD:
- tab.doStop();
- return true;
-
- case KeyEvent.KEYCODE_T:
- addTab();
- return true;
-
- case KeyEvent.KEYCODE_W:
- Tabs.getInstance().closeTab(tab);
- return true;
-
- case KeyEvent.KEYCODE_F:
- mFindInPageBar.show();
- return true;
- }
- }
-
- return false;
- }
-
- private Runnable mCheckLongPress;
- {
- // Only initialise the runnable if we are >= N.
- // See onKeyDown() for more details of the back-button long-press workaround
- if (!Versions.preN) {
- mCheckLongPress = new Runnable() {
- public void run() {
- handleBackLongPress();
- }
- };
- }
- }
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- // Bug 1304688: Android N has broken passing onKeyLongPress events for the back button, so we
- // instead copy the long-press-handler technique from Android's KeyButtonView.
- // - For short presses, we cancel the callback in onKeyUp
- // - For long presses, the normal keypress is marked as cancelled, hence won't be handled elsewhere
- // (but Android still provides the haptic feedback), and the runnable is run.
- if (!Versions.preN &&
- keyCode == KeyEvent.KEYCODE_BACK) {
- ThreadUtils.getUiHandler().removeCallbacks(mCheckLongPress);
- ThreadUtils.getUiHandler().postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());
- }
-
- if (!mBrowserToolbar.isEditing() && onKey(null, keyCode, event)) {
- return true;
- }
- return super.onKeyDown(keyCode, event);
- }
-
- @Override
- public boolean onKeyUp(int keyCode, KeyEvent event) {
- if (!Versions.preN &&
- keyCode == KeyEvent.KEYCODE_BACK) {
- ThreadUtils.getUiHandler().removeCallbacks(mCheckLongPress);
- }
-
- if (AndroidGamepadManager.handleKeyEvent(event)) {
- return true;
- }
- return super.onKeyUp(keyCode, event);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- if (!HardwareUtils.isSupportedSystem()) {
- // This build does not support the Android version of the device; Exit early.
- super.onCreate(savedInstanceState);
- return;
- }
-
- final SafeIntent intent = new SafeIntent(getIntent());
- final boolean isInAutomation = IntentUtils.getIsInAutomationFromEnvironment(intent);
-
- // This has to be prepared prior to calling GeckoApp.onCreate, because
- // widget code and BrowserToolbar need it, and they're created by the
- // layout, which GeckoApp takes care of.
- ((GeckoApplication) getApplication()).prepareLightweightTheme();
-
- super.onCreate(savedInstanceState);
-
- final Context appContext = getApplicationContext();
-
- initSwitchboard(this, intent, isInAutomation);
- initTelemetryUploader(isInAutomation);
-
- mBrowserChrome = (ViewGroup) findViewById(R.id.browser_chrome);
- mActionBarFlipper = (ViewFlipper) findViewById(R.id.browser_actionbar);
- mActionBar = (ActionModeCompatView) findViewById(R.id.actionbar);
-
- mVideoPlayer = (VideoPlayer) findViewById(R.id.video_player);
- mVideoPlayer.setFullScreenListener(new VideoPlayer.FullScreenListener() {
- @Override
- public void onFullScreenChanged(boolean fullScreen) {
- mVideoPlayer.setFullScreen(fullScreen);
- setFullScreen(fullScreen);
- }
- });
-
- mBrowserToolbar = (BrowserToolbar) findViewById(R.id.browser_toolbar);
- mBrowserToolbar.setTouchEventInterceptor(new TouchEventInterceptor() {
- @Override
- public boolean onInterceptTouchEvent(View view, MotionEvent event) {
- // Manually dismiss text selection bar if it's not overlaying the toolbar.
- mTextSelection.dismiss();
- return false;
- }
-
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- return false;
- }
- });
-
- mProgressView = (ToolbarProgressView) findViewById(R.id.progress);
- mBrowserToolbar.setProgressBar(mProgressView);
-
- // Initialize Tab History Controller.
- tabHistoryController = new TabHistoryController(new OnShowTabHistory() {
- @Override
- public void onShowHistory(final List<TabHistoryPage> historyPageList, final int toIndex) {
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- if (BrowserApp.this.isFinishing()) {
- // TabHistoryController is rather slow - and involves calling into Gecko
- // to retrieve tab history. That means there can be a significant
- // delay between the back-button long-press, and onShowHistory()
- // being called. Hence we need to guard against the Activity being
- // shut down (in which case trying to perform UI changes, such as showing
- // fragments below, will crash).
- return;
- }
-
- final TabHistoryFragment fragment = TabHistoryFragment.newInstance(historyPageList, toIndex);
- final FragmentManager fragmentManager = getSupportFragmentManager();
- GeckoAppShell.vibrateOnHapticFeedbackEnabled(getResources().getIntArray(R.array.long_press_vibrate_msec));
- fragment.show(R.id.tab_history_panel, fragmentManager.beginTransaction(), TAB_HISTORY_FRAGMENT_TAG);
- }
- });
- }
- });
- mBrowserToolbar.setTabHistoryController(tabHistoryController);
-
- final String action = intent.getAction();
- if (Intent.ACTION_VIEW.equals(action)) {
- // Show the target URL immediately in the toolbar.
- mBrowserToolbar.setTitle(intent.getDataString());
-
- showTabQueuePromptIfApplicable(intent);
- } else if (ACTION_VIEW_MULTIPLE.equals(action) && savedInstanceState == null) {
- // We only want to handle this intent if savedInstanceState is null. In the case where
- // savedInstanceState is not null this activity is being re-created and we already
- // opened tabs for the URLs the last time. Our session store will take care of restoring
- // them.
- openMultipleTabsFromIntent(intent);
- } else if (GuestSession.NOTIFICATION_INTENT.equals(action)) {
- GuestSession.onNotificationIntentReceived(this);
- } else if (TabQueueHelper.LOAD_URLS_ACTION.equals(action)) {
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, "tabqueue");
- }
-
- if (HardwareUtils.isTablet()) {
- mTabStrip = (TabStripInterface) (((ViewStub) findViewById(R.id.tablet_tab_strip)).inflate());
- }
-
- ((GeckoApp.MainLayout) mMainLayout).setTouchEventInterceptor(new HideOnTouchListener());
- ((GeckoApp.MainLayout) mMainLayout).setMotionEventInterceptor(new MotionEventInterceptor() {
- @Override
- public boolean onInterceptMotionEvent(View view, MotionEvent event) {
- // If we get a gamepad panning MotionEvent while the focus is not on the layerview,
- // put the focus on the layerview and carry on
- if (mLayerView != null && !mLayerView.hasFocus() && GamepadUtils.isPanningControl(event)) {
- if (mHomeScreen == null) {
- return false;
- }
-
- if (isHomePagerVisible()) {
- mLayerView.requestFocus();
- } else {
- mHomeScreen.requestFocus();
- }
- }
- return false;
- }
- });
-
- mHomeScreenContainer = (ViewGroup) findViewById(R.id.home_screen_container);
-
- mBrowserSearchContainer = findViewById(R.id.search_container);
- mBrowserSearch = (BrowserSearch) getSupportFragmentManager().findFragmentByTag(BROWSER_SEARCH_TAG);
- if (mBrowserSearch == null) {
- mBrowserSearch = BrowserSearch.newInstance();
- mBrowserSearch.setUserVisibleHint(false);
- }
-
- setBrowserToolbarListeners();
-
- mFindInPageBar = (FindInPageBar) findViewById(R.id.find_in_page);
- mMediaCastingBar = (MediaCastingBar) findViewById(R.id.media_casting);
-
- mDoorhangerOverlay = findViewById(R.id.doorhanger_overlay);
-
- EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener)this,
- "Gecko:DelayedStartup",
- "Menu:Open",
- "Menu:Update",
- "LightweightTheme:Update",
- "Search:Keyword",
- "Prompt:ShowTop",
- "Tab:Added",
- "Video:Play");
-
- EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener)this,
- "CharEncoding:Data",
- "CharEncoding:State",
- "Download:AndroidDownloadManager",
- "Experiments:GetActive",
- "Experiments:SetOverride",
- "Experiments:ClearOverride",
- "Favicon:CacheLoad",
- "Feedback:MaybeLater",
- "Menu:Add",
- "Menu:Remove",
- "Sanitize:ClearHistory",
- "Sanitize:ClearSyncedTabs",
- "Settings:Show",
- "Telemetry:Gather",
- "Updater:Launch",
- "Website:Metadata");
-
- final GeckoProfile profile = getProfile();
-
- // We want to upload the telemetry core ping as soon after startup as possible. It relies on the
- // Distribution being initialized. If you move this initialization, ensure it plays well with telemetry.
- final Distribution distribution = Distribution.init(getApplicationContext());
- distribution.addOnDistributionReadyCallback(
- new DistributionStoreCallback(getApplicationContext(), profile.getName()));
-
- mSearchEngineManager = new SearchEngineManager(this, distribution);
-
- // Init suggested sites engine in BrowserDB.
- final SuggestedSites suggestedSites = new SuggestedSites(appContext, distribution);
- final BrowserDB db = BrowserDB.from(profile);
- db.setSuggestedSites(suggestedSites);
-
- JavaAddonManager.getInstance().init(appContext);
- mSharedPreferencesHelper = new SharedPreferencesHelper(appContext);
- mReadingListHelper = new ReadingListHelper(appContext, profile);
- mAccountsHelper = new AccountsHelper(appContext, profile);
-
- if (AppConstants.MOZ_ANDROID_BEAM) {
- NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
- if (nfc != null) {
- nfc.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
- @Override
- public NdefMessage createNdefMessage(NfcEvent event) {
- Tab tab = Tabs.getInstance().getSelectedTab();
- if (tab == null || tab.isPrivate()) {
- return null;
- }
- return new NdefMessage(new NdefRecord[] { NdefRecord.createUri(tab.getURL()) });
- }
- }, this);
- }
- }
-
- if (savedInstanceState != null) {
- mDynamicToolbar.onRestoreInstanceState(savedInstanceState);
- mHomeScreenContainer.setPadding(0, savedInstanceState.getInt(STATE_ABOUT_HOME_TOP_PADDING), 0, 0);
- }
-
- mDynamicToolbar.setEnabledChangedListener(new DynamicToolbar.OnEnabledChangedListener() {
- @Override
- public void onEnabledChanged(boolean enabled) {
- setDynamicToolbarEnabled(enabled);
- }
- });
-
- // Set the maximum bits-per-pixel the favicon system cares about.
- IconDirectoryEntry.setMaxBPP(GeckoAppShell.getScreenDepth());
-
- // The update service is enabled for RELEASE_OR_BETA, which includes the release and beta channels.
- // However, no updates are served. Therefore, we don't trust the update service directly, and
- // try to avoid prompting unnecessarily. See Bug 1232798.
- if (!AppConstants.RELEASE_OR_BETA && UpdateServiceHelper.isUpdaterEnabled(this)) {
- Permissions.from(this)
- .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
- .doNotPrompt()
- .andFallback(new Runnable() {
- @Override
- public void run() {
- showUpdaterPermissionSnackbar();
- }
- })
- .run();
- }
-
- for (final BrowserAppDelegate delegate : delegates) {
- delegate.onCreate(this, savedInstanceState);
- }
-
- // We want to get an understanding of how our user base is spread (bug 1221646).
- final String installerPackageName = getPackageManager().getInstallerPackageName(getPackageName());
- Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, TelemetryContract.Method.SYSTEM, "installer_" + installerPackageName);
- }
-
- /**
- * Initializes the default Switchboard URLs the first time.
- * @param intent
- */
- private static void initSwitchboard(final Context context, final SafeIntent intent, final boolean isInAutomation) {
- if (isInAutomation) {
- Log.d(LOGTAG, "Switchboard disabled - in automation");
- return;
- } else if (!AppConstants.MOZ_SWITCHBOARD) {
- Log.d(LOGTAG, "Switchboard compile-time disabled");
- return;
- }
-
- final String serverExtra = intent.getStringExtra(INTENT_KEY_SWITCHBOARD_SERVER);
- final String serverUrl = TextUtils.isEmpty(serverExtra) ? SWITCHBOARD_SERVER : serverExtra;
- new AsyncConfigLoader(context, serverUrl).execute();
- }
-
- private static void initTelemetryUploader(final boolean isInAutomation) {
- TelemetryUploadService.setDisabled(isInAutomation);
- }
-
- private void showUpdaterPermissionSnackbar() {
- SnackbarBuilder.SnackbarCallback allowCallback = new SnackbarBuilder.SnackbarCallback() {
- @Override
- public void onClick(View v) {
- Permissions.from(BrowserApp.this)
- .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
- .run();
- }
- };
-
- SnackbarBuilder.builder(this)
- .message(R.string.updater_permission_text)
- .duration(Snackbar.LENGTH_INDEFINITE)
- .action(R.string.updater_permission_allow)
- .callback(allowCallback)
- .buildAndShow();
- }
-
- private void conditionallyNotifyEOL() {
- final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
- try {
- final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
- if (!prefs.contains(EOL_NOTIFIED)) {
-
- // Launch main App to load SUMO url on EOL notification.
- final String link = getString(R.string.eol_notification_url,
- AppConstants.MOZ_APP_VERSION,
- AppConstants.OS_TARGET,
- Locales.getLanguageTag(Locale.getDefault()));
-
- final Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
- intent.setData(Uri.parse(link));
- final PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
-
- final Notification notification = new NotificationCompat.Builder(this)
- .setContentTitle(getString(R.string.eol_notification_title))
- .setContentText(getString(R.string.eol_notification_summary))
- .setSmallIcon(R.drawable.ic_status_logo)
- .setAutoCancel(true)
- .setContentIntent(pendingIntent)
- .build();
-
- final NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- final int notificationID = EOL_NOTIFIED.hashCode();
- notificationManager.notify(notificationID, notification);
-
- GeckoSharedPrefs.forProfile(this)
- .edit()
- .putBoolean(EOL_NOTIFIED, true)
- .apply();
- }
- } finally {
- StrictMode.setThreadPolicy(savedPolicy);
- }
- }
-
- /**
- * Check and show the firstrun pane if the browser has never been launched and
- * is not opening an external link from another application.
- *
- * @param context Context of application; used to show firstrun pane if appropriate
- * @param intent Intent that launched this activity
- */
- private void checkFirstrun(Context context, SafeIntent intent) {
- if (getProfile().inGuestMode()) {
- // We do not want to show any first run tour for guest profiles.
- return;
- }
-
- if (intent.getBooleanExtra(EXTRA_SKIP_STARTPANE, false)) {
- // Note that we don't set the pref, so subsequent launches can result
- // in the firstrun pane being shown.
- return;
- }
- final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
-
- try {
- final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
-
- if (prefs.getBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, true)) {
- if (!Intent.ACTION_VIEW.equals(intent.getAction())) {
- showFirstrunPager();
-
- if (HardwareUtils.isTablet()) {
- mTabStrip.setOnTabChangedListener(new TabStripInterface.OnTabAddedOrRemovedListener() {
- @Override
- public void onTabChanged() {
- hideFirstrunPager(TelemetryContract.Method.BUTTON);
- mTabStrip.setOnTabChangedListener(null);
- }
- });
- }
- }
-
- // Don't bother trying again to show the v1 minimal first run.
- prefs.edit().putBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, false).apply();
-
- // We have no intention of stopping this session. The FIRSTRUN session
- // ends when the browsing session/activity has ended. All events
- // during firstrun will be tagged as FIRSTRUN.
- Telemetry.startUISession(TelemetryContract.Session.FIRSTRUN);
- }
- } finally {
- StrictMode.setThreadPolicy(savedPolicy);
- }
- }
-
- private Class<?> getMediaPlayerManager() {
- if (AppConstants.MOZ_MEDIA_PLAYER) {
- try {
- return Class.forName("org.mozilla.gecko.MediaPlayerManager");
- } catch (Exception ex) {
- // Ignore failures
- Log.e(LOGTAG, "No native casting support", ex);
- }
- }
-
- return null;
- }
-
- @Override
- public void onBackPressed() {
- if (mTextSelection.dismiss()) {
- return;
- }
-
- if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
- super.onBackPressed();
- return;
- }
-
- if (mBrowserToolbar.onBackPressed()) {
- return;
- }
-
- if (mActionMode != null) {
- endActionModeCompat();
- return;
- }
-
- if (hideFirstrunPager(TelemetryContract.Method.BACK)) {
- return;
- }
-
- if (mVideoPlayer.isFullScreen()) {
- mVideoPlayer.setFullScreen(false);
- setFullScreen(false);
- return;
- }
-
- if (mVideoPlayer.isPlaying()) {
- mVideoPlayer.stop();
- return;
- }
-
- super.onBackPressed();
- }
-
- @Override
- public void onAttachedToWindow() {
- // We can't show the first run experience until Gecko has finished initialization (bug 1077583).
- checkFirstrun(this, new SafeIntent(getIntent()));
- }
-
- @Override
- protected void processTabQueue() {
- if (TabQueueHelper.TAB_QUEUE_ENABLED && mInitialized) {
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- if (TabQueueHelper.shouldOpenTabQueueUrls(BrowserApp.this)) {
- openQueuedTabs();
- }
- }
- });
- }
- }
-
- @Override
- protected void openQueuedTabs() {
- ThreadUtils.assertNotOnUiThread();
-
- int queuedTabCount = TabQueueHelper.getTabQueueLength(BrowserApp.this);
-
- Telemetry.addToHistogram("FENNEC_TABQUEUE_QUEUESIZE", queuedTabCount);
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "tabqueue-delayed");
-
- TabQueueHelper.openQueuedUrls(BrowserApp.this, getProfile(), TabQueueHelper.FILE_NAME, false);
-
- // If there's more than one tab then also show the tabs panel.
- if (queuedTabCount > 1) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- showNormalTabs();
- }
- });
- }
- }
-
- private void openMultipleTabsFromIntent(final SafeIntent intent) {
- final List<String> urls = intent.getStringArrayListExtra("urls");
- if (urls != null) {
- openUrls(urls);
- }
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- if (mIsAbortingAppLaunch) {
- return;
- }
-
- if (!mHasResumed) {
- EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) this,
- "Prompt:ShowTop");
- mHasResumed = true;
- }
-
- processTabQueue();
-
- for (BrowserAppDelegate delegate : delegates) {
- delegate.onResume(this);
- }
- }
-
- @Override
- public void onPause() {
- super.onPause();
- if (mIsAbortingAppLaunch) {
- return;
- }
-
- if (mHasResumed) {
- // Register for Prompt:ShowTop so we can foreground this activity even if it's hidden.
- EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener) this,
- "Prompt:ShowTop");
- mHasResumed = false;
- }
-
- for (BrowserAppDelegate delegate : delegates) {
- delegate.onPause(this);
- }
- }
-
- @Override
- public void onRestart() {
- super.onRestart();
- if (mIsAbortingAppLaunch) {
- return;
- }
-
- for (final BrowserAppDelegate delegate : delegates) {
- delegate.onRestart(this);
- }
- }
-
- @Override
- public void onStart() {
- super.onStart();
- if (mIsAbortingAppLaunch) {
- return;
- }
-
- // Queue this work so that the first launch of the activity doesn't
- // trigger profile init too early.
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- final GeckoProfile profile = getProfile();
- if (profile.inGuestMode()) {
- GuestSession.showNotification(BrowserApp.this);
- } else {
- // If we're restarting, we won't destroy the activity.
- // Make sure we remove any guest notifications that might
- // have been shown.
- GuestSession.hideNotification(BrowserApp.this);
- }
-
- // It'd be better to launch this once, in onCreate, but there's ambiguity for when the
- // profile is created so we run here instead. Don't worry, call start short-circuits pretty fast.
- final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfileName(BrowserApp.this, profile.getName());
- FileCleanupController.startIfReady(BrowserApp.this, sharedPrefs, profile.getDir().getAbsolutePath());
- }
- });
-
- for (final BrowserAppDelegate delegate : delegates) {
- delegate.onStart(this);
- }
- }
-
- @Override
- public void onStop() {
- super.onStop();
- if (mIsAbortingAppLaunch) {
- return;
- }
-
- // We only show the guest mode notification when our activity is in the foreground.
- GuestSession.hideNotification(this);
-
- for (final BrowserAppDelegate delegate : delegates) {
- delegate.onStop(this);
- }
- }
-
- @Override
- public void onWindowFocusChanged(boolean hasFocus) {
- super.onWindowFocusChanged(hasFocus);
-
- // Sending a message to the toolbar when the browser window gains focus
- // This is needed for qr code input
- if (hasFocus) {
- mBrowserToolbar.onParentFocus();
- }
- }
-
- private void setBrowserToolbarListeners() {
- mBrowserToolbar.setOnActivateListener(new BrowserToolbar.OnActivateListener() {
- @Override
- public void onActivate() {
- enterEditingMode();
- }
- });
-
- mBrowserToolbar.setOnCommitListener(new BrowserToolbar.OnCommitListener() {
- @Override
- public void onCommit() {
- commitEditingMode();
- }
- });
-
- mBrowserToolbar.setOnDismissListener(new BrowserToolbar.OnDismissListener() {
- @Override
- public void onDismiss() {
- mBrowserToolbar.cancelEdit();
- }
- });
-
- mBrowserToolbar.setOnFilterListener(new BrowserToolbar.OnFilterListener() {
- @Override
- public void onFilter(String searchText, AutocompleteHandler handler) {
- filterEditingMode(searchText, handler);
- }
- });
-
- mBrowserToolbar.setOnFocusChangeListener(new View.OnFocusChangeListener() {
- @Override
- public void onFocusChange(View v, boolean hasFocus) {
- if (isHomePagerVisible()) {
- mHomeScreen.onToolbarFocusChange(hasFocus);
- }
- }
- });
-
- mBrowserToolbar.setOnStartEditingListener(new BrowserToolbar.OnStartEditingListener() {
- @Override
- public void onStartEditing() {
- final Tab selectedTab = Tabs.getInstance().getSelectedTab();
- if (selectedTab != null) {
- selectedTab.setIsEditing(true);
- }
-
- // Temporarily disable doorhanger notifications.
- if (mDoorHangerPopup != null) {
- mDoorHangerPopup.disable();
- }
- }
- });
-
- mBrowserToolbar.setOnStopEditingListener(new BrowserToolbar.OnStopEditingListener() {
- @Override
- public void onStopEditing() {
- final Tab selectedTab = Tabs.getInstance().getSelectedTab();
- if (selectedTab != null) {
- selectedTab.setIsEditing(false);
- }
-
- selectTargetTabForEditingMode();
-
- // Since the underlying LayerView is set visible in hideHomePager, we would
- // ordinarily want to call it first. However, hideBrowserSearch changes the
- // visibility of the HomePager and hideHomePager will take no action if the
- // HomePager is hidden, so we want to call hideBrowserSearch to restore the
- // HomePager visibility first.
- hideBrowserSearch();
- hideHomePager();
-
- // Re-enable doorhanger notifications. They may trigger on the selected tab above.
- if (mDoorHangerPopup != null) {
- mDoorHangerPopup.enable();
- }
- }
- });
-
- // Intercept key events for gamepad shortcuts
- mBrowserToolbar.setOnKeyListener(this);
- }
-
- private void setDynamicToolbarEnabled(boolean enabled) {
- ThreadUtils.assertOnUiThread();
-
- if (enabled) {
- if (mLayerView != null) {
- mLayerView.getDynamicToolbarAnimator().addTranslationListener(this);
- }
- setToolbarMargin(0);
- mHomeScreenContainer.setPadding(0, mBrowserChrome.getHeight(), 0, 0);
- } else {
- // Immediately show the toolbar when disabling the dynamic
- // toolbar.
- if (mLayerView != null) {
- mLayerView.getDynamicToolbarAnimator().removeTranslationListener(this);
- }
- mHomeScreenContainer.setPadding(0, 0, 0, 0);
- if (mBrowserChrome != null) {
- ViewHelper.setTranslationY(mBrowserChrome, 0);
- }
- if (mLayerView != null) {
- mLayerView.setSurfaceTranslation(0);
- }
- }
-
- refreshToolbarHeight();
- }
-
- private static boolean isAboutHome(final Tab tab) {
- return AboutPages.isAboutHome(tab.getURL());
- }
-
- @Override
- public boolean onSearchRequested() {
- enterEditingMode();
- return true;
- }
-
- @Override
- public boolean onContextItemSelected(MenuItem item) {
- final int itemId = item.getItemId();
- if (itemId == R.id.pasteandgo) {
- hideFirstrunPager(TelemetryContract.Method.CONTEXT_MENU);
-
- String text = Clipboard.getText();
- if (!TextUtils.isEmpty(text)) {
- loadUrlOrKeywordSearch(text);
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.CONTEXT_MENU);
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "pasteandgo");
- }
- return true;
- }
-
- if (itemId == R.id.paste) {
- String text = Clipboard.getText();
- if (!TextUtils.isEmpty(text)) {
- enterEditingMode(text);
- showBrowserSearch();
- mBrowserSearch.filter(text, null);
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "paste");
- }
- return true;
- }
-
- if (itemId == R.id.subscribe) {
- // This can be selected from either the browser menu or the contextmenu, depending on the size and version (v11+) of the phone.
- Tab tab = Tabs.getInstance().getSelectedTab();
- if (tab != null && tab.hasFeeds()) {
- JSONObject args = new JSONObject();
- try {
- args.put("tabId", tab.getId());
- } catch (JSONException e) {
- Log.e(LOGTAG, "error building json arguments", e);
- }
- GeckoAppShell.notifyObservers("Feeds:Subscribe", args.toString());
- }
- return true;
- }
-
- if (itemId == R.id.add_search_engine) {
- // This can be selected from either the browser menu or the contextmenu, depending on the size and version (v11+) of the phone.
- Tab tab = Tabs.getInstance().getSelectedTab();
- if (tab != null && tab.hasOpenSearch()) {
- JSONObject args = new JSONObject();
- try {
- args.put("tabId", tab.getId());
- } catch (JSONException e) {
- Log.e(LOGTAG, "error building json arguments", e);
- return true;
- }
- GeckoAppShell.notifyObservers("SearchEngines:Add", args.toString());
- }
- return true;
- }
-
- if (itemId == R.id.copyurl) {
- Tab tab = Tabs.getInstance().getSelectedTab();
- if (tab != null) {
- String url = ReaderModeUtils.stripAboutReaderUrl(tab.getURL());
- if (url != null) {
- Clipboard.setText(url);
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "copyurl");
- }
- }
- return true;
- }
-
- if (itemId == R.id.add_to_launcher) {
- final Tab tab = Tabs.getInstance().getSelectedTab();
- if (tab == null) {
- return true;
- }
-
- final String url = tab.getURL();
- final String title = tab.getDisplayTitle();
- if (url == null || title == null) {
- return true;
- }
-
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- GeckoAppShell.createShortcut(title, url);
-
- }
- });
-
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU,
- getResources().getResourceEntryName(itemId));
- return true;
- }
-
- return false;
- }
-
- @Override
- public void setAccessibilityEnabled(boolean enabled) {
- super.setAccessibilityEnabled(enabled);
- mDynamicToolbar.setAccessibilityEnabled(enabled);
- }
-
- @Override
- public void onDestroy() {
- if (mIsAbortingAppLaunch) {
- super.onDestroy();
- return;
- }
-
- mDynamicToolbar.destroy();
-
- if (mBrowserToolbar != null)
- mBrowserToolbar.onDestroy();
-
- if (mFindInPageBar != null) {
- mFindInPageBar.onDestroy();
- mFindInPageBar = null;
- }
-
- if (mMediaCastingBar != null) {
- mMediaCastingBar.onDestroy();
- mMediaCastingBar = null;
- }
-
- if (mSharedPreferencesHelper != null) {
- mSharedPreferencesHelper.uninit();
- mSharedPreferencesHelper = null;
- }
-
- if (mReadingListHelper != null) {
- mReadingListHelper.uninit();
- mReadingListHelper = null;
- }
-
- if (mAccountsHelper != null) {
- mAccountsHelper.uninit();
- mAccountsHelper = null;
- }
-
- if (mZoomedView != null) {
- mZoomedView.destroy();
- }
-
- mSearchEngineManager.unregisterListeners();
-
- EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) this,
- "Gecko:DelayedStartup",
- "Menu:Open",
- "Menu:Update",
- "LightweightTheme:Update",
- "Search:Keyword",
- "Prompt:ShowTop",
- "Tab:Added",
- "Video:Play");
-
- EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener) this,
- "CharEncoding:Data",
- "CharEncoding:State",
- "Download:AndroidDownloadManager",
- "Experiments:GetActive",
- "Experiments:SetOverride",
- "Experiments:ClearOverride",
- "Favicon:CacheLoad",
- "Feedback:MaybeLater",
- "Menu:Add",
- "Menu:Remove",
- "Sanitize:ClearHistory",
- "Sanitize:ClearSyncedTabs",
- "Settings:Show",
- "Telemetry:Gather",
- "Updater:Launch",
- "Website:Metadata");
-
- if (AppConstants.MOZ_ANDROID_BEAM) {
- NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
- if (nfc != null) {
- // null this out even though the docs say it's not needed,
- // because the source code looks like it will only do this
- // automatically on API 14+
- nfc.setNdefPushMessageCallback(null, this);
- }
- }
-
- for (final BrowserAppDelegate delegate : delegates) {
- delegate.onDestroy(this);
- }
-
- deleteTempFiles();
-
- if (mDoorHangerPopup != null)
- mDoorHangerPopup.destroy();
- if (mFormAssistPopup != null)
- mFormAssistPopup.destroy();
- if (mTextSelection != null)
- mTextSelection.destroy();
- NotificationHelper.destroy();
- IntentHelper.destroy();
- GeckoNetworkManager.destroy();
-
- super.onDestroy();
-
- if (!isFinishing()) {
- // GeckoApp was not intentionally destroyed, so keep our process alive.
- return;
- }
-
- // Wait for Gecko to handle our pause event sent in onPause.
- if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
- GeckoThread.waitOnGecko();
- }
-
- if (mRestartIntent != null) {
- // Restarting, so let Restarter kill us.
- final Intent intent = new Intent();
- intent.setClass(getApplicationContext(), Restarter.class)
- .putExtra("pid", Process.myPid())
- .putExtra(Intent.EXTRA_INTENT, mRestartIntent);
- startService(intent);
- } else {
- // Exiting, so kill our own process.
- Process.killProcess(Process.myPid());
- }
- }
-
- @Override
- protected void initializeChrome() {
- super.initializeChrome();
-
- mDoorHangerPopup.setAnchor(mBrowserToolbar.getDoorHangerAnchor());
- mDoorHangerPopup.setOnVisibilityChangeListener(this);
-
- mDynamicToolbar.setLayerView(mLayerView);
- setDynamicToolbarEnabled(mDynamicToolbar.isEnabled());
-
- // Intercept key events for gamepad shortcuts
- mLayerView.setOnKeyListener(this);
-
- // Initialize the actionbar menu items on startup for both large and small tablets
- if (HardwareUtils.isTablet()) {
- onCreatePanelMenu(Window.FEATURE_OPTIONS_PANEL, null);
- invalidateOptionsMenu();
- }
- }
-
- @Override
- public void onDoorHangerShow() {
- mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
-
- final Animator alphaAnimator = ObjectAnimator.ofFloat(mDoorhangerOverlay, "alpha", 1);
- alphaAnimator.setDuration(250);
-
- alphaAnimator.start();
- }
-
- @Override
- public void onDoorHangerHide() {
- final Animator alphaAnimator = ObjectAnimator.ofFloat(mDoorhangerOverlay, "alpha", 0);
- alphaAnimator.setDuration(200);
-
- alphaAnimator.start();
- }
-
- private void handleClearHistory(final boolean clearSearchHistory) {
- final BrowserDB db = BrowserDB.from(getProfile());
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- db.clearHistory(getContentResolver(), clearSearchHistory);
- }
- });
- }
-
- private void handleClearSyncedTabs() {
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- FennecTabsRepository.deleteNonLocalClientsAndTabs(getContext());
- }
- });
- }
-
- private void setToolbarMargin(int margin) {
- ((RelativeLayout.LayoutParams) mGeckoLayout.getLayoutParams()).topMargin = margin;
- mGeckoLayout.requestLayout();
- }
-
- @Override
- public void onTranslationChanged(float aToolbarTranslation, float aLayerViewTranslation) {
- if (mBrowserChrome == null) {
- return;
- }
-
- final View browserChrome = mBrowserChrome;
- final ToolbarProgressView progressView = mProgressView;
-
- ViewHelper.setTranslationY(browserChrome, -aToolbarTranslation);
- mLayerView.setSurfaceTranslation(mToolbarHeight - aLayerViewTranslation);
-
- // Stop the progressView from moving all the way up so that we can still see a good chunk of it
- // when the chrome is offscreen.
- final float offset = getResources().getDimensionPixelOffset(R.dimen.progress_bar_scroll_offset);
- final float progressTranslationY = Math.min(aToolbarTranslation, mToolbarHeight - offset);
- ViewHelper.setTranslationY(progressView, -progressTranslationY);
-
- if (mFormAssistPopup != null) {
- mFormAssistPopup.onTranslationChanged();
- }
- }
-
- @Override
- public void onMetricsChanged(ImmutableViewportMetrics aMetrics) {
- if (isHomePagerVisible() || mBrowserChrome == null) {
- return;
- }
-
- if (mFormAssistPopup != null) {
- mFormAssistPopup.onMetricsChanged(aMetrics);
- }
- }
-
- @Override
- public void onPanZoomStopped() {
- if (!mDynamicToolbar.isEnabled() || isHomePagerVisible() ||
- mBrowserChrome.getVisibility() != View.VISIBLE) {
- return;
- }
-
- // Make sure the toolbar is fully hidden or fully shown when the user
- // lifts their finger, depending on various conditions.
- ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics();
- float toolbarTranslation = mLayerView.getDynamicToolbarAnimator().getToolbarTranslation();
-
- boolean shortPage = metrics.getPageHeight() < metrics.getHeight();
- boolean atBottomOfLongPage =
- FloatUtils.fuzzyEquals(metrics.pageRectBottom, metrics.viewportRectBottom())
- && (metrics.pageRectBottom > 2 * metrics.getHeight());
- Log.v(LOGTAG, "On pan/zoom stopped, short page: " + shortPage
- + "; atBottomOfLongPage: " + atBottomOfLongPage);
- if (shortPage || atBottomOfLongPage) {
- mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
- }
- }
-
- public void refreshToolbarHeight() {
- ThreadUtils.assertOnUiThread();
-
- int height = 0;
- if (mBrowserChrome != null) {
- height = mBrowserChrome.getHeight();
- }
-
- if (!mDynamicToolbar.isEnabled() || isHomePagerVisible()) {
- // Use aVisibleHeight here so that when the dynamic toolbar is
- // enabled, the padding will animate with the toolbar becoming
- // visible.
- if (mDynamicToolbar.isEnabled()) {
- // When the dynamic toolbar is enabled, set the padding on the
- // about:home widget directly - this is to avoid resizing the
- // LayerView, which can cause visible artifacts.
- mHomeScreenContainer.setPadding(0, height, 0, 0);
- } else {
- setToolbarMargin(height);
- height = 0;
- }
- } else {
- setToolbarMargin(0);
- }
-
- if (mLayerView != null && height != mToolbarHeight) {
- mToolbarHeight = height;
- mLayerView.setMaxTranslation(height);
- mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
- }
- }
-
- @Override
- void toggleChrome(final boolean aShow) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- if (aShow) {
- mBrowserChrome.setVisibility(View.VISIBLE);
- } else {
- mBrowserChrome.setVisibility(View.GONE);
- }
- }
- });
-
- super.toggleChrome(aShow);
- }
-
- @Override
- void focusChrome() {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- mBrowserChrome.setVisibility(View.VISIBLE);
- mActionBarFlipper.requestFocusFromTouch();
- }
- });
- }
-
- @Override
- public void refreshChrome() {
- invalidateOptionsMenu();
-
- if (mTabsPanel != null) {
- mTabsPanel.refresh();
- }
-
- if (mTabStrip != null) {
- mTabStrip.refresh();
- }
-
- mBrowserToolbar.refresh();
- }
-
- @Override
- public void handleMessage(final String event, final NativeJSObject message,
- final EventCallback callback) {
- switch (event) {
- case "CharEncoding:Data":
- final NativeJSObject[] charsets = message.getObjectArray("charsets");
- final int selected = message.getInt("selected");
-
- final String[] titleArray = new String[charsets.length];
- final String[] codeArray = new String[charsets.length];
- for (int i = 0; i < charsets.length; i++) {
- final NativeJSObject charset = charsets[i];
- titleArray[i] = charset.getString("title");
- codeArray[i] = charset.getString("code");
- }
-
- final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
- dialogBuilder.setSingleChoiceItems(titleArray, selected,
- new AlertDialog.OnClickListener() {
- @Override
- public void onClick(final DialogInterface dialog, final int which) {
- GeckoAppShell.notifyObservers("CharEncoding:Set", codeArray[which]);
- dialog.dismiss();
- }
- });
- dialogBuilder.setNegativeButton(R.string.button_cancel,
- new AlertDialog.OnClickListener() {
- @Override
- public void onClick(final DialogInterface dialog, final int which) {
- dialog.dismiss();
- }
- });
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- dialogBuilder.show();
- }
- });
- break;
-
- case "CharEncoding:State":
- final boolean visible = message.getString("visible").equals("true");
- GeckoPreferences.setCharEncodingState(visible);
- final Menu menu = mMenu;
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- if (menu != null) {
- menu.findItem(R.id.char_encoding).setVisible(visible);
- }
- }
- });
- break;
-
- case "Experiments:GetActive":
- final List<String> experiments = SwitchBoard.getActiveExperiments(this);
- final JSONArray json = new JSONArray(experiments);
- callback.sendSuccess(json.toString());
- break;
-
- case "Experiments:SetOverride":
- Experiments.setOverride(getContext(), message.getString("name"), message.getBoolean("isEnabled"));
- break;
-
- case "Experiments:ClearOverride":
- Experiments.clearOverride(getContext(), message.getString("name"));
- break;
-
- case "Favicon:CacheLoad":
- final String url = message.getString("url");
- getFaviconFromCache(callback, url);
- break;
-
- case "Feedback:MaybeLater":
- resetFeedbackLaunchCount();
- break;
-
- case "Menu:Add":
- final MenuItemInfo info = new MenuItemInfo();
- info.label = message.getString("name");
- info.id = message.getInt("id") + ADDON_MENU_OFFSET;
- info.checked = message.optBoolean("checked", false);
- info.enabled = message.optBoolean("enabled", true);
- info.visible = message.optBoolean("visible", true);
- info.checkable = message.optBoolean("checkable", false);
- final int parent = message.optInt("parent", 0);
- info.parent = parent <= 0 ? parent : parent + ADDON_MENU_OFFSET;
- final MenuItemInfo menuItemInfo = info;
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- addAddonMenuItem(menuItemInfo);
- }
- });
- break;
-
- case "Menu:Remove":
- final int id = message.getInt("id") + ADDON_MENU_OFFSET;
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- removeAddonMenuItem(id);
- }
- });
- break;
-
- case "Sanitize:ClearHistory":
- handleClearHistory(message.optBoolean("clearSearchHistory", false));
- callback.sendSuccess(true);
- break;
-
- case "Sanitize:ClearSyncedTabs":
- handleClearSyncedTabs();
- callback.sendSuccess(true);
- break;
-
- case "Settings:Show":
- final String resource =
- message.optString(GeckoPreferences.INTENT_EXTRA_RESOURCES, null);
- final Intent settingsIntent = new Intent(this, GeckoPreferences.class);
- GeckoPreferences.setResourceToOpen(settingsIntent, resource);
- startActivityForResult(settingsIntent, ACTIVITY_REQUEST_PREFERENCES);
-
- // Don't use a transition to settings if we're on a device where that
- // would look bad.
- if (HardwareUtils.IS_KINDLE_DEVICE) {
- overridePendingTransition(0, 0);
- }
- break;
-
- case "Telemetry:Gather":
- final BrowserDB db = BrowserDB.from(getProfile());
- final ContentResolver cr = getContentResolver();
- Telemetry.addToHistogram("PLACES_PAGES_COUNT", db.getCount(cr, "history"));
- Telemetry.addToHistogram("FENNEC_BOOKMARKS_COUNT", db.getCount(cr, "bookmarks"));
- Telemetry.addToHistogram("BROWSER_IS_USER_DEFAULT", (isDefaultBrowser(Intent.ACTION_VIEW) ? 1 : 0));
- Telemetry.addToHistogram("FENNEC_CUSTOM_HOMEPAGE", (TextUtils.isEmpty(getHomepage()) ? 0 : 1));
- final SharedPreferences prefs = GeckoSharedPrefs.forProfile(getContext());
- final boolean hasCustomHomepanels =
- prefs.contains(HomeConfigPrefsBackend.PREFS_CONFIG_KEY) || prefs.contains(HomeConfigPrefsBackend.PREFS_CONFIG_KEY_OLD);
- Telemetry.addToHistogram("FENNEC_HOMEPANELS_CUSTOM", hasCustomHomepanels ? 1 : 0);
-
- Telemetry.addToHistogram("FENNEC_READER_VIEW_CACHE_SIZE",
- SavedReaderViewHelper.getSavedReaderViewHelper(getContext()).getDiskSpacedUsedKB());
-
- if (Versions.feature16Plus) {
- Telemetry.addToHistogram("BROWSER_IS_ASSIST_DEFAULT", (isDefaultBrowser(Intent.ACTION_ASSIST) ? 1 : 0));
- }
-
- Telemetry.addToHistogram("FENNEC_ORBOT_INSTALLED",
- ContextUtils.isPackageInstalled(getContext(), "org.torproject.android") ? 1 : 0);
- break;
-
- case "Updater:Launch":
- handleUpdaterLaunch();
- break;
-
- case "Download:AndroidDownloadManager":
- // Downloading via Android's download manager
-
- final String uri = message.getString("uri");
- final String filename = message.getString("filename");
- final String mimeType = message.getString("mimeType");
-
- final DownloadManager.Request request = new DownloadManager.Request(Uri.parse(uri));
- request.setMimeType(mimeType);
-
- try {
- request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename);
- } catch (IllegalStateException e) {
- Log.e(LOGTAG, "Cannot create download directory");
- return;
- }
-
- request.allowScanningByMediaScanner();
- request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
- request.addRequestHeader("User-Agent", HardwareUtils.isTablet() ?
- AppConstants.USER_AGENT_FENNEC_TABLET :
- AppConstants.USER_AGENT_FENNEC_MOBILE);
-
- try {
- DownloadManager manager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
- manager.enqueue(request);
-
- Log.d(LOGTAG, "Enqueued download (Download Manager)");
- } catch (RuntimeException e) {
- Log.e(LOGTAG, "Download failed: " + e);
- }
- break;
-
- case "Website:Metadata":
- final NativeJSObject metadata = message.getObject("metadata");
- final String location = message.getString("location");
-
- final boolean hasImage = !TextUtils.isEmpty(metadata.optString("image_url", null));
- final String metadataJSON = metadata.toString();
-
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- final ContentProviderClient contentProviderClient = getContentResolver()
- .acquireContentProviderClient(BrowserContract.PageMetadata.CONTENT_URI);
- if (contentProviderClient == null) {
- Log.w(LOGTAG, "Failed to obtain content provider client for: " + BrowserContract.PageMetadata.CONTENT_URI);
- return;
- }
- try {
- GlobalPageMetadata.getInstance().add(
- BrowserDB.from(getProfile()),
- contentProviderClient,
- location, hasImage, metadataJSON);
- } finally {
- contentProviderClient.release();
- }
- }
- });
-
- break;
-
- default:
- super.handleMessage(event, message, callback);
- break;
- }
- }
-
- private void getFaviconFromCache(final EventCallback callback, final String url) {
- Icons.with(this)
- .pageUrl(url)
- .skipNetwork()
- .executeCallbackOnBackgroundThread()
- .build()
- .execute(new IconCallback() {
- @Override
- public void onIconResponse(IconResponse response) {
- ByteArrayOutputStream out = null;
- Base64OutputStream b64 = null;
-
- try {
- out = new ByteArrayOutputStream();
- out.write("data:image/png;base64,".getBytes());
- b64 = new Base64OutputStream(out, Base64.NO_WRAP);
- response.getBitmap().compress(Bitmap.CompressFormat.PNG, 100, b64);
- callback.sendSuccess(new String(out.toByteArray()));
- } catch (IOException e) {
- Log.w(LOGTAG, "Failed to convert to base64 data URI");
- callback.sendError("Failed to convert favicon to a base64 data URI");
- } finally {
- try {
- if (out != null) {
- out.close();
- }
- if (b64 != null) {
- b64.close();
- }
- } catch (IOException e) {
- Log.w(LOGTAG, "Failed to close the streams");
- }
- }
- }
- });
- }
-
- /**
- * Use a dummy Intent to do a default browser check.
- *
- * @return true if this package is the default browser on this device, false otherwise.
- */
- private boolean isDefaultBrowser(String action) {
- final Intent viewIntent = new Intent(action, Uri.parse("http://www.mozilla.org"));
- final ResolveInfo info = getPackageManager().resolveActivity(viewIntent, PackageManager.MATCH_DEFAULT_ONLY);
- if (info == null) {
- // No default is set
- return false;
- }
-
- final String packageName = info.activityInfo.packageName;
- return (TextUtils.equals(packageName, getPackageName()));
- }
-
- @Override
- public void handleMessage(String event, JSONObject message) {
- try {
- switch (event) {
- case "Menu:Open":
- if (mBrowserToolbar.isEditing()) {
- mBrowserToolbar.cancelEdit();
- }
-
- openOptionsMenu();
- break;
-
- case "Menu:Update":
- final int id = message.getInt("id") + ADDON_MENU_OFFSET;
- final JSONObject options = message.getJSONObject("options");
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- updateAddonMenuItem(id, options);
- }
- });
- break;
-
- case "Gecko:DelayedStartup":
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- // Force tabs panel inflation once the initial
- // pageload is finished.
- ensureTabsPanelExists();
- if (AppConstants.NIGHTLY_BUILD && mZoomedView == null) {
- ViewStub stub = (ViewStub) findViewById(R.id.zoomed_view_stub);
- mZoomedView = (ZoomedView) stub.inflate();
- }
- }
- });
-
- if (AppConstants.MOZ_MEDIA_PLAYER) {
- // Check if the fragment is already added. This should never be true here, but this is
- // a nice safety check.
- // If casting is disabled, these classes aren't built. We use reflection to initialize them.
- final Class<?> mediaManagerClass = getMediaPlayerManager();
-
- if (mediaManagerClass != null) {
- try {
- final String tag = "";
- mediaManagerClass.getDeclaredField("MEDIA_PLAYER_TAG").get(tag);
- Log.i(LOGTAG, "Found tag " + tag);
- final Fragment frag = getSupportFragmentManager().findFragmentByTag(tag);
- if (frag == null) {
- final Method getInstance = mediaManagerClass.getMethod("getInstance", (Class[]) null);
- final Fragment mpm = (Fragment) getInstance.invoke(null);
- getSupportFragmentManager().beginTransaction().disallowAddToBackStack().add(mpm, tag).commit();
- }
- } catch (Exception ex) {
- Log.e(LOGTAG, "Error initializing media manager", ex);
- }
- }
- }
-
- if (AppConstants.MOZ_STUMBLER_BUILD_TIME_ENABLED && Restrictions.isAllowed(this, Restrictable.DATA_CHOICES)) {
- // Start (this acts as ping if started already) the stumbler lib; if the stumbler has queued data it will upload it.
- // Stumbler operates on its own thread, and startup impact is further minimized by delaying work (such as upload) a few seconds.
- // Avoid any potential startup CPU/thread contention by delaying the pref broadcast.
- final long oneSecondInMillis = 1000;
- ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() {
- @Override
- public void run() {
- GeckoPreferences.broadcastStumblerPref(BrowserApp.this);
- }
- }, oneSecondInMillis);
- }
-
- if (AppConstants.MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE) {
- // TODO: Better scheduling of sync action (Bug 1257492)
- DownloadContentService.startSync(this);
-
- DownloadContentService.startVerification(this);
- }
-
- FeedService.setup(this);
-
- super.handleMessage(event, message);
- break;
-
- case "Gecko:Ready":
- // Handle this message in GeckoApp, but also enable the Settings
- // menuitem, which is specific to BrowserApp.
- super.handleMessage(event, message);
- final Menu menu = mMenu;
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- if (menu != null) {
- menu.findItem(R.id.settings).setEnabled(true);
- menu.findItem(R.id.help).setEnabled(true);
- }
- }
- });
-
- // Display notification for Mozilla data reporting, if data should be collected.
- if (AppConstants.MOZ_DATA_REPORTING && Restrictions.isAllowed(this, Restrictable.DATA_CHOICES)) {
- DataReportingNotification.checkAndNotifyPolicy(GeckoAppShell.getContext());
- }
- break;
-
- case "Search:Keyword":
- storeSearchQuery(message.getString("query"));
- recordSearch(GeckoSharedPrefs.forProfile(this), message.getString("identifier"),
- TelemetryContract.Method.ACTIONBAR);
- break;
-
- case "LightweightTheme:Update":
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
- }
- });
- break;
-
- case "Video:Play":
- if (SwitchBoard.isInExperiment(this, Experiments.HLS_VIDEO_PLAYBACK)) {
- final String uri = message.getString("uri");
- final String uuid = message.getString("uuid");
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- mVideoPlayer.start(Uri.parse(uri));
- Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.CONTENT, "playhls");
- }
- });
- }
- break;
-
- case "Prompt:ShowTop":
- // Bring this activity to front so the prompt is visible..
- Intent bringToFrontIntent = new Intent();
- bringToFrontIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
- bringToFrontIntent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
- startActivity(bringToFrontIntent);
- break;
-
- case "Tab:Added":
- if (message.getBoolean("cancelEditMode")) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- // Set the target tab to null so it does not get selected (on editing
- // mode exit) in lieu of the tab that we're going to open and select.
- mTargetTabForEditingMode = null;
- mBrowserToolbar.cancelEdit();
- }
- });
- }
- break;
-
- default:
- super.handleMessage(event, message);
- break;
- }
- } catch (Exception e) {
- Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
- }
- }
-
- @Override
- public void addTab() {
- Tabs.getInstance().addTab();
- }
-
- @Override
- public void addPrivateTab() {
- Tabs.getInstance().addPrivateTab();
- }
-
- public void showTrackingProtectionPromptIfApplicable() {
- final SharedPreferences prefs = getSharedPreferences();
-
- final boolean hasTrackingProtectionPromptBeShownBefore = prefs.getBoolean(GeckoPreferences.PREFS_TRACKING_PROTECTION_PROMPT_SHOWN, false);
-
- if (hasTrackingProtectionPromptBeShownBefore) {
- return;
- }
-
- prefs.edit().putBoolean(GeckoPreferences.PREFS_TRACKING_PROTECTION_PROMPT_SHOWN, true).apply();
-
- startActivity(new Intent(BrowserApp.this, TrackingProtectionPrompt.class));
- }
-
- @Override
- public void showNormalTabs() {
- showTabs(TabsPanel.Panel.NORMAL_TABS);
- }
-
- @Override
- public void showPrivateTabs() {
- showTabs(TabsPanel.Panel.PRIVATE_TABS);
- }
- /**
- * Ensure the TabsPanel view is properly inflated and returns
- * true when the view has been inflated, false otherwise.
- */
- private boolean ensureTabsPanelExists() {
- if (mTabsPanel != null) {
- return false;
- }
-
- ViewStub tabsPanelStub = (ViewStub) findViewById(R.id.tabs_panel);
- mTabsPanel = (TabsPanel) tabsPanelStub.inflate();
-
- mTabsPanel.setTabsLayoutChangeListener(this);
-
- return true;
- }
-
- private void showTabs(final TabsPanel.Panel panel) {
- if (Tabs.getInstance().getDisplayCount() == 0)
- return;
-
- hideFirstrunPager(TelemetryContract.Method.BUTTON);
-
- if (ensureTabsPanelExists()) {
- // If we've just inflated the tabs panel, only show it once the current
- // layout pass is done to avoid displayed temporary UI states during
- // relayout.
- ViewTreeObserver vto = mTabsPanel.getViewTreeObserver();
- if (vto.isAlive()) {
- vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
- @Override
- public void onGlobalLayout() {
- mTabsPanel.getViewTreeObserver().removeGlobalOnLayoutListener(this);
- showTabs(panel);
- }
- });
- }
- } else {
- if (mDoorHangerPopup != null) {
- mDoorHangerPopup.disable();
- }
- mTabsPanel.show(panel);
-
- // Hide potentially visible "find in page" bar (Bug 1177338)
- mFindInPageBar.hide();
-
- for (final BrowserAppDelegate delegate : delegates) {
- delegate.onTabsTrayShown(this, mTabsPanel);
- }
- }
- }
-
- @Override
- public void hideTabs() {
- mTabsPanel.hide();
- if (mDoorHangerPopup != null) {
- mDoorHangerPopup.enable();
- }
-
- for (final BrowserAppDelegate delegate : delegates) {
- delegate.onTabsTrayHidden(this, mTabsPanel);
- }
- }
-
- @Override
- public boolean autoHideTabs() {
- if (areTabsShown()) {
- hideTabs();
- return true;
- }
- return false;
- }
-
- @Override
- public boolean areTabsShown() {
- return (mTabsPanel != null && mTabsPanel.isShown());
- }
-
- @Override
- public String getHomepage() {
- final SharedPreferences preferences = GeckoSharedPrefs.forProfile(this);
- final String homepagePreference = preferences.getString(GeckoPreferences.PREFS_HOMEPAGE, null);
-
- final boolean readFromPartnerProvider = preferences.getBoolean(
- GeckoPreferences.PREFS_READ_PARTNER_CUSTOMIZATIONS_PROVIDER, false);
-
- if (!readFromPartnerProvider) {
- // Just return homepage as set by the user (or null).
- return homepagePreference;
- }
-
-
- final String homepagePrevious = preferences.getString(GeckoPreferences.PREFS_HOMEPAGE_PARTNER_COPY, null);
- if (homepagePrevious != null && !homepagePrevious.equals(homepagePreference)) {
- // We have read the homepage once and the user has changed it since then. Just use the
- // value the user has set.
- return homepagePreference;
- }
-
- // This is the first time we read the partner provider or the value has not been altered by the user
- final String homepagePartner = PartnerBrowserCustomizationsClient.getHomepage(this);
-
- if (homepagePartner == null) {
- // We didn't get anything from the provider. Let's just use what we have locally.
- return homepagePreference;
- }
-
- if (!homepagePartner.equals(homepagePrevious)) {
- // We have a new value. Update the preferences.
- preferences.edit()
- .putString(GeckoPreferences.PREFS_HOMEPAGE, homepagePartner)
- .putString(GeckoPreferences.PREFS_HOMEPAGE_PARTNER_COPY, homepagePartner)
- .apply();
- }
-
- return homepagePartner;
- }
-
- @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
- @Override
- public void onTabsLayoutChange(int width, int height) {
- int animationLength = TABS_ANIMATION_DURATION;
-
- if (mMainLayoutAnimator != null) {
- animationLength = Math.max(1, animationLength - (int)mMainLayoutAnimator.getRemainingTime());
- mMainLayoutAnimator.stop(false);
- }
-
- if (areTabsShown()) {
- mTabsPanel.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
- // Hide the web content from accessibility tools even though it's visible
- // so that you can't examine it as long as the tabs are being shown.
- if (Versions.feature16Plus) {
- mLayerView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
- }
- } else {
- if (Versions.feature16Plus) {
- mLayerView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
- }
- }
-
- mMainLayoutAnimator = new PropertyAnimator(animationLength, sTabsInterpolator);
- mMainLayoutAnimator.addPropertyAnimationListener(this);
- mMainLayoutAnimator.attach(mMainLayout,
- PropertyAnimator.Property.SCROLL_Y,
- -height);
-
- mTabsPanel.prepareTabsAnimation(mMainLayoutAnimator);
- mBrowserToolbar.triggerTabsPanelTransition(mMainLayoutAnimator, areTabsShown());
-
- // If the tabs panel is animating onto the screen, pin the dynamic
- // toolbar.
- if (mDynamicToolbar.isEnabled()) {
- if (width > 0 && height > 0) {
- mDynamicToolbar.setPinned(true, PinReason.RELAYOUT);
- mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
- } else {
- mDynamicToolbar.setPinned(false, PinReason.RELAYOUT);
- }
- }
-
- mMainLayoutAnimator.start();
- }
-
- @Override
- public void onPropertyAnimationStart() {
- }
-
- @Override
- public void onPropertyAnimationEnd() {
- if (!areTabsShown()) {
- mTabsPanel.setVisibility(View.INVISIBLE);
- mTabsPanel.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
- } else {
- // Cancel editing mode to return to page content when the TabsPanel closes. We cancel
- // it here because there are graphical glitches if it's canceled while it's visible.
- mBrowserToolbar.cancelEdit();
- }
-
- mTabsPanel.finishTabsAnimation();
-
- mMainLayoutAnimator = null;
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- mDynamicToolbar.onSaveInstanceState(outState);
- outState.putInt(STATE_ABOUT_HOME_TOP_PADDING, mHomeScreenContainer.getPaddingTop());
- }
-
- /**
- * Attempts to switch to an open tab with the given URL.
- * <p>
- * If the tab exists, this method cancels any in-progress editing as well as
- * calling {@link Tabs#selectTab(int)}.
- *
- * @param url of tab to switch to.
- * @param flags to obey: if {@link OnUrlOpenListener.Flags#ALLOW_SWITCH_TO_TAB}
- * is not present, return false.
- * @return true if we successfully switched to a tab, false otherwise.
- */
- private boolean maybeSwitchToTab(String url, EnumSet<OnUrlOpenListener.Flags> flags) {
- if (!flags.contains(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)) {
- return false;
- }
-
- final Tabs tabs = Tabs.getInstance();
- final Tab tab;
-
- if (AboutPages.isAboutReader(url)) {
- tab = tabs.getFirstReaderTabForUrl(url, tabs.getSelectedTab().isPrivate());
- } else {
- tab = tabs.getFirstTabForUrl(url, tabs.getSelectedTab().isPrivate());
- }
-
- if (tab == null) {
- return false;
- }
-
- return maybeSwitchToTab(tab.getId());
- }
-
- /**
- * Attempts to switch to an open tab with the given unique tab ID.
- * <p>
- * If the tab exists, this method cancels any in-progress editing as well as
- * calling {@link Tabs#selectTab(int)}.
- *
- * @param id of tab to switch to.
- * @return true if we successfully switched to the tab, false otherwise.
- */
- private boolean maybeSwitchToTab(int id) {
- final Tabs tabs = Tabs.getInstance();
- final Tab tab = tabs.getTab(id);
-
- if (tab == null) {
- return false;
- }
-
- final Tab oldTab = tabs.getSelectedTab();
- if (oldTab != null) {
- oldTab.setIsEditing(false);
- }
-
- // Set the target tab to null so it does not get selected (on editing
- // mode exit) in lieu of the tab we are about to select.
- mTargetTabForEditingMode = null;
- tabs.selectTab(tab.getId());
-
- mBrowserToolbar.cancelEdit();
-
- return true;
- }
-
- public void openUrlAndStopEditing(String url) {
- openUrlAndStopEditing(url, null, false);
- }
-
- private void openUrlAndStopEditing(String url, boolean newTab) {
- openUrlAndStopEditing(url, null, newTab);
- }
-
- private void openUrlAndStopEditing(String url, String searchEngine) {
- openUrlAndStopEditing(url, searchEngine, false);
- }
-
- private void openUrlAndStopEditing(String url, String searchEngine, boolean newTab) {
- int flags = Tabs.LOADURL_NONE;
- if (newTab) {
- flags |= Tabs.LOADURL_NEW_TAB;
- if (Tabs.getInstance().getSelectedTab().isPrivate()) {
- flags |= Tabs.LOADURL_PRIVATE;
- }
- }
-
- Tabs.getInstance().loadUrl(url, searchEngine, -1, flags);
-
- mBrowserToolbar.cancelEdit();
- }
-
- private boolean isHomePagerVisible() {
- return (mHomeScreen != null && mHomeScreen.isVisible()
- && mHomeScreenContainer != null && mHomeScreenContainer.getVisibility() == View.VISIBLE);
- }
-
- private boolean isFirstrunVisible() {
- return (mFirstrunAnimationContainer != null && mFirstrunAnimationContainer.isVisible()
- && mHomeScreenContainer != null && mHomeScreenContainer.getVisibility() == View.VISIBLE);
- }
-
- /**
- * Enters editing mode with the current tab's URL. There might be no
- * tabs loaded by the time the user enters editing mode e.g. just after
- * the app starts. In this case, we simply fallback to an empty URL.
- */
- private void enterEditingMode() {
- String url = "";
- String telemetryMsg = "urlbar-empty";
-
- final Tab tab = Tabs.getInstance().getSelectedTab();
- if (tab != null) {
- final String userSearchTerm = tab.getUserRequested();
- final String tabURL = tab.getURL();
-
- // Check to see if there's a user-entered search term,
- // which we save whenever the user performs a search.
- if (!TextUtils.isEmpty(userSearchTerm)) {
- url = userSearchTerm;
- telemetryMsg = "urlbar-userentered";
- } else if (!TextUtils.isEmpty(tabURL)) {
- url = tabURL;
- telemetryMsg = "urlbar-url";
- }
- }
-
- enterEditingMode(url);
- Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.ACTIONBAR, telemetryMsg);
- }
-
- /**
- * Enters editing mode with the specified URL. If a null
- * url is given, the empty String will be used instead.
- */
- private void enterEditingMode(@NonNull String url) {
- hideFirstrunPager(TelemetryContract.Method.ACTIONBAR);
-
- if (mBrowserToolbar.isEditing() || mBrowserToolbar.isAnimating()) {
- return;
- }
-
- final Tab selectedTab = Tabs.getInstance().getSelectedTab();
- final String panelId;
- if (selectedTab != null) {
- mTargetTabForEditingMode = selectedTab.getId();
- panelId = selectedTab.getMostRecentHomePanel();
- } else {
- mTargetTabForEditingMode = null;
- panelId = null;
- }
-
- final PropertyAnimator animator = new PropertyAnimator(250);
- animator.setUseHardwareLayer(false);
-
- mBrowserToolbar.startEditing(url, animator);
-
- showHomePagerWithAnimator(panelId, null, animator);
-
- animator.start();
- Telemetry.startUISession(TelemetryContract.Session.AWESOMESCREEN);
- }
-
- private void commitEditingMode() {
- if (!mBrowserToolbar.isEditing()) {
- return;
- }
-
- Telemetry.stopUISession(TelemetryContract.Session.AWESOMESCREEN,
- TelemetryContract.Reason.COMMIT);
-
- final String url = mBrowserToolbar.commitEdit();
-
- // HACK: We don't know the url that will be loaded when hideHomePager is initially called
- // in BrowserToolbar's onStopEditing listener so on the awesomescreen, hideHomePager will
- // use the url "about:home" and return without taking any action. hideBrowserSearch is
- // then called, but since hideHomePager changes both HomePager and LayerView visibility
- // and exited without taking an action, no Views are displayed and graphical corruption is
- // visible instead.
- //
- // Here we call hideHomePager for the second time with the URL to be loaded so that
- // hideHomePager is called with the correct state for the upcoming page load.
- //
- // Expected to be fixed by bug 915825.
- hideHomePager(url);
- loadUrlOrKeywordSearch(url);
- clearSelectedTabApplicationId();
- }
-
- private void clearSelectedTabApplicationId() {
- final Tab selected = Tabs.getInstance().getSelectedTab();
- if (selected != null) {
- selected.setApplicationId(null);
- }
- }
-
- private void loadUrlOrKeywordSearch(final String url) {
- // Don't do anything if the user entered an empty URL.
- if (TextUtils.isEmpty(url)) {
- return;
- }
-
- // If the URL doesn't look like a search query, just load it.
- if (!StringUtils.isSearchQuery(url, true)) {
- Tabs.getInstance().loadUrl(url, Tabs.LOADURL_USER_ENTERED);
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.ACTIONBAR, "user");
- return;
- }
-
- // Otherwise, check for a bookmark keyword.
- final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfile(this);
- final BrowserDB db = BrowserDB.from(getProfile());
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- final String keyword;
- final String keywordSearch;
-
- final int index = url.indexOf(" ");
- if (index == -1) {
- keyword = url;
- keywordSearch = "";
- } else {
- keyword = url.substring(0, index);
- keywordSearch = url.substring(index + 1);
- }
-
- final String keywordUrl = db.getUrlForKeyword(getContentResolver(), keyword);
-
- // If there isn't a bookmark keyword, load the url. This may result in a query
- // using the default search engine.
- if (TextUtils.isEmpty(keywordUrl)) {
- Tabs.getInstance().loadUrl(url, Tabs.LOADURL_USER_ENTERED);
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.ACTIONBAR, "user");
- return;
- }
-
- // Otherwise, construct a search query from the bookmark keyword.
- // Replace lower case bookmark keywords with URLencoded search query or
- // replace upper case bookmark keywords with un-encoded search query.
- // This makes it match the same behaviour as on Firefox for the desktop.
- final String searchUrl = keywordUrl.replace("%s", URLEncoder.encode(keywordSearch)).replace("%S", keywordSearch);
-
- Tabs.getInstance().loadUrl(searchUrl, Tabs.LOADURL_USER_ENTERED);
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL,
- TelemetryContract.Method.ACTIONBAR,
- "keyword");
- }
- });
- }
-
- /**
- * Records in telemetry that a search has occurred.
- *
- * @param where where the search was started from
- */
- private static void recordSearch(@NonNull final SharedPreferences prefs, @NonNull final String engineIdentifier,
- @NonNull final TelemetryContract.Method where) {
- // We could include the engine identifier as an extra but we'll
- // just capture that with core ping telemetry (bug 1253319).
- Telemetry.sendUIEvent(TelemetryContract.Event.SEARCH, where);
- SearchCountMeasurements.incrementSearch(prefs, engineIdentifier, where.toString());
- }
-
- /**
- * Store search query in SearchHistoryProvider.
- *
- * @param query
- * a search query to store. We won't store empty queries.
- */
- private void storeSearchQuery(final String query) {
- if (TextUtils.isEmpty(query)) {
- return;
- }
-
- // Filter out URLs and long suggestions
- if (query.length() > 50 || Pattern.matches("^(https?|ftp|file)://.*", query)) {
- return;
- }
-
- final GeckoProfile profile = getProfile();
- // Don't bother storing search queries in guest mode
- if (profile.inGuestMode()) {
- return;
- }
-
- final BrowserDB db = BrowserDB.from(profile);
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- db.getSearches().insert(getContentResolver(), query);
- }
- });
- }
-
- void filterEditingMode(String searchTerm, AutocompleteHandler handler) {
- if (TextUtils.isEmpty(searchTerm)) {
- hideBrowserSearch();
- } else {
- showBrowserSearch();
- mBrowserSearch.filter(searchTerm, handler);
- }
- }
-
- /**
- * Selects the target tab for editing mode. This is expected to be the tab selected on editing
- * mode entry, unless it is subsequently overridden.
- *
- * A background tab may be selected while editing mode is active (e.g. popups), causing the
- * new url to load in the newly selected tab. Call this method on editing mode exit to
- * mitigate this.
- *
- * Note that this method is disabled for new tablets because we can see the selected tab in the
- * tab strip and, when the selected tab changes during editing mode as in this hack, the
- * temporarily selected tab is visible to users.
- */
- private void selectTargetTabForEditingMode() {
- if (HardwareUtils.isTablet()) {
- return;
- }
-
- if (mTargetTabForEditingMode != null) {
- Tabs.getInstance().selectTab(mTargetTabForEditingMode);
- }
-
- mTargetTabForEditingMode = null;
- }
-
- /**
- * Shows or hides the home pager for the given tab.
- */
- private void updateHomePagerForTab(Tab tab) {
- // Don't change the visibility of the home pager if we're in editing mode.
- if (mBrowserToolbar.isEditing()) {
- return;
- }
-
- // History will only store that we were visiting about:home, however the specific panel
- // isn't stored. (We are able to navigate directly to homepanels using an about:home?panel=...
- // URL, but the reverse doesn't apply: manually switching panels doesn't update the URL.)
- // Hence we need to restore the panel, in addition to panel state, here.
- if (isAboutHome(tab)) {
- String panelId = AboutPages.getPanelIdFromAboutHomeUrl(tab.getURL());
- Bundle panelRestoreData = null;
- if (panelId == null) {
- // No panel was specified in the URL. Try loading the most recent
- // home panel for this tab.
- // Note: this isn't necessarily correct. We don't update the URL when we switch tabs.
- // If a user explicitly navigated to about:reader?panel=FOO, and then switches
- // to panel BAR, the history URL still contains FOO, and we restore to FOO. In most
- // cases however we aren't supplying a panel ID in the URL so this code still works
- // for most cases.
- // We can't fix this directly since we can't ignore the panelId if we're explicitly
- // loading a specific panel, and we currently can't distinguish between loading
- // history, and loading new pages, see Bug 1268887
- panelId = tab.getMostRecentHomePanel();
- panelRestoreData = tab.getMostRecentHomePanelData();
- } else if (panelId.equals(HomeConfig.getIdForBuiltinPanelType(PanelType.DEPRECATED_RECENT_TABS))) {
- // Redirect to the Combined History panel.
- panelId = HomeConfig.getIdForBuiltinPanelType(PanelType.COMBINED_HISTORY);
- panelRestoreData = new Bundle();
- // Jump directly to the Recent Tabs subview of the Combined History panel.
- panelRestoreData.putBoolean("goToRecentTabs", true);
- }
- showHomePager(panelId, panelRestoreData);
-
- if (mDynamicToolbar.isEnabled()) {
- mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
- }
- } else {
- hideHomePager();
- }
- }
-
- @Override
- public void onLocaleReady(final String locale) {
- Log.d(LOGTAG, "onLocaleReady: " + locale);
- super.onLocaleReady(locale);
-
- HomePanelsManager.getInstance().onLocaleReady(locale);
-
- if (mMenu != null) {
- mMenu.clear();
- onCreateOptionsMenu(mMenu);
- }
- }
-
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- Log.d(LOGTAG, "onActivityResult: " + requestCode + ", " + resultCode + ", " + data);
- switch (requestCode) {
- case ACTIVITY_REQUEST_PREFERENCES:
- // We just returned from preferences. If our locale changed,
- // we need to redisplay at this point, and do any other browser-level
- // bookkeeping that we associate with a locale change.
- if (resultCode != GeckoPreferences.RESULT_CODE_LOCALE_DID_CHANGE) {
- Log.d(LOGTAG, "No locale change returning from preferences; nothing to do.");
- return;
- }
-
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- final LocaleManager localeManager = BrowserLocaleManager.getInstance();
- final Locale locale = localeManager.getCurrentLocale(getApplicationContext());
- Log.d(LOGTAG, "Read persisted locale " + locale);
- if (locale == null) {
- return;
- }
- onLocaleChanged(Locales.getLanguageTag(locale));
- }
- });
- break;
-
- case ACTIVITY_REQUEST_TAB_QUEUE:
- TabQueueHelper.processTabQueuePromptResponse(resultCode, this);
- break;
-
- default:
- for (final BrowserAppDelegate delegate : delegates) {
- delegate.onActivityResult(this, requestCode, resultCode, data);
- }
-
- super.onActivityResult(requestCode, resultCode, data);
- }
- }
-
- private void showFirstrunPager() {
- if (Experiments.isInExperimentLocal(getContext(), Experiments.ONBOARDING3_A)) {
- Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING3_A);
- GeckoSharedPrefs.forProfile(getContext()).edit().putString(Experiments.PREF_ONBOARDING_VERSION, Experiments.ONBOARDING3_A).apply();
- Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING3_A);
- return;
- }
-
- if (mFirstrunAnimationContainer == null) {
- final ViewStub firstrunPagerStub = (ViewStub) findViewById(R.id.firstrun_pager_stub);
- mFirstrunAnimationContainer = (FirstrunAnimationContainer) firstrunPagerStub.inflate();
- mFirstrunAnimationContainer.load(getApplicationContext(), getSupportFragmentManager());
- mFirstrunAnimationContainer.registerOnFinishListener(new FirstrunAnimationContainer.OnFinishListener() {
- @Override
- public void onFinish() {
- if (mFirstrunAnimationContainer.showBrowserHint() &&
- TextUtils.isEmpty(getHomepage())) {
- enterEditingMode();
- }
- }
- });
- }
-
- mHomeScreenContainer.setVisibility(View.VISIBLE);
- }
-
- private void showHomePager(String panelId, Bundle panelRestoreData) {
- showHomePagerWithAnimator(panelId, panelRestoreData, null);
- }
-
- private void showHomePagerWithAnimator(String panelId, Bundle panelRestoreData, PropertyAnimator animator) {
- if (isHomePagerVisible()) {
- // Home pager already visible, make sure it shows the correct panel.
- mHomeScreen.showPanel(panelId, panelRestoreData);
- return;
- }
-
- // This must be called before the dynamic toolbar is set visible because it calls
- // FormAssistPopup.onMetricsChanged, which queues a runnable that undoes the effect of hide.
- // With hide first, onMetricsChanged will return early instead.
- mFormAssistPopup.hide();
- mFindInPageBar.hide();
-
- // Refresh toolbar height to possibly restore the toolbar padding
- refreshToolbarHeight();
-
- // Show the toolbar before hiding about:home so the
- // onMetricsChanged callback still works.
- if (mDynamicToolbar.isEnabled()) {
- mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
- }
-
- if (mHomeScreen == null) {
- if (ActivityStream.isEnabled(this) &&
- !ActivityStream.isHomePanel()) {
- final ViewStub asStub = (ViewStub) findViewById(R.id.activity_stream_stub);
- mHomeScreen = (HomeScreen) asStub.inflate();
- } else {
- final ViewStub homePagerStub = (ViewStub) findViewById(R.id.home_pager_stub);
- mHomeScreen = (HomeScreen) homePagerStub.inflate();
-
- // For now these listeners are HomePager specific. In future we might want
- // to have a more abstracted data storage, with one Bundle containing all
- // relevant restore data.
- mHomeScreen.setOnPanelChangeListener(new HomeScreen.OnPanelChangeListener() {
- @Override
- public void onPanelSelected(String panelId) {
- final Tab currentTab = Tabs.getInstance().getSelectedTab();
- if (currentTab != null) {
- currentTab.setMostRecentHomePanel(panelId);
- }
- }
- });
-
- // Set this listener to persist restore data (via the Tab) every time panel state changes.
- mHomeScreen.setPanelStateChangeListener(new HomeFragment.PanelStateChangeListener() {
- @Override
- public void onStateChanged(Bundle bundle) {
- final Tab currentTab = Tabs.getInstance().getSelectedTab();
- if (currentTab != null) {
- currentTab.setMostRecentHomePanelData(bundle);
- }
- }
-
- @Override
- public void setCachedRecentTabsCount(int count) {
- mCachedRecentTabsCount = count;
- }
-
- @Override
- public int getCachedRecentTabsCount() {
- return mCachedRecentTabsCount;
- }
- });
- }
-
- // Don't show the banner in guest mode.
- if (!Restrictions.isUserRestricted()) {
- final ViewStub homeBannerStub = (ViewStub) findViewById(R.id.home_banner_stub);
- final HomeBanner homeBanner = (HomeBanner) homeBannerStub.inflate();
- mHomeScreen.setBanner(homeBanner);
-
- // Remove the banner from the view hierarchy if it is dismissed.
- homeBanner.setOnDismissListener(new HomeBanner.OnDismissListener() {
- @Override
- public void onDismiss() {
- mHomeScreen.setBanner(null);
- mHomeScreenContainer.removeView(homeBanner);
- }
- });
- }
- }
-
- mHomeScreenContainer.setVisibility(View.VISIBLE);
- mHomeScreen.load(getSupportLoaderManager(),
- getSupportFragmentManager(),
- panelId,
- panelRestoreData,
- animator);
-
- // Hide the web content so it cannot be focused by screen readers.
- hideWebContentOnPropertyAnimationEnd(animator);
- }
-
- private void hideWebContentOnPropertyAnimationEnd(final PropertyAnimator animator) {
- if (animator == null) {
- hideWebContent();
- return;
- }
-
- animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
- @Override
- public void onPropertyAnimationStart() {
- mHideWebContentOnAnimationEnd = true;
- }
-
- @Override
- public void onPropertyAnimationEnd() {
- if (mHideWebContentOnAnimationEnd) {
- hideWebContent();
- }
- }
- });
- }
-
- private void hideWebContent() {
- // The view is set to INVISIBLE, rather than GONE, to avoid
- // the additional requestLayout() call.
- mLayerView.setVisibility(View.INVISIBLE);
- }
-
- /**
- * Hide the Onboarding pager on user action, and don't show any onFinish hints.
- * @param method TelemetryContract method by which action was taken
- * @return boolean of whether pager was visible
- */
- private boolean hideFirstrunPager(TelemetryContract.Method method) {
- if (!isFirstrunVisible()) {
- return false;
- }
-
- Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, method, "firstrun-pane");
-
- // Don't show any onFinish actions when hiding from this Activity.
- mFirstrunAnimationContainer.registerOnFinishListener(null);
- mFirstrunAnimationContainer.hide();
- return true;
- }
-
- /**
- * Hides the HomePager, using the url of the currently selected tab as the url to be
- * loaded.
- */
- private void hideHomePager() {
- final Tab selectedTab = Tabs.getInstance().getSelectedTab();
- final String url = (selectedTab != null) ? selectedTab.getURL() : null;
-
- hideHomePager(url);
- }
-
- /**
- * Hides the HomePager. The given url should be the url of the page to be loaded, or null
- * if a new page is not being loaded.
- */
- private void hideHomePager(final String url) {
- if (!isHomePagerVisible() || AboutPages.isAboutHome(url)) {
- return;
- }
-
- // Prevent race in hiding web content - see declaration for more info.
- mHideWebContentOnAnimationEnd = false;
-
- // Display the previously hidden web content (which prevented screen reader access).
- mLayerView.setVisibility(View.VISIBLE);
- mHomeScreenContainer.setVisibility(View.GONE);
-
- if (mHomeScreen != null) {
- mHomeScreen.unload();
- }
-
- mBrowserToolbar.setNextFocusDownId(R.id.layer_view);
-
- // Refresh toolbar height to possibly restore the toolbar padding
- refreshToolbarHeight();
- }
-
- private void showBrowserSearchAfterAnimation(PropertyAnimator animator) {
- if (animator == null) {
- showBrowserSearch();
- return;
- }
-
- animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
- @Override
- public void onPropertyAnimationStart() {
- }
-
- @Override
- public void onPropertyAnimationEnd() {
- showBrowserSearch();
- }
- });
- }
-
- private void showBrowserSearch() {
- if (mBrowserSearch.getUserVisibleHint()) {
- return;
- }
-
- mBrowserSearchContainer.setVisibility(View.VISIBLE);
-
- // Prevent overdraw by hiding the underlying web content and HomePager View
- hideWebContent();
- mHomeScreenContainer.setVisibility(View.INVISIBLE);
-
- final FragmentManager fm = getSupportFragmentManager();
-
- // In certain situations, showBrowserSearch() can be called immediately after hideBrowserSearch()
- // (see bug 925012). Because of an Android bug (http://code.google.com/p/android/issues/detail?id=61179),
- // calling FragmentTransaction#add immediately after FragmentTransaction#remove won't add the fragment's
- // view to the layout. Calling FragmentManager#executePendingTransactions before re-adding the fragment
- // prevents this issue.
- fm.executePendingTransactions();
-
- Fragment f = fm.findFragmentById(R.id.search_container);
-
- // checking if fragment is already present
- if (f != null) {
- fm.beginTransaction().show(f).commitAllowingStateLoss();
- mBrowserSearch.resetScrollState();
- } else {
- // add fragment if not already present
- fm.beginTransaction().add(R.id.search_container, mBrowserSearch, BROWSER_SEARCH_TAG).commitAllowingStateLoss();
- }
- mBrowserSearch.setUserVisibleHint(true);
-
- // We want to adjust the window size when the keyboard appears to bring the
- // SearchEngineBar above the keyboard. However, adjusting the window size
- // when hiding the keyboard results in graphical glitches where the keyboard was
- // because nothing was being drawn underneath (bug 933422). This can be
- // prevented drawing content under the keyboard (i.e. in the Window).
- //
- // We do this here because there are glitches when unlocking a device with
- // BrowserSearch in the foreground if we use BrowserSearch.onStart/Stop.
- getActivity().getWindow().setBackgroundDrawableResource(android.R.color.white);
- }
-
- private void hideBrowserSearch() {
- if (!mBrowserSearch.getUserVisibleHint()) {
- return;
- }
-
- // To prevent overdraw, the HomePager is hidden when BrowserSearch is displayed:
- // reverse that.
- showHomePager(Tabs.getInstance().getSelectedTab().getMostRecentHomePanel(),
- Tabs.getInstance().getSelectedTab().getMostRecentHomePanelData());
-
- mBrowserSearchContainer.setVisibility(View.INVISIBLE);
-
- getSupportFragmentManager().beginTransaction()
- .hide(mBrowserSearch).commitAllowingStateLoss();
- mBrowserSearch.setUserVisibleHint(false);
-
- getWindow().setBackgroundDrawable(null);
- }
-
- /**
- * Hides certain UI elements (e.g. button toast, tabs panel) when the
- * user touches the main layout.
- */
- private class HideOnTouchListener implements TouchEventInterceptor {
- private boolean mIsHidingTabs;
- private final Rect mTempRect = new Rect();
-
- @Override
- public boolean onInterceptTouchEvent(View view, MotionEvent event) {
- if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
- SnackbarBuilder.dismissCurrentSnackbar();
- }
-
-
-
- // We need to account for scroll state for the touched view otherwise
- // tapping on an "empty" part of the view will still be considered a
- // valid touch event.
- if (view.getScrollX() != 0 || view.getScrollY() != 0) {
- view.getHitRect(mTempRect);
- mTempRect.offset(-view.getScrollX(), -view.getScrollY());
-
- int[] viewCoords = new int[2];
- view.getLocationOnScreen(viewCoords);
-
- int x = (int) event.getRawX() - viewCoords[0];
- int y = (int) event.getRawY() - viewCoords[1];
-
- if (!mTempRect.contains(x, y))
- return false;
- }
-
- // If the tabs panel is showing, hide the tab panel and don't send the event to content.
- if (event.getActionMasked() == MotionEvent.ACTION_DOWN && autoHideTabs()) {
- mIsHidingTabs = true;
- return true;
- }
- return false;
- }
-
- @Override
- public boolean onTouch(View view, MotionEvent event) {
- if (mIsHidingTabs) {
- // Keep consuming events until the gesture finishes.
- int action = event.getActionMasked();
- if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
- mIsHidingTabs = false;
- }
- return true;
- }
- return false;
- }
- }
-
- private static Menu findParentMenu(Menu menu, MenuItem item) {
- final int itemId = item.getItemId();
-
- final int count = (menu != null) ? menu.size() : 0;
- for (int i = 0; i < count; i++) {
- MenuItem menuItem = menu.getItem(i);
- if (menuItem.getItemId() == itemId) {
- return menu;
- }
- if (menuItem.hasSubMenu()) {
- Menu parent = findParentMenu(menuItem.getSubMenu(), item);
- if (parent != null) {
- return parent;
- }
- }
- }
-
- return null;
- }
-
- /**
- * Add the provided item to the provided menu, which should be
- * the root (mMenu).
- */
- private void addAddonMenuItemToMenu(final Menu menu, final MenuItemInfo info) {
- info.added = true;
-
- final Menu destination;
- if (info.parent == 0) {
- destination = menu;
- } else if (info.parent == GECKO_TOOLS_MENU) {
- // The tools menu only exists in our -v11 resources.
- final MenuItem tools = menu.findItem(R.id.tools);
- destination = tools != null ? tools.getSubMenu() : menu;
- } else {
- final MenuItem parent = menu.findItem(info.parent);
- if (parent == null) {
- return;
- }
-
- Menu parentMenu = findParentMenu(menu, parent);
-
- if (!parent.hasSubMenu()) {
- parentMenu.removeItem(parent.getItemId());
- destination = parentMenu.addSubMenu(Menu.NONE, parent.getItemId(), Menu.NONE, parent.getTitle());
- if (parent.getIcon() != null) {
- ((SubMenu) destination).getItem().setIcon(parent.getIcon());
- }
- } else {
- destination = parent.getSubMenu();
- }
- }
-
- final MenuItem item = destination.add(Menu.NONE, info.id, Menu.NONE, info.label);
-
- item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
- @Override
- public boolean onMenuItemClick(MenuItem item) {
- GeckoAppShell.notifyObservers("Menu:Clicked", Integer.toString(info.id - ADDON_MENU_OFFSET));
- return true;
- }
- });
-
- item.setCheckable(info.checkable);
- item.setChecked(info.checked);
- item.setEnabled(info.enabled);
- item.setVisible(info.visible);
- }
-
- private void addAddonMenuItem(final MenuItemInfo info) {
- if (mAddonMenuItemsCache == null) {
- mAddonMenuItemsCache = new Vector<MenuItemInfo>();
- }
-
- // Mark it as added if the menu was ready.
- info.added = (mMenu != null);
-
- // Always cache so we can rebuild after a locale switch.
- mAddonMenuItemsCache.add(info);
-
- if (mMenu == null) {
- return;
- }
-
- addAddonMenuItemToMenu(mMenu, info);
- }
-
- private void removeAddonMenuItem(int id) {
- // Remove add-on menu item from cache, if available.
- if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
- for (MenuItemInfo item : mAddonMenuItemsCache) {
- if (item.id == id) {
- mAddonMenuItemsCache.remove(item);
- break;
- }
- }
- }
-
- if (mMenu == null)
- return;
-
- final MenuItem menuItem = mMenu.findItem(id);
- if (menuItem != null)
- mMenu.removeItem(id);
- }
-
- private void updateAddonMenuItem(int id, JSONObject options) {
- // Set attribute for the menu item in cache, if available
- if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
- for (MenuItemInfo item : mAddonMenuItemsCache) {
- if (item.id == id) {
- item.label = options.optString("name", item.label);
- item.checkable = options.optBoolean("checkable", item.checkable);
- item.checked = options.optBoolean("checked", item.checked);
- item.enabled = options.optBoolean("enabled", item.enabled);
- item.visible = options.optBoolean("visible", item.visible);
- item.added = (mMenu != null);
- break;
- }
- }
- }
-
- if (mMenu == null) {
- return;
- }
-
- final MenuItem menuItem = mMenu.findItem(id);
- if (menuItem != null) {
- menuItem.setTitle(options.optString("name", menuItem.getTitle().toString()));
- menuItem.setCheckable(options.optBoolean("checkable", menuItem.isCheckable()));
- menuItem.setChecked(options.optBoolean("checked", menuItem.isChecked()));
- menuItem.setEnabled(options.optBoolean("enabled", menuItem.isEnabled()));
- menuItem.setVisible(options.optBoolean("visible", menuItem.isVisible()));
- }
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- // Sets mMenu = menu.
- super.onCreateOptionsMenu(menu);
-
- // Inform the menu about the action-items bar.
- if (menu instanceof GeckoMenu &&
- HardwareUtils.isTablet()) {
- ((GeckoMenu) menu).setActionItemBarPresenter(mBrowserToolbar);
- }
-
- MenuInflater inflater = getMenuInflater();
- inflater.inflate(R.menu.browser_app_menu, mMenu);
-
- // Add add-on menu items, if any exist.
- if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
- for (MenuItemInfo item : mAddonMenuItemsCache) {
- addAddonMenuItemToMenu(mMenu, item);
- }
- }
-
- // Action providers are available only ICS+.
- GeckoMenuItem share = (GeckoMenuItem) mMenu.findItem(R.id.share);
-
- GeckoActionProvider provider = GeckoActionProvider.getForType(GeckoActionProvider.DEFAULT_MIME_TYPE, this);
-
- share.setActionProvider(provider);
-
- return true;
- }
-
- @Override
- public void openOptionsMenu() {
- hideFirstrunPager(TelemetryContract.Method.MENU);
-
- // Disable menu access (for hardware buttons) when the software menu button is inaccessible.
- // Note that the software button is always accessible on new tablet.
- if (mBrowserToolbar.isEditing() && !HardwareUtils.isTablet()) {
- return;
- }
-
- if (ActivityUtils.isFullScreen(this)) {
- return;
- }
-
- if (areTabsShown()) {
- mTabsPanel.showMenu();
- return;
- }
-
- // Scroll custom menu to the top
- if (mMenuPanel != null)
- mMenuPanel.scrollTo(0, 0);
-
- // Scroll menu ListView (potentially in MenuPanel ViewGroup) to top.
- if (mMenu instanceof GeckoMenu) {
- ((GeckoMenu) mMenu).setSelection(0);
- }
-
- if (!mBrowserToolbar.openOptionsMenu())
- super.openOptionsMenu();
-
- if (mDynamicToolbar.isEnabled()) {
- mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
- }
- }
-
- @Override
- public void closeOptionsMenu() {
- if (!mBrowserToolbar.closeOptionsMenu())
- super.closeOptionsMenu();
- }
-
- @Override
- public void setFullScreen(final boolean fullscreen) {
- super.setFullScreen(fullscreen);
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- if (fullscreen) {
- if (mDynamicToolbar.isEnabled()) {
- mDynamicToolbar.setVisible(false, VisibilityTransition.IMMEDIATE);
- mDynamicToolbar.setPinned(true, PinReason.FULL_SCREEN);
- } else {
- setToolbarMargin(0);
- }
- mBrowserChrome.setVisibility(View.GONE);
- } else {
- mBrowserChrome.setVisibility(View.VISIBLE);
- if (mDynamicToolbar.isEnabled()) {
- mDynamicToolbar.setPinned(false, PinReason.FULL_SCREEN);
- mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
- } else {
- setToolbarMargin(mBrowserChrome.getHeight());
- }
- }
- }
- });
- }
-
- @Override
- public boolean onPrepareOptionsMenu(Menu aMenu) {
- if (aMenu == null)
- return false;
-
- // Hide the tab history panel when hardware menu button is pressed.
- TabHistoryFragment frag = (TabHistoryFragment) getSupportFragmentManager().findFragmentByTag(TAB_HISTORY_FRAGMENT_TAG);
- if (frag != null) {
- frag.dismiss();
- }
-
- if (!GeckoThread.isRunning()) {
- aMenu.findItem(R.id.settings).setEnabled(false);
- aMenu.findItem(R.id.help).setEnabled(false);
- }
-
- Tab tab = Tabs.getInstance().getSelectedTab();
- // Unlike other menu items, the bookmark star is not tinted. See {@link ThemedImageButton#setTintedDrawable}.
- final MenuItem bookmark = aMenu.findItem(R.id.bookmark);
- final MenuItem back = aMenu.findItem(R.id.back);
- final MenuItem forward = aMenu.findItem(R.id.forward);
- final MenuItem share = aMenu.findItem(R.id.share);
- final MenuItem bookmarksList = aMenu.findItem(R.id.bookmarks_list);
- final MenuItem historyList = aMenu.findItem(R.id.history_list);
- final MenuItem saveAsPDF = aMenu.findItem(R.id.save_as_pdf);
- final MenuItem print = aMenu.findItem(R.id.print);
- final MenuItem charEncoding = aMenu.findItem(R.id.char_encoding);
- final MenuItem findInPage = aMenu.findItem(R.id.find_in_page);
- final MenuItem desktopMode = aMenu.findItem(R.id.desktop_mode);
- final MenuItem enterGuestMode = aMenu.findItem(R.id.new_guest_session);
- final MenuItem exitGuestMode = aMenu.findItem(R.id.exit_guest_session);
-
- // Only show the "Quit" menu item on pre-ICS, television devices,
- // or if the user has explicitly enabled the clear on shutdown pref.
- // (We check the pref last to save the pref read.)
- // In ICS+, it's easy to kill an app through the task switcher.
- final boolean visible = HardwareUtils.isTelevision() ||
- !PrefUtils.getStringSet(GeckoSharedPrefs.forProfile(this),
- ClearOnShutdownPref.PREF,
- new HashSet<String>()).isEmpty();
- aMenu.findItem(R.id.quit).setVisible(visible);
-
- // If tab data is unavailable we disable most of the context menu and related items and
- // return early.
- if (tab == null || tab.getURL() == null) {
- bookmark.setEnabled(false);
- back.setEnabled(false);
- forward.setEnabled(false);
- share.setEnabled(false);
- saveAsPDF.setEnabled(false);
- print.setEnabled(false);
- findInPage.setEnabled(false);
-
- // NOTE: Use MenuUtils.safeSetEnabled because some actions might
- // be on the BrowserToolbar context menu.
- MenuUtils.safeSetEnabled(aMenu, R.id.page, false);
- MenuUtils.safeSetEnabled(aMenu, R.id.subscribe, false);
- MenuUtils.safeSetEnabled(aMenu, R.id.add_search_engine, false);
- MenuUtils.safeSetEnabled(aMenu, R.id.add_to_launcher, false);
-
- return true;
- }
-
- // If tab data IS available we need to manually enable items as necessary. They may have
- // been disabled if returning early above, hence every item must be toggled, even if it's
- // always expected to be enabled (e.g. the bookmark star is always enabled, except when
- // we don't have tab data).
-
- final boolean inGuestMode = GeckoProfile.get(this).inGuestMode();
-
- bookmark.setEnabled(true); // Might have been disabled above, ensure it's reenabled
- bookmark.setVisible(!inGuestMode);
- bookmark.setCheckable(true);
- bookmark.setChecked(tab.isBookmark());
- bookmark.setTitle(resolveBookmarkTitleID(tab.isBookmark()));
-
- // We don't use icons on GB builds so not resolving icons might conserve resources.
- bookmark.setIcon(resolveBookmarkIconID(tab.isBookmark()));
-
- back.setEnabled(tab.canDoBack());
- forward.setEnabled(tab.canDoForward());
- desktopMode.setChecked(tab.getDesktopMode());
-
- View backButtonView = MenuItemCompat.getActionView(back);
-
- if (backButtonView != null) {
- backButtonView.setOnLongClickListener(new Button.OnLongClickListener() {
- @Override
- public boolean onLongClick(View view) {
- Tab tab = Tabs.getInstance().getSelectedTab();
- if (tab != null) {
- closeOptionsMenu();
- return tabHistoryController.showTabHistory(tab,
- TabHistoryController.HistoryAction.BACK);
- }
- return false;
- }
- });
- }
-
- View forwardButtonView = MenuItemCompat.getActionView(forward);
-
- if (forwardButtonView != null) {
- forwardButtonView.setOnLongClickListener(new Button.OnLongClickListener() {
- @Override
- public boolean onLongClick(View view) {
- Tab tab = Tabs.getInstance().getSelectedTab();
- if (tab != null) {
- closeOptionsMenu();
- return tabHistoryController.showTabHistory(tab,
- TabHistoryController.HistoryAction.FORWARD);
- }
- return false;
- }
- });
- }
-
- String url = tab.getURL();
- if (AboutPages.isAboutReader(url)) {
- url = ReaderModeUtils.stripAboutReaderUrl(url);
- }
-
- // Disable share menuitem for about:, chrome:, file:, and resource: URIs
- final boolean shareVisible = Restrictions.isAllowed(this, Restrictable.SHARE);
- share.setVisible(shareVisible);
- final boolean shareEnabled = StringUtils.isShareableUrl(url) && shareVisible;
- share.setEnabled(shareEnabled);
- MenuUtils.safeSetEnabled(aMenu, R.id.downloads, Restrictions.isAllowed(this, Restrictable.DOWNLOAD));
-
- // NOTE: Use MenuUtils.safeSetEnabled because some actions might
- // be on the BrowserToolbar context menu.
- MenuUtils.safeSetEnabled(aMenu, R.id.page, !isAboutHome(tab));
- MenuUtils.safeSetEnabled(aMenu, R.id.subscribe, tab.hasFeeds());
- MenuUtils.safeSetEnabled(aMenu, R.id.add_search_engine, tab.hasOpenSearch());
- MenuUtils.safeSetEnabled(aMenu, R.id.add_to_launcher, !isAboutHome(tab));
-
- // This provider also applies to the quick share menu item.
- final GeckoActionProvider provider = ((GeckoMenuItem) share).getGeckoActionProvider();
- if (provider != null) {
- Intent shareIntent = provider.getIntent();
-
- // For efficiency, the provider's intent is only set once
- if (shareIntent == null) {
- shareIntent = new Intent(Intent.ACTION_SEND);
- shareIntent.setType("text/plain");
- provider.setIntent(shareIntent);
- }
-
- // Replace the existing intent's extras
- shareIntent.putExtra(Intent.EXTRA_TEXT, url);
- shareIntent.putExtra(Intent.EXTRA_SUBJECT, tab.getDisplayTitle());
- shareIntent.putExtra(Intent.EXTRA_TITLE, tab.getDisplayTitle());
- shareIntent.putExtra(ShareDialog.INTENT_EXTRA_DEVICES_ONLY, true);
-
- // Clear the existing thumbnail extras so we don't share an old thumbnail.
- shareIntent.removeExtra("share_screenshot_uri");
-
- // Include the thumbnail of the page being shared.
- BitmapDrawable drawable = tab.getThumbnail();
- if (drawable != null) {
- Bitmap thumbnail = drawable.getBitmap();
-
- // Kobo uses a custom intent extra for sharing thumbnails.
- if (Build.MANUFACTURER.equals("Kobo") && thumbnail != null) {
- File cacheDir = getExternalCacheDir();
-
- if (cacheDir != null) {
- File outFile = new File(cacheDir, "thumbnail.png");
-
- try {
- final java.io.FileOutputStream out = new java.io.FileOutputStream(outFile);
- try {
- thumbnail.compress(Bitmap.CompressFormat.PNG, 90, out);
- } finally {
- try {
- out.close();
- } catch (final IOException e) { /* Nothing to do here. */ }
- }
- } catch (FileNotFoundException e) {
- Log.e(LOGTAG, "File not found", e);
- }
-
- shareIntent.putExtra("share_screenshot_uri", Uri.parse(outFile.getPath()));
- }
- }
- }
- }
-
- final boolean privateTabVisible = Restrictions.isAllowed(this, Restrictable.PRIVATE_BROWSING);
- MenuUtils.safeSetVisible(aMenu, R.id.new_private_tab, privateTabVisible);
-
- // Disable PDF generation (save and print) for about:home and xul pages.
- boolean allowPDF = (!(isAboutHome(tab) ||
- tab.getContentType().equals("application/vnd.mozilla.xul+xml") ||
- tab.getContentType().startsWith("video/")));
- saveAsPDF.setEnabled(allowPDF);
- print.setEnabled(allowPDF);
- print.setVisible(Versions.feature19Plus);
-
- // Disable find in page for about:home, since it won't work on Java content.
- findInPage.setEnabled(!isAboutHome(tab));
-
- charEncoding.setVisible(GeckoPreferences.getCharEncodingState());
-
- if (getProfile().inGuestMode()) {
- exitGuestMode.setVisible(true);
- } else {
- enterGuestMode.setVisible(true);
- }
-
- if (!Restrictions.isAllowed(this, Restrictable.GUEST_BROWSING)) {
- MenuUtils.safeSetVisible(aMenu, R.id.new_guest_session, false);
- }
-
- if (!Restrictions.isAllowed(this, Restrictable.INSTALL_EXTENSION)) {
- MenuUtils.safeSetVisible(aMenu, R.id.addons, false);
- }
-
- // Hide panel menu items if the panels themselves are hidden.
- // If we don't know whether the panels are hidden, just show the menu items.
- final SharedPreferences prefs = GeckoSharedPrefs.forProfile(getContext());
- bookmarksList.setVisible(prefs.getBoolean(HomeConfig.PREF_KEY_BOOKMARKS_PANEL_ENABLED, true));
- historyList.setVisible(prefs.getBoolean(HomeConfig.PREF_KEY_HISTORY_PANEL_ENABLED, true));
-
- return true;
- }
-
- private int resolveBookmarkIconID(final boolean isBookmark) {
- if (isBookmark) {
- return R.drawable.star_blue;
- } else {
- return R.drawable.ic_menu_bookmark_add;
- }
- }
-
- private int resolveBookmarkTitleID(final boolean isBookmark) {
- return (isBookmark ? R.string.bookmark_remove : R.string.bookmark);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- Tab tab = null;
- Intent intent = null;
-
- final int itemId = item.getItemId();
-
- // Track the menu action. We don't know much about the context, but we can use this to determine
- // the frequency of use for various actions.
- String extras = getResources().getResourceEntryName(itemId);
- if (TextUtils.equals(extras, "new_private_tab")) {
- // Mask private browsing
- extras = "new_tab";
- }
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, extras);
-
- mBrowserToolbar.cancelEdit();
-
- if (itemId == R.id.bookmark) {
- tab = Tabs.getInstance().getSelectedTab();
- if (tab != null) {
- final String extra;
- if (AboutPages.isAboutReader(tab.getURL())) {
- extra = "bookmark_reader";
- } else {
- extra = "bookmark";
- }
-
- if (item.isChecked()) {
- Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.MENU, extra);
- tab.removeBookmark();
- item.setTitle(resolveBookmarkTitleID(false));
- item.setIcon(resolveBookmarkIconID(false));
- } else {
- Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.MENU, extra);
- tab.addBookmark();
- item.setTitle(resolveBookmarkTitleID(true));
- item.setIcon(resolveBookmarkIconID(true));
- }
- }
- return true;
- }
-
- if (itemId == R.id.share) {
- tab = Tabs.getInstance().getSelectedTab();
- if (tab != null) {
- String url = tab.getURL();
- if (url != null) {
- url = ReaderModeUtils.stripAboutReaderUrl(url);
-
- // Context: Sharing via chrome list (no explicit session is active)
- Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "menu");
-
- IntentHelper.openUriExternal(url, "text/plain", "", "", Intent.ACTION_SEND, tab.getDisplayTitle(), false);
- }
- }
- return true;
- }
-
- if (itemId == R.id.reload) {
- tab = Tabs.getInstance().getSelectedTab();
- if (tab != null)
- tab.doReload(false);
- return true;
- }
-
- if (itemId == R.id.back) {
- tab = Tabs.getInstance().getSelectedTab();
- if (tab != null)
- tab.doBack();
- return true;
- }
-
- if (itemId == R.id.forward) {
- tab = Tabs.getInstance().getSelectedTab();
- if (tab != null)
- tab.doForward();
- return true;
- }
-
- if (itemId == R.id.bookmarks_list) {
- final String url = AboutPages.getURLForBuiltinPanelType(PanelType.BOOKMARKS);
- Tabs.getInstance().loadUrl(url);
- return true;
- }
-
- if (itemId == R.id.history_list) {
- final String url = AboutPages.getURLForBuiltinPanelType(PanelType.COMBINED_HISTORY);
- Tabs.getInstance().loadUrl(url);
- return true;
- }
-
- if (itemId == R.id.save_as_pdf) {
- Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.MENU, "pdf");
- GeckoAppShell.notifyObservers("SaveAs:PDF", null);
- return true;
- }
-
- if (itemId == R.id.print) {
- Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.MENU, "print");
- PrintHelper.printPDF(this);
- return true;
- }
-
- if (itemId == R.id.settings) {
- intent = new Intent(this, GeckoPreferences.class);
-
- // We want to know when the Settings activity returns, because
- // we might need to redisplay based on a locale change.
- startActivityForResult(intent, ACTIVITY_REQUEST_PREFERENCES);
- return true;
- }
-
- if (itemId == R.id.help) {
- final String VERSION = AppConstants.MOZ_APP_VERSION;
- final String OS = AppConstants.OS_TARGET;
- final String LOCALE = Locales.getLanguageTag(Locale.getDefault());
-
- final String URL = getResources().getString(R.string.help_link, VERSION, OS, LOCALE);
- Tabs.getInstance().loadUrlInTab(URL);
- return true;
- }
-
- if (itemId == R.id.addons) {
- Tabs.getInstance().loadUrlInTab(AboutPages.ADDONS);
- return true;
- }
-
- if (itemId == R.id.logins) {
- Tabs.getInstance().loadUrlInTab(AboutPages.LOGINS);
- return true;
- }
-
- if (itemId == R.id.downloads) {
- Tabs.getInstance().loadUrlInTab(AboutPages.DOWNLOADS);
- return true;
- }
-
- if (itemId == R.id.char_encoding) {
- GeckoAppShell.notifyObservers("CharEncoding:Get", null);
- return true;
- }
-
- if (itemId == R.id.find_in_page) {
- mFindInPageBar.show();
- return true;
- }
-
- if (itemId == R.id.desktop_mode) {
- Tab selectedTab = Tabs.getInstance().getSelectedTab();
- if (selectedTab == null)
- return true;
- JSONObject args = new JSONObject();
- try {
- args.put("desktopMode", !item.isChecked());
- args.put("tabId", selectedTab.getId());
- } catch (JSONException e) {
- Log.e(LOGTAG, "error building json arguments", e);
- }
- GeckoAppShell.notifyObservers("DesktopMode:Change", args.toString());
- return true;
- }
-
- if (itemId == R.id.new_tab) {
- addTab();
- return true;
- }
-
- if (itemId == R.id.new_private_tab) {
- addPrivateTab();
- return true;
- }
-
- if (itemId == R.id.new_guest_session) {
- showGuestModeDialog(GuestModeDialog.ENTERING);
- return true;
- }
-
- if (itemId == R.id.exit_guest_session) {
- showGuestModeDialog(GuestModeDialog.LEAVING);
- return true;
- }
-
- // We have a few menu items that can also be in the context menu. If
- // we have not already handled the item, give the context menu handler
- // a chance.
- if (onContextItemSelected(item)) {
- return true;
- }
-
- return super.onOptionsItemSelected(item);
- }
-
- @Override
- public boolean onMenuItemLongClick(MenuItem item) {
- if (item.getItemId() == R.id.reload) {
- Tab tab = Tabs.getInstance().getSelectedTab();
- if (tab != null) {
- tab.doReload(true);
-
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, "reload_force");
- }
- return true;
- }
-
- return super.onMenuItemLongClick(item);
- }
-
- public void showGuestModeDialog(final GuestModeDialog type) {
- if ((type == GuestModeDialog.ENTERING) == getProfile().inGuestMode()) {
- // Don't show enter dialog if we are already in guest mode; same with leaving.
- return;
- }
-
- final Prompt ps = new Prompt(this, new Prompt.PromptCallback() {
- @Override
- public void onPromptFinished(String result) {
- try {
- int itemId = new JSONObject(result).getInt("button");
- if (itemId == 0) {
- final Context context = GeckoAppShell.getApplicationContext();
- if (type == GuestModeDialog.ENTERING) {
- GeckoProfile.enterGuestMode(context);
- } else {
- GeckoProfile.leaveGuestMode(context);
- // Now's a good time to make sure we're not displaying the
- // Guest Browsing notification.
- GuestSession.hideNotification(context);
- }
- doRestart();
- }
- } catch (JSONException ex) {
- Log.e(LOGTAG, "Exception reading guest mode prompt result", ex);
- }
- }
- });
-
- Resources res = getResources();
- ps.setButtons(new String[] {
- res.getString(R.string.guest_session_dialog_continue),
- res.getString(R.string.guest_session_dialog_cancel)
- });
-
- int titleString = 0;
- int msgString = 0;
- if (type == GuestModeDialog.ENTERING) {
- titleString = R.string.new_guest_session_title;
- msgString = R.string.new_guest_session_text;
- } else {
- titleString = R.string.exit_guest_session_title;
- msgString = R.string.exit_guest_session_text;
- }
-
- ps.show(res.getString(titleString), res.getString(msgString), null, ListView.CHOICE_MODE_NONE);
- }
-
- /**
- * Handle a long press on the back button
- */
- private boolean handleBackLongPress() {
- // If the tab search history is already shown, do nothing.
- TabHistoryFragment frag = (TabHistoryFragment) getSupportFragmentManager().findFragmentByTag(TAB_HISTORY_FRAGMENT_TAG);
- if (frag != null) {
- return true;
- }
-
- Tab tab = Tabs.getInstance().getSelectedTab();
- if (tab != null && !tab.isEditing()) {
- return tabHistoryController.showTabHistory(tab, TabHistoryController.HistoryAction.ALL);
- }
-
- return false;
- }
-
- /**
- * This will detect if the key pressed is back. If so, will show the history.
- */
- @Override
- public boolean onKeyLongPress(int keyCode, KeyEvent event) {
- // onKeyLongPress is broken in Android N, see onKeyDown() for more information. We add a version
- // check here to match our fallback code in order to avoid handling a long press twice (which
- // could happen if newer versions of android and/or other vendors were to fix this problem).
- if (Versions.preN &&
- keyCode == KeyEvent.KEYCODE_BACK) {
- if (handleBackLongPress()) {
- return true;
- }
-
- }
- return super.onKeyLongPress(keyCode, event);
- }
-
- /*
- * If the app has been launched a certain number of times, and we haven't asked for feedback before,
- * open a new tab with about:feedback when launching the app from the icon shortcut.
- */
- @Override
- protected void onNewIntent(Intent externalIntent) {
- final SafeIntent intent = new SafeIntent(externalIntent);
- String action = intent.getAction();
-
- final boolean isViewAction = Intent.ACTION_VIEW.equals(action);
- final boolean isBookmarkAction = GeckoApp.ACTION_HOMESCREEN_SHORTCUT.equals(action);
- final boolean isTabQueueAction = TabQueueHelper.LOAD_URLS_ACTION.equals(action);
- final boolean isViewMultipleAction = ACTION_VIEW_MULTIPLE.equals(action);
-
- if (mInitialized && (isViewAction || isBookmarkAction)) {
- // Dismiss editing mode if the user is loading a URL from an external app.
- mBrowserToolbar.cancelEdit();
-
- // Hide firstrun-pane if the user is loading a URL from an external app.
- hideFirstrunPager(TelemetryContract.Method.NONE);
-
- if (isBookmarkAction) {
- // GeckoApp.ACTION_HOMESCREEN_SHORTCUT means we're opening a bookmark that
- // was added to Android's homescreen.
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.HOMESCREEN);
- }
- }
-
- showTabQueuePromptIfApplicable(intent);
-
- // GeckoApp will wrap this unsafe external intent in a SafeIntent.
- super.onNewIntent(externalIntent);
-
- if (AppConstants.MOZ_ANDROID_BEAM && NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
- String uri = intent.getDataString();
- mLayerView.loadUri(uri, GeckoView.LOAD_NEW_TAB);
- }
-
- // Only solicit feedback when the app has been launched from the icon shortcut.
- if (GuestSession.NOTIFICATION_INTENT.equals(action)) {
- GuestSession.onNotificationIntentReceived(this);
- }
-
- // If the user has clicked the tab queue notification then load the tabs.
- if (TabQueueHelper.TAB_QUEUE_ENABLED && mInitialized && isTabQueueAction) {
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, "tabqueue");
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- openQueuedTabs();
- }
- });
- }
-
- // Custom intent action for opening multiple URLs at once
- if (isViewMultipleAction) {
- openMultipleTabsFromIntent(intent);
- }
-
- for (final BrowserAppDelegate delegate : delegates) {
- delegate.onNewIntent(this, intent);
- }
-
- if (!mInitialized || !Intent.ACTION_MAIN.equals(action)) {
- return;
- }
-
- // Check to see how many times the app has been launched.
- final String keyName = getPackageName() + ".feedback_launch_count";
- final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
-
- // Faster on main thread with an async apply().
- try {
- SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE);
- int launchCount = settings.getInt(keyName, 0);
- if (launchCount < FEEDBACK_LAUNCH_COUNT) {
- // Increment the launch count and store the new value.
- launchCount++;
- settings.edit().putInt(keyName, launchCount).apply();
-
- // If we've reached our magic number, show the feedback page.
- if (launchCount == FEEDBACK_LAUNCH_COUNT) {
- GeckoAppShell.notifyObservers("Feedback:Show", null);
- }
- }
- } finally {
- StrictMode.setThreadPolicy(savedPolicy);
- }
- }
-
- public void openUrls(List<String> urls) {
- try {
- JSONArray array = new JSONArray();
- for (String url : urls) {
- array.put(url);
- }
-
- JSONObject object = new JSONObject();
- object.put("urls", array);
-
- GeckoAppShell.notifyObservers("Tabs:OpenMultiple", object.toString());
- } catch (JSONException e) {
- Log.e(LOGTAG, "Unable to create JSON for opening multiple URLs");
- }
- }
-
- private void showTabQueuePromptIfApplicable(final SafeIntent intent) {
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- // We only want to show the prompt if the browser has been opened from an external url
- if (TabQueueHelper.TAB_QUEUE_ENABLED && mInitialized
- && Intent.ACTION_VIEW.equals(intent.getAction())
- && !intent.getBooleanExtra(BrowserContract.SKIP_TAB_QUEUE_FLAG, false)
- && TabQueueHelper.shouldShowTabQueuePrompt(BrowserApp.this)) {
- Intent promptIntent = new Intent(BrowserApp.this, TabQueuePrompt.class);
- startActivityForResult(promptIntent, ACTIVITY_REQUEST_TAB_QUEUE);
- }
- }
- });
- }
-
- private void resetFeedbackLaunchCount() {
- SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE);
- settings.edit().putInt(getPackageName() + ".feedback_launch_count", 0).apply();
- }
-
- // HomePager.OnUrlOpenListener
- @Override
- public void onUrlOpen(String url, EnumSet<OnUrlOpenListener.Flags> flags) {
- if (flags.contains(OnUrlOpenListener.Flags.OPEN_WITH_INTENT)) {
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setData(Uri.parse(url));
- startActivity(intent);
- } else {
- // By default this listener is used for lists where the offline reader-view icon
- // is shown - hence we need to redirect to the reader-view page by default.
- // However there are some cases where we might not want to use this, e.g.
- // for topsites where we do not indicate that a page is an offline reader-view bookmark too.
- final String pageURL;
- if (!flags.contains(OnUrlOpenListener.Flags.NO_READER_VIEW)) {
- pageURL = SavedReaderViewHelper.getReaderURLIfCached(getContext(), url);
- } else {
- pageURL = url;
- }
-
- if (!maybeSwitchToTab(pageURL, flags)) {
- openUrlAndStopEditing(pageURL);
- clearSelectedTabApplicationId();
- }
- }
- }
-
- // HomePager.OnUrlOpenInBackgroundListener
- @Override
- public void onUrlOpenInBackground(final String url, EnumSet<OnUrlOpenInBackgroundListener.Flags> flags) {
- if (url == null) {
- throw new IllegalArgumentException("url must not be null");
- }
- if (flags == null) {
- throw new IllegalArgumentException("flags must not be null");
- }
-
- // We only use onUrlOpenInBackgroundListener for the homepanel context menus, hence
- // we should always be checking whether we want the readermode version
- final String pageURL = SavedReaderViewHelper.getReaderURLIfCached(getContext(), url);
-
- final boolean isPrivate = flags.contains(OnUrlOpenInBackgroundListener.Flags.PRIVATE);
-
- int loadFlags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_BACKGROUND;
- if (isPrivate) {
- loadFlags |= Tabs.LOADURL_PRIVATE;
- }
-
- final Tab newTab = Tabs.getInstance().loadUrl(pageURL, loadFlags);
-
- // We switch to the desired tab by unique ID, which closes any window
- // for a race between opening the tab and closing it, and switching to
- // it. We could also switch to the Tab explicitly, but we don't want to
- // hold a reference to the Tab itself in the anonymous listener class.
- final int newTabId = newTab.getId();
-
- final SnackbarBuilder.SnackbarCallback callback = new SnackbarBuilder.SnackbarCallback() {
- @Override
- public void onClick(View v) {
- Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.TOAST, "switchtab");
-
- maybeSwitchToTab(newTabId);
- }
- };
-
- final String message = isPrivate ?
- getResources().getString(R.string.new_private_tab_opened) :
- getResources().getString(R.string.new_tab_opened);
- final String buttonMessage = getResources().getString(R.string.switch_button_message);
-
- SnackbarBuilder.builder(this)
- .message(message)
- .duration(Snackbar.LENGTH_LONG)
- .action(buttonMessage)
- .callback(callback)
- .buildAndShow();
- }
-
- // BrowserSearch.OnSearchListener
- @Override
- public void onSearch(SearchEngine engine, final String text, final TelemetryContract.Method method) {
- // Don't store searches that happen in private tabs. This assumes the user can only
- // perform a search inside the currently selected tab, which is true for searches
- // that come from SearchEngineRow.
- if (!Tabs.getInstance().getSelectedTab().isPrivate()) {
- storeSearchQuery(text);
- }
-
- // We don't use SearchEngine.getEngineIdentifier because it can
- // return a custom search engine name, which is a privacy concern.
- final String identifierToRecord = (engine.identifier != null) ? engine.identifier : "other";
- recordSearch(GeckoSharedPrefs.forProfile(this), identifierToRecord, method);
- openUrlAndStopEditing(text, engine.name);
- }
-
- // BrowserSearch.OnEditSuggestionListener
- @Override
- public void onEditSuggestion(String suggestion) {
- mBrowserToolbar.onEditSuggestion(suggestion);
- }
-
- @Override
- public int getLayout() { return R.layout.gecko_app; }
-
- public SearchEngineManager getSearchEngineManager() {
- return mSearchEngineManager;
- }
-
- // For use from tests only.
- @RobocopTarget
- public ReadingListHelper getReadingListHelper() {
- return mReadingListHelper;
- }
-
- /**
- * Launch UI that lets the user update Firefox.
- *
- * This depends on the current channel: Release and Beta both direct to the
- * Google Play Store. If updating is enabled, Aurora, Nightly, and custom
- * builds open about:, which provides an update interface.
- *
- * If updating is not enabled, this simply logs an error.
- *
- * @return true if update UI was launched.
- */
- protected boolean handleUpdaterLaunch() {
- if (AppConstants.RELEASE_OR_BETA) {
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setData(Uri.parse("market://details?id=" + getPackageName()));
- startActivity(intent);
- return true;
- }
-
- if (AppConstants.MOZ_UPDATER) {
- Tabs.getInstance().loadUrlInTab(AboutPages.UPDATER);
- return true;
- }
-
- Log.w(LOGTAG, "No candidate updater found; ignoring launch request.");
- return false;
- }
-
- /* Implementing ActionModeCompat.Presenter */
- @Override
- public void startActionModeCompat(final ActionModeCompat.Callback callback) {
- // If actionMode is null, we're not currently showing one. Flip to the action mode view
- if (mActionMode == null) {
- mActionBarFlipper.showNext();
- DynamicToolbarAnimator toolbar = mLayerView.getDynamicToolbarAnimator();
-
- // If the toolbar is dynamic and not currently showing, just slide it in
- if (mDynamicToolbar.isEnabled() && toolbar.getToolbarTranslation() != 0) {
- mDynamicToolbar.setTemporarilyVisible(true, VisibilityTransition.ANIMATE);
- }
- mDynamicToolbar.setPinned(true, PinReason.ACTION_MODE);
-
- } else {
- // Otherwise, we're already showing an action mode. Just finish it and show the new one
- mActionMode.finish();
- }
-
- mActionMode = new ActionModeCompat(BrowserApp.this, callback, mActionBar);
- if (callback.onCreateActionMode(mActionMode, mActionMode.getMenu())) {
- mActionMode.invalidate();
- }
- }
-
- /* Implementing ActionModeCompat.Presenter */
- @Override
- public void endActionModeCompat() {
- if (mActionMode == null) {
- return;
- }
-
- mActionMode.finish();
- mActionMode = null;
- mDynamicToolbar.setPinned(false, PinReason.ACTION_MODE);
-
- mActionBarFlipper.showPrevious();
-
- // Only slide the urlbar out if it was hidden when the action mode started
- // Don't animate hiding it so that there's no flash as we switch back to url mode
- mDynamicToolbar.setTemporarilyVisible(false, VisibilityTransition.IMMEDIATE);
- }
-
- public static interface TabStripInterface {
- public void refresh();
- void setOnTabChangedListener(OnTabAddedOrRemovedListener listener);
- interface OnTabAddedOrRemovedListener {
- void onTabChanged();
- }
- }
-
- @Override
- protected void recordStartupActionTelemetry(final String passedURL, final String action) {
- final TelemetryContract.Method method;
- if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
- // This action is also recorded via "loadurl.1" > "homescreen".
- method = TelemetryContract.Method.HOMESCREEN;
- } else if (passedURL == null) {
- Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, TelemetryContract.Method.HOMESCREEN, "launcher");
- method = TelemetryContract.Method.HOMESCREEN;
- } else {
- // This is action is also recorded via "loadurl.1" > "intent".
- method = TelemetryContract.Method.INTENT;
- }
-
- if (GeckoProfile.get(this).inGuestMode()) {
- Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, method, "guest");
- } else if (Restrictions.isRestrictedProfile(this)) {
- Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, method, "restricted");
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/BrowserLocaleManager.java b/mobile/android/base/java/org/mozilla/gecko/BrowserLocaleManager.java
deleted file mode 100644
index c5c041c7a..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserLocaleManager.java
+++ /dev/null
@@ -1,439 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import java.io.File;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.Locale;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.annotation.ReflectionTarget;
-import org.mozilla.gecko.util.GeckoJarReader;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.SharedPreferences;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.util.Log;
-
-/**
- * This class manages persistence, application, and otherwise handling of
- * user-specified locales.
- *
- * Of note:
- *
- * * It's a singleton, because its scope extends to that of the application,
- * and definitionally all changes to the locale of the app must go through
- * this.
- * * It's lazy.
- * * It has ties into the Gecko event system, because it has to tell Gecko when
- * to switch locale.
- * * It relies on using the SharedPreferences file owned by the browser (in
- * Fennec's case, "GeckoApp") for performance.
- */
-public class BrowserLocaleManager implements LocaleManager {
- private static final String LOG_TAG = "GeckoLocales";
-
- private static final String EVENT_LOCALE_CHANGED = "Locale:Changed";
- private static final String PREF_LOCALE = "locale";
-
- private static final String FALLBACK_LOCALE_TAG = "en-US";
-
- // These are volatile because we don't impose restrictions
- // over which thread calls our methods.
- private volatile Locale currentLocale;
- private volatile Locale systemLocale = Locale.getDefault();
-
- private final AtomicBoolean inited = new AtomicBoolean(false);
- private boolean systemLocaleDidChange;
- private BroadcastReceiver receiver;
-
- private static final AtomicReference<LocaleManager> instance = new AtomicReference<LocaleManager>();
-
- @ReflectionTarget
- public static LocaleManager getInstance() {
- LocaleManager localeManager = instance.get();
- if (localeManager != null) {
- return localeManager;
- }
-
- localeManager = new BrowserLocaleManager();
- if (instance.compareAndSet(null, localeManager)) {
- return localeManager;
- } else {
- return instance.get();
- }
- }
-
- @Override
- public boolean isEnabled() {
- return AppConstants.MOZ_LOCALE_SWITCHER;
- }
-
- /**
- * Ensure that you call this early in your application startup,
- * and with a context that's sufficiently long-lived (typically
- * the application context).
- *
- * Calling multiple times is harmless.
- */
- @Override
- public void initialize(final Context context) {
- if (!inited.compareAndSet(false, true)) {
- return;
- }
-
- receiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- final Locale current = systemLocale;
-
- // We don't trust Locale.getDefault() here, because we make a
- // habit of mutating it! Use the one Android supplies, because
- // that gets regularly reset.
- // The default value of systemLocale is fine, because we haven't
- // yet swizzled Locale during static initialization.
- systemLocale = context.getResources().getConfiguration().locale;
- systemLocaleDidChange = true;
-
- Log.d(LOG_TAG, "System locale changed from " + current + " to " + systemLocale);
- }
- };
- context.registerReceiver(receiver, new IntentFilter(Intent.ACTION_LOCALE_CHANGED));
- }
-
- @Override
- public boolean systemLocaleDidChange() {
- return systemLocaleDidChange;
- }
-
- /**
- * Every time the system gives us a new configuration, it
- * carries the external locale. Fix it.
- */
- @Override
- public void correctLocale(Context context, Resources res, Configuration config) {
- final Locale current = getCurrentLocale(context);
- if (current == null) {
- Log.d(LOG_TAG, "No selected locale. No correction needed.");
- return;
- }
-
- // I know it's tempting to short-circuit here if the config seems to be
- // up-to-date, but the rest is necessary.
-
- config.locale = current;
-
- // The following two lines are heavily commented in case someone
- // decides to chase down performance improvements and decides to
- // question what's going on here.
- // Both lines should be cheap, *but*...
-
- // This is unnecessary for basic string choice, but it almost
- // certainly comes into play when rendering numbers, deciding on RTL,
- // etc. Take it out if you can prove that's not the case.
- Locale.setDefault(current);
-
- // This seems to be a no-op, but every piece of documentation under the
- // sun suggests that it's necessary, and it certainly makes sense.
- res.updateConfiguration(config, null);
- }
-
- /**
- * We can be in one of two states.
- *
- * If the user has not explicitly chosen a Firefox-specific locale, we say
- * we are "mirroring" the system locale.
- *
- * When we are not mirroring, system locale changes do not impact Firefox
- * and are essentially ignored; the user's locale selection is the only
- * thing we care about, and we actively correct incoming configuration
- * changes to reflect the user's chosen locale.
- *
- * By contrast, when we are mirroring, system locale changes cause Firefox
- * to reflect the new system locale, as if the user picked the new locale.
- *
- * If we're currently mirroring the system locale, this method returns the
- * supplied configuration's locale, unless the current activity locale is
- * correct. If we're not currently mirroring, this method updates the
- * configuration object to match the user's currently selected locale, and
- * returns that, unless the current activity locale is correct.
- *
- * If the current activity locale is correct, returns null.
- *
- * The caller is expected to redisplay themselves accordingly.
- *
- * This method is intended to be called from inside
- * <code>onConfigurationChanged(Configuration)</code> as part of a strategy
- * to detect and either apply or undo system locale changes.
- */
- @Override
- public Locale onSystemConfigurationChanged(final Context context, final Resources resources, final Configuration configuration, final Locale currentActivityLocale) {
- if (!isMirroringSystemLocale(context)) {
- correctLocale(context, resources, configuration);
- }
-
- final Locale changed = configuration.locale;
- if (changed.equals(currentActivityLocale)) {
- return null;
- }
-
- return changed;
- }
-
- /**
- * Gecko needs to know the OS locale to compute a useful Accept-Language
- * header. If it changed since last time, send a message to Gecko and
- * persist the new value. If unchanged, returns immediately.
- *
- * @param prefs the SharedPreferences instance to use. Cannot be null.
- * @param osLocale the new locale instance. Safe if null.
- */
- public static void storeAndNotifyOSLocale(final SharedPreferences prefs,
- final Locale osLocale) {
- if (osLocale == null) {
- return;
- }
-
- final String lastOSLocale = prefs.getString("osLocale", null);
- final String osLocaleString = osLocale.toString();
-
- if (osLocaleString.equals(lastOSLocale)) {
- return;
- }
-
- // Store the Java-native form.
- prefs.edit().putString("osLocale", osLocaleString).apply();
-
- // The value we send to Gecko should be a language tag, not
- // a Java locale string.
- final String osLanguageTag = Locales.getLanguageTag(osLocale);
- GeckoAppShell.notifyObservers("Locale:OS", osLanguageTag);
- }
-
- @Override
- public String getAndApplyPersistedLocale(Context context) {
- initialize(context);
-
- final long t1 = android.os.SystemClock.uptimeMillis();
- final String localeCode = getPersistedLocale(context);
- if (localeCode == null) {
- return null;
- }
-
- // Note that we don't tell Gecko about this. We notify Gecko when the
- // locale is set, not when we update Java.
- final String resultant = updateLocale(context, localeCode);
-
- if (resultant == null) {
- // Update the configuration anyway.
- updateConfiguration(context, currentLocale);
- }
-
- final long t2 = android.os.SystemClock.uptimeMillis();
- Log.i(LOG_TAG, "Locale read and update took: " + (t2 - t1) + "ms.");
- return resultant;
- }
-
- /**
- * Returns the set locale if it changed.
- *
- * Always persists and notifies Gecko.
- */
- @Override
- public String setSelectedLocale(Context context, String localeCode) {
- final String resultant = updateLocale(context, localeCode);
-
- // We always persist and notify Gecko, even if nothing seemed to
- // change. This might happen if you're picking a locale that's the same
- // as the current OS locale. The OS locale might change next time we
- // launch, and we need the Gecko pref and persisted locale to have been
- // set by the time that happens.
- persistLocale(context, localeCode);
-
- // Tell Gecko.
- GeckoAppShell.notifyObservers(EVENT_LOCALE_CHANGED, Locales.getLanguageTag(getCurrentLocale(context)));
-
- return resultant;
- }
-
- @Override
- public void resetToSystemLocale(Context context) {
- // Wipe the pref.
- final SharedPreferences settings = getSharedPreferences(context);
- settings.edit().remove(PREF_LOCALE).apply();
-
- // Apply the system locale.
- updateLocale(context, systemLocale);
-
- // Tell Gecko.
- GeckoAppShell.notifyObservers(EVENT_LOCALE_CHANGED, "");
- }
-
- /**
- * This is public to allow for an activity to force the
- * current locale to be applied if necessary (e.g., when
- * a new activity launches).
- */
- @Override
- public void updateConfiguration(Context context, Locale locale) {
- Resources res = context.getResources();
- Configuration config = res.getConfiguration();
-
- // We should use setLocale, but it's unexpectedly missing
- // on real devices.
- config.locale = locale;
- res.updateConfiguration(config, null);
- }
-
- private SharedPreferences getSharedPreferences(Context context) {
- return GeckoSharedPrefs.forApp(context);
- }
-
- /**
- * @return the persisted locale in Java format: "en_US".
- */
- private String getPersistedLocale(Context context) {
- final SharedPreferences settings = getSharedPreferences(context);
- final String locale = settings.getString(PREF_LOCALE, "");
-
- if ("".equals(locale)) {
- return null;
- }
- return locale;
- }
-
- private void persistLocale(Context context, String localeCode) {
- final SharedPreferences settings = getSharedPreferences(context);
- settings.edit().putString(PREF_LOCALE, localeCode).apply();
- }
-
- @Override
- public Locale getCurrentLocale(Context context) {
- if (currentLocale != null) {
- return currentLocale;
- }
-
- final String current = getPersistedLocale(context);
- if (current == null) {
- return null;
- }
- return currentLocale = Locales.parseLocaleCode(current);
- }
-
- /**
- * Updates the Java locale and the Android configuration.
- *
- * Returns the persisted locale if it differed.
- *
- * Does not notify Gecko.
- *
- * @param localeCode a locale string in Java format: "en_US".
- * @return if it differed, a locale string in Java format: "en_US".
- */
- private String updateLocale(Context context, String localeCode) {
- // Fast path.
- final Locale defaultLocale = Locale.getDefault();
- if (defaultLocale.toString().equals(localeCode)) {
- return null;
- }
-
- final Locale locale = Locales.parseLocaleCode(localeCode);
-
- return updateLocale(context, locale);
- }
-
- /**
- * @return the Java locale string: e.g., "en_US".
- */
- private String updateLocale(Context context, final Locale locale) {
- // Fast path.
- if (Locale.getDefault().equals(locale)) {
- return null;
- }
-
- Locale.setDefault(locale);
- currentLocale = locale;
-
- // Update resources.
- updateConfiguration(context, locale);
-
- return locale.toString();
- }
-
- private boolean isMirroringSystemLocale(final Context context) {
- return getPersistedLocale(context) == null;
- }
-
- /**
- * Examines <code>multilocale.json</code>, returning the included list of
- * locale codes.
- *
- * If <code>multilocale.json</code> is not present, returns
- * <code>null</code>. In that case, consider {@link #getFallbackLocaleTag()}.
- *
- * multilocale.json currently looks like this:
- *
- * <code>
- * {"locales": ["en-US", "be", "ca", "cs", "da", "de", "en-GB",
- * "en-ZA", "es-AR", "es-ES", "es-MX", "et", "fi",
- * "fr", "ga-IE", "hu", "id", "it", "ja", "ko",
- * "lt", "lv", "nb-NO", "nl", "pl", "pt-BR",
- * "pt-PT", "ro", "ru", "sk", "sl", "sv-SE", "th",
- * "tr", "uk", "zh-CN", "zh-TW", "en-US"]}
- * </code>
- */
- public static Collection<String> getPackagedLocaleTags(final Context context) {
- final String resPath = "res/multilocale.json";
- final String jarURL = GeckoJarReader.getJarURL(context, resPath);
-
- final String contents = GeckoJarReader.getText(context, jarURL);
- if (contents == null) {
- // GeckoJarReader logs and swallows exceptions.
- return null;
- }
-
- try {
- final JSONObject multilocale = new JSONObject(contents);
- final JSONArray locales = multilocale.getJSONArray("locales");
- if (locales == null) {
- Log.e(LOG_TAG, "No 'locales' array in multilocales.json!");
- return null;
- }
-
- final Set<String> out = new HashSet<String>(locales.length());
- for (int i = 0; i < locales.length(); ++i) {
- // If any item in the array is invalid, this will throw,
- // and the entire clause will fail, being caught below
- // and returning null.
- out.add(locales.getString(i));
- }
-
- return out;
- } catch (JSONException e) {
- Log.e(LOG_TAG, "Unable to parse multilocale.json.", e);
- return null;
- }
- }
-
- /**
- * @return the single default locale baked into this application.
- * Applicable when there is no multilocale.json present.
- */
- @SuppressWarnings("static-method")
- public String getFallbackLocaleTag() {
- return FALLBACK_LOCALE_TAG;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/ChromeCastDisplay.java b/mobile/android/base/java/org/mozilla/gecko/ChromeCastDisplay.java
deleted file mode 100644
index cff6ea643..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/ChromeCastDisplay.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * vim: ts=4 sw=4 expandtab:
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.json.JSONObject;
-import org.json.JSONException;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.EventCallback;
-
-import com.google.android.gms.cast.CastDevice;
-import com.google.android.gms.cast.CastRemoteDisplayLocalService;
-import com.google.android.gms.common.ConnectionResult;
-import com.google.android.gms.common.GooglePlayServicesUtil;
-import com.google.android.gms.common.api.Status;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.support.v7.media.MediaRouter.RouteInfo;
-import android.util.Log;
-
-public class ChromeCastDisplay implements GeckoPresentationDisplay {
-
- static final String REMOTE_DISPLAY_APP_ID = "4574A331";
-
- private static final String LOGTAG = "GeckoChromeCastDisplay";
- private final Context context;
- private final RouteInfo route;
- private CastDevice castDevice;
-
- public ChromeCastDisplay(Context context, RouteInfo route) {
- int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable(context);
- if (status != ConnectionResult.SUCCESS) {
- throw new IllegalStateException("Play services are required for Chromecast support (got status code " + status + ")");
- }
-
- this.context = context;
- this.route = route;
- this.castDevice = CastDevice.getFromBundle(route.getExtras());
- }
-
- public JSONObject toJSON() {
- final JSONObject obj = new JSONObject();
- try {
- if (castDevice == null) {
- return null;
- }
- obj.put("uuid", route.getId());
- obj.put("friendlyName", castDevice.getFriendlyName());
- obj.put("type", "chromecast");
- } catch (JSONException ex) {
- Log.d(LOGTAG, "Error building route", ex);
- }
-
- return obj;
- }
-
- @Override
- public void start(final EventCallback callback) {
-
- if (CastRemoteDisplayLocalService.getInstance() != null) {
- Log.d(LOGTAG, "CastRemoteDisplayLocalService already existed.");
- GeckoAppShell.notifyObservers("presentation-view-ready", route.getId());
- callback.sendSuccess("Succeed to start presentation.");
- return;
- }
-
- Intent intent = new Intent(context, RemotePresentationService.class);
- intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
- PendingIntent notificationPendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
-
- CastRemoteDisplayLocalService.NotificationSettings settings =
- new CastRemoteDisplayLocalService.NotificationSettings.Builder()
- .setNotificationPendingIntent(notificationPendingIntent).build();
-
- CastRemoteDisplayLocalService.startService(
- context,
- RemotePresentationService.class,
- REMOTE_DISPLAY_APP_ID,
- castDevice,
- settings,
- new CastRemoteDisplayLocalService.Callbacks() {
- @Override
- public void onServiceCreated(CastRemoteDisplayLocalService service) {
- ((RemotePresentationService) service).setDeviceId(route.getId());
- }
-
- @Override
- public void onRemoteDisplaySessionStarted(CastRemoteDisplayLocalService service) {
- Log.d(LOGTAG, "Remote presentation launched!");
- callback.sendSuccess("Succeed to start presentation.");
- }
-
- @Override
- public void onRemoteDisplaySessionError(Status errorReason) {
- int code = errorReason.getStatusCode();
- callback.sendError("Fail to start presentation. Error code: " + code);
- }
- });
- }
-
- @Override
- public void stop(EventCallback callback) {
- CastRemoteDisplayLocalService.stopService();
- callback.sendSuccess("Succeed to stop presentation.");
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/ChromeCastPlayer.java b/mobile/android/base/java/org/mozilla/gecko/ChromeCastPlayer.java
deleted file mode 100644
index c531b8c37..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/ChromeCastPlayer.java
+++ /dev/null
@@ -1,509 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import java.io.IOException;
-
-import org.mozilla.gecko.util.EventCallback;
-import org.json.JSONObject;
-import org.json.JSONException;
-
-import com.google.android.gms.cast.Cast.MessageReceivedCallback;
-import com.google.android.gms.cast.ApplicationMetadata;
-import com.google.android.gms.cast.Cast;
-import com.google.android.gms.cast.Cast.ApplicationConnectionResult;
-import com.google.android.gms.cast.CastDevice;
-import com.google.android.gms.cast.CastMediaControlIntent;
-import com.google.android.gms.cast.MediaInfo;
-import com.google.android.gms.cast.MediaMetadata;
-import com.google.android.gms.cast.MediaStatus;
-import com.google.android.gms.cast.RemoteMediaPlayer;
-import com.google.android.gms.cast.RemoteMediaPlayer.MediaChannelResult;
-import com.google.android.gms.common.ConnectionResult;
-import com.google.android.gms.common.api.GoogleApiClient;
-import com.google.android.gms.common.api.ResultCallback;
-import com.google.android.gms.common.api.Status;
-import com.google.android.gms.common.GooglePlayServicesUtil;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.support.v7.media.MediaRouter.RouteInfo;
-import android.util.Log;
-
-/* Implementation of GeckoMediaPlayer for talking to ChromeCast devices */
-class ChromeCastPlayer implements GeckoMediaPlayer {
- private static final boolean SHOW_DEBUG = false;
-
- static final String MIRROR_RECEIVER_APP_ID = "08FF1091";
-
- private final Context context;
- private final RouteInfo route;
- private GoogleApiClient apiClient;
- private RemoteMediaPlayer remoteMediaPlayer;
- private final boolean canMirror;
- private String mSessionId;
- private MirrorChannel mMirrorChannel;
- private boolean mApplicationStarted = false;
-
- // EventCallback which is actually a GeckoEventCallback is sometimes being invoked more
- // than once. That causes the IllegalStateException to be thrown. To prevent a crash,
- // catch the exception and report it as an error to the log.
- private static void sendSuccess(final EventCallback callback, final String msg) {
- try {
- callback.sendSuccess(msg);
- } catch (final IllegalStateException e) {
- Log.e(LOGTAG, "Attempting to invoke callback.sendSuccess more than once.", e);
- }
- }
-
- private static void sendError(final EventCallback callback, final String msg) {
- try {
- callback.sendError(msg);
- } catch (final IllegalStateException e) {
- Log.e(LOGTAG, "Attempting to invoke callback.sendError more than once.", e);
- }
- }
-
- // Callback to start playback of a url on a remote device
- private class VideoPlayCallback implements ResultCallback<ApplicationConnectionResult>,
- RemoteMediaPlayer.OnStatusUpdatedListener,
- RemoteMediaPlayer.OnMetadataUpdatedListener {
- private final String url;
- private final String type;
- private final String title;
- private final EventCallback callback;
-
- public VideoPlayCallback(String url, String type, String title, EventCallback callback) {
- this.url = url;
- this.type = type;
- this.title = title;
- this.callback = callback;
- }
-
- @Override
- public void onStatusUpdated() {
- MediaStatus mediaStatus = remoteMediaPlayer.getMediaStatus();
-
- switch (mediaStatus.getPlayerState()) {
- case MediaStatus.PLAYER_STATE_PLAYING:
- GeckoAppShell.notifyObservers("MediaPlayer:Playing", null);
- break;
- case MediaStatus.PLAYER_STATE_PAUSED:
- GeckoAppShell.notifyObservers("MediaPlayer:Paused", null);
- break;
- case MediaStatus.PLAYER_STATE_IDLE:
- // TODO: Do we want to shutdown when there are errors?
- if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_FINISHED) {
- GeckoAppShell.notifyObservers("Casting:Stop", null);
- }
- break;
- default:
- // TODO: Do we need to handle other status such as buffering / unknown?
- break;
- }
- }
-
- @Override
- public void onMetadataUpdated() { }
-
- @Override
- public void onResult(ApplicationConnectionResult result) {
- Status status = result.getStatus();
- debug("ApplicationConnectionResultCallback.onResult: statusCode" + status.getStatusCode());
- if (status.isSuccess()) {
- remoteMediaPlayer = new RemoteMediaPlayer();
- remoteMediaPlayer.setOnStatusUpdatedListener(this);
- remoteMediaPlayer.setOnMetadataUpdatedListener(this);
- mSessionId = result.getSessionId();
- if (!verifySession(callback)) {
- return;
- }
-
- try {
- Cast.CastApi.setMessageReceivedCallbacks(apiClient, remoteMediaPlayer.getNamespace(), remoteMediaPlayer);
- } catch (IOException e) {
- debug("Exception while creating media channel", e);
- }
-
- startPlayback();
- } else {
- sendError(callback, status.toString());
- }
- }
-
- private void startPlayback() {
- MediaMetadata mediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
- mediaMetadata.putString(MediaMetadata.KEY_TITLE, title);
- MediaInfo mediaInfo = new MediaInfo.Builder(url)
- .setContentType(type)
- .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
- .setMetadata(mediaMetadata)
- .build();
- try {
- remoteMediaPlayer.load(apiClient, mediaInfo, true).setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {
- @Override
- public void onResult(MediaChannelResult result) {
- if (result.getStatus().isSuccess()) {
- sendSuccess(callback, null);
- debug("Media loaded successfully");
- return;
- }
-
- debug("Media load failed " + result.getStatus());
- sendError(callback, result.getStatus().toString());
- }
- });
-
- return;
- } catch (IllegalStateException e) {
- debug("Problem occurred with media during loading", e);
- } catch (Exception e) {
- debug("Problem opening media during loading", e);
- }
-
- sendError(callback, "");
- }
- }
-
- public ChromeCastPlayer(Context context, RouteInfo route) {
- int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable(context);
- if (status != ConnectionResult.SUCCESS) {
- throw new IllegalStateException("Play services are required for Chromecast support (got status code " + status + ")");
- }
-
- this.context = context;
- this.route = route;
- this.canMirror = route.supportsControlCategory(CastMediaControlIntent.categoryForCast(MIRROR_RECEIVER_APP_ID));
- }
-
- /**
- * This dumps everything we can find about the device into JSON. This will hopefully make it
- * easier to filter out duplicate devices from different sources in JS.
- * Returns null if the device can't be found.
- */
- @Override
- public JSONObject toJSON() {
- final JSONObject obj = new JSONObject();
- try {
- final CastDevice device = CastDevice.getFromBundle(route.getExtras());
- if (device == null) {
- return null;
- }
-
- obj.put("uuid", route.getId());
- obj.put("version", device.getDeviceVersion());
- obj.put("friendlyName", device.getFriendlyName());
- obj.put("location", device.getIpAddress().toString());
- obj.put("modelName", device.getModelName());
- obj.put("mirror", canMirror);
- // For now we just assume all of these are Google devices
- obj.put("manufacturer", "Google Inc.");
- } catch (JSONException ex) {
- debug("Error building route", ex);
- }
-
- return obj;
- }
-
- @Override
- public void load(final String title, final String url, final String type, final EventCallback callback) {
- final CastDevice device = CastDevice.getFromBundle(route.getExtras());
- Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(device, new Cast.Listener() {
- @Override
- public void onApplicationStatusChanged() { }
-
- @Override
- public void onVolumeChanged() { }
-
- @Override
- public void onApplicationDisconnected(int errorCode) { }
- });
-
- apiClient = new GoogleApiClient.Builder(context)
- .addApi(Cast.API, apiOptionsBuilder.build())
- .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
- @Override
- public void onConnected(Bundle connectionHint) {
- // Sometimes apiClient is null here. See bug 1061032
- if (apiClient != null && !apiClient.isConnected()) {
- debug("Connection failed");
- sendError(callback, "Not connected");
- return;
- }
-
- // Launch the media player app and launch this url once its loaded
- try {
- Cast.CastApi.launchApplication(apiClient, CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID, true)
- .setResultCallback(new VideoPlayCallback(url, type, title, callback));
- } catch (Exception e) {
- debug("Failed to launch application", e);
- }
- }
-
- @Override
- public void onConnectionSuspended(int cause) {
- debug("suspended");
- }
- }).build();
-
- apiClient.connect();
- }
-
- @Override
- public void start(final EventCallback callback) {
- // Nothing to be done here
- sendSuccess(callback, null);
- }
-
- @Override
- public void stop(final EventCallback callback) {
- // Nothing to be done here
- sendSuccess(callback, null);
- }
-
- public boolean verifySession(final EventCallback callback) {
- String msg = null;
- if (apiClient == null || !apiClient.isConnected()) {
- msg = "Not connected";
- }
-
- if (mSessionId == null) {
- msg = "No session";
- }
-
- if (msg != null) {
- debug(msg);
- if (callback != null) {
- sendError(callback, msg);
- }
- return false;
- }
-
- return true;
- }
-
- @Override
- public void play(final EventCallback callback) {
- if (!verifySession(callback)) {
- return;
- }
-
- try {
- remoteMediaPlayer.play(apiClient).setResultCallback(new ResultCallback<MediaChannelResult>() {
- @Override
- public void onResult(MediaChannelResult result) {
- Status status = result.getStatus();
- if (!status.isSuccess()) {
- debug("Unable to play: " + status.getStatusCode());
- sendError(callback, status.toString());
- } else {
- sendSuccess(callback, null);
- }
- }
- });
- } catch (IllegalStateException ex) {
- // The media player may throw if the session has been killed. For now, we're just catching this here.
- sendError(callback, "Error playing");
- }
- }
-
- @Override
- public void pause(final EventCallback callback) {
- if (!verifySession(callback)) {
- return;
- }
-
- try {
- remoteMediaPlayer.pause(apiClient).setResultCallback(new ResultCallback<MediaChannelResult>() {
- @Override
- public void onResult(MediaChannelResult result) {
- Status status = result.getStatus();
- if (!status.isSuccess()) {
- debug("Unable to pause: " + status.getStatusCode());
- sendError(callback, status.toString());
- } else {
- sendSuccess(callback, null);
- }
- }
- });
- } catch (IllegalStateException ex) {
- // The media player may throw if the session has been killed. For now, we're just catching this here.
- sendError(callback, "Error pausing");
- }
- }
-
- @Override
- public void end(final EventCallback callback) {
- if (!verifySession(callback)) {
- return;
- }
-
- try {
- Cast.CastApi.stopApplication(apiClient).setResultCallback(new ResultCallback<Status>() {
- @Override
- public void onResult(Status result) {
- if (result.isSuccess()) {
- try {
- Cast.CastApi.removeMessageReceivedCallbacks(apiClient, remoteMediaPlayer.getNamespace());
- remoteMediaPlayer = null;
- mSessionId = null;
- apiClient.disconnect();
- apiClient = null;
-
- if (callback != null) {
- sendSuccess(callback, null);
- }
-
- return;
- } catch (Exception ex) {
- debug("Error ending", ex);
- }
- }
-
- if (callback != null) {
- sendError(callback, result.getStatus().toString());
- }
- }
- });
- } catch (IllegalStateException ex) {
- // The media player may throw if the session has been killed. For now, we're just catching this here.
- sendError(callback, "Error stopping");
- }
- }
-
- class MirrorChannel implements MessageReceivedCallback {
- /**
- * @return custom namespace
- */
- public String getNamespace() {
- return "urn:x-cast:org.mozilla.mirror";
- }
-
- /*
- * Receive message from the receiver app
- */
- @Override
- public void onMessageReceived(CastDevice castDevice, String namespace,
- String message) {
- GeckoAppShell.notifyObservers("MediaPlayer:Response", message);
- }
-
- public void sendMessage(String message) {
- if (apiClient != null && mMirrorChannel != null) {
- try {
- Cast.CastApi.sendMessage(apiClient, mMirrorChannel.getNamespace(), message)
- .setResultCallback(
- new ResultCallback<Status>() {
- @Override
- public void onResult(Status result) {
- }
- });
- } catch (Exception e) {
- Log.e(LOGTAG, "Exception while sending message", e);
- }
- }
- }
- }
- private class MirrorCallback implements ResultCallback<ApplicationConnectionResult> {
- final EventCallback callback;
- MirrorCallback(final EventCallback callback) {
- this.callback = callback;
- }
-
-
- @Override
- public void onResult(ApplicationConnectionResult result) {
- Status status = result.getStatus();
- if (status.isSuccess()) {
- ApplicationMetadata applicationMetadata = result.getApplicationMetadata();
- mSessionId = result.getSessionId();
- String applicationStatus = result.getApplicationStatus();
- boolean wasLaunched = result.getWasLaunched();
- mApplicationStarted = true;
-
- // Create the custom message
- // channel
- mMirrorChannel = new MirrorChannel();
- try {
- Cast.CastApi.setMessageReceivedCallbacks(apiClient,
- mMirrorChannel
- .getNamespace(),
- mMirrorChannel);
- sendSuccess(callback, null);
- } catch (IOException e) {
- Log.e(LOGTAG, "Exception while creating channel", e);
- }
-
- GeckoAppShell.notifyObservers("Casting:Mirror", route.getId());
- } else {
- sendError(callback, status.toString());
- }
- }
- }
-
- @Override
- public void message(String msg, final EventCallback callback) {
- if (mMirrorChannel != null) {
- mMirrorChannel.sendMessage(msg);
- }
- }
-
- @Override
- public void mirror(final EventCallback callback) {
- final CastDevice device = CastDevice.getFromBundle(route.getExtras());
- Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(device, new Cast.Listener() {
- @Override
- public void onApplicationStatusChanged() { }
-
- @Override
- public void onVolumeChanged() { }
-
- @Override
- public void onApplicationDisconnected(int errorCode) { }
- });
-
- apiClient = new GoogleApiClient.Builder(context)
- .addApi(Cast.API, apiOptionsBuilder.build())
- .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
- @Override
- public void onConnected(Bundle connectionHint) {
- // Sometimes apiClient is null here. See bug 1061032
- if (apiClient == null || !apiClient.isConnected()) {
- return;
- }
-
- // Launch the media player app and launch this url once its loaded
- try {
- Cast.CastApi.launchApplication(apiClient, MIRROR_RECEIVER_APP_ID, true)
- .setResultCallback(new MirrorCallback(callback));
- } catch (Exception e) {
- debug("Failed to launch application", e);
- }
- }
-
- @Override
- public void onConnectionSuspended(int cause) {
- debug("suspended");
- }
- }).build();
-
- apiClient.connect();
- }
-
- private static final String LOGTAG = "GeckoChromeCastPlayer";
- private void debug(String msg, Exception e) {
- if (SHOW_DEBUG) {
- Log.e(LOGTAG, msg, e);
- }
- }
-
- private void debug(String msg) {
- if (SHOW_DEBUG) {
- Log.d(LOGTAG, msg);
- }
- }
-
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/CrashReporter.java b/mobile/android/base/java/org/mozilla/gecko/CrashReporter.java
deleted file mode 100644
index ce2384a4d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/CrashReporter.java
+++ /dev/null
@@ -1,480 +0,0 @@
-/* -*- Mode: Java; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.FileReader;
-import java.io.InputStreamReader;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.nio.channels.Channels;
-import java.nio.channels.FileChannel;
-import java.util.zip.GZIPOutputStream;
-
-import org.mozilla.gecko.AppConstants.Versions;
-
-import android.annotation.SuppressLint;
-import android.app.AlertDialog;
-import android.app.ProgressDialog;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.support.v7.app.AppCompatActivity;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.View;
-import android.widget.CheckBox;
-import android.widget.CompoundButton;
-import android.widget.EditText;
-
-@SuppressLint("Registered") // This activity is only registered in the manifest if MOZ_CRASHREPORTER is set
-public class CrashReporter extends AppCompatActivity
-{
- private static final String LOGTAG = "GeckoCrashReporter";
-
- private static final String PASSED_MINI_DUMP_KEY = "minidumpPath";
- private static final String PASSED_MINI_DUMP_SUCCESS_KEY = "minidumpSuccess";
- private static final String MINI_DUMP_PATH_KEY = "upload_file_minidump";
- private static final String PAGE_URL_KEY = "URL";
- private static final String NOTES_KEY = "Notes";
- private static final String SERVER_URL_KEY = "ServerURL";
-
- private static final String CRASH_REPORT_SUFFIX = "/mozilla/Crash Reports/";
- private static final String PENDING_SUFFIX = CRASH_REPORT_SUFFIX + "pending";
- private static final String SUBMITTED_SUFFIX = CRASH_REPORT_SUFFIX + "submitted";
-
- private static final String PREFS_SEND_REPORT = "sendReport";
- private static final String PREFS_INCLUDE_URL = "includeUrl";
- private static final String PREFS_ALLOW_CONTACT = "allowContact";
- private static final String PREFS_CONTACT_EMAIL = "contactEmail";
-
- private Handler mHandler;
- private ProgressDialog mProgressDialog;
- private File mPendingMinidumpFile;
- private File mPendingExtrasFile;
- private HashMap<String, String> mExtrasStringMap;
- private boolean mMinidumpSucceeded;
-
- private boolean moveFile(File inFile, File outFile) {
- Log.i(LOGTAG, "moving " + inFile + " to " + outFile);
- if (inFile.renameTo(outFile))
- return true;
- try {
- outFile.createNewFile();
- Log.i(LOGTAG, "couldn't rename minidump file");
- // so copy it instead
- FileChannel inChannel = new FileInputStream(inFile).getChannel();
- FileChannel outChannel = new FileOutputStream(outFile).getChannel();
- long transferred = inChannel.transferTo(0, inChannel.size(), outChannel);
- inChannel.close();
- outChannel.close();
-
- if (transferred > 0)
- inFile.delete();
- } catch (Exception e) {
- Log.e(LOGTAG, "exception while copying minidump file: ", e);
- return false;
- }
- return true;
- }
-
- private void doFinish() {
- if (mHandler != null) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- finish();
- }
- });
- }
- }
-
- @Override
- public void finish() {
- try {
- if (mProgressDialog.isShowing()) {
- mProgressDialog.dismiss();
- }
- } catch (Exception e) {
- Log.e(LOGTAG, "exception while closing progress dialog: ", e);
- }
- super.finish();
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- // mHandler is created here so runnables can be run on the main thread
- mHandler = new Handler();
- setContentView(R.layout.crash_reporter);
- mProgressDialog = new ProgressDialog(this);
- mProgressDialog.setMessage(getString(R.string.sending_crash_report));
-
- mMinidumpSucceeded = getIntent().getBooleanExtra(PASSED_MINI_DUMP_SUCCESS_KEY, false);
- if (!mMinidumpSucceeded) {
- Log.i(LOGTAG, "Failed to get minidump.");
- }
- String passedMinidumpPath = getIntent().getStringExtra(PASSED_MINI_DUMP_KEY);
- File passedMinidumpFile = new File(passedMinidumpPath);
- File pendingDir = new File(getFilesDir(), PENDING_SUFFIX);
- pendingDir.mkdirs();
- mPendingMinidumpFile = new File(pendingDir, passedMinidumpFile.getName());
- moveFile(passedMinidumpFile, mPendingMinidumpFile);
-
- File extrasFile = new File(passedMinidumpPath.replaceAll("\\.dmp", ".extra"));
- mPendingExtrasFile = new File(pendingDir, extrasFile.getName());
- moveFile(extrasFile, mPendingExtrasFile);
-
- mExtrasStringMap = new HashMap<String, String>();
- readStringsFromFile(mPendingExtrasFile.getPath(), mExtrasStringMap);
-
- // Notify GeckoApp that we've crashed, so it can react appropriately during the next start.
- try {
- File crashFlag = new File(GeckoProfileDirectories.getMozillaDirectory(this), "CRASHED");
- crashFlag.createNewFile();
- } catch (GeckoProfileDirectories.NoMozillaDirectoryException | IOException e) {
- Log.e(LOGTAG, "Cannot set crash flag: ", e);
- }
-
- final CheckBox allowContactCheckBox = (CheckBox) findViewById(R.id.allow_contact);
- final CheckBox includeUrlCheckBox = (CheckBox) findViewById(R.id.include_url);
- final CheckBox sendReportCheckBox = (CheckBox) findViewById(R.id.send_report);
- final EditText commentsEditText = (EditText) findViewById(R.id.comment);
- final EditText emailEditText = (EditText) findViewById(R.id.email);
-
- // Load CrashReporter preferences to avoid redundant user input.
- SharedPreferences prefs = GeckoSharedPrefs.forCrashReporter(this);
- final boolean sendReport = prefs.getBoolean(PREFS_SEND_REPORT, true);
- final boolean includeUrl = prefs.getBoolean(PREFS_INCLUDE_URL, false);
- final boolean allowContact = prefs.getBoolean(PREFS_ALLOW_CONTACT, false);
- final String contactEmail = prefs.getString(PREFS_CONTACT_EMAIL, "");
-
- allowContactCheckBox.setChecked(allowContact);
- includeUrlCheckBox.setChecked(includeUrl);
- sendReportCheckBox.setChecked(sendReport);
- emailEditText.setText(contactEmail);
-
- sendReportCheckBox.setOnCheckedChangeListener(new CheckBox.OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) {
- commentsEditText.setEnabled(isChecked);
- commentsEditText.requestFocus();
-
- includeUrlCheckBox.setEnabled(isChecked);
- allowContactCheckBox.setEnabled(isChecked);
- emailEditText.setEnabled(isChecked && allowContactCheckBox.isChecked());
- }
- });
-
- allowContactCheckBox.setOnCheckedChangeListener(new CheckBox.OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) {
- // We need to check isEnabled() here because this listener is
- // fired on rotation -- even when the checkbox is disabled.
- emailEditText.setEnabled(checkbox.isEnabled() && isChecked);
- emailEditText.requestFocus();
- }
- });
-
- emailEditText.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- // Even if the email EditText is disabled, allow it to be
- // clicked and focused.
- if (sendReportCheckBox.isChecked() && !v.isEnabled()) {
- allowContactCheckBox.setChecked(true);
- v.setEnabled(true);
- v.requestFocus();
- }
- }
- });
- }
-
- @Override
- public void onBackPressed() {
- AlertDialog.Builder builder = new AlertDialog.Builder(this);
- builder.setMessage(R.string.crash_closing_alert);
- builder.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- dialog.dismiss();
- }
- });
- builder.setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- CrashReporter.this.finish();
- }
- });
- builder.show();
- }
-
- private void backgroundSendReport() {
- final CheckBox sendReportCheckbox = (CheckBox) findViewById(R.id.send_report);
- if (!sendReportCheckbox.isChecked()) {
- doFinish();
- return;
- }
-
- // Persist settings to avoid redundant user input.
- savePrefs();
-
- mProgressDialog.show();
- new Thread(new Runnable() {
- @Override
- public void run() {
- sendReport(mPendingMinidumpFile, mExtrasStringMap, mPendingExtrasFile);
- }
- }, "CrashReporter Thread").start();
- }
-
- private void savePrefs() {
- SharedPreferences.Editor editor = GeckoSharedPrefs.forCrashReporter(this).edit();
-
- final boolean allowContact = ((CheckBox) findViewById(R.id.allow_contact)).isChecked();
- final boolean includeUrl = ((CheckBox) findViewById(R.id.include_url)).isChecked();
- final boolean sendReport = ((CheckBox) findViewById(R.id.send_report)).isChecked();
- final String contactEmail = ((EditText) findViewById(R.id.email)).getText().toString();
-
- editor.putBoolean(PREFS_ALLOW_CONTACT, allowContact);
- editor.putBoolean(PREFS_INCLUDE_URL, includeUrl);
- editor.putBoolean(PREFS_SEND_REPORT, sendReport);
- editor.putString(PREFS_CONTACT_EMAIL, contactEmail);
-
- // A slight performance improvement via async apply() vs. blocking on commit().
- editor.apply();
- }
-
- public void onCloseClick(View v) { // bound via crash_reporter.xml
- backgroundSendReport();
- }
-
- public void onRestartClick(View v) { // bound via crash_reporter.xml
- doRestart();
- backgroundSendReport();
- }
-
- private boolean readStringsFromFile(String filePath, Map<String, String> stringMap) {
- try {
- BufferedReader reader = new BufferedReader(new FileReader(filePath));
- return readStringsFromReader(reader, stringMap);
- } catch (Exception e) {
- Log.e(LOGTAG, "exception while reading strings: ", e);
- return false;
- }
- }
-
- private boolean readStringsFromReader(BufferedReader reader, Map<String, String> stringMap) throws IOException {
- String line;
- while ((line = reader.readLine()) != null) {
- int equalsPos = -1;
- if ((equalsPos = line.indexOf('=')) != -1) {
- String key = line.substring(0, equalsPos);
- String val = unescape(line.substring(equalsPos + 1));
- stringMap.put(key, val);
- }
- }
- reader.close();
- return true;
- }
-
- private String generateBoundary() {
- // Generate some random numbers to fill out the boundary
- int r0 = (int)(Integer.MAX_VALUE * Math.random());
- int r1 = (int)(Integer.MAX_VALUE * Math.random());
- return String.format("---------------------------%08X%08X", r0, r1);
- }
-
- private void sendPart(OutputStream os, String boundary, String name, String data) {
- try {
- os.write(("--" + boundary + "\r\n" +
- "Content-Disposition: form-data; name=\"" + name + "\"\r\n" +
- "\r\n" +
- data + "\r\n"
- ).getBytes());
- } catch (Exception ex) {
- Log.e(LOGTAG, "Exception when sending \"" + name + "\"", ex);
- }
- }
-
- private void sendFile(OutputStream os, String boundary, String name, File file) throws IOException {
- os.write(("--" + boundary + "\r\n" +
- "Content-Disposition: form-data; name=\"" + name + "\"; " +
- "filename=\"" + file.getName() + "\"\r\n" +
- "Content-Type: application/octet-stream\r\n" +
- "\r\n"
- ).getBytes());
- FileChannel fc = new FileInputStream(file).getChannel();
- fc.transferTo(0, fc.size(), Channels.newChannel(os));
- fc.close();
- }
-
- private String readLogcat() {
- final String crashReporterProc = " " + android.os.Process.myPid() + ' ';
- BufferedReader br = null;
- try {
- // get at most the last 400 lines of logcat
- Process proc = Runtime.getRuntime().exec(new String[] {
- "logcat", "-v", "threadtime", "-t", "400", "-d", "*:D"
- });
- StringBuilder sb = new StringBuilder();
- br = new BufferedReader(new InputStreamReader(proc.getInputStream()));
- for (String s = br.readLine(); s != null; s = br.readLine()) {
- if (s.contains(crashReporterProc)) {
- // Don't include logs from the crash reporter's process.
- break;
- }
- sb.append(s).append('\n');
- }
- return sb.toString();
- } catch (Exception e) {
- return "Unable to get logcat: " + e.toString();
- } finally {
- if (br != null) {
- try {
- br.close();
- } catch (Exception e) {
- // ignore
- }
- }
- }
- }
-
- private void sendReport(File minidumpFile, Map<String, String> extras, File extrasFile) {
- Log.i(LOGTAG, "sendReport: " + minidumpFile.getPath());
- final CheckBox includeURLCheckbox = (CheckBox) findViewById(R.id.include_url);
-
- String spec = extras.get(SERVER_URL_KEY);
- if (spec == null) {
- doFinish();
- return;
- }
-
- Log.i(LOGTAG, "server url: " + spec);
- try {
- URL url = new URL(spec);
- HttpURLConnection conn = (HttpURLConnection)url.openConnection();
- conn.setRequestMethod("POST");
- String boundary = generateBoundary();
- conn.setDoOutput(true);
- conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
- conn.setRequestProperty("Content-Encoding", "gzip");
-
- OutputStream os = new GZIPOutputStream(conn.getOutputStream());
- for (String key : extras.keySet()) {
- if (key.equals(PAGE_URL_KEY)) {
- if (includeURLCheckbox.isChecked())
- sendPart(os, boundary, key, extras.get(key));
- } else if (!key.equals(SERVER_URL_KEY) && !key.equals(NOTES_KEY)) {
- sendPart(os, boundary, key, extras.get(key));
- }
- }
-
- // Add some extra information to notes so its displayed by
- // crash-stats.mozilla.org. Remove this when bug 607942 is fixed.
- StringBuilder sb = new StringBuilder();
- sb.append(extras.containsKey(NOTES_KEY) ? extras.get(NOTES_KEY) + "\n" : "");
- if (AppConstants.MOZ_MIN_CPU_VERSION < 7) {
- sb.append("nothumb Build\n");
- }
- sb.append(Build.MANUFACTURER).append(' ')
- .append(Build.MODEL).append('\n')
- .append(Build.FINGERPRINT);
- sendPart(os, boundary, NOTES_KEY, sb.toString());
-
- sendPart(os, boundary, "Min_ARM_Version", Integer.toString(AppConstants.MOZ_MIN_CPU_VERSION));
- sendPart(os, boundary, "Android_Manufacturer", Build.MANUFACTURER);
- sendPart(os, boundary, "Android_Model", Build.MODEL);
- sendPart(os, boundary, "Android_Board", Build.BOARD);
- sendPart(os, boundary, "Android_Brand", Build.BRAND);
- sendPart(os, boundary, "Android_Device", Build.DEVICE);
- sendPart(os, boundary, "Android_Display", Build.DISPLAY);
- sendPart(os, boundary, "Android_Fingerprint", Build.FINGERPRINT);
- sendPart(os, boundary, "Android_APP_ABI", AppConstants.MOZ_APP_ABI);
- sendPart(os, boundary, "Android_CPU_ABI", Build.CPU_ABI);
- sendPart(os, boundary, "Android_MIN_SDK", Integer.toString(AppConstants.Versions.MIN_SDK_VERSION));
- sendPart(os, boundary, "Android_MAX_SDK", Integer.toString(AppConstants.Versions.MAX_SDK_VERSION));
- try {
- sendPart(os, boundary, "Android_CPU_ABI2", Build.CPU_ABI2);
- sendPart(os, boundary, "Android_Hardware", Build.HARDWARE);
- } catch (Exception ex) {
- Log.e(LOGTAG, "Exception while sending SDK version 8 keys", ex);
- }
- sendPart(os, boundary, "Android_Version", Build.VERSION.SDK_INT + " (" + Build.VERSION.CODENAME + ")");
- if (Versions.feature16Plus && includeURLCheckbox.isChecked()) {
- sendPart(os, boundary, "Android_Logcat", readLogcat());
- }
-
- String comment = ((EditText) findViewById(R.id.comment)).getText().toString();
- if (!TextUtils.isEmpty(comment)) {
- sendPart(os, boundary, "Comments", comment);
- }
-
- if (((CheckBox) findViewById(R.id.allow_contact)).isChecked()) {
- String email = ((EditText) findViewById(R.id.email)).getText().toString();
- sendPart(os, boundary, "Email", email);
- }
-
- sendPart(os, boundary, PASSED_MINI_DUMP_SUCCESS_KEY, mMinidumpSucceeded ? "True" : "False");
- sendFile(os, boundary, MINI_DUMP_PATH_KEY, minidumpFile);
- os.write(("\r\n--" + boundary + "--\r\n").getBytes());
- os.flush();
- os.close();
- BufferedReader br = new BufferedReader(
- new InputStreamReader(conn.getInputStream()));
- HashMap<String, String> responseMap = new HashMap<String, String>();
- readStringsFromReader(br, responseMap);
-
- if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
- File submittedDir = new File(getFilesDir(),
- SUBMITTED_SUFFIX);
- submittedDir.mkdirs();
- minidumpFile.delete();
- extrasFile.delete();
- String crashid = responseMap.get("CrashID");
- File file = new File(submittedDir, crashid + ".txt");
- FileOutputStream fos = new FileOutputStream(file);
- fos.write("Crash ID: ".getBytes());
- fos.write(crashid.getBytes());
- fos.close();
- } else {
- Log.i(LOGTAG, "Received failure HTTP response code from server: " + conn.getResponseCode());
- }
- } catch (IOException e) {
- Log.e(LOGTAG, "exception during send: ", e);
- }
-
- doFinish();
- }
-
- private void doRestart() {
- try {
- String action = "android.intent.action.MAIN";
- Intent intent = new Intent(action);
- intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME,
- AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
- intent.putExtra("didRestart", true);
- Log.i(LOGTAG, intent.toString());
- startActivity(intent);
- } catch (Exception e) {
- Log.e(LOGTAG, "error while trying to restart", e);
- }
- }
-
- private String unescape(String string) {
- return string.replaceAll("\\\\\\\\", "\\").replaceAll("\\\\n", "\n").replaceAll("\\\\t", "\t");
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/CustomEditText.java b/mobile/android/base/java/org/mozilla/gecko/CustomEditText.java
deleted file mode 100644
index 98274b752..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/CustomEditText.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.widget.themed.ThemedEditText;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.KeyEvent;
-import android.view.View;
-
-public class CustomEditText extends ThemedEditText {
- private OnKeyPreImeListener mOnKeyPreImeListener;
- private OnSelectionChangedListener mOnSelectionChangedListener;
- private OnWindowFocusChangeListener mOnWindowFocusChangeListener;
- private int mHighlightColor;
-
- public CustomEditText(Context context, AttributeSet attrs) {
- super(context, attrs);
- setPrivateMode(false); // Initialize mHighlightColor.
- }
-
- public interface OnKeyPreImeListener {
- public boolean onKeyPreIme(View v, int keyCode, KeyEvent event);
- }
-
- public void setOnKeyPreImeListener(OnKeyPreImeListener listener) {
- mOnKeyPreImeListener = listener;
- }
-
- @Override
- public boolean onKeyPreIme(int keyCode, KeyEvent event) {
- if (mOnKeyPreImeListener != null)
- return mOnKeyPreImeListener.onKeyPreIme(this, keyCode, event);
-
- return false;
- }
-
- public interface OnSelectionChangedListener {
- public void onSelectionChanged(int selStart, int selEnd);
- }
-
- public void setOnSelectionChangedListener(OnSelectionChangedListener listener) {
- mOnSelectionChangedListener = listener;
- }
-
- @Override
- protected void onSelectionChanged(int selStart, int selEnd) {
- if (mOnSelectionChangedListener != null)
- mOnSelectionChangedListener.onSelectionChanged(selStart, selEnd);
-
- super.onSelectionChanged(selStart, selEnd);
- }
-
- public interface OnWindowFocusChangeListener {
- public void onWindowFocusChanged(boolean hasFocus);
- }
-
- public void setOnWindowFocusChangeListener(OnWindowFocusChangeListener listener) {
- mOnWindowFocusChangeListener = listener;
- }
-
- @Override
- public void onWindowFocusChanged(boolean hasFocus) {
- super.onWindowFocusChanged(hasFocus);
- if (mOnWindowFocusChangeListener != null)
- mOnWindowFocusChangeListener.onWindowFocusChanged(hasFocus);
- }
-
- // Provide a getHighlightColor implementation for API level < 16.
- @Override
- public int getHighlightColor() {
- return mHighlightColor;
- }
-
- @Override
- public void setPrivateMode(boolean isPrivate) {
- super.setPrivateMode(isPrivate);
-
- mHighlightColor = ContextCompat.getColor(getContext(), isPrivate
- ? R.color.url_bar_text_highlight_pb : R.color.fennec_ui_orange);
- // android:textColorHighlight cannot support a ColorStateList.
- setHighlightColor(mHighlightColor);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/DataReportingNotification.java b/mobile/android/base/java/org/mozilla/gecko/DataReportingNotification.java
deleted file mode 100644
index 725c25d6e..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/DataReportingNotification.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.res.Resources;
-import android.graphics.Typeface;
-import android.support.v4.app.NotificationCompat;
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.TextUtils;
-import android.text.style.StyleSpan;
-
-public class DataReportingNotification {
-
- private static final String LOGTAG = "DataReportNotification";
-
- public static final String ALERT_NAME_DATAREPORTING_NOTIFICATION = "datareporting-notification";
-
- private static final String PREFS_POLICY_NOTIFIED_TIME = "datareporting.policy.dataSubmissionPolicyNotifiedTime";
- private static final String PREFS_POLICY_VERSION = "datareporting.policy.dataSubmissionPolicyVersion";
- private static final int DATA_REPORTING_VERSION = 2;
-
- public static void checkAndNotifyPolicy(Context context) {
- SharedPreferences dataPrefs = GeckoSharedPrefs.forApp(context);
- final int currentVersion = dataPrefs.getInt(PREFS_POLICY_VERSION, -1);
-
- if (currentVersion < 1) {
- // This is a first run, so notify user about data policy.
- notifyDataPolicy(context, dataPrefs);
-
- // If healthreport is enabled, set default preference value.
- if (AppConstants.MOZ_SERVICES_HEALTHREPORT) {
- SharedPreferences.Editor editor = dataPrefs.edit();
- editor.putBoolean(GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true);
- editor.apply();
- }
- return;
- }
-
- if (currentVersion == 1) {
- // Redisplay notification only for Beta because version 2 updates Beta policy and update version.
- if (TextUtils.equals("beta", AppConstants.MOZ_UPDATE_CHANNEL)) {
- notifyDataPolicy(context, dataPrefs);
- } else {
- // Silently update the version.
- SharedPreferences.Editor editor = dataPrefs.edit();
- editor.putInt(PREFS_POLICY_VERSION, DATA_REPORTING_VERSION);
- editor.apply();
- }
- return;
- }
-
- if (currentVersion >= DATA_REPORTING_VERSION) {
- // Do nothing, we're at a current (or future) version.
- return;
- }
- }
-
- /**
- * Launch a notification of the data policy, and record notification time and version.
- */
- public static void notifyDataPolicy(Context context, SharedPreferences sharedPrefs) {
- boolean result = false;
- try {
- // Launch main App to launch Data choices when notification is clicked.
- Intent prefIntent = new Intent(GeckoApp.ACTION_LAUNCH_SETTINGS);
- prefIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
-
- GeckoPreferences.setResourceToOpen(prefIntent, "preferences_privacy");
- prefIntent.putExtra(ALERT_NAME_DATAREPORTING_NOTIFICATION, true);
-
- PendingIntent contentIntent = PendingIntent.getActivity(context, 0, prefIntent, PendingIntent.FLAG_UPDATE_CURRENT);
- final Resources resources = context.getResources();
-
- // Create and send notification.
- String notificationTitle = resources.getString(R.string.datareporting_notification_title);
- String notificationSummary;
- if (Versions.preJB) {
- notificationSummary = resources.getString(R.string.datareporting_notification_action);
- } else {
- // Display partial version of Big Style notification for supporting devices.
- notificationSummary = resources.getString(R.string.datareporting_notification_summary);
- }
- String notificationAction = resources.getString(R.string.datareporting_notification_action);
- String notificationBigSummary = resources.getString(R.string.datareporting_notification_summary);
-
- // Make styled ticker text for display in notification bar.
- String tickerString = resources.getString(R.string.datareporting_notification_ticker_text);
- SpannableString tickerText = new SpannableString(tickerString);
- // Bold the notification title of the ticker text, which is the same string as notificationTitle.
- tickerText.setSpan(new StyleSpan(Typeface.BOLD), 0, notificationTitle.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
-
- Notification notification = new NotificationCompat.Builder(context)
- .setContentTitle(notificationTitle)
- .setContentText(notificationSummary)
- .setSmallIcon(R.drawable.ic_status_logo)
- .setAutoCancel(true)
- .setContentIntent(contentIntent)
- .setStyle(new NotificationCompat.BigTextStyle()
- .bigText(notificationBigSummary))
- .addAction(R.drawable.firefox_settings_alert, notificationAction, contentIntent)
- .setTicker(tickerText)
- .build();
-
- NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- int notificationID = ALERT_NAME_DATAREPORTING_NOTIFICATION.hashCode();
- notificationManager.notify(notificationID, notification);
-
- // Record version and notification time.
- SharedPreferences.Editor editor = sharedPrefs.edit();
- long now = System.currentTimeMillis();
- editor.putLong(PREFS_POLICY_NOTIFIED_TIME, now);
- editor.putInt(PREFS_POLICY_VERSION, DATA_REPORTING_VERSION);
- editor.apply();
- result = true;
- } finally {
- // We want to track any errors, so record notification outcome.
- Telemetry.sendUIEvent(TelemetryContract.Event.POLICY_NOTIFICATION_SUCCESS, result);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/DevToolsAuthHelper.java b/mobile/android/base/java/org/mozilla/gecko/DevToolsAuthHelper.java
deleted file mode 100644
index 44aaa14a0..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/DevToolsAuthHelper.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.util.Log;
-import org.mozilla.gecko.util.ActivityResultHandler;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.InputOptionsUtils;
-
-/**
- * Supports the DevTools WiFi debugging authentication flow by invoking a QR decoder.
- */
-public class DevToolsAuthHelper {
-
- private static final String LOGTAG = "GeckoDevToolsAuthHelper";
-
- public static void scan(Context context, final EventCallback callback) {
- final Intent intent = InputOptionsUtils.createQRCodeReaderIntent();
-
- intent.putExtra("PROMPT_MESSAGE", context.getString(R.string.devtools_auth_scan_header));
-
- // Check ahead of time if an activity exists for the intent. This
- // avoids a case where we get both an ActivityNotFoundException *and*
- // an activity result when the activity is missing.
- PackageManager pm = context.getPackageManager();
- if (pm.resolveActivity(intent, 0) == null) {
- Log.w(LOGTAG, "PackageManager can't resolve the activity.");
- callback.sendError("PackageManager can't resolve the activity.");
- return;
- }
-
- ActivityHandlerHelper.startIntent(intent, new ActivityResultHandler() {
- @Override
- public void onActivityResult(int resultCode, Intent intent) {
- if (resultCode == Activity.RESULT_OK) {
- String text = intent.getStringExtra("SCAN_RESULT");
- callback.sendSuccess(text);
- } else {
- callback.sendError(resultCode);
- }
- }
- });
- }
-
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/DoorHangerPopup.java b/mobile/android/base/java/org/mozilla/gecko/DoorHangerPopup.java
deleted file mode 100644
index 9aa3f96a4..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/DoorHangerPopup.java
+++ /dev/null
@@ -1,361 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import java.util.HashSet;
-
-import android.text.TextUtils;
-import android.widget.PopupWindow;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.json.JSONArray;
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.widget.AnchoredPopup;
-import org.mozilla.gecko.widget.DoorHanger;
-
-import android.content.Context;
-import android.util.Log;
-import android.view.View;
-import org.mozilla.gecko.widget.DoorhangerConfig;
-
-public class DoorHangerPopup extends AnchoredPopup
- implements GeckoEventListener,
- Tabs.OnTabsChangedListener,
- PopupWindow.OnDismissListener,
- DoorHanger.OnButtonClickListener {
- private static final String LOGTAG = "GeckoDoorHangerPopup";
-
- // Stores a set of all active DoorHanger notifications. A DoorHanger is
- // uniquely identified by its tabId and value.
- private final HashSet<DoorHanger> mDoorHangers;
-
- // Whether or not the doorhanger popup is disabled.
- private boolean mDisabled;
-
- public DoorHangerPopup(Context context) {
- super(context);
-
- mDoorHangers = new HashSet<DoorHanger>();
-
- GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
- "Doorhanger:Add",
- "Doorhanger:Remove");
- Tabs.registerOnTabsChangedListener(this);
-
- setOnDismissListener(this);
- }
-
- void destroy() {
- GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
- "Doorhanger:Add",
- "Doorhanger:Remove");
- Tabs.unregisterOnTabsChangedListener(this);
- }
-
- /**
- * Temporarily disables the doorhanger popup. If the popup is disabled,
- * it will not be shown to the user, but it will continue to process
- * calls to add/remove doorhanger notifications.
- */
- void disable() {
- mDisabled = true;
- updatePopup();
- }
-
- /**
- * Re-enables the doorhanger popup.
- */
- void enable() {
- mDisabled = false;
- updatePopup();
- }
-
- @Override
- public void handleMessage(String event, JSONObject geckoObject) {
- try {
- if (event.equals("Doorhanger:Add")) {
- final DoorhangerConfig config = makeConfigFromJSON(geckoObject);
-
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- addDoorHanger(config);
- }
- });
- } else if (event.equals("Doorhanger:Remove")) {
- final int tabId = geckoObject.getInt("tabID");
- final String value = geckoObject.getString("value");
-
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- DoorHanger doorHanger = getDoorHanger(tabId, value);
- if (doorHanger == null)
- return;
-
- removeDoorHanger(doorHanger);
- updatePopup();
- }
- });
- }
- } catch (Exception e) {
- Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
- }
- }
-
- private DoorhangerConfig makeConfigFromJSON(JSONObject json) throws JSONException {
- final int tabId = json.getInt("tabID");
- final String id = json.getString("value");
-
- final String typeString = json.optString("category");
- DoorHanger.Type doorhangerType = DoorHanger.Type.DEFAULT;
- if (DoorHanger.Type.LOGIN.toString().equals(typeString)) {
- doorhangerType = DoorHanger.Type.LOGIN;
- } else if (DoorHanger.Type.GEOLOCATION.toString().equals(typeString)) {
- doorhangerType = DoorHanger.Type.GEOLOCATION;
- } else if (DoorHanger.Type.DESKTOPNOTIFICATION2.toString().equals(typeString)) {
- doorhangerType = DoorHanger.Type.DESKTOPNOTIFICATION2;
- } else if (DoorHanger.Type.WEBRTC.toString().equals(typeString)) {
- doorhangerType = DoorHanger.Type.WEBRTC;
- } else if (DoorHanger.Type.VIBRATION.toString().equals(typeString)) {
- doorhangerType = DoorHanger.Type.VIBRATION;
- }
-
- final DoorhangerConfig config = new DoorhangerConfig(tabId, id, doorhangerType, this);
-
- config.setMessage(json.getString("message"));
- config.setOptions(json.getJSONObject("options"));
-
- final JSONArray buttonArray = json.getJSONArray("buttons");
- int numButtons = buttonArray.length();
- if (numButtons > 2) {
- Log.e(LOGTAG, "Doorhanger can have a maximum of two buttons!");
- numButtons = 2;
- }
-
- for (int i = 0; i < numButtons; i++) {
- final JSONObject buttonJSON = buttonArray.getJSONObject(i);
- final boolean isPositive = buttonJSON.optBoolean("positive", false);
- config.setButton(buttonJSON.getString("label"), buttonJSON.getInt("callback"), isPositive);
- }
-
- return config;
- }
-
- // This callback is automatically executed on the UI thread.
- @Override
- public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final String data) {
- switch (msg) {
- case CLOSED:
- // Remove any doorhangers for a tab when it's closed (make
- // a temporary set to avoid a ConcurrentModificationException)
- removeTabDoorHangers(tab.getId(), true);
- break;
-
- case LOCATION_CHANGE:
- // Only remove doorhangers if the popup is hidden or if we're navigating to a new URL
- if (!isShowing() || !data.equals(tab.getURL()))
- removeTabDoorHangers(tab.getId(), false);
-
- // Update the popup if the location change was on the current tab
- if (Tabs.getInstance().isSelectedTab(tab))
- updatePopup();
- break;
-
- case SELECTED:
- // Always update the popup when a new tab is selected. This will cover cases
- // where a different tab was closed, since we always need to select a new tab.
- updatePopup();
- break;
- }
- }
-
- /**
- * Adds a doorhanger.
- *
- * This method must be called on the UI thread.
- */
- void addDoorHanger(DoorhangerConfig config) {
- final int tabId = config.getTabId();
- // Don't add a doorhanger for a tab that doesn't exist
- if (Tabs.getInstance().getTab(tabId) == null) {
- return;
- }
-
- // Replace the doorhanger if it already exists
- DoorHanger oldDoorHanger = getDoorHanger(tabId, config.getId());
- if (oldDoorHanger != null) {
- removeDoorHanger(oldDoorHanger);
- }
-
- if (!mInflated) {
- init();
- }
-
- final DoorHanger newDoorHanger = DoorHanger.Get(mContext, config);
-
- mDoorHangers.add(newDoorHanger);
- mContent.addView(newDoorHanger);
-
- // Only update the popup if we're adding a notification to the selected tab
- if (tabId == Tabs.getInstance().getSelectedTab().getId())
- updatePopup();
- }
-
-
- /*
- * DoorHanger.OnButtonClickListener implementation
- */
- @Override
- public void onButtonClick(JSONObject response, DoorHanger doorhanger) {
- GeckoAppShell.notifyObservers("Doorhanger:Reply", response.toString());
- removeDoorHanger(doorhanger);
- updatePopup();
- }
-
- /**
- * Gets a doorhanger.
- *
- * This method must be called on the UI thread.
- */
- DoorHanger getDoorHanger(int tabId, String value) {
- for (DoorHanger dh : mDoorHangers) {
- if (dh.getTabId() == tabId && dh.getIdentifier().equals(value))
- return dh;
- }
-
- // If there's no doorhanger for the given tabId and value, return null
- return null;
- }
-
- /**
- * Removes a doorhanger.
- *
- * This method must be called on the UI thread.
- */
- void removeDoorHanger(final DoorHanger doorHanger) {
- mDoorHangers.remove(doorHanger);
- mContent.removeView(doorHanger);
- }
-
- /**
- * Removes doorhangers for a given tab.
- * @param tabId identifier of the tab to remove doorhangers from
- * @param forceRemove boolean for force-removing tabs. If true, all doorhangers associated
- * with the tab specified are removed; if false, only remove the doorhangers
- * that are not persistent, as specified by the doorhanger options.
- *
- * This method must be called on the UI thread.
- */
- void removeTabDoorHangers(int tabId, boolean forceRemove) {
- // Make a temporary set to avoid a ConcurrentModificationException
- HashSet<DoorHanger> doorHangersToRemove = new HashSet<DoorHanger>();
- for (DoorHanger dh : mDoorHangers) {
- // Only remove transient doorhangers for the given tab
- if (dh.getTabId() == tabId
- && (forceRemove || (!forceRemove && dh.shouldRemove(isShowing())))) {
- doorHangersToRemove.add(dh);
- }
- }
-
- for (DoorHanger dh : doorHangersToRemove) {
- removeDoorHanger(dh);
- }
- }
-
- /**
- * Updates the popup state.
- *
- * This method must be called on the UI thread.
- */
- void updatePopup() {
- // Bail if the selected tab is null, if there are no active doorhangers,
- // if we haven't inflated the layout yet (this can happen if updatePopup()
- // is called before the runnable from addDoorHanger() runs), or if the
- // doorhanger popup is temporarily disabled.
- Tab tab = Tabs.getInstance().getSelectedTab();
- if (tab == null || mDoorHangers.size() == 0 || !mInflated || mDisabled) {
- dismiss();
- return;
- }
-
- // Show doorhangers for the selected tab
- int tabId = tab.getId();
- boolean shouldShowPopup = false;
- DoorHanger firstDoorhanger = null;
- for (DoorHanger dh : mDoorHangers) {
- if (dh.getTabId() == tabId) {
- dh.setVisibility(View.VISIBLE);
- shouldShowPopup = true;
- if (firstDoorhanger == null) {
- firstDoorhanger = dh;
- } else {
- dh.hideTitle();
- }
- } else {
- dh.setVisibility(View.GONE);
- }
- }
-
- // Dismiss the popup if there are no doorhangers to show for this tab
- if (!shouldShowPopup) {
- dismiss();
- return;
- }
-
- showDividers();
-
- final String baseDomain = tab.getBaseDomain();
-
- if (TextUtils.isEmpty(baseDomain)) {
- firstDoorhanger.hideTitle();
- } else {
- firstDoorhanger.showTitle(tab.getFavicon(), baseDomain);
- }
-
- if (isShowing()) {
- show();
- return;
- }
-
- setFocusable(true);
-
- show();
- }
-
- //Show all inter-DoorHanger dividers (ie. Dividers on all visible DoorHangers except the last one)
- private void showDividers() {
- int count = mContent.getChildCount();
- DoorHanger lastVisibleDoorHanger = null;
-
- for (int i = 0; i < count; i++) {
- DoorHanger dh = (DoorHanger) mContent.getChildAt(i);
- dh.showDivider();
- if (dh.getVisibility() == View.VISIBLE) {
- lastVisibleDoorHanger = dh;
- }
- }
- if (lastVisibleDoorHanger != null) {
- lastVisibleDoorHanger.hideDivider();
- }
- }
-
- @Override
- public void onDismiss() {
- final int tabId = Tabs.getInstance().getSelectedTab().getId();
- removeTabDoorHangers(tabId, true);
- }
-
- @Override
- public void dismiss() {
- // If the popup is focusable while it is hidden, we run into crashes
- // on pre-ICS devices when the popup gets focus before it is shown.
- setFocusable(false);
- super.dismiss();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/DownloadsIntegration.java b/mobile/android/base/java/org/mozilla/gecko/DownloadsIntegration.java
deleted file mode 100644
index ff3ac6110..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/DownloadsIntegration.java
+++ /dev/null
@@ -1,235 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.permissions.Permissions;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.EventCallback;
-
-import java.io.File;
-import java.lang.IllegalArgumentException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import android.app.DownloadManager;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.database.Cursor;
-import android.media.MediaScannerConnection;
-import android.media.MediaScannerConnection.MediaScannerConnectionClient;
-import android.net.Uri;
-import android.os.Environment;
-import android.text.TextUtils;
-import android.util.Log;
-
-public class DownloadsIntegration implements NativeEventListener
-{
- private static final String LOGTAG = "GeckoDownloadsIntegration";
-
- private static final List<String> UNKNOWN_MIME_TYPES;
- static {
- final ArrayList<String> tempTypes = new ArrayList<>(3);
- tempTypes.add("unknown/unknown"); // This will be used as a default mime type for unknown files
- tempTypes.add("application/unknown");
- tempTypes.add("application/octet-stream"); // Github uses this for APK files
- UNKNOWN_MIME_TYPES = Collections.unmodifiableList(tempTypes);
- }
-
- private static final String DOWNLOAD_REMOVE = "Download:Remove";
-
- private DownloadsIntegration() {
- EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener)this, DOWNLOAD_REMOVE);
- }
-
- private static DownloadsIntegration sInstance;
-
- private static class Download {
- final File file;
- final long id;
-
- final private static int UNKNOWN_ID = -1;
-
- public Download(final String path) {
- this(path, UNKNOWN_ID);
- }
-
- public Download(final String path, final long id) {
- file = new File(path);
- this.id = id;
- }
-
- public static Download fromJSON(final NativeJSObject obj) {
- final String path = obj.getString("path");
- return new Download(path);
- }
-
- public static Download fromCursor(final Cursor c) {
- final String path = c.getString(c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME));
- final long id = c.getLong(c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID));
- return new Download(path, id);
- }
-
- public boolean equals(final Download download) {
- return file.equals(download.file);
- }
- }
-
- public static void init() {
- if (sInstance == null) {
- sInstance = new DownloadsIntegration();
- }
- }
-
- @Override
- public void handleMessage(final String event, final NativeJSObject message,
- final EventCallback callback) {
- if (DOWNLOAD_REMOVE.equals(event)) {
- final Download d = Download.fromJSON(message);
- removeDownload(d);
- }
- }
-
- private static boolean useSystemDownloadManager() {
- if (!AppConstants.ANDROID_DOWNLOADS_INTEGRATION) {
- return false;
- }
-
- int state = PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
- try {
- state = GeckoAppShell.getContext().getPackageManager().getApplicationEnabledSetting("com.android.providers.downloads");
- } catch (IllegalArgumentException e) {
- // Download Manager package does not exist
- return false;
- }
-
- return (PackageManager.COMPONENT_ENABLED_STATE_ENABLED == state ||
- PackageManager.COMPONENT_ENABLED_STATE_DEFAULT == state);
- }
-
- @WrapForJNI(calledFrom = "gecko")
- public static String getTemporaryDownloadDirectory() {
- Context context = GeckoAppShell.getApplicationContext();
-
- if (Permissions.has(context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
- // We do have the STORAGE permission, so we can save the file directly to the public
- // downloads directory.
- return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
- .getAbsolutePath();
- } else {
- // Without the permission we are going to start to download the file to the cache
- // directory. Later in the process we will ask for the permission and the download
- // process will move the file to the actual downloads directory. If we do not get the
- // permission then the download will be cancelled.
- return context.getCacheDir().getAbsolutePath();
- }
- }
-
-
- @WrapForJNI(calledFrom = "gecko")
- public static void scanMedia(final String aFile, String aMimeType) {
- String mimeType = aMimeType;
- if (UNKNOWN_MIME_TYPES.contains(mimeType)) {
- // If this is a generic undefined mimetype, erase it so that we can try to determine
- // one from the file extension below.
- mimeType = "";
- }
-
- // If the platform didn't give us a mimetype, try to guess one from the filename
- if (TextUtils.isEmpty(mimeType)) {
- final int extPosition = aFile.lastIndexOf(".");
- if (extPosition > 0 && extPosition < aFile.length() - 1) {
- mimeType = GeckoAppShell.getMimeTypeFromExtension(aFile.substring(extPosition + 1));
- }
- }
-
- // addCompletedDownload will throw if it received any null parameters. Use aMimeType or a default
- // if we still don't have one.
- if (TextUtils.isEmpty(mimeType)) {
- if (TextUtils.isEmpty(aMimeType)) {
- mimeType = UNKNOWN_MIME_TYPES.get(0);
- } else {
- mimeType = aMimeType;
- }
- }
-
- if (useSystemDownloadManager()) {
- final File f = new File(aFile);
- final DownloadManager dm = (DownloadManager) GeckoAppShell.getContext().getSystemService(Context.DOWNLOAD_SERVICE);
- dm.addCompletedDownload(f.getName(),
- f.getName(),
- true, // Media scanner should scan this
- mimeType,
- f.getAbsolutePath(),
- Math.max(1, f.length()), // Some versions of Android require downloads to be at least length 1
- false); // Don't show a notification.
- } else {
- final Context context = GeckoAppShell.getContext();
- final GeckoMediaScannerClient client = new GeckoMediaScannerClient(context, aFile, mimeType);
- client.connect();
- }
- }
-
- public static void removeDownload(final Download download) {
- if (!useSystemDownloadManager()) {
- return;
- }
-
- final DownloadManager dm = (DownloadManager) GeckoAppShell.getContext().getSystemService(Context.DOWNLOAD_SERVICE);
-
- Cursor c = null;
- try {
- c = dm.query((new DownloadManager.Query()).setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL));
- if (c == null || !c.moveToFirst()) {
- return;
- }
-
- do {
- final Download d = Download.fromCursor(c);
- // Try hard as we can to verify this download is the one we think it is
- if (download.equals(d)) {
- dm.remove(d.id);
- }
- } while (c.moveToNext());
- } finally {
- if (c != null) {
- c.close();
- }
- }
- }
-
- private static final class GeckoMediaScannerClient implements MediaScannerConnectionClient {
- private final String mFile;
- private final String mMimeType;
- private MediaScannerConnection mScanner;
-
- public GeckoMediaScannerClient(Context context, String file, String mimeType) {
- mFile = file;
- mMimeType = mimeType;
- mScanner = new MediaScannerConnection(context, this);
- }
-
- public void connect() {
- mScanner.connect();
- }
-
- @Override
- public void onMediaScannerConnected() {
- mScanner.scanFile(mFile, mMimeType);
- }
-
- @Override
- public void onScanCompleted(String path, Uri uri) {
- if (path.equals(mFile)) {
- mScanner.disconnect();
- mScanner = null;
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/DynamicToolbar.java b/mobile/android/base/java/org/mozilla/gecko/DynamicToolbar.java
deleted file mode 100644
index 28f542d5c..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/DynamicToolbar.java
+++ /dev/null
@@ -1,218 +0,0 @@
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.PrefsHelper.PrefHandlerBase;
-import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
-import org.mozilla.gecko.gfx.LayerView;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.os.Build;
-import android.os.Bundle;
-import android.util.Log;
-
-public class DynamicToolbar {
- private static final String LOGTAG = "DynamicToolbar";
-
- private static final String STATE_ENABLED = "dynamic_toolbar";
- private static final String CHROME_PREF = "browser.chrome.dynamictoolbar";
-
- // DynamicToolbar is enabled iff prefEnabled is true *and* accessibilityEnabled is false,
- // so it is disabled by default on startup. We do not enable it until we explicitly get
- // the pref from Gecko telling us to turn it on.
- private volatile boolean prefEnabled;
- private boolean accessibilityEnabled;
- // On some device we have to force-disable the dynamic toolbar because of
- // bugs in the Android code. See bug 1231554.
- private final boolean forceDisabled;
-
- private final PrefsHelper.PrefHandler prefObserver;
- private LayerView layerView;
- private OnEnabledChangedListener enabledChangedListener;
- private boolean temporarilyVisible;
-
- public enum VisibilityTransition {
- IMMEDIATE,
- ANIMATE
- }
-
- /**
- * Listener for changes to the dynamic toolbar's enabled state.
- */
- public interface OnEnabledChangedListener {
- /**
- * This callback is executed on the UI thread.
- */
- public void onEnabledChanged(boolean enabled);
- }
-
- public DynamicToolbar() {
- // Listen to the dynamic toolbar pref
- prefObserver = new PrefHandler();
- PrefsHelper.addObserver(new String[] { CHROME_PREF }, prefObserver);
- forceDisabled = isForceDisabled();
- if (forceDisabled) {
- Log.i(LOGTAG, "Force-disabling dynamic toolbar for " + Build.MODEL + " (" + Build.DEVICE + "/" + Build.PRODUCT + ")");
- }
- }
-
- public static boolean isForceDisabled() {
- // Force-disable dynamic toolbar on the variants of the Galaxy Note 10.1
- // and Note 8.0 running Android 4.1.2. (Bug 1231554). This includes
- // the following model numbers:
- // GT-N8000, GT-N8005, GT-N8010, GT-N8013, GT-N8020
- // GT-N5100, GT-N5110, GT-N5120
- if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN
- && (Build.MODEL.startsWith("GT-N80") ||
- Build.MODEL.startsWith("GT-N51"))) {
- return true;
- }
- // Also disable variants of the Galaxy Note 4 on Android 5.0.1 (Bug 1301593)
- if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP
- && (Build.MODEL.startsWith("SM-N910"))) {
- return true;
- }
- return false;
- }
-
- public void destroy() {
- PrefsHelper.removeObserver(prefObserver);
- }
-
- public void setLayerView(LayerView layerView) {
- ThreadUtils.assertOnUiThread();
-
- this.layerView = layerView;
- }
-
- public void setEnabledChangedListener(OnEnabledChangedListener listener) {
- ThreadUtils.assertOnUiThread();
-
- enabledChangedListener = listener;
- }
-
- public void onSaveInstanceState(Bundle outState) {
- ThreadUtils.assertOnUiThread();
-
- outState.putBoolean(STATE_ENABLED, prefEnabled);
- }
-
- public void onRestoreInstanceState(Bundle savedInstanceState) {
- ThreadUtils.assertOnUiThread();
-
- if (savedInstanceState != null) {
- prefEnabled = savedInstanceState.getBoolean(STATE_ENABLED);
- }
- }
-
- public boolean isEnabled() {
- ThreadUtils.assertOnUiThread();
-
- if (forceDisabled) {
- return false;
- }
-
- return prefEnabled && !accessibilityEnabled;
- }
-
- public void setAccessibilityEnabled(boolean enabled) {
- ThreadUtils.assertOnUiThread();
-
- if (accessibilityEnabled == enabled) {
- return;
- }
-
- // Disable the dynamic toolbar when accessibility features are enabled,
- // and re-read the preference when they're disabled.
- accessibilityEnabled = enabled;
- if (prefEnabled) {
- triggerEnabledListener();
- }
- }
-
- public void setVisible(boolean visible, VisibilityTransition transition) {
- ThreadUtils.assertOnUiThread();
-
- if (layerView == null) {
- return;
- }
-
- // Don't hide the ActionBar/Toolbar, if it's pinned open by TextSelection.
- if (visible == false &&
- layerView.getDynamicToolbarAnimator().isPinnedBy(PinReason.ACTION_MODE)) {
- return;
- }
-
- final boolean isImmediate = transition == VisibilityTransition.IMMEDIATE;
- if (visible) {
- layerView.getDynamicToolbarAnimator().showToolbar(isImmediate);
- } else {
- layerView.getDynamicToolbarAnimator().hideToolbar(isImmediate);
- }
- }
-
- public void setTemporarilyVisible(boolean visible, VisibilityTransition transition) {
- ThreadUtils.assertOnUiThread();
-
- if (layerView == null) {
- return;
- }
-
- if (visible == temporarilyVisible) {
- // nothing to do
- return;
- }
-
- temporarilyVisible = visible;
- final boolean isImmediate = transition == VisibilityTransition.IMMEDIATE;
- if (visible) {
- layerView.getDynamicToolbarAnimator().showToolbar(isImmediate);
- } else {
- layerView.getDynamicToolbarAnimator().hideToolbar(isImmediate);
- }
- }
-
- public void persistTemporaryVisibility() {
- ThreadUtils.assertOnUiThread();
-
- if (temporarilyVisible) {
- temporarilyVisible = false;
- setVisible(true, VisibilityTransition.IMMEDIATE);
- }
- }
-
- public void setPinned(boolean pinned, PinReason reason) {
- ThreadUtils.assertOnUiThread();
- if (layerView == null) {
- return;
- }
-
- layerView.getDynamicToolbarAnimator().setPinned(pinned, reason);
- }
-
- private void triggerEnabledListener() {
- if (enabledChangedListener != null) {
- enabledChangedListener.onEnabledChanged(isEnabled());
- }
- }
-
- private class PrefHandler extends PrefHandlerBase {
- @Override
- public void prefValue(String pref, boolean value) {
- if (value == prefEnabled) {
- return;
- }
-
- prefEnabled = value;
-
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- // If accessibility is enabled, the dynamic toolbar is
- // forced to be off.
- if (!accessibilityEnabled) {
- triggerEnabledListener();
- }
- }
- });
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/EditBookmarkDialog.java b/mobile/android/base/java/org/mozilla/gecko/EditBookmarkDialog.java
deleted file mode 100644
index 38c38a9eb..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/EditBookmarkDialog.java
+++ /dev/null
@@ -1,252 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.db.BrowserContract.Bookmarks;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.UIAsyncTask;
-
-import android.app.Activity;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.app.AlertDialog;
-import android.content.DialogInterface;
-import android.database.Cursor;
-import android.support.design.widget.Snackbar;
-import android.text.Editable;
-import android.text.TextWatcher;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.EditText;
-
-/**
- * A dialog that allows editing a bookmarks url, title, or keywords
- * <p>
- * Invoked by calling one of the {@link org.mozilla.gecko.EditBookmarkDialog#show(String)}
- * methods.
- */
-public class EditBookmarkDialog {
- private final Context mContext;
-
- public EditBookmarkDialog(Context context) {
- mContext = context;
- }
-
- /**
- * A private struct to make it easier to pass bookmark data across threads
- */
- private class Bookmark {
- final int id;
- final String title;
- final String url;
- final String keyword;
-
- public Bookmark(int aId, String aTitle, String aUrl, String aKeyword) {
- id = aId;
- title = aTitle;
- url = aUrl;
- keyword = aKeyword;
- }
- }
-
- /**
- * This text watcher to enable or disable the OK button if the dialog contains
- * valid information. This class is overridden to do data checking on different fields.
- * By itself, it always enables the button.
- *
- * Callers can also assign a paired partner to the TextWatcher, and callers will check
- * that both are enabled before enabling the ok button.
- */
- private class EditBookmarkTextWatcher implements TextWatcher {
- // A stored reference to the dialog containing the text field being watched
- protected AlertDialog mDialog;
-
- // A stored text watcher to do the real verification of a field
- protected EditBookmarkTextWatcher mPairedTextWatcher;
-
- // Whether or not the ok button should be enabled.
- protected boolean mEnabled = true;
-
- public EditBookmarkTextWatcher(AlertDialog aDialog) {
- mDialog = aDialog;
- }
-
- public void setPairedTextWatcher(EditBookmarkTextWatcher aTextWatcher) {
- mPairedTextWatcher = aTextWatcher;
- }
-
- public boolean isEnabled() {
- return mEnabled;
- }
-
- // Textwatcher interface
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- // Disable if the we're disabled or the paired partner is disabled
- boolean enabled = mEnabled && (mPairedTextWatcher == null || mPairedTextWatcher.isEnabled());
- mDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(enabled);
- }
-
- @Override
- public void afterTextChanged(Editable s) {}
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
- }
-
- /**
- * A version of the EditBookmarkTextWatcher for the url field of the dialog.
- * Only checks if the field is empty or not.
- */
- private class LocationTextWatcher extends EditBookmarkTextWatcher {
- public LocationTextWatcher(AlertDialog aDialog) {
- super(aDialog);
- }
-
- // Disables the ok button if the location field is empty.
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- mEnabled = (s.toString().trim().length() > 0);
- super.onTextChanged(s, start, before, count);
- }
- }
-
- /**
- * A version of the EditBookmarkTextWatcher for the keyword field of the dialog.
- * Checks if the field has any (non leading or trailing) spaces.
- */
- private class KeywordTextWatcher extends EditBookmarkTextWatcher {
- public KeywordTextWatcher(AlertDialog aDialog) {
- super(aDialog);
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- // Disable if the keyword contains spaces
- mEnabled = (s.toString().trim().indexOf(' ') == -1);
- super.onTextChanged(s, start, before, count);
- }
- }
-
- /**
- * Show the Edit bookmark dialog for a particular url. If the url is bookmarked multiple times
- * this will just edit the first instance it finds.
- *
- * @param url The url of the bookmark to edit. The dialog will look up other information like the id,
- * current title, or keywords associated with this url. If the url isn't bookmarked, the
- * dialog will fail silently. If the url is bookmarked multiple times, this will only show
- * information about the first it finds.
- */
- public void show(final String url) {
- final ContentResolver cr = mContext.getContentResolver();
- final BrowserDB db = BrowserDB.from(mContext);
- (new UIAsyncTask.WithoutParams<Bookmark>(ThreadUtils.getBackgroundHandler()) {
- @Override
- public Bookmark doInBackground() {
- final Cursor cursor = db.getBookmarkForUrl(cr, url);
- if (cursor == null) {
- return null;
- }
-
- Bookmark bookmark = null;
- try {
- cursor.moveToFirst();
- bookmark = new Bookmark(cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID)),
- cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.TITLE)),
- cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.URL)),
- cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.KEYWORD)));
- } finally {
- cursor.close();
- }
- return bookmark;
- }
-
- @Override
- public void onPostExecute(Bookmark bookmark) {
- if (bookmark == null) {
- return;
- }
-
- show(bookmark.id, bookmark.title, bookmark.url, bookmark.keyword);
- }
- }).execute();
- }
-
- /**
- * Show the Edit bookmark dialog for a set of data. This will show the dialog whether
- * a bookmark with this url exists or not, but the results will NOT be saved if the id
- * is not a valid bookmark id.
- *
- * @param id The id of the bookmark to change. If there is no bookmark with this ID, the dialog
- * will fail silently.
- * @param title The initial title to show in the dialog
- * @param url The initial url to show in the dialog
- * @param keyword The initial keyword to show in the dialog
- */
- public void show(final int id, final String title, final String url, final String keyword) {
- final Context context = mContext;
-
- AlertDialog.Builder editPrompt = new AlertDialog.Builder(context);
- final View editView = LayoutInflater.from(context).inflate(R.layout.bookmark_edit, null);
- editPrompt.setTitle(R.string.bookmark_edit_title);
- editPrompt.setView(editView);
-
- final EditText nameText = ((EditText) editView.findViewById(R.id.edit_bookmark_name));
- final EditText locationText = ((EditText) editView.findViewById(R.id.edit_bookmark_location));
- final EditText keywordText = ((EditText) editView.findViewById(R.id.edit_bookmark_keyword));
- nameText.setText(title);
- locationText.setText(url);
- keywordText.setText(keyword);
-
- final BrowserDB db = BrowserDB.from(mContext);
- editPrompt.setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int whichButton) {
- (new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
- @Override
- public Void doInBackground() {
- String newUrl = locationText.getText().toString().trim();
- String newKeyword = keywordText.getText().toString().trim();
-
- db.updateBookmark(context.getContentResolver(), id, newUrl, nameText.getText().toString(), newKeyword);
- return null;
- }
-
- @Override
- public void onPostExecute(Void result) {
- SnackbarBuilder.builder((Activity) context)
- .message(R.string.bookmark_updated)
- .duration(Snackbar.LENGTH_LONG)
- .buildAndShow();
- }
- }).execute();
- }
- });
-
- editPrompt.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int whichButton) {
- // do nothing
- }
- });
-
- final AlertDialog dialog = editPrompt.create();
-
- // Create our TextWatchers
- LocationTextWatcher locationTextWatcher = new LocationTextWatcher(dialog);
- KeywordTextWatcher keywordTextWatcher = new KeywordTextWatcher(dialog);
-
- // Cross reference the TextWatchers
- locationTextWatcher.setPairedTextWatcher(keywordTextWatcher);
- keywordTextWatcher.setPairedTextWatcher(locationTextWatcher);
-
- // Add the TextWatcher Listeners
- locationText.addTextChangedListener(locationTextWatcher);
- keywordText.addTextChangedListener(keywordTextWatcher);
-
- dialog.show();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/Experiments.java b/mobile/android/base/java/org/mozilla/gecko/Experiments.java
deleted file mode 100644
index e71bb4c52..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/Experiments.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.content.Context;
-
-import android.util.Log;
-import android.text.TextUtils;
-
-import com.keepsafe.switchboard.Preferences;
-import com.keepsafe.switchboard.SwitchBoard;
-
-import java.util.LinkedList;
-import java.util.List;
-
-/**
- * This class should reflect the experiment names found in the Switchboard experiments config here:
- * https://github.com/mozilla-services/switchboard-experiments
- */
-public class Experiments {
- private static final String LOGTAG = "GeckoExperiments";
-
- // Show a system notification linking to a "What's New" page on app update.
- public static final String WHATSNEW_NOTIFICATION = "whatsnew-notification";
-
- // Subscribe to known, bookmarked sites and show a notification if new content is available.
- public static final String CONTENT_NOTIFICATIONS_12HRS = "content-notifications-12hrs";
- public static final String CONTENT_NOTIFICATIONS_8AM = "content-notifications-8am";
- public static final String CONTENT_NOTIFICATIONS_5PM = "content-notifications-5pm";
-
- // Onboarding: "Features and Story". These experiments are determined
- // on the client, they are not part of the server config.
- public static final String ONBOARDING3_A = "onboarding3-a"; // Control: No first run
- public static final String ONBOARDING3_B = "onboarding3-b"; // 4 static Feature + 1 dynamic slides
- public static final String ONBOARDING3_C = "onboarding3-c"; // Differentiating features slides
-
- // Synchronizing the catalog of downloadable content from Kinto
- public static final String DOWNLOAD_CONTENT_CATALOG_SYNC = "download-content-catalog-sync";
-
- // Promotion for "Add to homescreen"
- public static final String PROMOTE_ADD_TO_HOMESCREEN = "promote-add-to-homescreen";
-
- public static final String PREF_ONBOARDING_VERSION = "onboarding_version";
-
- // Promotion to bookmark reader-view items after entering reader view three times (Bug 1247689)
- public static final String TRIPLE_READERVIEW_BOOKMARK_PROMPT = "triple-readerview-bookmark-prompt";
-
- // Only show origin in URL bar instead of full URL (Bug 1236431)
- public static final String URLBAR_SHOW_ORIGIN_ONLY = "urlbar-show-origin-only";
-
- // Show name of organization (EV cert) instead of full URL in URL bar (Bug 1249594).
- public static final String URLBAR_SHOW_EV_CERT_OWNER = "urlbar-show-ev-cert-owner";
-
- // Play HLS videos in a VideoView (Bug 1313391)
- public static final String HLS_VIDEO_PLAYBACK = "hls-video-playback";
-
- // Make new activity stream panel available (to replace top sites) (Bug 1313316)
- public static final String ACTIVITY_STREAM = "activity-stream";
-
- /**
- * Returns if a user is in certain local experiment.
- * @param experiment Name of experiment to look up
- * @return returns value for experiment or false if experiment does not exist.
- */
- public static boolean isInExperimentLocal(Context context, String experiment) {
- if (SwitchBoard.isInBucket(context, 0, 20)) {
- return Experiments.ONBOARDING3_A.equals(experiment);
- } else if (SwitchBoard.isInBucket(context, 20, 60)) {
- return Experiments.ONBOARDING3_B.equals(experiment);
- } else if (SwitchBoard.isInBucket(context, 60, 100)) {
- return Experiments.ONBOARDING3_C.equals(experiment);
- } else {
- return false;
- }
- }
-
- /**
- * Returns list of all active experiments, remote and local.
- * @return List of experiment names Strings
- */
- public static List<String> getActiveExperiments(Context c) {
- final List<String> experiments = new LinkedList<>();
- experiments.addAll(SwitchBoard.getActiveExperiments(c));
-
- // Add onboarding version.
- final String onboardingExperiment = GeckoSharedPrefs.forProfile(c).getString(Experiments.PREF_ONBOARDING_VERSION, null);
- if (!TextUtils.isEmpty(onboardingExperiment)) {
- experiments.add(onboardingExperiment);
- }
-
- return experiments;
- }
-
- /**
- * Sets an override to force an experiment to be enabled or disabled. This value
- * will be read and used before reading the switchboard server configuration.
- *
- * @param c Context
- * @param experimentName Experiment name
- * @param isEnabled Whether or not the experiment should be enabled
- */
- public static void setOverride(Context c, String experimentName, boolean isEnabled) {
- Log.d(LOGTAG, "setOverride: " + experimentName + " = " + isEnabled);
- Preferences.setOverrideValue(c, experimentName, isEnabled);
- }
-
- /**
- * Clears the override value for an experiment.
- *
- * @param c Context
- * @param experimentName Experiment name
- */
- public static void clearOverride(Context c, String experimentName) {
- Log.d(LOGTAG, "clearOverride: " + experimentName);
- Preferences.clearOverrideValue(c, experimentName);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/FilePicker.java b/mobile/android/base/java/org/mozilla/gecko/FilePicker.java
deleted file mode 100644
index 8ac5428a4..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/FilePicker.java
+++ /dev/null
@@ -1,227 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.util.GeckoEventListener;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.net.Uri;
-import android.os.Environment;
-import android.os.Parcelable;
-import android.provider.MediaStore;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-
-public class FilePicker implements GeckoEventListener {
- private static final String LOGTAG = "GeckoFilePicker";
- private static FilePicker sFilePicker;
- private final Context context;
-
- public interface ResultHandler {
- public void gotFile(String filename);
- }
-
- public static void init(Context context) {
- if (sFilePicker == null) {
- sFilePicker = new FilePicker(context.getApplicationContext());
- }
- }
-
- protected FilePicker(Context context) {
- this.context = context;
- EventDispatcher.getInstance().registerGeckoThreadListener(this, "FilePicker:Show");
- }
-
- @Override
- public void handleMessage(String event, final JSONObject message) {
- if (event.equals("FilePicker:Show")) {
- String mimeType = "*/*";
- final String mode = message.optString("mode");
- final int tabId = message.optInt("tabId", -1);
- final String title = message.optString("title");
-
- if ("mimeType".equals(mode))
- mimeType = message.optString("mimeType");
- else if ("extension".equals(mode))
- mimeType = GeckoAppShell.getMimeTypeFromExtensions(message.optString("extensions"));
-
- showFilePickerAsync(title, mimeType, new ResultHandler() {
- @Override
- public void gotFile(String filename) {
- try {
- message.put("file", filename);
- } catch (JSONException ex) {
- Log.i(LOGTAG, "Can't add filename to message " + filename);
- }
-
-
- GeckoAppShell.notifyObservers("FilePicker:Result", message.toString());
- }
- }, tabId);
- }
- }
-
- private void addActivities(Intent intent, HashMap<String, Intent> intents, HashMap<String, Intent> filters) {
- PackageManager pm = context.getPackageManager();
- List<ResolveInfo> lri = pm.queryIntentActivities(intent, 0);
- for (ResolveInfo ri : lri) {
- ComponentName cn = new ComponentName(ri.activityInfo.applicationInfo.packageName, ri.activityInfo.name);
- if (filters != null && !filters.containsKey(cn.toString())) {
- Intent rintent = new Intent(intent);
- rintent.setComponent(cn);
- intents.put(cn.toString(), rintent);
- }
- }
- }
-
- private Intent getIntent(String mimeType) {
- Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
- intent.setType(mimeType);
- intent.addCategory(Intent.CATEGORY_OPENABLE);
- return intent;
- }
-
- private List<Intent> getIntentsForFilePicker(final String mimeType,
- final FilePickerResultHandler fileHandler) {
- // The base intent to use for the file picker. Even if this is an implicit intent, Android will
- // still show a list of Activities that match this action/type.
- Intent baseIntent;
- // A HashMap of Activities the base intent will show in the chooser. This is used
- // to filter activities from other intents so that we don't show duplicates.
- HashMap<String, Intent> baseIntents = new HashMap<String, Intent>();
- // A list of other activities to shwo in the picker (and the intents to launch them).
- HashMap<String, Intent> intents = new HashMap<String, Intent> ();
-
- if ("audio/*".equals(mimeType)) {
- // For audio the only intent is the mimetype
- baseIntent = getIntent(mimeType);
- addActivities(baseIntent, baseIntents, null);
- } else if ("image/*".equals(mimeType)) {
- // For images the base is a capture intent
- baseIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
- baseIntent.putExtra(MediaStore.EXTRA_OUTPUT,
- Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
- fileHandler.generateImageName())));
- addActivities(baseIntent, baseIntents, null);
-
- // We also add the mimetype intent
- addActivities(getIntent(mimeType), intents, baseIntents);
- } else if ("video/*".equals(mimeType)) {
- // For videos the base is a capture intent
- baseIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
- addActivities(baseIntent, baseIntents, null);
-
- // We also add the mimetype intent
- addActivities(getIntent(mimeType), intents, baseIntents);
- } else {
- baseIntent = getIntent("*/*");
- addActivities(baseIntent, baseIntents, null);
-
- Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
- intent.putExtra(MediaStore.EXTRA_OUTPUT,
- Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
- fileHandler.generateImageName())));
- addActivities(intent, intents, baseIntents);
- intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
- addActivities(intent, intents, baseIntents);
- }
-
- // If we didn't find any activities, we fall back to the */* mimetype intent
- if (baseIntents.size() == 0 && intents.size() == 0) {
- intents.clear();
-
- baseIntent = getIntent("*/*");
- addActivities(baseIntent, baseIntents, null);
- }
-
- ArrayList<Intent> vals = new ArrayList<Intent>(intents.values());
- vals.add(0, baseIntent);
- return vals;
- }
-
- private String getFilePickerTitle(String mimeType) {
- if (mimeType.equals("audio/*")) {
- return context.getString(R.string.filepicker_audio_title);
- } else if (mimeType.equals("image/*")) {
- return context.getString(R.string.filepicker_image_title);
- } else if (mimeType.equals("video/*")) {
- return context.getString(R.string.filepicker_video_title);
- } else {
- return context.getString(R.string.filepicker_title);
- }
- }
-
- private interface IntentHandler {
- public void gotIntent(Intent intent);
- }
-
- /* Gets an intent that can open a particular mimetype. Will show a prompt with a list
- * of Activities that can handle the mietype. Asynchronously calls the handler when
- * one of the intents is selected. If the caller passes in null for the handler, will still
- * prompt for the activity, but will throw away the result.
- */
- private void getFilePickerIntentAsync(String title,
- final String mimeType,
- final FilePickerResultHandler fileHandler,
- final IntentHandler handler) {
- List<Intent> intents = getIntentsForFilePicker(mimeType, fileHandler);
-
- if (intents.size() == 0) {
- Log.i(LOGTAG, "no activities for the file picker!");
- handler.gotIntent(null);
- return;
- }
-
- Intent base = intents.remove(0);
-
- if (intents.size() == 0) {
- handler.gotIntent(base);
- return;
- }
-
- if (TextUtils.isEmpty(title)) {
- title = getFilePickerTitle(mimeType);
- }
- Intent chooser = Intent.createChooser(base, title);
- chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents.toArray(new Parcelable[intents.size()]));
- handler.gotIntent(chooser);
- }
-
- /* Allows the user to pick an activity to load files from using a list prompt. Then opens the activity and
- * sends the file returned to the passed in handler. If a null handler is passed in, will still
- * pick and launch the file picker, but will throw away the result.
- */
- protected void showFilePickerAsync(final String title, final String mimeType, final ResultHandler handler, final int tabId) {
- final FilePickerResultHandler fileHandler = new FilePickerResultHandler(handler, context, tabId);
- getFilePickerIntentAsync(title, mimeType, fileHandler, new IntentHandler() {
- @Override
- public void gotIntent(Intent intent) {
- if (handler == null) {
- return;
- }
-
- if (intent == null) {
- handler.gotFile("");
- return;
- }
-
- ActivityHandlerHelper.startIntent(intent, fileHandler);
- }
- });
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/FilePickerResultHandler.java b/mobile/android/base/java/org/mozilla/gecko/FilePickerResultHandler.java
deleted file mode 100644
index 7629ea546..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/FilePickerResultHandler.java
+++ /dev/null
@@ -1,282 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-
-import org.mozilla.gecko.util.ActivityResultHandler;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.app.Activity;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Environment;
-import android.os.Process;
-import android.provider.MediaStore;
-import android.provider.OpenableColumns;
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.app.LoaderManager;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.CursorLoader;
-import android.support.v4.content.Loader;
-import android.text.TextUtils;
-import android.text.format.Time;
-import android.util.Log;
-
-class FilePickerResultHandler implements ActivityResultHandler {
- private static final String LOGTAG = "GeckoFilePickerResultHandler";
- private static final String UPLOADS_DIR = "uploads";
-
- private final FilePicker.ResultHandler handler;
- private final int tabId;
- private final File cacheDir;
-
- // this code is really hacky and doesn't belong anywhere so I'm putting it here for now
- // until I can come up with a better solution.
- private String mImageName = "";
-
- /* Use this constructor to asynchronously listen for results */
- public FilePickerResultHandler(final FilePicker.ResultHandler handler, final Context context, final int tabId) {
- this.tabId = tabId;
- this.cacheDir = new File(context.getCacheDir(), UPLOADS_DIR);
- this.handler = handler;
- }
-
- void sendResult(String res) {
- if (handler != null) {
- handler.gotFile(res);
- }
- }
-
- @Override
- public void onActivityResult(int resultCode, Intent intent) {
- if (resultCode != Activity.RESULT_OK) {
- sendResult("");
- return;
- }
-
- // Camera results won't return an Intent. Use the file name we passed to the original intent.
- // In Android M, camera results return an empty Intent rather than null.
- if (intent == null || (intent.getAction() == null && intent.getData() == null)) {
- if (mImageName != null) {
- File file = new File(Environment.getExternalStorageDirectory(), mImageName);
- sendResult(file.getAbsolutePath());
- } else {
- sendResult("");
- }
- return;
- }
-
- Uri uri = intent.getData();
- if (uri == null) {
- sendResult("");
- return;
- }
-
- // Some file pickers may return a file uri
- if ("file".equals(uri.getScheme())) {
- String path = uri.getPath();
- sendResult(path == null ? "" : path);
- return;
- }
-
- final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
- final LoaderManager lm = fa.getSupportLoaderManager();
-
- // Finally, Video pickers and some file pickers may return a content provider.
- final ContentResolver cr = fa.getContentResolver();
- final Cursor cursor = cr.query(uri, new String[] { MediaStore.Video.Media.DATA }, null, null, null);
- if (cursor != null) {
- try {
- // Try a query to make sure the expected columns exist
- int index = cursor.getColumnIndex(MediaStore.Video.Media.DATA);
- if (index >= 0) {
- lm.initLoader(intent.hashCode(), null, new VideoLoaderCallbacks(uri));
- return;
- }
- } catch (Exception ex) {
- // We'll try a different loader below
- } finally {
- cursor.close();
- }
- }
-
- lm.initLoader(uri.hashCode(), null, new FileLoaderCallbacks(uri, cacheDir, tabId));
- }
-
- public String generateImageName() {
- Time now = new Time();
- now.setToNow();
- mImageName = now.format("%Y-%m-%d %H.%M.%S") + ".jpg";
- return mImageName;
- }
-
- private class VideoLoaderCallbacks implements LoaderCallbacks<Cursor> {
- final private Uri uri;
- public VideoLoaderCallbacks(Uri uri) {
- this.uri = uri;
- }
-
- @Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
- return new CursorLoader(fa,
- uri,
- new String[] { MediaStore.Video.Media.DATA },
- null, // selection
- null, // selectionArgs
- null); // sortOrder
- }
-
- @Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
- if (cursor.moveToFirst()) {
- String res = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA));
-
- // Some pickers (the KitKat Documents one for instance) won't return a temporary file here.
- // Fall back to the normal FileLoader if we didn't find anything.
- if (TextUtils.isEmpty(res)) {
- tryFileLoaderCallback();
- return;
- }
-
- sendResult(res);
- } else {
- tryFileLoaderCallback();
- }
- }
-
- private void tryFileLoaderCallback() {
- final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
- final LoaderManager lm = fa.getSupportLoaderManager();
- lm.initLoader(uri.hashCode(), null, new FileLoaderCallbacks(uri, cacheDir, tabId));
- }
-
- @Override
- public void onLoaderReset(Loader<Cursor> loader) { }
- }
-
- /**
- * This class's only dependency on FilePickerResultHandler is sendResult.
- */
- private class FileLoaderCallbacks implements LoaderCallbacks<Cursor>,
- Tabs.OnTabsChangedListener {
- private final Uri uri;
- private final File cacheDir;
- private final int tabId;
- String tempFile;
-
- public FileLoaderCallbacks(Uri uri, File cacheDir, int tabId) {
- this.uri = uri;
- this.cacheDir = cacheDir;
- this.tabId = tabId;
- }
-
- @Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
- return new CursorLoader(fa,
- uri,
- new String[] { OpenableColumns.DISPLAY_NAME },
- null, // selection
- null, // selectionArgs
- null); // sortOrder
- }
-
- @Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
- if (cursor.moveToFirst()) {
- String name = cursor.getString(0);
- // tmp filenames must be at least 3 characters long. Add a prefix to make sure that happens
- String fileName = "tmp_" + Process.myPid() + "-";
- String fileExt;
- int period;
-
- final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
- final ContentResolver cr = fa.getContentResolver();
-
- // Generate an extension if we don't already have one
- if (name == null || (period = name.lastIndexOf('.')) == -1) {
- String mimeType = cr.getType(uri);
- fileExt = "." + GeckoAppShell.getExtensionFromMimeType(mimeType);
- } else {
- fileExt = name.substring(period);
- fileName += name.substring(0, period);
- }
-
- // Now write the data to the temp file
- FileOutputStream fos = null;
- try {
- cacheDir.mkdir();
-
- File file = File.createTempFile(fileName, fileExt, cacheDir);
- fos = new FileOutputStream(file);
- InputStream is = cr.openInputStream(uri);
- byte[] buf = new byte[4096];
- int len = is.read(buf);
- while (len != -1) {
- fos.write(buf, 0, len);
- len = is.read(buf);
- }
- fos.close();
- is.close();
- tempFile = file.getAbsolutePath();
- sendResult((tempFile == null) ? "" : tempFile);
-
- if (tabId > -1 && !TextUtils.isEmpty(tempFile)) {
- Tabs.registerOnTabsChangedListener(this);
- }
- } catch (IOException ex) {
- Log.i(LOGTAG, "Error writing file", ex);
- } finally {
- if (fos != null) {
- try {
- fos.close();
- } catch (IOException e) { /* not much to do here */ }
- }
- }
- } else {
- sendResult("");
- }
- }
-
- @Override
- public void onLoaderReset(Loader<Cursor> loader) { }
-
- /*Tabs.OnTabsChangedListener*/
- // This cleans up our temp file. If it doesn't run, we just hope that Android
- // will eventually does the cleanup for us.
- @Override
- public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
- if ((tab == null) || (tab.getId() != tabId)) {
- return;
- }
-
- if (msg == Tabs.TabEvents.LOCATION_CHANGE ||
- msg == Tabs.TabEvents.CLOSED) {
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- File f = new File(tempFile);
- f.delete();
- }
- });
-
- // Tabs' listener array is safe to modify during use: its
- // iteration pattern is based on snapshots.
- Tabs.unregisterOnTabsChangedListener(this);
- }
- }
- }
-
-}
-
diff --git a/mobile/android/base/java/org/mozilla/gecko/FindInPageBar.java b/mobile/android/base/java/org/mozilla/gecko/FindInPageBar.java
deleted file mode 100644
index efa04a04e..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/FindInPageBar.java
+++ /dev/null
@@ -1,256 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.GeckoRequest;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import org.json.JSONObject;
-
-import android.content.Context;
-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.LayoutInflater;
-import android.view.View;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-public class FindInPageBar extends LinearLayout implements TextWatcher, View.OnClickListener, GeckoEventListener {
- private static final String LOGTAG = "GeckoFindInPageBar";
- private static final String REQUEST_ID = "FindInPageBar";
-
- private final Context mContext;
- private CustomEditText mFindText;
- private TextView mStatusText;
- private boolean mInflated;
-
- public FindInPageBar(Context context, AttributeSet attrs) {
- super(context, attrs);
- mContext = context;
- setFocusable(true);
- }
-
- public void inflateContent() {
- LayoutInflater inflater = LayoutInflater.from(mContext);
- View content = inflater.inflate(R.layout.find_in_page_content, this);
-
- content.findViewById(R.id.find_prev).setOnClickListener(this);
- content.findViewById(R.id.find_next).setOnClickListener(this);
- content.findViewById(R.id.find_close).setOnClickListener(this);
-
- // Capture clicks on the rest of the view to prevent them from
- // leaking into other views positioned below.
- content.setOnClickListener(this);
-
- mFindText = (CustomEditText) content.findViewById(R.id.find_text);
- mFindText.addTextChangedListener(this);
- mFindText.setOnKeyPreImeListener(new CustomEditText.OnKeyPreImeListener() {
- @Override
- public boolean onKeyPreIme(View v, int keyCode, KeyEvent event) {
- if (keyCode == KeyEvent.KEYCODE_BACK) {
- hide();
- return true;
- }
- return false;
- }
- });
-
- mStatusText = (TextView) content.findViewById(R.id.find_status);
-
- mInflated = true;
- GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
- "FindInPage:MatchesCountResult",
- "TextSelection:Data");
- }
-
- public void show() {
- if (!mInflated)
- inflateContent();
-
- setVisibility(VISIBLE);
- mFindText.requestFocus();
-
- // handleMessage() receives response message and determines initial state of softInput
- GeckoAppShell.notifyObservers("TextSelection:Get", REQUEST_ID);
- GeckoAppShell.notifyObservers("FindInPage:Opened", null);
- }
-
- public void hide() {
- if (!mInflated || getVisibility() == View.GONE) {
- // There's nothing to hide yet.
- return;
- }
-
- // Always clear the Find string, primarily for privacy.
- mFindText.setText("");
-
- // Only close the IMM if its EditText is the one with focus.
- if (mFindText.isFocused()) {
- getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0);
- }
-
- // Close the FIPB / FindHelper state.
- setVisibility(GONE);
- GeckoAppShell.notifyObservers("FindInPage:Closed", null);
- }
-
- private InputMethodManager getInputMethodManager(View view) {
- Context context = view.getContext();
- return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
- }
-
- public void onDestroy() {
- if (!mInflated) {
- return;
- }
- GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
- "FindInPage:MatchesCountResult",
- "TextSelection:Data");
- }
-
- private void onMatchesCountResult(final int total, final int current, final int limit, final String searchString) {
- if (total == -1) {
- updateResult(Integer.toString(limit) + "+");
- } else if (total > 0) {
- updateResult(Integer.toString(current) + "/" + Integer.toString(total));
- } else if (TextUtils.isEmpty(searchString)) {
- updateResult("");
- } else {
- // We display 0/0, when there were no
- // matches found, or if matching has been turned off by setting
- // pref accessibility.typeaheadfind.matchesCountLimit to 0.
- updateResult("0/0");
- }
- }
-
- private void updateResult(final String statusText) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- mStatusText.setVisibility(statusText.isEmpty() ? View.GONE : View.VISIBLE);
- mStatusText.setText(statusText);
- }
- });
- }
-
- // TextWatcher implementation
-
- @Override
- public void afterTextChanged(Editable s) {
- sendRequestToFinderHelper("FindInPage:Find", s.toString());
- }
-
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- // ignore
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- // ignore
- }
-
- // View.OnClickListener implementation
-
- @Override
- public void onClick(View v) {
- final int viewId = v.getId();
-
- String extras = getResources().getResourceEntryName(viewId);
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, extras);
-
- if (viewId == R.id.find_prev) {
- sendRequestToFinderHelper("FindInPage:Prev", mFindText.getText().toString());
- getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0);
- return;
- }
-
- if (viewId == R.id.find_next) {
- sendRequestToFinderHelper("FindInPage:Next", mFindText.getText().toString());
- getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0);
- return;
- }
-
- if (viewId == R.id.find_close) {
- hide();
- }
- }
-
- // GeckoEventListener implementation
-
- @Override
- public void handleMessage(String event, JSONObject message) {
- if (event.equals("FindInPage:MatchesCountResult")) {
- onMatchesCountResult(message.optInt("total", 0),
- message.optInt("current", 0),
- message.optInt("limit", 0),
- message.optString("searchString"));
- return;
- }
-
- if (!event.equals("TextSelection:Data") || !REQUEST_ID.equals(message.optString("requestId"))) {
- return;
- }
-
- final String text = message.optString("text");
-
- // Populate an initial find string, virtual keyboard not required.
- if (!TextUtils.isEmpty(text)) {
- // Populate initial selection
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- mFindText.setText(text);
- }
- });
- return;
- }
-
- // Show the virtual keyboard.
- if (mFindText.hasWindowFocus()) {
- getInputMethodManager(mFindText).showSoftInput(mFindText, 0);
- } else {
- // showSoftInput won't work until after the window is focused.
- mFindText.setOnWindowFocusChangeListener(new CustomEditText.OnWindowFocusChangeListener() {
- @Override
- public void onWindowFocusChanged(boolean hasFocus) {
- if (!hasFocus)
- return;
-
- mFindText.setOnWindowFocusChangeListener(null);
- getInputMethodManager(mFindText).showSoftInput(mFindText, 0);
- }
- });
- }
- }
-
- /**
- * Request find operation, and update matchCount results (current count and total).
- */
- private void sendRequestToFinderHelper(final String request, final String searchString) {
- GeckoAppShell.sendRequestToGecko(new GeckoRequest(request, searchString) {
- @Override
- public void onResponse(NativeJSObject nativeJSObject) {
- // We don't care about the return value, because `onMatchesCountResult`
- // does the heavy lifting.
- }
-
- @Override
- public void onError(NativeJSObject error) {
- // Gecko didn't respond due to state change, javascript error, etc.
- Log.d(LOGTAG, "No response from Gecko on request to match string: [" +
- searchString + "]");
- updateResult("");
- }
- });
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java b/mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java
deleted file mode 100644
index 5c7f932c0..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java
+++ /dev/null
@@ -1,459 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.animation.ViewHelper;
-import org.mozilla.gecko.gfx.FloatSize;
-import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.widget.SwipeDismissListViewTouchListener;
-import org.mozilla.gecko.widget.SwipeDismissListViewTouchListener.OnDismissCallback;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.PointF;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.util.Pair;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.animation.Animation;
-import android.view.animation.AnimationUtils;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
-import android.widget.ArrayAdapter;
-import android.widget.ImageView;
-import android.widget.ListView;
-import android.widget.RelativeLayout;
-import android.widget.RelativeLayout.LayoutParams;
-import android.widget.TextView;
-
-import java.util.Arrays;
-import java.util.Collection;
-
-public class FormAssistPopup extends RelativeLayout implements GeckoEventListener {
- private final Context mContext;
- private final Animation mAnimation;
-
- private ListView mAutoCompleteList;
- private RelativeLayout mValidationMessage;
- private TextView mValidationMessageText;
- private ImageView mValidationMessageArrow;
- private ImageView mValidationMessageArrowInverted;
-
- private double mX;
- private double mY;
- private double mW;
- private double mH;
-
- private enum PopupType {
- AUTOCOMPLETE,
- VALIDATIONMESSAGE;
- }
- private PopupType mPopupType;
-
- private static final int MAX_VISIBLE_ROWS = 5;
-
- private static int sAutoCompleteMinWidth;
- private static int sAutoCompleteRowHeight;
- private static int sValidationMessageHeight;
- private static int sValidationTextMarginTop;
- private static LayoutParams sValidationTextLayoutNormal;
- private static LayoutParams sValidationTextLayoutInverted;
-
- private static final String LOGTAG = "GeckoFormAssistPopup";
-
- // The blocklist is so short that ArrayList is probably cheaper than HashSet.
- private static final Collection<String> sInputMethodBlocklist = Arrays.asList(
- InputMethods.METHOD_GOOGLE_JAPANESE_INPUT, // bug 775850
- InputMethods.METHOD_OPENWNN_PLUS, // bug 768108
- InputMethods.METHOD_SIMEJI, // bug 768108
- InputMethods.METHOD_SWYPE, // bug 755909
- InputMethods.METHOD_SWYPE_BETA // bug 755909
- );
-
- public FormAssistPopup(Context context, AttributeSet attrs) {
- super(context, attrs);
- mContext = context;
-
- mAnimation = AnimationUtils.loadAnimation(context, R.anim.grow_fade_in);
- mAnimation.setDuration(75);
-
- setFocusable(false);
-
- GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
- "FormAssist:AutoComplete",
- "FormAssist:ValidationMessage",
- "FormAssist:Hide");
- }
-
- void destroy() {
- GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
- "FormAssist:AutoComplete",
- "FormAssist:ValidationMessage",
- "FormAssist:Hide");
- }
-
- @Override
- public void handleMessage(String event, JSONObject message) {
- try {
- if (event.equals("FormAssist:AutoComplete")) {
- handleAutoCompleteMessage(message);
- } else if (event.equals("FormAssist:ValidationMessage")) {
- handleValidationMessage(message);
- } else if (event.equals("FormAssist:Hide")) {
- handleHideMessage(message);
- }
- } catch (Exception e) {
- Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
- }
- }
-
- private void handleAutoCompleteMessage(JSONObject message) throws JSONException {
- final JSONArray suggestions = message.getJSONArray("suggestions");
- final JSONObject rect = message.getJSONObject("rect");
- final boolean isEmpty = message.getBoolean("isEmpty");
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- showAutoCompleteSuggestions(suggestions, rect, isEmpty);
- }
- });
- }
-
- private void handleValidationMessage(JSONObject message) throws JSONException {
- final String validationMessage = message.getString("validationMessage");
- final JSONObject rect = message.getJSONObject("rect");
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- showValidationMessage(validationMessage, rect);
- }
- });
- }
-
- private void handleHideMessage(JSONObject message) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- hide();
- }
- });
- }
-
- private void showAutoCompleteSuggestions(JSONArray suggestions, JSONObject rect, boolean isEmpty) {
- final String inputMethod = InputMethods.getCurrentInputMethod(mContext);
- if (!isEmpty && sInputMethodBlocklist.contains(inputMethod)) {
- // Don't display the form auto-complete popup after the user starts typing
- // to avoid confusing somes IME. See bug 758820 and bug 632744.
- hide();
- return;
- }
-
- if (mAutoCompleteList == null) {
- LayoutInflater inflater = LayoutInflater.from(mContext);
- mAutoCompleteList = (ListView) inflater.inflate(R.layout.autocomplete_list, null);
-
- mAutoCompleteList.setOnItemClickListener(new OnItemClickListener() {
- @Override
- public void onItemClick(AdapterView<?> parentView, View view, int position, long id) {
- // Use the value stored with the autocomplete view, not the label text,
- // since they can be different.
- TextView textView = (TextView) view;
- String value = (String) textView.getTag();
- broadcastGeckoEvent("FormAssist:AutoComplete", value);
- hide();
- }
- });
-
- // Create a ListView-specific touch listener. ListViews are given special treatment because
- // by default they handle touches for their list items... i.e. they're in charge of drawing
- // the pressed state (the list selector), handling list item clicks, etc.
- final SwipeDismissListViewTouchListener touchListener = new SwipeDismissListViewTouchListener(mAutoCompleteList, new OnDismissCallback() {
- @Override
- public void onDismiss(ListView listView, final int position) {
- // Use the value stored with the autocomplete view, not the label text,
- // since they can be different.
- AutoCompleteListAdapter adapter = (AutoCompleteListAdapter) listView.getAdapter();
- Pair<String, String> item = adapter.getItem(position);
-
- // Remove the item from form history.
- broadcastGeckoEvent("FormAssist:Remove", item.second);
-
- // Update the list
- adapter.remove(item);
- adapter.notifyDataSetChanged();
- positionAndShowPopup();
- }
- });
- mAutoCompleteList.setOnTouchListener(touchListener);
-
- // Setting this scroll listener is required to ensure that during ListView scrolling,
- // we don't look for swipes.
- mAutoCompleteList.setOnScrollListener(touchListener.makeScrollListener());
-
- // Setting this recycler listener is required to make sure animated views are reset.
- mAutoCompleteList.setRecyclerListener(touchListener.makeRecyclerListener());
-
- addView(mAutoCompleteList);
- }
-
- AutoCompleteListAdapter adapter = new AutoCompleteListAdapter(mContext, R.layout.autocomplete_list_item);
- adapter.populateSuggestionsList(suggestions);
- mAutoCompleteList.setAdapter(adapter);
-
- if (setGeckoPositionData(rect, true)) {
- positionAndShowPopup();
- }
- }
-
- private void showValidationMessage(String validationMessage, JSONObject rect) {
- if (mValidationMessage == null) {
- LayoutInflater inflater = LayoutInflater.from(mContext);
- mValidationMessage = (RelativeLayout) inflater.inflate(R.layout.validation_message, null);
-
- addView(mValidationMessage);
- mValidationMessageText = (TextView) mValidationMessage.findViewById(R.id.validation_message_text);
-
- sValidationTextMarginTop = (int) (mContext.getResources().getDimension(R.dimen.validation_message_margin_top));
-
- sValidationTextLayoutNormal = new LayoutParams(mValidationMessageText.getLayoutParams());
- sValidationTextLayoutNormal.setMargins(0, sValidationTextMarginTop, 0, 0);
-
- sValidationTextLayoutInverted = new LayoutParams((ViewGroup.MarginLayoutParams) sValidationTextLayoutNormal);
- sValidationTextLayoutInverted.setMargins(0, 0, 0, 0);
-
- mValidationMessageArrow = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow);
- mValidationMessageArrowInverted = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow_inverted);
- }
-
- mValidationMessageText.setText(validationMessage);
-
- // We need to set the text as selected for the marquee text to work.
- mValidationMessageText.setSelected(true);
-
- if (setGeckoPositionData(rect, false)) {
- positionAndShowPopup();
- }
- }
-
- private boolean setGeckoPositionData(JSONObject rect, boolean isAutoComplete) {
- try {
- mX = rect.getDouble("x");
- mY = rect.getDouble("y");
- mW = rect.getDouble("w");
- mH = rect.getDouble("h");
- } catch (JSONException e) {
- // Bail if we can't get the correct dimensions for the popup.
- Log.e(LOGTAG, "Error getting FormAssistPopup dimensions", e);
- return false;
- }
-
- mPopupType = (isAutoComplete ?
- PopupType.AUTOCOMPLETE : PopupType.VALIDATIONMESSAGE);
- return true;
- }
-
- private void positionAndShowPopup() {
- positionAndShowPopup(GeckoAppShell.getLayerView().getViewportMetrics());
- }
-
- private void positionAndShowPopup(ImmutableViewportMetrics aMetrics) {
- ThreadUtils.assertOnUiThread();
-
- // Don't show the form assist popup when using fullscreen VKB
- InputMethodManager imm =
- (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
- if (imm.isFullscreenMode()) {
- return;
- }
-
- // Hide/show the appropriate popup contents
- if (mAutoCompleteList != null) {
- mAutoCompleteList.setVisibility((mPopupType == PopupType.AUTOCOMPLETE) ? VISIBLE : GONE);
- }
- if (mValidationMessage != null) {
- mValidationMessage.setVisibility((mPopupType == PopupType.AUTOCOMPLETE) ? GONE : VISIBLE);
- }
-
- if (sAutoCompleteMinWidth == 0) {
- Resources res = mContext.getResources();
- sAutoCompleteMinWidth = (int) (res.getDimension(R.dimen.autocomplete_min_width));
- sAutoCompleteRowHeight = (int) (res.getDimension(R.dimen.autocomplete_row_height));
- sValidationMessageHeight = (int) (res.getDimension(R.dimen.validation_message_height));
- }
-
- float zoom = aMetrics.zoomFactor;
-
- // These values correspond to the input box for which we want to
- // display the FormAssistPopup.
- int left = (int) (mX * zoom - aMetrics.viewportRectLeft);
- int top = (int) (mY * zoom - aMetrics.viewportRectTop + GeckoAppShell.getLayerView().getSurfaceTranslation());
- int width = (int) (mW * zoom);
- int height = (int) (mH * zoom);
-
- int popupWidth = LayoutParams.MATCH_PARENT;
- int popupLeft = left < 0 ? 0 : left;
-
- FloatSize viewport = aMetrics.getSize();
-
- // For autocomplete suggestions, if the input is smaller than the screen-width,
- // shrink the popup's width. Otherwise, keep it as MATCH_PARENT.
- if ((mPopupType == PopupType.AUTOCOMPLETE) && (left + width) < viewport.width) {
- popupWidth = left < 0 ? left + width : width;
-
- // Ensure the popup has a minimum width.
- if (popupWidth < sAutoCompleteMinWidth) {
- popupWidth = sAutoCompleteMinWidth;
-
- // Move the popup to the left if there isn't enough room for it.
- if ((popupLeft + popupWidth) > viewport.width) {
- popupLeft = (int) (viewport.width - popupWidth);
- }
- }
- }
-
- int popupHeight;
- if (mPopupType == PopupType.AUTOCOMPLETE) {
- // Limit the amount of visible rows.
- int rows = mAutoCompleteList.getAdapter().getCount();
- if (rows > MAX_VISIBLE_ROWS) {
- rows = MAX_VISIBLE_ROWS;
- }
-
- popupHeight = sAutoCompleteRowHeight * rows;
- } else {
- popupHeight = sValidationMessageHeight;
- }
-
- int popupTop = top + height;
-
- if (mPopupType == PopupType.VALIDATIONMESSAGE) {
- mValidationMessageText.setLayoutParams(sValidationTextLayoutNormal);
- mValidationMessageArrow.setVisibility(VISIBLE);
- mValidationMessageArrowInverted.setVisibility(GONE);
- }
-
- // If the popup doesn't fit below the input box, shrink its height, or
- // see if we can place it above the input instead.
- if ((popupTop + popupHeight) > viewport.height) {
- // Find where the maximum space is, and put the popup there.
- if ((viewport.height - popupTop) > top) {
- // Shrink the height to fit it below the input box.
- popupHeight = (int) (viewport.height - popupTop);
- } else {
- if (popupHeight < top) {
- // No shrinking needed to fit on top.
- popupTop = (top - popupHeight);
- } else {
- // Shrink to available space on top.
- popupTop = 0;
- popupHeight = top;
- }
-
- if (mPopupType == PopupType.VALIDATIONMESSAGE) {
- mValidationMessageText.setLayoutParams(sValidationTextLayoutInverted);
- mValidationMessageArrow.setVisibility(GONE);
- mValidationMessageArrowInverted.setVisibility(VISIBLE);
- }
- }
- }
-
- LayoutParams layoutParams = new LayoutParams(popupWidth, popupHeight);
- layoutParams.setMargins(popupLeft, popupTop, 0, 0);
- setLayoutParams(layoutParams);
- requestLayout();
-
- if (!isShown()) {
- setVisibility(VISIBLE);
- startAnimation(mAnimation);
- }
- }
-
- public void hide() {
- if (isShown()) {
- setVisibility(GONE);
- broadcastGeckoEvent("FormAssist:Hidden", null);
- }
- }
-
- void onTranslationChanged() {
- ThreadUtils.assertOnUiThread();
- if (!isShown()) {
- return;
- }
- positionAndShowPopup();
- }
-
- void onMetricsChanged(final ImmutableViewportMetrics aMetrics) {
- if (!isShown()) {
- return;
- }
-
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- positionAndShowPopup(aMetrics);
- }
- });
- }
-
- private static void broadcastGeckoEvent(String eventName, String eventData) {
- GeckoAppShell.notifyObservers(eventName, eventData);
- }
-
- private class AutoCompleteListAdapter extends ArrayAdapter<Pair<String, String>> {
- private final LayoutInflater mInflater;
- private final int mTextViewResourceId;
-
- public AutoCompleteListAdapter(Context context, int textViewResourceId) {
- super(context, textViewResourceId);
-
- mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- mTextViewResourceId = textViewResourceId;
- }
-
- // This method takes an array of autocomplete suggestions with label/value properties
- // and adds label/value Pair objects to the array that backs the adapter.
- public void populateSuggestionsList(JSONArray suggestions) {
- try {
- for (int i = 0; i < suggestions.length(); i++) {
- JSONObject suggestion = suggestions.getJSONObject(i);
- String label = suggestion.getString("label");
- String value = suggestion.getString("value");
- add(new Pair<String, String>(label, value));
- }
- } catch (JSONException e) {
- Log.e(LOGTAG, "JSONException", e);
- }
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- if (convertView == null) {
- convertView = mInflater.inflate(mTextViewResourceId, null);
- }
-
- Pair<String, String> item = getItem(position);
- TextView itemView = (TextView) convertView;
-
- // Set the text with the suggestion label
- itemView.setText(item.first);
-
- // Set a tag with the suggestion value
- itemView.setTag(item.second);
-
- return convertView;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoActivity.java b/mobile/android/base/java/org/mozilla/gecko/GeckoActivity.java
deleted file mode 100644
index 774ca6024..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoActivity.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.content.ComponentName;
-import android.content.Intent;
-import android.support.v7.app.AppCompatActivity;
-
-public abstract class GeckoActivity extends AppCompatActivity implements GeckoActivityStatus {
- // has this activity recently started another Gecko activity?
- private boolean mGeckoActivityOpened;
-
- /**
- * Display any resources that show strings or encompass locale-specific
- * representations.
- *
- * onLocaleReady must always be called on the UI thread.
- */
- public void onLocaleReady(final String locale) {
- }
-
- @Override
- public void onPause() {
- super.onPause();
-
- if (getApplication() instanceof GeckoApplication) {
- ((GeckoApplication) getApplication()).onActivityPause(this);
- }
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- if (getApplication() instanceof GeckoApplication) {
- ((GeckoApplication) getApplication()).onActivityResume(this);
- mGeckoActivityOpened = false;
- }
- }
-
- @Override
- public void onCreate(android.os.Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (AppConstants.MOZ_ANDROID_ANR_REPORTER) {
- ANRReporter.register(getApplicationContext());
- }
- }
-
- @Override
- public void onDestroy() {
- if (AppConstants.MOZ_ANDROID_ANR_REPORTER) {
- ANRReporter.unregister();
- }
- super.onDestroy();
- }
-
- @Override
- public void startActivity(Intent intent) {
- mGeckoActivityOpened = checkIfGeckoActivity(intent);
- super.startActivity(intent);
- }
-
- @Override
- public void startActivityForResult(Intent intent, int request) {
- mGeckoActivityOpened = checkIfGeckoActivity(intent);
- super.startActivityForResult(intent, request);
- }
-
- private static boolean checkIfGeckoActivity(Intent intent) {
- // Whenever we call our own activity, the component and its package name is set.
- // If we call an activity from another package, or an open intent (leaving android to resolve)
- // component has a different package name or it is null.
- ComponentName component = intent.getComponent();
- return (component != null &&
- AppConstants.ANDROID_PACKAGE_NAME.equals(component.getPackageName()));
- }
-
- @Override
- public boolean isGeckoActivityOpened() {
- return mGeckoActivityOpened;
- }
-
- public boolean isApplicationInBackground() {
- return ((GeckoApplication) getApplication()).isApplicationInBackground();
- }
-
- @Override
- public void onLowMemory() {
- MemoryMonitor.getInstance().onLowMemory();
- super.onLowMemory();
- }
-
- @Override
- public void onTrimMemory(int level) {
- MemoryMonitor.getInstance().onTrimMemory(level);
- super.onTrimMemory(level);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoActivityStatus.java b/mobile/android/base/java/org/mozilla/gecko/GeckoActivityStatus.java
deleted file mode 100644
index ce6b8abd0..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoActivityStatus.java
+++ /dev/null
@@ -1,10 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-public interface GeckoActivityStatus {
- public boolean isGeckoActivityOpened();
- public boolean isFinishing(); // typically from android.app.Activity
-};
diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
deleted file mode 100644
index 05fa2bbf8..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ /dev/null
@@ -1,2878 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.db.UrlAnnotations;
-import org.mozilla.gecko.gfx.BitmapUtils;
-import org.mozilla.gecko.gfx.FullScreenState;
-import org.mozilla.gecko.gfx.LayerView;
-import org.mozilla.gecko.health.HealthRecorder;
-import org.mozilla.gecko.health.SessionInformation;
-import org.mozilla.gecko.health.StubbedHealthRecorder;
-import org.mozilla.gecko.home.HomeConfig.PanelType;
-import org.mozilla.gecko.icons.IconCallback;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.icons.Icons;
-import org.mozilla.gecko.menu.GeckoMenu;
-import org.mozilla.gecko.menu.GeckoMenuInflater;
-import org.mozilla.gecko.menu.MenuPanel;
-import org.mozilla.gecko.notifications.NotificationClient;
-import org.mozilla.gecko.notifications.NotificationHelper;
-import org.mozilla.gecko.util.IntentUtils;
-import org.mozilla.gecko.mozglue.SafeIntent;
-import org.mozilla.gecko.mozglue.GeckoLoader;
-import org.mozilla.gecko.permissions.Permissions;
-import org.mozilla.gecko.preferences.ClearOnShutdownPref;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-import org.mozilla.gecko.prompts.PromptService;
-import org.mozilla.gecko.restrictions.Restrictions;
-import org.mozilla.gecko.tabqueue.TabQueueHelper;
-import org.mozilla.gecko.text.FloatingToolbarTextSelection;
-import org.mozilla.gecko.text.TextSelection;
-import org.mozilla.gecko.updater.UpdateServiceHelper;
-import org.mozilla.gecko.util.ActivityResultHandler;
-import org.mozilla.gecko.util.ActivityUtils;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.FileUtils;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.GeckoRequest;
-import org.mozilla.gecko.util.HardwareUtils;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.PrefUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.annotation.SuppressLint;
-import android.annotation.TargetApi;
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.res.Configuration;
-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.hardware.Sensor;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Environment;
-import android.os.Handler;
-import android.os.PowerManager;
-import android.os.Process;
-import android.os.StrictMode;
-import android.provider.ContactsContract;
-import android.provider.MediaStore.Images.Media;
-import android.support.annotation.WorkerThread;
-import android.support.design.widget.Snackbar;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.util.Base64;
-import android.util.Log;
-import android.util.SparseBooleanArray;
-import android.view.Gravity;
-import android.view.KeyEvent;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.MotionEvent;
-import android.view.OrientationEventListener;
-import android.view.SurfaceView;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
-import android.view.Window;
-import android.widget.AbsoluteLayout;
-import android.widget.AdapterView;
-import android.widget.Button;
-import android.widget.FrameLayout;
-import android.widget.ListView;
-import android.widget.RelativeLayout;
-import android.widget.SimpleAdapter;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.ref.WeakReference;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-public abstract class GeckoApp
- extends GeckoActivity
- implements
- ContextGetter,
- GeckoAppShell.GeckoInterface,
- GeckoEventListener,
- GeckoMenu.Callback,
- GeckoMenu.MenuPresenter,
- NativeEventListener,
- Tabs.OnTabsChangedListener,
- ViewTreeObserver.OnGlobalLayoutListener {
-
- private static final String LOGTAG = "GeckoApp";
- private static final long ONE_DAY_MS = TimeUnit.MILLISECONDS.convert(1, TimeUnit.DAYS);
-
- public static final String ACTION_ALERT_CALLBACK = "org.mozilla.gecko.ALERT_CALLBACK";
- public static final String ACTION_HOMESCREEN_SHORTCUT = "org.mozilla.gecko.BOOKMARK";
- public static final String ACTION_DEBUG = "org.mozilla.gecko.DEBUG";
- public static final String ACTION_LAUNCH_SETTINGS = "org.mozilla.gecko.SETTINGS";
- public static final String ACTION_LOAD = "org.mozilla.gecko.LOAD";
- public static final String ACTION_INIT_PW = "org.mozilla.gecko.INIT_PW";
- public static final String ACTION_SWITCH_TAB = "org.mozilla.gecko.SWITCH_TAB";
-
- public static final String INTENT_REGISTER_STUMBLER_LISTENER = "org.mozilla.gecko.STUMBLER_REGISTER_LOCAL_LISTENER";
-
- public static final String EXTRA_STATE_BUNDLE = "stateBundle";
-
- public static final String LAST_SELECTED_TAB = "lastSelectedTab";
-
- public static final String PREFS_ALLOW_STATE_BUNDLE = "allowStateBundle";
- public static final String PREFS_VERSION_CODE = "versionCode";
- public static final String PREFS_WAS_STOPPED = "wasStopped";
- public static final String PREFS_CRASHED_COUNT = "crashedCount";
- public static final String PREFS_CLEANUP_TEMP_FILES = "cleanupTempFiles";
-
- public static final String SAVED_STATE_IN_BACKGROUND = "inBackground";
- public static final String SAVED_STATE_PRIVATE_SESSION = "privateSession";
-
- // Delay before running one-time "cleanup" tasks that may be needed
- // after a version upgrade.
- private static final int CLEANUP_DEFERRAL_SECONDS = 15;
-
- private static boolean sAlreadyLoaded;
-
- private static WeakReference<GeckoApp> lastActiveGeckoApp;
-
- protected RelativeLayout mRootLayout;
- protected RelativeLayout mMainLayout;
-
- protected RelativeLayout mGeckoLayout;
- private OrientationEventListener mCameraOrientationEventListener;
- public List<GeckoAppShell.AppStateListener> mAppStateListeners = new LinkedList<GeckoAppShell.AppStateListener>();
- protected MenuPanel mMenuPanel;
- protected Menu mMenu;
- protected boolean mIsRestoringActivity;
-
- /** Tells if we're aborting app launch, e.g. if this is an unsupported device configuration. */
- protected boolean mIsAbortingAppLaunch;
-
- private PromptService mPromptService;
- protected TextSelection mTextSelection;
-
- protected DoorHangerPopup mDoorHangerPopup;
- protected FormAssistPopup mFormAssistPopup;
-
-
- protected GeckoView mLayerView;
- private AbsoluteLayout mPluginContainer;
-
- private FullScreenHolder mFullScreenPluginContainer;
- private View mFullScreenPluginView;
-
- private final HashMap<String, PowerManager.WakeLock> mWakeLocks = new HashMap<String, PowerManager.WakeLock>();
-
- protected boolean mLastSessionCrashed;
- protected boolean mShouldRestore;
- private boolean mSessionRestoreParsingFinished = false;
-
- private EventDispatcher eventDispatcher;
-
- private int lastSelectedTabId = -1;
-
- private static final class LastSessionParser extends SessionParser {
- private JSONArray tabs;
- private JSONObject windowObject;
- private boolean isExternalURL;
-
- private boolean selectNextTab;
- private boolean tabsWereSkipped;
- private boolean tabsWereProcessed;
-
- public LastSessionParser(JSONArray tabs, JSONObject windowObject, boolean isExternalURL) {
- this.tabs = tabs;
- this.windowObject = windowObject;
- this.isExternalURL = isExternalURL;
- }
-
- public boolean allTabsSkipped() {
- return tabsWereSkipped && !tabsWereProcessed;
- }
-
- @Override
- public void onTabRead(final SessionTab sessionTab) {
- if (sessionTab.isAboutHomeWithoutHistory()) {
- // This is a tab pointing to about:home with no history. We won't restore
- // this tab. If we end up restoring no tabs then the browser will decide
- // whether it needs to open about:home or a different 'homepage'. If we'd
- // always restore about:home only tabs then we'd never open the homepage.
- // See bug 1261008.
-
- if (sessionTab.isSelected()) {
- // Unfortunately this tab is the selected tab. Let's just try to select
- // the first tab. If we haven't restored any tabs so far then remember
- // to select the next tab that gets restored.
-
- if (!Tabs.getInstance().selectLastTab()) {
- selectNextTab = true;
- }
- }
-
- // Do not restore this tab.
- tabsWereSkipped = true;
- return;
- }
-
- tabsWereProcessed = true;
-
- JSONObject tabObject = sessionTab.getTabObject();
-
- int flags = Tabs.LOADURL_NEW_TAB;
- flags |= ((isExternalURL || !sessionTab.isSelected()) ? Tabs.LOADURL_DELAY_LOAD : 0);
- flags |= (tabObject.optBoolean("desktopMode") ? Tabs.LOADURL_DESKTOP : 0);
- flags |= (tabObject.optBoolean("isPrivate") ? Tabs.LOADURL_PRIVATE : 0);
-
- final Tab tab = Tabs.getInstance().loadUrl(sessionTab.getUrl(), flags);
-
- if (selectNextTab) {
- // We did not restore the selected tab previously. Now let's select this tab.
- Tabs.getInstance().selectTab(tab.getId());
- selectNextTab = false;
- }
-
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- tab.updateTitle(sessionTab.getTitle());
- }
- });
-
- try {
- tabObject.put("tabId", tab.getId());
- } catch (JSONException e) {
- Log.e(LOGTAG, "JSON error", e);
- }
- tabs.put(tabObject);
- }
-
- @Override
- public void onClosedTabsRead(final JSONArray closedTabData) throws JSONException {
- windowObject.put("closedTabs", closedTabData);
- }
- };
-
- protected boolean mInitialized;
- protected boolean mWindowFocusInitialized;
- private Telemetry.Timer mJavaUiStartupTimer;
- private Telemetry.Timer mGeckoReadyStartupTimer;
-
- private String mPrivateBrowsingSession;
-
- private volatile HealthRecorder mHealthRecorder;
- private volatile Locale mLastLocale;
-
- protected Intent mRestartIntent;
-
- private boolean mWasFirstTabShownAfterActivityUnhidden;
-
- abstract public int getLayout();
-
- protected void processTabQueue() {};
-
- protected void openQueuedTabs() {};
-
- @SuppressWarnings("serial")
- class SessionRestoreException extends Exception {
- public SessionRestoreException(Exception e) {
- super(e);
- }
-
- public SessionRestoreException(String message) {
- super(message);
- }
- }
-
- void toggleChrome(final boolean aShow) { }
-
- void focusChrome() { }
-
- @Override
- public Context getContext() {
- return this;
- }
-
- @Override
- public SharedPreferences getSharedPreferences() {
- return GeckoSharedPrefs.forApp(this);
- }
-
- @Override
- public Activity getActivity() {
- return this;
- }
-
- @Override
- public void addAppStateListener(GeckoAppShell.AppStateListener listener) {
- mAppStateListeners.add(listener);
- }
-
- @Override
- public void removeAppStateListener(GeckoAppShell.AppStateListener listener) {
- mAppStateListeners.remove(listener);
- }
-
- @Override
- public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
- // When a tab is closed, it is always unselected first.
- // When a tab is unselected, another tab is always selected first.
- switch (msg) {
- case UNSELECTED:
- break;
-
- case LOCATION_CHANGE:
- // We only care about location change for the selected tab.
- if (!Tabs.getInstance().isSelectedTab(tab))
- break;
- // Fall through...
- case SELECTED:
- invalidateOptionsMenu();
- if (mFormAssistPopup != null)
- mFormAssistPopup.hide();
- break;
-
- case DESKTOP_MODE_CHANGE:
- if (Tabs.getInstance().isSelectedTab(tab))
- invalidateOptionsMenu();
- break;
- }
- }
-
- public void refreshChrome() { }
-
- @Override
- public void invalidateOptionsMenu() {
- if (mMenu == null) {
- return;
- }
-
- onPrepareOptionsMenu(mMenu);
-
- super.invalidateOptionsMenu();
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- mMenu = menu;
-
- MenuInflater inflater = getMenuInflater();
- inflater.inflate(R.menu.gecko_app_menu, mMenu);
- return true;
- }
-
- @Override
- public MenuInflater getMenuInflater() {
- return new GeckoMenuInflater(this);
- }
-
- public MenuPanel getMenuPanel() {
- if (mMenuPanel == null) {
- onCreatePanelMenu(Window.FEATURE_OPTIONS_PANEL, null);
- invalidateOptionsMenu();
- }
- return mMenuPanel;
- }
-
- @Override
- public boolean onMenuItemClick(MenuItem item) {
- return onOptionsItemSelected(item);
- }
-
- @Override
- public boolean onMenuItemLongClick(MenuItem item) {
- return false;
- }
-
- @Override
- public void openMenu() {
- openOptionsMenu();
- }
-
- @Override
- public void showMenu(final View menu) {
- // On devices using the custom menu, focus is cleared from the menu when its tapped.
- // Close and then reshow it to avoid these issues. See bug 794581 and bug 968182.
- closeMenu();
-
- // Post the reshow code back to the UI thread to avoid some optimizations Android
- // has put in place for menus that hide/show themselves quickly. See bug 985400.
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- mMenuPanel.removeAllViews();
- mMenuPanel.addView(menu);
- openOptionsMenu();
- }
- });
- }
-
- @Override
- public void closeMenu() {
- closeOptionsMenu();
- }
-
- @Override
- public View onCreatePanelView(int featureId) {
- if (featureId == Window.FEATURE_OPTIONS_PANEL) {
- if (mMenuPanel == null) {
- mMenuPanel = new MenuPanel(this, null);
- } else {
- // Prepare the panel every time before showing the menu.
- onPreparePanel(featureId, mMenuPanel, mMenu);
- }
-
- return mMenuPanel;
- }
-
- return super.onCreatePanelView(featureId);
- }
-
- @Override
- public boolean onCreatePanelMenu(int featureId, Menu menu) {
- if (featureId == Window.FEATURE_OPTIONS_PANEL) {
- if (mMenuPanel == null) {
- mMenuPanel = (MenuPanel) onCreatePanelView(featureId);
- }
-
- GeckoMenu gMenu = new GeckoMenu(this, null);
- gMenu.setCallback(this);
- gMenu.setMenuPresenter(this);
- menu = gMenu;
- mMenuPanel.addView(gMenu);
-
- return onCreateOptionsMenu(menu);
- }
-
- return super.onCreatePanelMenu(featureId, menu);
- }
-
- @Override
- public boolean onPreparePanel(int featureId, View view, Menu menu) {
- if (featureId == Window.FEATURE_OPTIONS_PANEL) {
- return onPrepareOptionsMenu(menu);
- }
-
- return super.onPreparePanel(featureId, view, menu);
- }
-
- @Override
- public boolean onMenuOpened(int featureId, Menu menu) {
- // exit full-screen mode whenever the menu is opened
- if (mLayerView != null && mLayerView.isFullScreen()) {
- GeckoAppShell.notifyObservers("FullScreen:Exit", null);
- }
-
- if (featureId == Window.FEATURE_OPTIONS_PANEL) {
- if (mMenu == null) {
- // getMenuPanel() will force the creation of the menu as well
- MenuPanel panel = getMenuPanel();
- onPreparePanel(featureId, panel, mMenu);
- }
-
- // Scroll custom menu to the top
- if (mMenuPanel != null)
- mMenuPanel.scrollTo(0, 0);
-
- return true;
- }
-
- return super.onMenuOpened(featureId, menu);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- if (item.getItemId() == R.id.quit) {
- // Make sure the Guest Browsing notification goes away when we quit.
- GuestSession.hideNotification(this);
-
- final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
- final Set<String> clearSet =
- PrefUtils.getStringSet(prefs, ClearOnShutdownPref.PREF, new HashSet<String>());
-
- final JSONObject clearObj = new JSONObject();
- for (String clear : clearSet) {
- try {
- clearObj.put(clear, true);
- } catch (JSONException ex) {
- Log.e(LOGTAG, "Error adding clear object " + clear, ex);
- }
- }
-
- final JSONObject res = new JSONObject();
- try {
- res.put("sanitize", clearObj);
- } catch (JSONException ex) {
- Log.e(LOGTAG, "Error adding sanitize object", ex);
- }
-
- // If the user has opted out of session restore, and does want to clear history
- // we also want to prevent the current session info from being saved.
- if (clearObj.has("private.data.history")) {
- final String sessionRestore = getSessionRestorePreference(getSharedPreferences());
- try {
- res.put("dontSaveSession", "quit".equals(sessionRestore));
- } catch (JSONException ex) {
- Log.e(LOGTAG, "Error adding session restore data", ex);
- }
- }
-
- GeckoAppShell.notifyObservers("Browser:Quit", res.toString());
- // We don't call doShutdown() here because this creates a race condition which can
- // cause the clearing of private data to fail. Instead, we shut down the UI only after
- // we're done sanitizing.
- return true;
- }
-
- return super.onOptionsItemSelected(item);
- }
-
- @Override
- public void onOptionsMenuClosed(Menu menu) {
- mMenuPanel.removeAllViews();
- mMenuPanel.addView((GeckoMenu) mMenu);
- }
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- // Handle hardware menu key presses separately so that we can show a custom menu in some cases.
- if (keyCode == KeyEvent.KEYCODE_MENU) {
- openOptionsMenu();
- return true;
- }
-
- return super.onKeyDown(keyCode, event);
- }
-
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
-
- outState.putBoolean(SAVED_STATE_IN_BACKGROUND, isApplicationInBackground());
- outState.putString(SAVED_STATE_PRIVATE_SESSION, mPrivateBrowsingSession);
- outState.putInt(LAST_SELECTED_TAB, lastSelectedTabId);
- }
-
- @Override
- protected void onRestoreInstanceState(final Bundle inState) {
- lastSelectedTabId = inState.getInt(LAST_SELECTED_TAB);
- }
-
- public void addTab() { }
-
- public void addPrivateTab() { }
-
- public void showNormalTabs() { }
-
- public void showPrivateTabs() { }
-
- public void hideTabs() { }
-
- /**
- * Close the tab UI indirectly (not as the result of a direct user
- * action). This does not force the UI to close; for example in Firefox
- * tablet mode it will remain open unless the user explicitly closes it.
- *
- * @return True if the tab UI was hidden.
- */
- public boolean autoHideTabs() { return false; }
-
- @Override
- public boolean areTabsShown() { return false; }
-
- @Override
- public void handleMessage(final String event, final NativeJSObject message,
- final EventCallback callback) {
- if ("Accessibility:Ready".equals(event)) {
- GeckoAccessibility.updateAccessibilitySettings(this);
-
- } else if ("Bookmark:Insert".equals(event)) {
- final String url = message.getString("url");
- final String title = message.getString("title");
- final Context context = this;
- final BrowserDB db = BrowserDB.from(getProfile());
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- final boolean bookmarkAdded = db.addBookmark(getContentResolver(), title, url);
- final int resId = bookmarkAdded ? R.string.bookmark_added : R.string.bookmark_already_added;
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- SnackbarBuilder.builder(GeckoApp.this)
- .message(resId)
- .duration(Snackbar.LENGTH_LONG)
- .buildAndShow();
- }
- });
- }
- });
-
- } else if ("Contact:Add".equals(event)) {
- final String email = message.optString("email", null);
- final String phone = message.optString("phone", null);
- if (email != null) {
- Uri contactUri = Uri.parse(email);
- Intent i = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT, contactUri);
- startActivity(i);
- } else if (phone != null) {
- Uri contactUri = Uri.parse(phone);
- Intent i = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT, contactUri);
- startActivity(i);
- } else {
- // something went wrong.
- Log.e(LOGTAG, "Received Contact:Add message with no email nor phone number");
- }
-
- } else if ("DevToolsAuth:Scan".equals(event)) {
- DevToolsAuthHelper.scan(this, callback);
-
- } else if ("DOMFullScreen:Start".equals(event)) {
- // Local ref to layerView for thread safety
- LayerView layerView = mLayerView;
- if (layerView != null) {
- layerView.setFullScreenState(message.getBoolean("rootElement")
- ? FullScreenState.ROOT_ELEMENT : FullScreenState.NON_ROOT_ELEMENT);
- }
-
- } else if ("DOMFullScreen:Stop".equals(event)) {
- // Local ref to layerView for thread safety
- LayerView layerView = mLayerView;
- if (layerView != null) {
- layerView.setFullScreenState(FullScreenState.NONE);
- }
-
- } else if ("Image:SetAs".equals(event)) {
- String src = message.getString("url");
- setImageAs(src);
-
- } else if ("Locale:Set".equals(event)) {
- setLocale(message.getString("locale"));
-
- } else if ("Permissions:Data".equals(event)) {
- final NativeJSObject[] permissions = message.getObjectArray("permissions");
- showSiteSettingsDialog(permissions);
-
- } else if ("PrivateBrowsing:Data".equals(event)) {
- mPrivateBrowsingSession = message.optString("session", null);
-
- } else if ("Session:StatePurged".equals(event)) {
- onStatePurged();
-
- } else if ("Sanitize:Finished".equals(event)) {
- if (message.getBoolean("shutdown")) {
- // Gecko is shutting down and has called our sanitize handlers,
- // so we can start exiting, too.
- doShutdown();
- }
-
- } else if ("Share:Text".equals(event)) {
- final String text = message.getString("text");
- final Tab tab = Tabs.getInstance().getSelectedTab();
- String title = "";
- if (tab != null) {
- title = tab.getDisplayTitle();
- }
- IntentHelper.openUriExternal(text, "text/plain", "", "", Intent.ACTION_SEND, title, false);
-
- // Context: Sharing via chrome list (no explicit session is active)
- Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "text");
-
- } else if ("Snackbar:Show".equals(event)) {
- SnackbarBuilder.builder(this)
- .fromEvent(message)
- .callback(callback)
- .buildAndShow();
-
- } else if ("SystemUI:Visibility".equals(event)) {
- setSystemUiVisible(message.getBoolean("visible"));
-
- } else if ("ToggleChrome:Focus".equals(event)) {
- focusChrome();
-
- } else if ("ToggleChrome:Hide".equals(event)) {
- toggleChrome(false);
-
- } else if ("ToggleChrome:Show".equals(event)) {
- toggleChrome(true);
-
- } else if ("Update:Check".equals(event)) {
- UpdateServiceHelper.checkForUpdate(this);
- } else if ("Update:Download".equals(event)) {
- UpdateServiceHelper.downloadUpdate(this);
- } else if ("Update:Install".equals(event)) {
- UpdateServiceHelper.applyUpdate(this);
- } else if ("RuntimePermissions:Prompt".equals(event)) {
- String[] permissions = message.getStringArray("permissions");
- if (callback == null || permissions == null) {
- return;
- }
-
- Permissions.from(this)
- .withPermissions(permissions)
- .andFallback(new Runnable() {
- @Override
- public void run() {
- callback.sendSuccess(false);
- }
- })
- .run(new Runnable() {
- @Override
- public void run() {
- callback.sendSuccess(true);
- }
- });
- }
- }
-
- @Override
- public void handleMessage(String event, JSONObject message) {
- try {
- if (event.equals("Gecko:Ready")) {
- mGeckoReadyStartupTimer.stop();
- geckoConnected();
-
- // This method is already running on the background thread, so we
- // know that mHealthRecorder will exist. That doesn't stop us being
- // paranoid.
- // This method is cheap, so don't spawn a new runnable.
- final HealthRecorder rec = mHealthRecorder;
- if (rec != null) {
- rec.recordGeckoStartupTime(mGeckoReadyStartupTimer.getElapsed());
- }
-
- GeckoApplication.get().onDelayedStartup();
-
- } else if (event.equals("Gecko:Exited")) {
- // Gecko thread exited first; let GeckoApp die too.
- doShutdown();
- return;
-
- } else if (event.equals("Accessibility:Event")) {
- GeckoAccessibility.sendAccessibilityEvent(message);
- }
- } catch (Exception e) {
- Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
- }
- }
-
- void onStatePurged() { }
-
- /**
- * @param permissions
- * Array of JSON objects to represent site permissions.
- * Example: { type: "offline-app", setting: "Store Offline Data", value: "Allow" }
- */
- private void showSiteSettingsDialog(final NativeJSObject[] permissions) {
- final AlertDialog.Builder builder = new AlertDialog.Builder(this);
- builder.setTitle(R.string.site_settings_title);
-
- final ArrayList<HashMap<String, String>> itemList =
- new ArrayList<HashMap<String, String>>();
- for (final NativeJSObject permObj : permissions) {
- final HashMap<String, String> map = new HashMap<String, String>();
- map.put("setting", permObj.getString("setting"));
- map.put("value", permObj.getString("value"));
- itemList.add(map);
- }
-
- // setMultiChoiceItems doesn't support using an adapter, so we're creating a hack with
- // setSingleChoiceItems and changing the choiceMode below when we create the dialog
- builder.setSingleChoiceItems(new SimpleAdapter(
- GeckoApp.this,
- itemList,
- R.layout.site_setting_item,
- new String[] { "setting", "value" },
- new int[] { R.id.setting, R.id.value }
- ), -1, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int id) { }
- });
-
- builder.setPositiveButton(R.string.site_settings_clear, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int id) {
- ListView listView = ((AlertDialog) dialog).getListView();
- SparseBooleanArray checkedItemPositions = listView.getCheckedItemPositions();
-
- // An array of the indices of the permissions we want to clear
- JSONArray permissionsToClear = new JSONArray();
- for (int i = 0; i < checkedItemPositions.size(); i++)
- if (checkedItemPositions.get(i))
- permissionsToClear.put(i);
-
- GeckoAppShell.notifyObservers("Permissions:Clear", permissionsToClear.toString());
- }
- });
-
- builder.setNegativeButton(R.string.site_settings_cancel, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int id) {
- dialog.cancel();
- }
- });
-
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- AlertDialog dialog = builder.create();
- dialog.show();
-
- final ListView listView = dialog.getListView();
- if (listView != null) {
- listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
- }
-
- final Button clearButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
- clearButton.setEnabled(false);
-
- dialog.getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() {
- @Override
- public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
- if (listView.getCheckedItemCount() == 0) {
- clearButton.setEnabled(false);
- } else {
- clearButton.setEnabled(true);
- }
- }
- });
- }
- });
- }
-
-
-
- /* package */ void addFullScreenPluginView(View view) {
- if (mFullScreenPluginView != null) {
- Log.w(LOGTAG, "Already have a fullscreen plugin view");
- return;
- }
-
- setFullScreen(true);
-
- view.setWillNotDraw(false);
- if (view instanceof SurfaceView) {
- ((SurfaceView) view).setZOrderOnTop(true);
- }
-
- mFullScreenPluginContainer = new FullScreenHolder(this);
-
- FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT,
- Gravity.CENTER);
- mFullScreenPluginContainer.addView(view, layoutParams);
-
-
- FrameLayout decor = (FrameLayout)getWindow().getDecorView();
- decor.addView(mFullScreenPluginContainer, layoutParams);
-
- mFullScreenPluginView = view;
- }
-
- @Override
- public void addPluginView(final View view) {
-
- if (ThreadUtils.isOnUiThread()) {
- addFullScreenPluginView(view);
- } else {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- addFullScreenPluginView(view);
- }
- });
- }
- }
-
- /* package */ void removeFullScreenPluginView(View view) {
- if (mFullScreenPluginView == null) {
- Log.w(LOGTAG, "Don't have a fullscreen plugin view");
- return;
- }
-
- if (mFullScreenPluginView != view) {
- Log.w(LOGTAG, "Passed view is not the current full screen view");
- return;
- }
-
- mFullScreenPluginContainer.removeView(mFullScreenPluginView);
-
- // We need do do this on the next iteration in order to avoid
- // a deadlock, see comment below in FullScreenHolder
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- mLayerView.showSurface();
- }
- });
-
- FrameLayout decor = (FrameLayout)getWindow().getDecorView();
- decor.removeView(mFullScreenPluginContainer);
-
- mFullScreenPluginView = null;
-
- GeckoScreenOrientation.getInstance().unlock();
- setFullScreen(false);
- }
-
- @Override
- public void removePluginView(final View view) {
- if (ThreadUtils.isOnUiThread()) {
- removePluginView(view);
- } else {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- removeFullScreenPluginView(view);
- }
- });
- }
- }
-
- // This method starts downloading an image synchronously and displays the Chooser activity to set the image as wallpaper.
- private void setImageAs(final String aSrc) {
- boolean isDataURI = aSrc.startsWith("data:");
- Bitmap image = null;
- InputStream is = null;
- ByteArrayOutputStream os = null;
- try {
- if (isDataURI) {
- int dataStart = aSrc.indexOf(",");
- byte[] buf = Base64.decode(aSrc.substring(dataStart + 1), Base64.DEFAULT);
- image = BitmapUtils.decodeByteArray(buf);
- } else {
- int byteRead;
- byte[] buf = new byte[4192];
- os = new ByteArrayOutputStream();
- URL url = new URL(aSrc);
- is = url.openStream();
-
- // Cannot read from same stream twice. Also, InputStream from
- // URL does not support reset. So converting to byte array.
-
- while ((byteRead = is.read(buf)) != -1) {
- os.write(buf, 0, byteRead);
- }
- byte[] imgBuffer = os.toByteArray();
- image = BitmapUtils.decodeByteArray(imgBuffer);
- }
- if (image != null) {
- // Some devices don't have a DCIM folder and the Media.insertImage call will fail.
- File dcimDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
-
- if (!dcimDir.mkdirs() && !dcimDir.isDirectory()) {
- SnackbarBuilder.builder(this)
- .message(R.string.set_image_path_fail)
- .duration(Snackbar.LENGTH_LONG)
- .buildAndShow();
- return;
- }
- String path = Media.insertImage(getContentResolver(), image, null, null);
- if (path == null) {
- SnackbarBuilder.builder(this)
- .message(R.string.set_image_path_fail)
- .duration(Snackbar.LENGTH_LONG)
- .buildAndShow();
- return;
- }
- final Intent intent = new Intent(Intent.ACTION_ATTACH_DATA);
- intent.addCategory(Intent.CATEGORY_DEFAULT);
- intent.setData(Uri.parse(path));
-
- // Removes the image from storage once the chooser activity ends.
- Intent chooser = Intent.createChooser(intent, getString(R.string.set_image_chooser_title));
- ActivityResultHandler handler = new ActivityResultHandler() {
- @Override
- public void onActivityResult (int resultCode, Intent data) {
- getContentResolver().delete(intent.getData(), null, null);
- }
- };
- ActivityHandlerHelper.startIntentForActivity(this, chooser, handler);
- } else {
- SnackbarBuilder.builder(this)
- .message(R.string.set_image_fail)
- .duration(Snackbar.LENGTH_LONG)
- .buildAndShow();
- }
- } catch (OutOfMemoryError ome) {
- Log.e(LOGTAG, "Out of Memory when converting to byte array", ome);
- } catch (IOException ioe) {
- Log.e(LOGTAG, "I/O Exception while setting wallpaper", ioe);
- } finally {
- if (is != null) {
- try {
- is.close();
- } catch (IOException ioe) {
- Log.w(LOGTAG, "I/O Exception while closing stream", ioe);
- }
- }
- if (os != null) {
- try {
- os.close();
- } catch (IOException ioe) {
- Log.w(LOGTAG, "I/O Exception while closing stream", ioe);
- }
- }
- }
- }
-
- private int getBitmapSampleSize(BitmapFactory.Options options, int idealWidth, int idealHeight) {
- int width = options.outWidth;
- int height = options.outHeight;
- int inSampleSize = 1;
- if (height > idealHeight || width > idealWidth) {
- if (width > height) {
- inSampleSize = Math.round((float)height / idealHeight);
- } else {
- inSampleSize = Math.round((float)width / idealWidth);
- }
- }
- return inSampleSize;
- }
-
- public void requestRender() {
- mLayerView.requestRender();
- }
-
- @Override
- public void setFullScreen(final boolean fullscreen) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- ActivityUtils.setFullScreen(GeckoApp.this, fullscreen);
- }
- });
- }
-
- /**
- * Check and start the Java profiler if MOZ_PROFILER_STARTUP env var is specified.
- **/
- protected static void earlyStartJavaSampler(SafeIntent intent) {
- String env = intent.getStringExtra("env0");
- for (int i = 1; env != null; i++) {
- if (env.startsWith("MOZ_PROFILER_STARTUP=")) {
- if (!env.endsWith("=")) {
- GeckoJavaSampler.start(10, 1000);
- Log.d(LOGTAG, "Profiling Java on startup");
- }
- break;
- }
- env = intent.getStringExtra("env" + i);
- }
- }
-
- /**
- * Called when the activity is first created.
- *
- * Here we initialize all of our profile settings, Firefox Health Report,
- * and other one-shot constructions.
- **/
- @Override
- public void onCreate(Bundle savedInstanceState) {
- GeckoAppShell.ensureCrashHandling();
-
- eventDispatcher = new EventDispatcher();
-
- // Enable Android Strict Mode for developers' local builds (the "default" channel).
- if ("default".equals(AppConstants.MOZ_UPDATE_CHANNEL)) {
- enableStrictMode();
- }
-
- if (!HardwareUtils.isSupportedSystem()) {
- // This build does not support the Android version of the device: Show an error and finish the app.
- mIsAbortingAppLaunch = true;
- super.onCreate(savedInstanceState);
- showSDKVersionError();
- finish();
- return;
- }
-
- // The clock starts...now. Better hurry!
- mJavaUiStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_JAVAUI");
- mGeckoReadyStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_GECKOREADY");
-
- final SafeIntent intent = new SafeIntent(getIntent());
-
- earlyStartJavaSampler(intent);
-
- // GeckoLoader wants to dig some environment variables out of the
- // incoming intent, so pass it in here. GeckoLoader will do its
- // business later and dispose of the reference.
- GeckoLoader.setLastIntent(intent);
-
- // Workaround for <http://code.google.com/p/android/issues/detail?id=20915>.
- try {
- Class.forName("android.os.AsyncTask");
- } catch (ClassNotFoundException e) { }
-
- MemoryMonitor.getInstance().init(getApplicationContext());
-
- // GeckoAppShell is tightly coupled to us, rather than
- // the app context, because various parts of Fennec (e.g.,
- // GeckoScreenOrientation) use GAS to access the Activity in
- // the guise of fetching a Context.
- // When that's fixed, `this` can change to
- // `(GeckoApplication) getApplication()` here.
- GeckoAppShell.setContextGetter(this);
- GeckoAppShell.setGeckoInterface(this);
-
- // Tell Stumbler to register a local broadcast listener to listen for preference intents.
- // We do this via intents since we can't easily access Stumbler directly,
- // as it might be compiled outside of Fennec.
- getApplicationContext().sendBroadcast(
- new Intent(INTENT_REGISTER_STUMBLER_LISTENER)
- );
-
- // Did the OS locale change while we were backgrounded? If so,
- // we need to die so that Gecko will re-init add-ons that touch
- // the UI.
- // This is using a sledgehammer to crack a nut, but it'll do for
- // now.
- // Our OS locale pref will be detected as invalid after the
- // restart, and will be propagated to Gecko accordingly, so there's
- // no need to touch that here.
- if (BrowserLocaleManager.getInstance().systemLocaleDidChange()) {
- Log.i(LOGTAG, "System locale changed. Restarting.");
- doRestart();
- return;
- }
-
- if (sAlreadyLoaded) {
- // This happens when the GeckoApp activity is destroyed by Android
- // without killing the entire application (see Bug 769269).
- mIsRestoringActivity = true;
- Telemetry.addToHistogram("FENNEC_RESTORING_ACTIVITY", 1);
-
- } else {
- final String action = intent.getAction();
- final String args = intent.getStringExtra("args");
-
- sAlreadyLoaded = true;
- GeckoThread.init(/* profile */ null, args, action,
- /* debugging */ ACTION_DEBUG.equals(action));
-
- // Speculatively pre-fetch the profile in the background.
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- getProfile();
- }
- });
-
- final String uri = getURIFromIntent(intent);
- if (!TextUtils.isEmpty(uri)) {
- // Start a speculative connection as soon as Gecko loads.
- GeckoThread.speculativeConnect(uri);
- }
- }
-
- // GeckoThread has to register for "Gecko:Ready" first, so GeckoApp registers
- // for events after initializing GeckoThread but before launching it.
-
- getAppEventDispatcher().registerGeckoThreadListener((GeckoEventListener)this,
- "Gecko:Ready",
- "Gecko:Exited",
- "Accessibility:Event");
-
- getAppEventDispatcher().registerGeckoThreadListener((NativeEventListener)this,
- "Accessibility:Ready",
- "Bookmark:Insert",
- "Contact:Add",
- "DevToolsAuth:Scan",
- "DOMFullScreen:Start",
- "DOMFullScreen:Stop",
- "Image:SetAs",
- "Locale:Set",
- "Permissions:Data",
- "PrivateBrowsing:Data",
- "RuntimePermissions:Prompt",
- "Sanitize:Finished",
- "Session:StatePurged",
- "Share:Text",
- "Snackbar:Show",
- "SystemUI:Visibility",
- "ToggleChrome:Focus",
- "ToggleChrome:Hide",
- "ToggleChrome:Show",
- "Update:Check",
- "Update:Download",
- "Update:Install");
-
- GeckoThread.launch();
-
- Bundle stateBundle = IntentUtils.getBundleExtraSafe(getIntent(), EXTRA_STATE_BUNDLE);
- if (stateBundle != null) {
- // Use the state bundle if it was given as an intent extra. This is
- // only intended to be used internally via Robocop, so a boolean
- // is read from a private shared pref to prevent other apps from
- // injecting states.
- final SharedPreferences prefs = getSharedPreferences();
- if (prefs.getBoolean(PREFS_ALLOW_STATE_BUNDLE, false)) {
- prefs.edit().remove(PREFS_ALLOW_STATE_BUNDLE).apply();
- savedInstanceState = stateBundle;
- }
- } else if (savedInstanceState != null) {
- // Bug 896992 - This intent has already been handled; reset the intent.
- setIntent(new Intent(Intent.ACTION_MAIN));
- }
-
- super.onCreate(savedInstanceState);
-
- GeckoScreenOrientation.getInstance().update(getResources().getConfiguration().orientation);
-
- setContentView(getLayout());
-
- // Set up Gecko layout.
- mRootLayout = (RelativeLayout) findViewById(R.id.root_layout);
- mGeckoLayout = (RelativeLayout) findViewById(R.id.gecko_layout);
- mMainLayout = (RelativeLayout) findViewById(R.id.main_layout);
- mLayerView = (GeckoView) findViewById(R.id.layer_view);
-
- Tabs.getInstance().attachToContext(this, mLayerView);
-
- // Use global layout state change to kick off additional initialization
- mMainLayout.getViewTreeObserver().addOnGlobalLayoutListener(this);
-
- if (Versions.preMarshmallow) {
- mTextSelection = new ActionBarTextSelection(this);
- } else {
- mTextSelection = new FloatingToolbarTextSelection(this, mLayerView);
- }
- mTextSelection.create();
-
- // Determine whether we should restore tabs.
- mLastSessionCrashed = updateCrashedState();
- mShouldRestore = getSessionRestoreState(savedInstanceState);
- if (mShouldRestore && savedInstanceState != null) {
- boolean wasInBackground =
- savedInstanceState.getBoolean(SAVED_STATE_IN_BACKGROUND, false);
-
- // Don't log OOM-kills if only one activity was destroyed. (For example
- // from "Don't keep activities" on ICS)
- if (!wasInBackground && !mIsRestoringActivity) {
- Telemetry.addToHistogram("FENNEC_WAS_KILLED", 1);
- }
-
- mPrivateBrowsingSession = savedInstanceState.getString(SAVED_STATE_PRIVATE_SESSION);
- }
-
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- // If we are doing a restore, read the session data so we can send it to Gecko later.
- String restoreMessage = null;
- if (!mIsRestoringActivity && mShouldRestore) {
- final boolean isExternalURL = invokedWithExternalURL(getIntentURI(new SafeIntent(getIntent())));
- try {
- // restoreSessionTabs() will create simple tab stubs with the
- // URL and title for each page, but we also need to restore
- // session history. restoreSessionTabs() will inject the IDs
- // of the tab stubs into the JSON data (which holds the session
- // history). This JSON data is then sent to Gecko so session
- // history can be restored for each tab.
- restoreMessage = restoreSessionTabs(isExternalURL, false);
- } catch (SessionRestoreException e) {
- // If mShouldRestore was set to false in restoreSessionTabs(), this means
- // either that we intentionally skipped all tabs read from the session file,
- // or else that the file was syntactically valid, but didn't contain any
- // tabs (e.g. because the user cleared history), therefore we don't need
- // to switch to the backup copy.
- if (mShouldRestore) {
- Log.e(LOGTAG, "An error occurred during restore, switching to backup file", e);
- // To be on the safe side, we will always attempt to restore from the backup
- // copy if we end up here.
- // Since we will also hit this situation regularly during first run though,
- // we'll only report it in telemetry if we failed to restore despite the
- // file existing, which means it's very probably damaged.
- if (getProfile().sessionFileExists()) {
- Telemetry.addToHistogram("FENNEC_SESSIONSTORE_DAMAGED_SESSION_FILE", 1);
- }
- try {
- restoreMessage = restoreSessionTabs(isExternalURL, true);
- Telemetry.addToHistogram("FENNEC_SESSIONSTORE_RESTORING_FROM_BACKUP", 1);
- } catch (SessionRestoreException ex) {
- if (!mShouldRestore) {
- // Restoring only "failed" because the backup copy was deliberately empty, too.
- Telemetry.addToHistogram("FENNEC_SESSIONSTORE_RESTORING_FROM_BACKUP", 1);
- } else {
- // Restoring the backup failed, too, so do a normal startup.
- Log.e(LOGTAG, "An error occurred during restore", ex);
- mShouldRestore = false;
- }
- }
- }
- }
- }
-
- synchronized (GeckoApp.this) {
- mSessionRestoreParsingFinished = true;
- GeckoApp.this.notifyAll();
- }
-
- // If we are doing a restore, send the parsed session data to Gecko.
- if (!mIsRestoringActivity) {
- GeckoAppShell.notifyObservers("Session:Restore", restoreMessage);
- }
-
- // Make sure sessionstore.old is either updated or deleted as necessary.
- getProfile().updateSessionFile(mShouldRestore);
- }
- });
-
- // Perform background initialization.
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- final SharedPreferences prefs = GeckoApp.this.getSharedPreferences();
-
- // Wait until now to set this, because we'd rather throw an exception than
- // have a caller of BrowserLocaleManager regress startup.
- final LocaleManager localeManager = BrowserLocaleManager.getInstance();
- localeManager.initialize(getApplicationContext());
-
- SessionInformation previousSession = SessionInformation.fromSharedPrefs(prefs);
- if (previousSession.wasKilled()) {
- Telemetry.addToHistogram("FENNEC_WAS_KILLED", 1);
- }
-
- SharedPreferences.Editor editor = prefs.edit();
- editor.putBoolean(GeckoAppShell.PREFS_OOM_EXCEPTION, false);
-
- // Put a flag to check if we got a normal `onSaveInstanceState`
- // on exit, or if we were suddenly killed (crash or native OOM).
- editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false);
-
- editor.apply();
-
- // The lifecycle of mHealthRecorder is "shortly after onCreate"
- // through "onDestroy" -- essentially the same as the lifecycle
- // of the activity itself.
- final String profilePath = getProfile().getDir().getAbsolutePath();
- final EventDispatcher dispatcher = getAppEventDispatcher();
-
- // This is the locale prior to fixing it up.
- final Locale osLocale = Locale.getDefault();
-
- // Both of these are Java-format locale strings: "en_US", not "en-US".
- final String osLocaleString = osLocale.toString();
- String appLocaleString = localeManager.getAndApplyPersistedLocale(GeckoApp.this);
- Log.d(LOGTAG, "OS locale is " + osLocaleString + ", app locale is " + appLocaleString);
-
- if (appLocaleString == null) {
- appLocaleString = osLocaleString;
- }
-
- mHealthRecorder = GeckoApp.this.createHealthRecorder(GeckoApp.this,
- profilePath,
- dispatcher,
- osLocaleString,
- appLocaleString,
- previousSession);
-
- final String uiLocale = appLocaleString;
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- GeckoApp.this.onLocaleReady(uiLocale);
- }
- });
-
- // We use per-profile prefs here, because we're tracking against
- // a Gecko pref. The same applies to the locale switcher!
- BrowserLocaleManager.storeAndNotifyOSLocale(GeckoSharedPrefs.forProfile(GeckoApp.this), osLocale);
- }
- });
-
- IntentHelper.init(this);
- }
-
- @Override
- public void onStart() {
- super.onStart();
- if (mIsAbortingAppLaunch) {
- return;
- }
-
- mWasFirstTabShownAfterActivityUnhidden = false; // onStart indicates we were hidden.
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- // Overriding here is not necessary, but we do this so we don't
- // forget to add the abort if we override this method later.
- if (mIsAbortingAppLaunch) {
- return;
- }
- }
-
- /**
- * At this point, the resource system and the rest of the browser are
- * aware of the locale.
- *
- * Now we can display strings!
- *
- * You can think of this as being something like a second phase of onCreate,
- * where you can do string-related operations. Use this in place of embedding
- * strings in view XML.
- *
- * By contrast, onConfigurationChanged does some locale operations, but is in
- * response to device changes.
- */
- @Override
- public void onLocaleReady(final String locale) {
- if (!ThreadUtils.isOnUiThread()) {
- throw new RuntimeException("onLocaleReady must always be called from the UI thread.");
- }
-
- final Locale loc = Locales.parseLocaleCode(locale);
- if (loc.equals(mLastLocale)) {
- Log.d(LOGTAG, "New locale same as old; onLocaleReady has nothing to do.");
- }
-
- // The URL bar hint needs to be populated.
- TextView urlBar = (TextView) findViewById(R.id.url_bar_title);
- if (urlBar != null) {
- final String hint = getResources().getString(R.string.url_bar_default_text);
- urlBar.setHint(hint);
- } else {
- Log.d(LOGTAG, "No URL bar in GeckoApp. Not loading localized hint string.");
- }
-
- mLastLocale = loc;
-
- // Allow onConfigurationChanged to take care of the rest.
- // We don't call this.onConfigurationChanged, because (a) that does
- // work that's unnecessary after this locale action, and (b) it can
- // cause a loop! See Bug 1011008, Comment 12.
- super.onConfigurationChanged(getResources().getConfiguration());
- }
-
- protected void initializeChrome() {
- mDoorHangerPopup = new DoorHangerPopup(this);
- mPluginContainer = (AbsoluteLayout) findViewById(R.id.plugin_container);
- mFormAssistPopup = (FormAssistPopup) findViewById(R.id.form_assist_popup);
- }
-
- /**
- * Loads the initial tab at Fennec startup. If we don't restore tabs, this
- * tab will be about:home, or the homepage if the user has set one.
- * If we've temporarily disabled restoring to break out of a crash loop, we'll show
- * the Recent Tabs folder of the Combined History panel, so the user can manually
- * restore tabs as needed.
- * If we restore tabs, we don't need to create a new tab.
- */
- protected void loadStartupTab(final int flags) {
- if (!mShouldRestore) {
- if (mLastSessionCrashed) {
- // The Recent Tabs panel no longer exists, but BrowserApp will redirect us
- // to the Recent Tabs folder of the Combined History panel.
- Tabs.getInstance().loadUrl(AboutPages.getURLForBuiltinPanelType(PanelType.DEPRECATED_RECENT_TABS), flags);
- } else {
- final String homepage = getHomepage();
- Tabs.getInstance().loadUrl(!TextUtils.isEmpty(homepage) ? homepage : AboutPages.HOME, flags);
- }
- }
- }
-
- /**
- * Loads the initial tab at Fennec startup. This tab will load with the given
- * external URL. If that URL is invalid, a startup tab will be loaded.
- *
- * @param url External URL to load.
- * @param intent External intent whose extras modify the request
- * @param flags Flags used to load the load
- */
- protected void loadStartupTab(final String url, final SafeIntent intent, final int flags) {
- // Invalid url
- if (url == null) {
- loadStartupTab(flags);
- return;
- }
-
- Tabs.getInstance().loadUrlWithIntentExtras(url, intent, flags);
- }
-
- public String getHomepage() {
- return null;
- }
-
- private String getIntentURI(SafeIntent intent) {
- final String passedUri;
- final String uri = getURIFromIntent(intent);
-
- if (!TextUtils.isEmpty(uri)) {
- passedUri = uri;
- } else {
- passedUri = null;
- }
- return passedUri;
- }
-
- private boolean invokedWithExternalURL(String uri) {
- return uri != null && !AboutPages.isAboutHome(uri);
- }
-
- private void initialize() {
- mInitialized = true;
-
- final boolean isFirstTab = !mWasFirstTabShownAfterActivityUnhidden;
- mWasFirstTabShownAfterActivityUnhidden = true; // Reset since we'll be loading a tab.
-
- final SafeIntent intent = new SafeIntent(getIntent());
- final String action = intent.getAction();
-
- final String passedUri = getIntentURI(intent);
-
- final boolean isExternalURL = invokedWithExternalURL(passedUri);
-
- // Start migrating as early as possible, can do this in
- // parallel with Gecko load.
- checkMigrateProfile();
-
- Tabs.registerOnTabsChangedListener(this);
-
- initializeChrome();
-
- // We need to wait here because mShouldRestore can revert back to
- // false if a parsing error occurs and the startup tab we load
- // depends on whether we restore tabs or not.
- synchronized (this) {
- while (!mSessionRestoreParsingFinished) {
- try {
- wait();
- } catch (final InterruptedException e) {
- // Ignore and wait again.
- }
- }
- }
-
- // External URLs should always be loaded regardless of whether Gecko is
- // already running.
- if (isExternalURL) {
- // Restore tabs before opening an external URL so that the new tab
- // is animated properly.
- Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED);
- processActionViewIntent(new Runnable() {
- @Override
- public void run() {
- int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_USER_ENTERED | Tabs.LOADURL_EXTERNAL;
- if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
- flags |= Tabs.LOADURL_PINNED;
- }
- if (isFirstTab) {
- flags |= Tabs.LOADURL_FIRST_AFTER_ACTIVITY_UNHIDDEN;
- }
- loadStartupTab(passedUri, intent, flags);
- }
- });
- } else {
- if (!mIsRestoringActivity) {
- loadStartupTab(Tabs.LOADURL_NEW_TAB);
- }
-
- Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED);
-
- processTabQueue();
- }
-
- recordStartupActionTelemetry(passedUri, action);
-
- // Check if launched from data reporting notification.
- if (ACTION_LAUNCH_SETTINGS.equals(action)) {
- Intent settingsIntent = new Intent(GeckoApp.this, GeckoPreferences.class);
- // Copy extras.
- settingsIntent.putExtras(intent.getUnsafe());
- startActivity(settingsIntent);
- }
-
- //app state callbacks
- mAppStateListeners = new LinkedList<GeckoAppShell.AppStateListener>();
-
- mPromptService = new PromptService(this);
-
- // Trigger the completion of the telemetry timer that wraps activity startup,
- // then grab the duration to give to FHR.
- mJavaUiStartupTimer.stop();
- final long javaDuration = mJavaUiStartupTimer.getElapsed();
-
- ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() {
- @Override
- public void run() {
- final HealthRecorder rec = mHealthRecorder;
- if (rec != null) {
- rec.recordJavaStartupTime(javaDuration);
- }
- }
- }, 50);
-
- final int updateServiceDelay = 30 * 1000;
- ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() {
- @Override
- public void run() {
- UpdateServiceHelper.registerForUpdates(GeckoAppShell.getApplicationContext());
- }
- }, updateServiceDelay);
-
- if (mIsRestoringActivity) {
- Tab selectedTab = Tabs.getInstance().getSelectedTab();
- if (selectedTab != null) {
- Tabs.getInstance().notifyListeners(selectedTab, Tabs.TabEvents.SELECTED);
- }
-
- if (GeckoThread.isRunning()) {
- geckoConnected();
- if (mLayerView != null) {
- mLayerView.setPaintState(LayerView.PAINT_BEFORE_FIRST);
- }
- }
- }
- }
-
- @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
- @Override
- public void onGlobalLayout() {
- if (Versions.preJB) {
- mMainLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
- } else {
- mMainLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
- }
- if (!mInitialized) {
- initialize();
- }
- }
-
- protected void processActionViewIntent(final Runnable openTabsRunnable) {
- // We need to ensure that if we receive a VIEW action and there are tabs queued then the
- // site loaded from the intent is on top (last loaded) and selected with all other tabs
- // being opened behind it. We process the tab queue first and request a callback from the JS - the
- // listener will open the url from the intent as normal when the tab queue has been processed.
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- if (TabQueueHelper.TAB_QUEUE_ENABLED && TabQueueHelper.shouldOpenTabQueueUrls(GeckoApp.this)) {
-
- getAppEventDispatcher().registerGeckoThreadListener(new NativeEventListener() {
- @Override
- public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
- if ("Tabs:TabsOpened".equals(event)) {
- getAppEventDispatcher().unregisterGeckoThreadListener(this, "Tabs:TabsOpened");
- openTabsRunnable.run();
- }
- }
- }, "Tabs:TabsOpened");
- TabQueueHelper.openQueuedUrls(GeckoApp.this, getProfile(), TabQueueHelper.FILE_NAME, true);
- } else {
- openTabsRunnable.run();
- }
- }
- });
- }
-
- @WorkerThread
- private String restoreSessionTabs(final boolean isExternalURL, boolean useBackup) throws SessionRestoreException {
- try {
- String sessionString = getProfile().readSessionFile(useBackup);
- if (sessionString == null) {
- throw new SessionRestoreException("Could not read from session file");
- }
-
- // If we are doing an OOM restore, parse the session data and
- // stub the restored tabs immediately. This allows the UI to be
- // updated before Gecko has restored.
- final JSONArray tabs = new JSONArray();
- final JSONObject windowObject = new JSONObject();
- final boolean sessionDataValid;
-
- LastSessionParser parser = new LastSessionParser(tabs, windowObject, isExternalURL);
-
- if (mPrivateBrowsingSession == null) {
- sessionDataValid = parser.parse(sessionString);
- } else {
- sessionDataValid = parser.parse(sessionString, mPrivateBrowsingSession);
- }
-
- if (tabs.length() > 0) {
- windowObject.put("tabs", tabs);
- sessionString = new JSONObject().put("windows", new JSONArray().put(windowObject)).toString();
- } else {
- if (parser.allTabsSkipped() || sessionDataValid) {
- // If we intentionally skipped all tabs we've read from the session file, we
- // set mShouldRestore back to false at this point already, so the calling code
- // can infer that the exception wasn't due to a damaged session store file.
- // The same applies if the session file was syntactically valid and
- // simply didn't contain any tabs.
- mShouldRestore = false;
- }
- throw new SessionRestoreException("No tabs could be read from session file");
- }
-
- JSONObject restoreData = new JSONObject();
- restoreData.put("sessionString", sessionString);
- return restoreData.toString();
- } catch (JSONException e) {
- throw new SessionRestoreException(e);
- }
- }
-
- public static EventDispatcher getEventDispatcher() {
- final GeckoApp geckoApp = (GeckoApp) GeckoAppShell.getGeckoInterface();
- return geckoApp.getAppEventDispatcher();
- }
-
- @Override
- public EventDispatcher getAppEventDispatcher() {
- return eventDispatcher;
- }
-
- @Override
- public GeckoProfile getProfile() {
- return GeckoThread.getActiveProfile();
- }
-
- /**
- * Check whether we've crashed during the last browsing session.
- *
- * @return True if the crash reporter ran after the last session.
- */
- protected boolean updateCrashedState() {
- try {
- File crashFlag = new File(GeckoProfileDirectories.getMozillaDirectory(this), "CRASHED");
- if (crashFlag.exists() && crashFlag.delete()) {
- // Set the flag that indicates we were stopped as expected, as
- // the crash reporter has run, so it is not a silent OOM crash.
- getSharedPreferences().edit().putBoolean(PREFS_WAS_STOPPED, true).apply();
- return true;
- }
- } catch (NoMozillaDirectoryException e) {
- // If we can't access the Mozilla directory, we're in trouble anyway.
- Log.e(LOGTAG, "Cannot read crash flag: ", e);
- }
- return false;
- }
-
- /**
- * Determine whether the session should be restored.
- *
- * @param savedInstanceState Saved instance state given to the activity
- * @return Whether to restore
- */
- protected boolean getSessionRestoreState(Bundle savedInstanceState) {
- final SharedPreferences prefs = getSharedPreferences();
- boolean shouldRestore = false;
-
- final int versionCode = getVersionCode();
- if (mLastSessionCrashed) {
- if (incrementCrashCount(prefs) <= getSessionStoreMaxCrashResumes(prefs) &&
- getSessionRestoreAfterCrashPreference(prefs)) {
- shouldRestore = true;
- } else {
- shouldRestore = false;
- }
- } else if (prefs.getInt(PREFS_VERSION_CODE, 0) != versionCode) {
- // If the version has changed, the user has done an upgrade, so restore
- // previous tabs.
- prefs.edit().putInt(PREFS_VERSION_CODE, versionCode).apply();
- shouldRestore = true;
- } else if (savedInstanceState != null ||
- getSessionRestorePreference(prefs).equals("always") ||
- getRestartFromIntent()) {
- // We're coming back from a background kill by the OS, the user
- // has chosen to always restore, or we restarted.
- shouldRestore = true;
- }
-
- return shouldRestore;
- }
-
- private int incrementCrashCount(SharedPreferences prefs) {
- final int crashCount = getSuccessiveCrashesCount(prefs) + 1;
- prefs.edit().putInt(PREFS_CRASHED_COUNT, crashCount).apply();
- return crashCount;
- }
-
- private int getSuccessiveCrashesCount(SharedPreferences prefs) {
- return prefs.getInt(PREFS_CRASHED_COUNT, 0);
- }
-
- private int getSessionStoreMaxCrashResumes(SharedPreferences prefs) {
- return prefs.getInt(GeckoPreferences.PREFS_RESTORE_SESSION_MAX_CRASH_RESUMES, 1);
- }
-
- private boolean getSessionRestoreAfterCrashPreference(SharedPreferences prefs) {
- return prefs.getBoolean(GeckoPreferences.PREFS_RESTORE_SESSION_FROM_CRASH, true);
- }
-
- private String getSessionRestorePreference(SharedPreferences prefs) {
- return prefs.getString(GeckoPreferences.PREFS_RESTORE_SESSION, "always");
- }
-
- private boolean getRestartFromIntent() {
- return IntentUtils.getBooleanExtraSafe(getIntent(), "didRestart", false);
- }
-
- /**
- * Enable Android StrictMode checks (for supported OS versions).
- * http://developer.android.com/reference/android/os/StrictMode.html
- */
- private void enableStrictMode() {
- Log.d(LOGTAG, "Enabling Android StrictMode");
-
- StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
- .detectAll()
- .penaltyLog()
- .build());
-
- StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
- .detectAll()
- .penaltyLog()
- .build());
- }
-
- @Override
- public void enableOrientationListener() {
- // Start listening for orientation events
- mCameraOrientationEventListener = new OrientationEventListener(this) {
- @Override
- public void onOrientationChanged(int orientation) {
- if (mAppStateListeners != null) {
- for (GeckoAppShell.AppStateListener listener: mAppStateListeners) {
- listener.onOrientationChanged();
- }
- }
- }
- };
- mCameraOrientationEventListener.enable();
- }
-
- @Override
- public void disableOrientationListener() {
- if (mCameraOrientationEventListener != null) {
- mCameraOrientationEventListener.disable();
- mCameraOrientationEventListener = null;
- }
- }
-
- @Override
- public String getDefaultUAString() {
- return HardwareUtils.isTablet() ? AppConstants.USER_AGENT_FENNEC_TABLET :
- AppConstants.USER_AGENT_FENNEC_MOBILE;
- }
-
- @Override
- public void createShortcut(final String title, final String url) {
- Icons.with(this)
- .pageUrl(url)
- .skipNetwork()
- .skipMemory()
- .forLauncherIcon()
- .build()
- .execute(new IconCallback() {
- @Override
- public void onIconResponse(IconResponse response) {
- doCreateShortcut(title, url, response.getBitmap());
- }
- });
- }
-
- private void doCreateShortcut(final String aTitle, final String aURI, final Bitmap aIcon) {
- // The intent to be launched by the shortcut.
- Intent shortcutIntent = new Intent();
- shortcutIntent.setAction(GeckoApp.ACTION_HOMESCREEN_SHORTCUT);
- shortcutIntent.setData(Uri.parse(aURI));
- shortcutIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME,
- AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
-
- Intent intent = new Intent();
- intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
- intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, getLauncherIcon(aIcon, GeckoAppShell.getPreferredIconSize()));
-
- if (aTitle != null) {
- intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, aTitle);
- } else {
- intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, aURI);
- }
-
- // Do not allow duplicate items.
- intent.putExtra("duplicate", false);
-
- intent.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
- getApplicationContext().sendBroadcast(intent);
-
- // Remember interaction
- final UrlAnnotations urlAnnotations = BrowserDB.from(getApplicationContext()).getUrlAnnotations();
- urlAnnotations.insertHomeScreenShortcut(getContentResolver(), aURI, true);
-
- // After shortcut is created, show the mobile desktop.
- ActivityUtils.goToHomeScreen(this);
- }
-
- private Bitmap getLauncherIcon(Bitmap aSource, int size) {
- final float[] DEFAULT_LAUNCHER_ICON_HSV = { 32.0f, 1.0f, 1.0f };
- final int kOffset = 6;
- final int kRadius = 5;
-
- int insetSize = aSource != null ? size * 2 / 3 : size;
-
- Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(bitmap);
-
- // draw a base color
- Paint paint = new Paint();
- if (aSource == null) {
- // If we aren't drawing a favicon, just use an orange color.
- paint.setColor(Color.HSVToColor(DEFAULT_LAUNCHER_ICON_HSV));
- canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint);
- } else if (aSource.getWidth() >= insetSize || aSource.getHeight() >= insetSize) {
- // Otherwise, if the icon is large enough, just draw it.
- Rect iconBounds = new Rect(0, 0, size, size);
- canvas.drawBitmap(aSource, null, iconBounds, null);
- return bitmap;
- } else {
- // otherwise use the dominant color from the icon + a layer of transparent white to lighten it somewhat
- int color = BitmapUtils.getDominantColor(aSource);
- paint.setColor(color);
- canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint);
- paint.setColor(Color.argb(100, 255, 255, 255));
- canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint);
- }
-
- // draw the overlay
- Bitmap overlay = BitmapUtils.decodeResource(this, R.drawable.home_bg);
- canvas.drawBitmap(overlay, null, new Rect(0, 0, size, size), null);
-
- // draw the favicon
- if (aSource == null)
- aSource = BitmapUtils.decodeResource(this, R.drawable.home_star);
-
- // by default, we scale the icon to this size
- int sWidth = insetSize / 2;
- int sHeight = sWidth;
-
- int halfSize = size / 2;
- canvas.drawBitmap(aSource,
- null,
- new Rect(halfSize - sWidth,
- halfSize - sHeight,
- halfSize + sWidth,
- halfSize + sHeight),
- null);
-
- return bitmap;
- }
-
- @Override
- protected void onNewIntent(Intent externalIntent) {
- final SafeIntent intent = new SafeIntent(externalIntent);
-
- final boolean isFirstTab = !mWasFirstTabShownAfterActivityUnhidden;
- mWasFirstTabShownAfterActivityUnhidden = true; // Reset since we'll be loading a tab.
-
- // if we were previously OOM killed, we can end up here when launching
- // from external shortcuts, so set this as the intent for initialization
- if (!mInitialized) {
- setIntent(externalIntent);
- return;
- }
-
- final String action = intent.getAction();
-
- final String uri = getURIFromIntent(intent);
- final String passedUri;
- if (!TextUtils.isEmpty(uri)) {
- passedUri = uri;
- } else {
- passedUri = null;
- }
-
- if (ACTION_LOAD.equals(action)) {
- Tabs.getInstance().loadUrl(intent.getDataString());
- lastSelectedTabId = -1;
- } else if (Intent.ACTION_VIEW.equals(action)) {
- processActionViewIntent(new Runnable() {
- @Override
- public void run() {
- final String url = intent.getDataString();
- int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_USER_ENTERED | Tabs.LOADURL_EXTERNAL;
- if (isFirstTab) {
- flags |= Tabs.LOADURL_FIRST_AFTER_ACTIVITY_UNHIDDEN;
- }
- Tabs.getInstance().loadUrlWithIntentExtras(url, intent, flags);
- }
- });
- lastSelectedTabId = -1;
- } else if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
- mLayerView.loadUri(uri, GeckoView.LOAD_SWITCH_TAB);
- } else if (Intent.ACTION_SEARCH.equals(action)) {
- mLayerView.loadUri(uri, GeckoView.LOAD_NEW_TAB);
- } else if (NotificationHelper.HELPER_BROADCAST_ACTION.equals(action)) {
- NotificationHelper.getInstance(getApplicationContext()).handleNotificationIntent(intent);
- } else if (ACTION_LAUNCH_SETTINGS.equals(action)) {
- // Check if launched from data reporting notification.
- Intent settingsIntent = new Intent(GeckoApp.this, GeckoPreferences.class);
- // Copy extras.
- settingsIntent.putExtras(intent.getUnsafe());
- startActivity(settingsIntent);
- } else if (ACTION_SWITCH_TAB.equals(action)) {
- final int tabId = intent.getIntExtra("TabId", -1);
- Tabs.getInstance().selectTab(tabId);
- lastSelectedTabId = -1;
- }
-
- recordStartupActionTelemetry(passedUri, action);
- }
-
- /**
- * Handles getting a URI from an intent in a way that is backwards-
- * compatible with our previous implementations.
- */
- protected String getURIFromIntent(SafeIntent intent) {
- final String action = intent.getAction();
- if (ACTION_ALERT_CALLBACK.equals(action) ||
- NotificationHelper.HELPER_BROADCAST_ACTION.equals(action)) {
- return null;
- }
-
- return intent.getDataString();
- }
-
- protected int getOrientation() {
- return GeckoScreenOrientation.getInstance().getAndroidOrientation();
- }
-
- @Override
- public void onResume()
- {
- // After an onPause, the activity is back in the foreground.
- // Undo whatever we did in onPause.
- super.onResume();
- if (mIsAbortingAppLaunch) {
- return;
- }
-
- GeckoAppShell.setGeckoInterface(this);
-
- if (lastSelectedTabId >= 0 && (lastActiveGeckoApp == null || lastActiveGeckoApp.get() != this)) {
- Tabs.getInstance().selectTab(lastSelectedTabId);
- }
-
- int newOrientation = getResources().getConfiguration().orientation;
- if (GeckoScreenOrientation.getInstance().update(newOrientation)) {
- refreshChrome();
- }
-
- if (mAppStateListeners != null) {
- for (GeckoAppShell.AppStateListener listener : mAppStateListeners) {
- listener.onResume();
- }
- }
-
- // We use two times: a pseudo-unique wall-clock time to identify the
- // current session across power cycles, and the elapsed realtime to
- // track the duration of the session.
- final long now = System.currentTimeMillis();
- final long realTime = android.os.SystemClock.elapsedRealtime();
-
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- // Now construct the new session on HealthRecorder's behalf. We do this here
- // so it can benefit from a single near-startup prefs commit.
- SessionInformation currentSession = new SessionInformation(now, realTime);
-
- SharedPreferences prefs = GeckoApp.this.getSharedPreferences();
- SharedPreferences.Editor editor = prefs.edit();
- editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false);
-
- if (!mLastSessionCrashed) {
- // The last session terminated normally,
- // so we can reset the count of successive crashes.
- editor.putInt(GeckoApp.PREFS_CRASHED_COUNT, 0);
- }
-
- currentSession.recordBegin(editor);
- editor.apply();
-
- final HealthRecorder rec = mHealthRecorder;
- if (rec != null) {
- rec.setCurrentSession(currentSession);
- rec.processDelayed();
- } else {
- Log.w(LOGTAG, "Can't record session: rec is null.");
- }
- }
- });
-
- Restrictions.update(this);
- }
-
- @Override
- public void onWindowFocusChanged(boolean hasFocus) {
- super.onWindowFocusChanged(hasFocus);
-
- if (!mWindowFocusInitialized && hasFocus) {
- mWindowFocusInitialized = true;
- // XXX our editor tests require the GeckoView to have focus to pass, so we have to
- // manually shift focus to the GeckoView. requestFocus apparently doesn't work at
- // this stage of starting up, so we have to unset and reset the focusability.
- mLayerView.setFocusable(false);
- mLayerView.setFocusable(true);
- mLayerView.setFocusableInTouchMode(true);
- getWindow().setBackgroundDrawable(null);
- }
- }
-
- @Override
- public void onPause()
- {
- if (mIsAbortingAppLaunch) {
- super.onPause();
- return;
- }
-
- final Tab selectedTab = Tabs.getInstance().getSelectedTab();
- if (selectedTab != null) {
- lastSelectedTabId = selectedTab.getId();
- }
- lastActiveGeckoApp = new WeakReference<GeckoApp>(this);
-
- final HealthRecorder rec = mHealthRecorder;
- final Context context = this;
-
- // In some way it's sad that Android will trigger StrictMode warnings
- // here as the whole point is to save to disk while the activity is not
- // interacting with the user.
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- SharedPreferences prefs = GeckoApp.this.getSharedPreferences();
- SharedPreferences.Editor editor = prefs.edit();
- editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, true);
- if (rec != null) {
- rec.recordSessionEnd("P", editor);
- }
-
- // onPause might in fact be called even after a crash, but in that case the
- // crash reporter will record this fact for us and we'll pick it up in onCreate.
- mLastSessionCrashed = false;
-
- // If we haven't done it before, cleanup any old files in our old temp dir
- if (prefs.getBoolean(GeckoApp.PREFS_CLEANUP_TEMP_FILES, true)) {
- File tempDir = GeckoLoader.getGREDir(GeckoApp.this);
- FileUtils.delTree(tempDir, new FileUtils.NameAndAgeFilter(null, ONE_DAY_MS), false);
-
- editor.putBoolean(GeckoApp.PREFS_CLEANUP_TEMP_FILES, false);
- }
-
- editor.apply();
- }
- });
-
- if (mAppStateListeners != null) {
- for (GeckoAppShell.AppStateListener listener : mAppStateListeners) {
- listener.onPause();
- }
- }
-
- super.onPause();
- }
-
- @Override
- public void onRestart() {
- if (mIsAbortingAppLaunch) {
- super.onRestart();
- return;
- }
-
- // Faster on main thread with an async apply().
- final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
- try {
- SharedPreferences.Editor editor = GeckoApp.this.getSharedPreferences().edit();
- editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false);
- editor.apply();
- } finally {
- StrictMode.setThreadPolicy(savedPolicy);
- }
-
- super.onRestart();
- }
-
- @Override
- public void onDestroy() {
- if (mIsAbortingAppLaunch) {
- // This build does not support the Android version of the device:
- // We did not initialize anything, so skip cleaning up.
- super.onDestroy();
- return;
- }
-
- getAppEventDispatcher().unregisterGeckoThreadListener((GeckoEventListener)this,
- "Gecko:Ready",
- "Gecko:Exited",
- "Accessibility:Event");
-
- getAppEventDispatcher().unregisterGeckoThreadListener((NativeEventListener)this,
- "Accessibility:Ready",
- "Bookmark:Insert",
- "Contact:Add",
- "DevToolsAuth:Scan",
- "DOMFullScreen:Start",
- "DOMFullScreen:Stop",
- "Image:SetAs",
- "Locale:Set",
- "Permissions:Data",
- "PrivateBrowsing:Data",
- "RuntimePermissions:Prompt",
- "Sanitize:Finished",
- "Session:StatePurged",
- "Share:Text",
- "Snackbar:Show",
- "SystemUI:Visibility",
- "ToggleChrome:Focus",
- "ToggleChrome:Hide",
- "ToggleChrome:Show",
- "Update:Check",
- "Update:Download",
- "Update:Install");
-
- if (mPromptService != null)
- mPromptService.destroy();
-
- final HealthRecorder rec = mHealthRecorder;
- mHealthRecorder = null;
- if (rec != null && rec.isEnabled()) {
- // Closing a HealthRecorder could incur a write.
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- rec.close(GeckoApp.this);
- }
- });
- }
-
- super.onDestroy();
-
- Tabs.unregisterOnTabsChangedListener(this);
- }
-
- public void showSDKVersionError() {
- final String message = getString(R.string.unsupported_sdk_version, Build.CPU_ABI, Build.VERSION.SDK_INT);
- Toast.makeText(this, message, Toast.LENGTH_LONG).show();
- }
-
- // Get a temporary directory, may return null
- public static File getTempDirectory() {
- File dir = GeckoApplication.get().getExternalFilesDir("temp");
- return dir;
- }
-
- // Delete any files in our temporary directory
- public static void deleteTempFiles() {
- File dir = getTempDirectory();
- if (dir == null)
- return;
- File[] files = dir.listFiles();
- if (files == null)
- return;
- for (File file : files) {
- file.delete();
- }
- }
-
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- Log.d(LOGTAG, "onConfigurationChanged: " + newConfig.locale);
-
- final LocaleManager localeManager = BrowserLocaleManager.getInstance();
- final Locale changed = localeManager.onSystemConfigurationChanged(this, getResources(), newConfig, mLastLocale);
- if (changed != null) {
- onLocaleChanged(Locales.getLanguageTag(changed));
- }
-
- // onConfigurationChanged is not called for 180 degree orientation changes,
- // we will miss such rotations and the screen orientation will not be
- // updated.
- if (GeckoScreenOrientation.getInstance().update(newConfig.orientation)) {
- if (mFormAssistPopup != null)
- mFormAssistPopup.hide();
- refreshChrome();
- }
- super.onConfigurationChanged(newConfig);
- }
-
- public String getContentProcessName() {
- return AppConstants.MOZ_CHILD_PROCESS_NAME;
- }
-
- public void addEnvToIntent(Intent intent) {
- Map<String, String> envMap = System.getenv();
- Set<Map.Entry<String, String>> envSet = envMap.entrySet();
- Iterator<Map.Entry<String, String>> envIter = envSet.iterator();
- int c = 0;
- while (envIter.hasNext()) {
- Map.Entry<String, String> entry = envIter.next();
- intent.putExtra("env" + c, entry.getKey() + "="
- + entry.getValue());
- c++;
- }
- }
-
- @Override
- public void doRestart() {
- doRestart(null, null);
- }
-
- public void doRestart(String args) {
- doRestart(args, null);
- }
-
- public void doRestart(Intent intent) {
- doRestart(null, intent);
- }
-
- public void doRestart(String args, Intent restartIntent) {
- if (restartIntent == null) {
- restartIntent = new Intent(Intent.ACTION_MAIN);
- }
-
- if (args != null) {
- restartIntent.putExtra("args", args);
- }
-
- mRestartIntent = restartIntent;
- Log.d(LOGTAG, "doRestart(\"" + restartIntent + "\")");
-
- doShutdown();
- }
-
- private void doShutdown() {
- // Shut down GeckoApp activity.
- runOnUiThread(new Runnable() {
- @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
- @Override public void run() {
- if (!isFinishing() && (Versions.preJBMR1 || !isDestroyed())) {
- finish();
- }
- }
- });
- }
-
- private void checkMigrateProfile() {
- final File profileDir = getProfile().getDir();
-
- if (profileDir != null) {
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- Handler handler = new Handler();
- handler.postDelayed(new DeferredCleanupTask(), CLEANUP_DEFERRAL_SECONDS * 1000);
- }
- });
- }
- }
-
- private static class DeferredCleanupTask implements Runnable {
- // The cleanup-version setting is recorded to avoid repeating the same
- // tasks on subsequent startups; CURRENT_CLEANUP_VERSION may be updated
- // if we need to do additional cleanup for future Gecko versions.
-
- private static final String CLEANUP_VERSION = "cleanup-version";
- private static final int CURRENT_CLEANUP_VERSION = 1;
-
- @Override
- public void run() {
- final Context context = GeckoAppShell.getApplicationContext();
- long cleanupVersion = GeckoSharedPrefs.forApp(context).getInt(CLEANUP_VERSION, 0);
-
- if (cleanupVersion < 1) {
- // Reduce device storage footprint by removing .ttf files from
- // the res/fonts directory: we no longer need to copy our
- // bundled fonts out of the APK in order to use them.
- // See https://bugzilla.mozilla.org/show_bug.cgi?id=878674.
- File dir = new File("res/fonts");
- if (dir.exists() && dir.isDirectory()) {
- for (File file : dir.listFiles()) {
- if (file.isFile() && file.getName().endsWith(".ttf")) {
- file.delete();
- }
- }
- if (!dir.delete()) {
- Log.w(LOGTAG, "unable to delete res/fonts directory (not empty?)");
- }
- }
- }
-
- // Additional cleanup needed for future versions would go here
-
- if (cleanupVersion != CURRENT_CLEANUP_VERSION) {
- SharedPreferences.Editor editor = GeckoSharedPrefs.forApp(context).edit();
- editor.putInt(CLEANUP_VERSION, CURRENT_CLEANUP_VERSION);
- editor.apply();
- }
- }
- }
-
- protected void onDone() {
- moveTaskToBack(true);
- }
-
- @Override
- public void onBackPressed() {
- if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
- super.onBackPressed();
- return;
- }
-
- if (autoHideTabs()) {
- return;
- }
-
- if (mDoorHangerPopup != null && mDoorHangerPopup.isShowing()) {
- mDoorHangerPopup.dismiss();
- return;
- }
-
- if (mFullScreenPluginView != null) {
- GeckoAppShell.onFullScreenPluginHidden(mFullScreenPluginView);
- removeFullScreenPluginView(mFullScreenPluginView);
- return;
- }
-
- if (mLayerView != null && mLayerView.isFullScreen()) {
- GeckoAppShell.notifyObservers("FullScreen:Exit", null);
- return;
- }
-
- final Tabs tabs = Tabs.getInstance();
- final Tab tab = tabs.getSelectedTab();
- if (tab == null) {
- onDone();
- return;
- }
-
- // Give Gecko a chance to handle the back press first, then fallback to the Java UI.
- GeckoAppShell.sendRequestToGecko(new GeckoRequest("Browser:OnBackPressed", null) {
- @Override
- public void onResponse(NativeJSObject nativeJSObject) {
- if (!nativeJSObject.getBoolean("handled")) {
- // Default behavior is Gecko didn't prevent.
- onDefault();
- }
- }
-
- @Override
- public void onError(NativeJSObject error) {
- // Default behavior is Gecko didn't prevent, via failure.
- onDefault();
- }
-
- // Return from Gecko thread, then back-press through the Java UI.
- private void onDefault() {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- if (tab.doBack()) {
- return;
- }
-
- if (tab.isExternal()) {
- onDone();
- Tab nextSelectedTab = Tabs.getInstance().getNextTab(tab);
- if (nextSelectedTab != null) {
- int nextSelectedTabId = nextSelectedTab.getId();
- GeckoAppShell.notifyObservers("Tab:KeepZombified", Integer.toString(nextSelectedTabId));
- }
- tabs.closeTab(tab);
- return;
- }
-
- final int parentId = tab.getParentId();
- final Tab parent = tabs.getTab(parentId);
- if (parent != null) {
- // The back button should always return to the parent (not a sibling).
- tabs.closeTab(tab, parent);
- return;
- }
-
- onDone();
- }
- });
- }
- });
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (!ActivityHandlerHelper.handleActivityResult(requestCode, resultCode, data)) {
- super.onActivityResult(requestCode, resultCode, data);
- }
- }
-
- @Override
- public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
- Permissions.onRequestPermissionsResult(this, permissions, grantResults);
- }
-
- @Override
- public AbsoluteLayout getPluginContainer() { return mPluginContainer; }
-
- private static final String CPU = "cpu";
- private static final String SCREEN = "screen";
-
- // Called when a Gecko Hal WakeLock is changed
- @Override
- // We keep the wake lock independent from the function scope, so we need to
- // suppress the linter warning.
- @SuppressLint("Wakelock")
- public void notifyWakeLockChanged(String topic, String state) {
- PowerManager.WakeLock wl = mWakeLocks.get(topic);
- if (state.equals("locked-foreground") && wl == null) {
- PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
-
- if (CPU.equals(topic)) {
- wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, topic);
- } else if (SCREEN.equals(topic)) {
- // ON_AFTER_RELEASE is set, the user activity timer will be reset when the
- // WakeLock is released, causing the illumination to remain on a bit longer.
- wl = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, topic);
- }
-
- if (wl != null) {
- wl.acquire();
- mWakeLocks.put(topic, wl);
- }
- } else if (!state.equals("locked-foreground") && wl != null) {
- wl.release();
- mWakeLocks.remove(topic);
- }
- }
-
- @Override
- public void notifyCheckUpdateResult(String result) {
- GeckoAppShell.notifyObservers("Update:CheckResult", result);
- }
-
- private void geckoConnected() {
- mLayerView.setOverScrollMode(View.OVER_SCROLL_NEVER);
- }
-
- @Override
- public void setAccessibilityEnabled(boolean enabled) {
- }
-
- @Override
- public boolean openUriExternal(String targetURI, String mimeType, String packageName, String className, String action, String title) {
- // Default to showing prompt in private browsing to be safe.
- return IntentHelper.openUriExternal(targetURI, mimeType, packageName, className, action, title, true);
- }
-
- public static class MainLayout extends RelativeLayout {
- private TouchEventInterceptor mTouchEventInterceptor;
- private MotionEventInterceptor mMotionEventInterceptor;
-
- public MainLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- }
-
- public void setTouchEventInterceptor(TouchEventInterceptor interceptor) {
- mTouchEventInterceptor = interceptor;
- }
-
- public void setMotionEventInterceptor(MotionEventInterceptor interceptor) {
- mMotionEventInterceptor = interceptor;
- }
-
- @Override
- public boolean onInterceptTouchEvent(MotionEvent event) {
- if (mTouchEventInterceptor != null && mTouchEventInterceptor.onInterceptTouchEvent(this, event)) {
- return true;
- }
- return super.onInterceptTouchEvent(event);
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- if (mTouchEventInterceptor != null && mTouchEventInterceptor.onTouch(this, event)) {
- return true;
- }
- return super.onTouchEvent(event);
- }
-
- @Override
- public boolean onGenericMotionEvent(MotionEvent event) {
- if (mMotionEventInterceptor != null && mMotionEventInterceptor.onInterceptMotionEvent(this, event)) {
- return true;
- }
- return super.onGenericMotionEvent(event);
- }
-
- @Override
- public void setDrawingCacheEnabled(boolean enabled) {
- // Instead of setting drawing cache in the view itself, we simply
- // enable drawing caching on its children. This is mainly used in
- // animations (see PropertyAnimator)
- super.setChildrenDrawnWithCacheEnabled(enabled);
- }
- }
-
- private class FullScreenHolder extends FrameLayout {
-
- public FullScreenHolder(Context ctx) {
- super(ctx);
- setBackgroundColor(0xff000000);
- }
-
- @Override
- public void addView(View view, int index) {
- /**
- * This normally gets called when Flash adds a separate SurfaceView
- * for the video. It is unhappy if we have the LayerView underneath
- * it for some reason so we need to hide that. Hiding the LayerView causes
- * its surface to be destroyed, which causes a pause composition
- * event to be sent to Gecko. We synchronously wait for that to be
- * processed. Simultaneously, however, Flash is waiting on a mutex so
- * the post() below is an attempt to avoid a deadlock.
- */
- super.addView(view, index);
-
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- mLayerView.hideSurface();
- }
- });
- }
-
- /**
- * The methods below are simply copied from what Android WebKit does.
- * It wasn't ever called in my testing, but might as well
- * keep it in case it is for some reason. The methods
- * all return true because we don't want any events
- * leaking out from the fullscreen view.
- */
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- if (event.isSystem()) {
- return super.onKeyDown(keyCode, event);
- }
- mFullScreenPluginView.onKeyDown(keyCode, event);
- return true;
- }
-
- @Override
- public boolean onKeyUp(int keyCode, KeyEvent event) {
- if (event.isSystem()) {
- return super.onKeyUp(keyCode, event);
- }
- mFullScreenPluginView.onKeyUp(keyCode, event);
- return true;
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- return true;
- }
-
- @Override
- public boolean onTrackballEvent(MotionEvent event) {
- mFullScreenPluginView.onTrackballEvent(event);
- return true;
- }
- }
-
- private int getVersionCode() {
- int versionCode = 0;
- try {
- versionCode = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode;
- } catch (NameNotFoundException e) {
- Log.wtf(LOGTAG, getPackageName() + " not found", e);
- }
- return versionCode;
- }
-
- // FHR reason code for a session end prior to a restart for a
- // locale change.
- private static final String SESSION_END_LOCALE_CHANGED = "L";
-
- /**
- * This exists so that a locale can be applied in two places: when saved
- * in a nested activity, and then again when we get back up to GeckoApp.
- *
- * GeckoApp needs to do a bunch more stuff than, say, GeckoPreferences.
- */
- protected void onLocaleChanged(final String locale) {
- final boolean startNewSession = true;
- final boolean shouldRestart = false;
-
- // If the HealthRecorder is not yet initialized (unlikely), the locale change won't
- // trigger a session transition and subsequent events will be recorded in an environment
- // with the wrong locale.
- final HealthRecorder rec = mHealthRecorder;
- if (rec != null) {
- rec.onAppLocaleChanged(locale);
- rec.onEnvironmentChanged(startNewSession, SESSION_END_LOCALE_CHANGED);
- }
-
- if (!shouldRestart) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- GeckoApp.this.onLocaleReady(locale);
- }
- });
- return;
- }
-
- // Do this in the background so that the health recorder has its
- // time to finish.
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- GeckoApp.this.doRestart();
- }
- });
- }
-
- /**
- * Use BrowserLocaleManager to change our persisted and current locales,
- * and poke the system to tell it of our changed state.
- */
- protected void setLocale(final String locale) {
- if (locale == null) {
- return;
- }
-
- final String resultant = BrowserLocaleManager.getInstance().setSelectedLocale(this, locale);
- if (resultant == null) {
- return;
- }
-
- onLocaleChanged(resultant);
- }
-
- private void setSystemUiVisible(final boolean visible) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- if (visible) {
- mMainLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
- } else {
- mMainLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
- }
- }
- });
- }
-
- protected HealthRecorder createHealthRecorder(final Context context,
- final String profilePath,
- final EventDispatcher dispatcher,
- final String osLocale,
- final String appLocale,
- final SessionInformation previousSession) {
- // GeckoApp does not need to record any health information - return a stub.
- return new StubbedHealthRecorder();
- }
-
- protected void recordStartupActionTelemetry(final String passedURL, final String action) {
- }
-
- @Override
- public void checkUriVisited(String uri) {
- GlobalHistory.getInstance().checkUriVisited(uri);
- }
-
- @Override
- public void markUriVisited(final String uri) {
- final Context context = getApplicationContext();
- final BrowserDB db = BrowserDB.from(context);
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- GlobalHistory.getInstance().add(context, db, uri);
- }
- });
- }
-
- @Override
- public void setUriTitle(final String uri, final String title) {
- final Context context = getApplicationContext();
- final BrowserDB db = BrowserDB.from(context);
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- GlobalHistory.getInstance().update(context.getContentResolver(), db, uri, title);
- }
- });
- }
-
- @Override
- public String[] getHandlersForMimeType(String mimeType, String action) {
- Intent intent = IntentHelper.getIntentForActionString(action);
- if (mimeType != null && mimeType.length() > 0)
- intent.setType(mimeType);
- return IntentHelper.getHandlersForIntent(intent);
- }
-
- @Override
- public String[] getHandlersForURL(String url, String action) {
- // May contain the whole URL or just the protocol.
- Uri uri = url.indexOf(':') >= 0 ? Uri.parse(url) : new Uri.Builder().scheme(url).build();
-
- Intent intent = IntentHelper.getOpenURIIntent(getApplicationContext(), uri.toString(), "",
- TextUtils.isEmpty(action) ? Intent.ACTION_VIEW : action, "");
-
- return IntentHelper.getHandlersForIntent(intent);
- }
-
- @Override
- public String getDefaultChromeURI() {
- // Use the chrome URI specified by Gecko's defaultChromeURI pref.
- return null;
- }
-
- public GeckoView getGeckoView() {
- return mLayerView;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java b/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
deleted file mode 100644
index 18a6e6535..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
+++ /dev/null
@@ -1,314 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.app.Application;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.res.Configuration;
-import android.os.Bundle;
-import android.os.SystemClock;
-import android.util.Log;
-
-import com.squareup.leakcanary.LeakCanary;
-import com.squareup.leakcanary.RefWatcher;
-
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.db.LocalBrowserDB;
-import org.mozilla.gecko.distribution.Distribution;
-import org.mozilla.gecko.dlc.DownloadContentService;
-import org.mozilla.gecko.home.HomePanelsManager;
-import org.mozilla.gecko.lwt.LightweightTheme;
-import org.mozilla.gecko.mdns.MulticastDNSManager;
-import org.mozilla.gecko.media.AudioFocusAgent;
-import org.mozilla.gecko.notifications.NotificationClient;
-import org.mozilla.gecko.notifications.NotificationHelper;
-import org.mozilla.gecko.preferences.DistroSharedPrefsImport;
-import org.mozilla.gecko.util.BundleEventListener;
-import org.mozilla.gecko.util.Clipboard;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.HardwareUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import java.io.File;
-import java.lang.reflect.Method;
-
-public class GeckoApplication extends Application
- implements ContextGetter {
- private static final String LOG_TAG = "GeckoApplication";
-
- private static volatile GeckoApplication instance;
-
- private boolean mInBackground;
- private boolean mPausedGecko;
-
- private LightweightTheme mLightweightTheme;
-
- private RefWatcher mRefWatcher;
-
- public GeckoApplication() {
- super();
- instance = this;
- }
-
- public static GeckoApplication get() {
- return instance;
- }
-
- public static RefWatcher getRefWatcher(Context context) {
- GeckoApplication app = (GeckoApplication) context.getApplicationContext();
- return app.mRefWatcher;
- }
-
- public static void watchReference(Context context, Object object) {
- if (context == null) {
- return;
- }
-
- getRefWatcher(context).watch(object);
- }
-
- @Override
- public Context getContext() {
- return this;
- }
-
- @Override
- public SharedPreferences getSharedPreferences() {
- return GeckoSharedPrefs.forApp(this);
- }
-
- /**
- * We need to do locale work here, because we need to intercept
- * each hit to onConfigurationChanged.
- */
- @Override
- public void onConfigurationChanged(Configuration config) {
- Log.d(LOG_TAG, "onConfigurationChanged: " + config.locale +
- ", background: " + mInBackground);
-
- // Do nothing if we're in the background. It'll simply cause a loop
- // (Bug 936756 Comment 11), and it's not necessary.
- if (mInBackground) {
- super.onConfigurationChanged(config);
- return;
- }
-
- // Otherwise, correct the locale. This catches some cases that GeckoApp
- // doesn't get a chance to.
- try {
- BrowserLocaleManager.getInstance().correctLocale(this, getResources(), config);
- } catch (IllegalStateException ex) {
- // GeckoApp hasn't started, so we have no ContextGetter in BrowserLocaleManager.
- Log.w(LOG_TAG, "Couldn't correct locale.", ex);
- }
-
- super.onConfigurationChanged(config);
- }
-
- public void onActivityPause(GeckoActivityStatus activity) {
- mInBackground = true;
-
- if ((activity.isFinishing() == false) &&
- (activity.isGeckoActivityOpened() == false)) {
- // Notify Gecko that we are pausing; the cache service will be
- // shutdown, closing the disk cache cleanly. If the android
- // low memory killer subsequently kills us, the disk cache will
- // be left in a consistent state, avoiding costly cleanup and
- // re-creation.
- GeckoThread.onPause();
- mPausedGecko = true;
-
- final BrowserDB db = BrowserDB.from(this);
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- db.expireHistory(getContentResolver(), BrowserContract.ExpirePriority.NORMAL);
- }
- });
- }
- GeckoNetworkManager.getInstance().stop();
- }
-
- public void onActivityResume(GeckoActivityStatus activity) {
- if (mPausedGecko) {
- GeckoThread.onResume();
- mPausedGecko = false;
- }
-
- GeckoBatteryManager.getInstance().start(this);
- GeckoNetworkManager.getInstance().start(this);
-
- mInBackground = false;
- }
-
- @Override
- protected void attachBaseContext(Context base) {
- super.attachBaseContext(base);
- AppConstants.maybeInstallMultiDex(base);
- }
-
- @Override
- public void onCreate() {
- Log.i(LOG_TAG, "zerdatime " + SystemClock.uptimeMillis() + " - Fennec application start");
-
- mRefWatcher = LeakCanary.install(this);
-
- final Context context = getApplicationContext();
- GeckoAppShell.setApplicationContext(context);
- HardwareUtils.init(context);
- Clipboard.init(context);
- FilePicker.init(context);
- DownloadsIntegration.init();
- HomePanelsManager.getInstance().init(context);
-
- GlobalPageMetadata.getInstance().init();
-
- // We need to set the notification client before launching Gecko, since Gecko could start
- // sending notifications immediately after startup, which we don't want to lose/crash on.
- GeckoAppShell.setNotificationListener(new NotificationClient(context));
- // This getInstance call will force initialization of the NotificationHelper, but does nothing with the result
- NotificationHelper.getInstance(context).init();
-
- MulticastDNSManager.getInstance(context).init();
-
- GeckoService.register();
-
- EventDispatcher.getInstance().registerBackgroundThreadListener(new EventListener(),
- "Profile:Create");
-
- super.onCreate();
- }
-
- public void onDelayedStartup() {
- if (AppConstants.MOZ_ANDROID_GCM) {
- // TODO: only run in main process.
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- // It's fine to throw GCM initialization onto a background thread; the registration process requires
- // network access, so is naturally asynchronous. This, of course, races against Gecko page load of
- // content requiring GCM-backed services, like Web Push. There's nothing to be done here.
- try {
- final Class<?> clazz = Class.forName("org.mozilla.gecko.push.PushService");
- final Method onCreate = clazz.getMethod("onCreate", Context.class);
- onCreate.invoke(null, getApplicationContext()); // Method is static.
- } catch (Exception e) {
- Log.e(LOG_TAG, "Got exception during startup; ignoring.", e);
- return;
- }
- }
- });
- }
-
- if (AppConstants.MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE) {
- DownloadContentService.startStudy(this);
- }
-
- GeckoAccessibility.setAccessibilityManagerListeners(this);
-
- AudioFocusAgent.getInstance().attachToContext(this);
- }
-
- private class EventListener implements BundleEventListener
- {
- private void onProfileCreate(final String name, final String path) {
- // Add everything when we're done loading the distribution.
- final Context context = GeckoApplication.this;
- final GeckoProfile profile = GeckoProfile.get(context, name);
- final Distribution distribution = Distribution.getInstance(context);
-
- distribution.addOnDistributionReadyCallback(new Distribution.ReadyCallback() {
- @Override
- public void distributionNotFound() {
- this.distributionFound(null);
- }
-
- @Override
- public void distributionFound(final Distribution distribution) {
- Log.d(LOG_TAG, "Running post-distribution task: bookmarks.");
- // Because we are running in the background, we want to synchronize on the
- // GeckoProfile instance so that we don't race with main thread operations
- // such as locking/unlocking/removing the profile.
- synchronized (profile.getLock()) {
- distributionFoundLocked(distribution);
- }
- }
-
- @Override
- public void distributionArrivedLate(final Distribution distribution) {
- Log.d(LOG_TAG, "Running late distribution task: bookmarks.");
- // Recover as best we can.
- synchronized (profile.getLock()) {
- distributionArrivedLateLocked(distribution);
- }
- }
-
- private void distributionFoundLocked(final Distribution distribution) {
- // Skip initialization if the profile directory has been removed.
- if (!(new File(path)).exists()) {
- return;
- }
-
- final ContentResolver cr = context.getContentResolver();
- final LocalBrowserDB db = new LocalBrowserDB(profile.getName());
-
- // We pass the number of added bookmarks to ensure that the
- // indices of the distribution and default bookmarks are
- // contiguous. Because there are always at least as many
- // bookmarks as there are favicons, we can also guarantee that
- // the favicon IDs won't overlap.
- final int offset = distribution == null ? 0 :
- db.addDistributionBookmarks(cr, distribution, 0);
- db.addDefaultBookmarks(context, cr, offset);
-
- Log.d(LOG_TAG, "Running post-distribution task: android preferences.");
- DistroSharedPrefsImport.importPreferences(context, distribution);
- }
-
- private void distributionArrivedLateLocked(final Distribution distribution) {
- // Skip initialization if the profile directory has been removed.
- if (!(new File(path)).exists()) {
- return;
- }
-
- final ContentResolver cr = context.getContentResolver();
- final LocalBrowserDB db = new LocalBrowserDB(profile.getName());
-
- // We assume we've been called very soon after startup, and so our offset
- // into "Mobile Bookmarks" is the number of bookmarks in the DB.
- final int offset = db.getCount(cr, "bookmarks");
- db.addDistributionBookmarks(cr, distribution, offset);
-
- Log.d(LOG_TAG, "Running late distribution task: android preferences.");
- DistroSharedPrefsImport.importPreferences(context, distribution);
- }
- });
- }
-
- @Override // BundleEventListener
- public void handleMessage(final String event, final Bundle message,
- final EventCallback callback) {
- if ("Profile:Create".equals(event)) {
- onProfileCreate(message.getCharSequence("name").toString(),
- message.getCharSequence("path").toString());
- }
- }
- }
-
- public boolean isApplicationInBackground() {
- return mInBackground;
- }
-
- public LightweightTheme getLightweightTheme() {
- return mLightweightTheme;
- }
-
- public void prepareLightweightTheme() {
- mLightweightTheme = new LightweightTheme(this);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoJavaSampler.java b/mobile/android/base/java/org/mozilla/gecko/GeckoJavaSampler.java
deleted file mode 100644
index 319eccec1..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoJavaSampler.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.os.SystemClock;
-import android.util.Log;
-import android.util.SparseArray;
-
-import org.mozilla.gecko.annotation.WrapForJNI;
-
-import java.lang.Thread;
-import java.util.Set;
-
-public class GeckoJavaSampler {
- private static final String LOGTAG = "JavaSampler";
- private static Thread sSamplingThread;
- private static SamplingThread sSamplingRunnable;
- private static Thread sMainThread;
-
- // Use the same timer primitive as the profiler
- // to get a perfect sample syncing.
- @WrapForJNI
- private static native double getProfilerTime();
-
- private static class Sample {
- public Frame[] mFrames;
- public double mTime;
- public long mJavaTime; // non-zero if Android system time is used
- public Sample(StackTraceElement[] aStack) {
- mFrames = new Frame[aStack.length];
- if (GeckoThread.isStateAtLeast(GeckoThread.State.LIBS_READY)) {
- mTime = getProfilerTime();
- }
- if (mTime == 0.0d) {
- // getProfilerTime is not available yet; either libs are not loaded,
- // or profiling hasn't started on the Gecko side yet
- mJavaTime = SystemClock.elapsedRealtime();
- }
- for (int i = 0; i < aStack.length; i++) {
- mFrames[aStack.length - 1 - i] = new Frame();
- mFrames[aStack.length - 1 - i].fileName = aStack[i].getFileName();
- mFrames[aStack.length - 1 - i].lineNo = aStack[i].getLineNumber();
- mFrames[aStack.length - 1 - i].methodName = aStack[i].getMethodName();
- mFrames[aStack.length - 1 - i].className = aStack[i].getClassName();
- }
- }
- }
- private static class Frame {
- public String fileName;
- public int lineNo;
- public String methodName;
- public String className;
- }
-
- private static class SamplingThread implements Runnable {
- private final int mInterval;
- private final int mSampleCount;
-
- private boolean mPauseSampler;
- private boolean mStopSampler;
-
- private final SparseArray<Sample[]> mSamples = new SparseArray<Sample[]>();
- private int mSamplePos;
-
- public SamplingThread(final int aInterval, final int aSampleCount) {
- // If we sample faster then 10ms we get to many missed samples
- mInterval = Math.max(10, aInterval);
- mSampleCount = aSampleCount;
- }
-
- @Override
- public void run() {
- synchronized (GeckoJavaSampler.class) {
- mSamples.put(0, new Sample[mSampleCount]);
- mSamplePos = 0;
-
- // Find the main thread
- Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
- for (Thread t : threadSet) {
- if (t.getName().compareToIgnoreCase("main") == 0) {
- sMainThread = t;
- break;
- }
- }
-
- if (sMainThread == null) {
- Log.e(LOGTAG, "Main thread not found");
- return;
- }
- }
-
- while (true) {
- try {
- Thread.sleep(mInterval);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- synchronized (GeckoJavaSampler.class) {
- if (!mPauseSampler) {
- StackTraceElement[] bt = sMainThread.getStackTrace();
- mSamples.get(0)[mSamplePos] = new Sample(bt);
- mSamplePos = (mSamplePos + 1) % mSamples.get(0).length;
- }
- if (mStopSampler) {
- break;
- }
- }
- }
- }
-
- private Sample getSample(int aThreadId, int aSampleId) {
- if (aThreadId < mSamples.size() && aSampleId < mSamples.get(aThreadId).length &&
- mSamples.get(aThreadId)[aSampleId] != null) {
- int startPos = 0;
- if (mSamples.get(aThreadId)[mSamplePos] != null) {
- startPos = mSamplePos;
- }
- int readPos = (startPos + aSampleId) % mSamples.get(aThreadId).length;
- return mSamples.get(aThreadId)[readPos];
- }
- return null;
- }
- }
-
-
- @WrapForJNI
- public synchronized static String getThreadName(int aThreadId) {
- if (aThreadId == 0 && sMainThread != null) {
- return sMainThread.getName();
- }
- return null;
- }
-
- private synchronized static Sample getSample(int aThreadId, int aSampleId) {
- return sSamplingRunnable.getSample(aThreadId, aSampleId);
- }
-
- @WrapForJNI
- public synchronized static double getSampleTime(int aThreadId, int aSampleId) {
- Sample sample = getSample(aThreadId, aSampleId);
- if (sample != null) {
- if (sample.mJavaTime != 0) {
- return (sample.mJavaTime -
- SystemClock.elapsedRealtime()) + getProfilerTime();
- }
- System.out.println("Sample: " + sample.mTime);
- return sample.mTime;
- }
- return 0;
- }
-
- @WrapForJNI
- public synchronized static String getFrameName(int aThreadId, int aSampleId, int aFrameId) {
- Sample sample = getSample(aThreadId, aSampleId);
- if (sample != null && aFrameId < sample.mFrames.length) {
- Frame frame = sample.mFrames[aFrameId];
- if (frame == null) {
- return null;
- }
- return frame.className + "." + frame.methodName + "()";
- }
- return null;
- }
-
- @WrapForJNI
- public static void start(int aInterval, int aSamples) {
- synchronized (GeckoJavaSampler.class) {
- if (sSamplingRunnable != null) {
- return;
- }
- sSamplingRunnable = new SamplingThread(aInterval, aSamples);
- sSamplingThread = new Thread(sSamplingRunnable, "Java Sampler");
- sSamplingThread.start();
- }
- }
-
- @WrapForJNI
- public static void pause() {
- synchronized (GeckoJavaSampler.class) {
- sSamplingRunnable.mPauseSampler = true;
- }
- }
-
- @WrapForJNI
- public static void unpause() {
- synchronized (GeckoJavaSampler.class) {
- sSamplingRunnable.mPauseSampler = false;
- }
- }
-
- @WrapForJNI
- public static void stop() {
- synchronized (GeckoJavaSampler.class) {
- if (sSamplingThread == null) {
- return;
- }
-
- sSamplingRunnable.mStopSampler = true;
- try {
- sSamplingThread.join();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- sSamplingThread = null;
- sSamplingRunnable = null;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoMediaPlayer.java b/mobile/android/base/java/org/mozilla/gecko/GeckoMediaPlayer.java
deleted file mode 100644
index c199aad55..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoMediaPlayer.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.json.JSONObject;
-import org.mozilla.gecko.util.EventCallback;
-
-/**
- * Wrapper for MediaRouter types supported by Android, such as Chromecast, Miracast, etc.
- */
-interface GeckoMediaPlayer {
- /**
- * Can return null.
- */
- JSONObject toJSON();
- void load(String title, String url, String type, EventCallback callback);
- void play(EventCallback callback);
- void pause(EventCallback callback);
- void stop(EventCallback callback);
- void start(EventCallback callback);
- void end(EventCallback callback);
- void mirror(EventCallback callback);
- void message(String message, EventCallback callback);
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoMessageReceiver.java b/mobile/android/base/java/org/mozilla/gecko/GeckoMessageReceiver.java
deleted file mode 100644
index b7f4870c2..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoMessageReceiver.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-
-public class GeckoMessageReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- final String action = intent.getAction();
- if (GeckoApp.ACTION_INIT_PW.equals(action)) {
- GeckoAppShell.notifyObservers("Passwords:Init", null);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoPresentationDisplay.java b/mobile/android/base/java/org/mozilla/gecko/GeckoPresentationDisplay.java
deleted file mode 100644
index df9844d7b..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoPresentationDisplay.java
+++ /dev/null
@@ -1,22 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.json.JSONObject;
-import org.mozilla.gecko.util.EventCallback;
-
-/**
- * Wrapper for MediaRouter types supported by Android to use for
- * Presentation API, such as Chromecast, Miracast, etc.
- */
-interface GeckoPresentationDisplay {
- /**
- * Can return null.
- */
- JSONObject toJSON();
- void start(EventCallback callback);
- void stop(EventCallback callback);
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoProfilesProvider.java b/mobile/android/base/java/org/mozilla/gecko/GeckoProfilesProvider.java
deleted file mode 100644
index 8a9c461c5..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoProfilesProvider.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import java.io.File;
-import java.util.Map;
-import java.util.Map.Entry;
-
-import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
-import org.mozilla.gecko.db.BrowserContract;
-
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.content.UriMatcher;
-import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.net.Uri;
-import android.util.Log;
-
-/**
- * This is not a per-profile provider. This provider allows read-only,
- * restricted access to certain attributes of Fennec profiles.
- */
-public class GeckoProfilesProvider extends ContentProvider {
- private static final String LOG_TAG = "GeckoProfilesProvider";
-
- private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
-
- private static final int PROFILES = 100;
- private static final int PROFILES_NAME = 101;
- private static final int PROFILES_DEFAULT = 200;
-
- private static final String[] DEFAULT_ARGS = {
- BrowserContract.Profiles.NAME,
- BrowserContract.Profiles.PATH,
- };
-
- static {
- URI_MATCHER.addURI(BrowserContract.PROFILES_AUTHORITY, "profiles", PROFILES);
- URI_MATCHER.addURI(BrowserContract.PROFILES_AUTHORITY, "profiles/*", PROFILES_NAME);
- URI_MATCHER.addURI(BrowserContract.PROFILES_AUTHORITY, "default", PROFILES_DEFAULT);
- }
-
- @Override
- public String getType(Uri uri) {
- return null;
- }
-
- @Override
- public boolean onCreate() {
- // Successfully loaded.
- return true;
- }
-
- private String[] profileValues(final String name, final String path, int len, int nameIndex, int pathIndex) {
- final String[] values = new String[len];
- if (nameIndex >= 0) {
- values[nameIndex] = name;
- }
- if (pathIndex >= 0) {
- values[pathIndex] = path;
- }
- return values;
- }
-
- protected void addRowForProfile(final MatrixCursor cursor, final int len, final int nameIndex, final int pathIndex, final String name, final String path) {
- if (path == null || name == null) {
- return;
- }
-
- cursor.addRow(profileValues(name, path, len, nameIndex, pathIndex));
- }
-
- protected Cursor getCursorForProfiles(final String[] args, Map<String, String> profiles) {
- // Compute the projection.
- int nameIndex = -1;
- int pathIndex = -1;
- for (int i = 0; i < args.length; ++i) {
- if (BrowserContract.Profiles.NAME.equals(args[i])) {
- nameIndex = i;
- } else if (BrowserContract.Profiles.PATH.equals(args[i])) {
- pathIndex = i;
- }
- }
-
- final MatrixCursor cursor = new MatrixCursor(args);
- for (Entry<String, String> entry : profiles.entrySet()) {
- addRowForProfile(cursor, args.length, nameIndex, pathIndex, entry.getKey(), entry.getValue());
- }
- return cursor;
- }
-
- @Override
- public Cursor query(Uri uri, String[] projection, String selection,
- String[] selectionArgs, String sortOrder) {
-
- final String[] args = (projection == null) ? DEFAULT_ARGS : projection;
-
- final File mozillaDir;
- try {
- mozillaDir = GeckoProfileDirectories.getMozillaDirectory(getContext());
- } catch (NoMozillaDirectoryException e) {
- Log.d(LOG_TAG, "No Mozilla directory; cannot query for profiles. Assuming there are none.");
- return new MatrixCursor(projection);
- }
-
- final Map<String, String> matchingProfiles;
-
- final int match = URI_MATCHER.match(uri);
- switch (match) {
- case PROFILES:
- // Return all profiles.
- matchingProfiles = GeckoProfileDirectories.getAllProfiles(mozillaDir);
- break;
- case PROFILES_NAME:
- // Return data about the specified profile.
- final String name = uri.getLastPathSegment();
- matchingProfiles = GeckoProfileDirectories.getProfilesNamed(mozillaDir,
- name);
- break;
- case PROFILES_DEFAULT:
- matchingProfiles = GeckoProfileDirectories.getDefaultProfile(mozillaDir);
- break;
- default:
- throw new UnsupportedOperationException("Unknown query URI " + uri);
- }
-
- return getCursorForProfiles(args, matchingProfiles);
- }
-
- @Override
- public Uri insert(Uri uri, ContentValues values) {
- throw new IllegalStateException("Inserts not supported.");
- }
-
- @Override
- public int delete(Uri uri, String selection, String[] selectionArgs) {
- throw new IllegalStateException("Deletes not supported.");
- }
-
- @Override
- public int update(Uri uri, ContentValues values, String selection,
- String[] selectionArgs) {
- throw new IllegalStateException("Updates not supported.");
- }
-
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoService.java b/mobile/android/base/java/org/mozilla/gecko/GeckoService.java
deleted file mode 100644
index 3a99fd2a1..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoService.java
+++ /dev/null
@@ -1,236 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.app.AlarmManager;
-import android.app.Service;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.os.IBinder;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.Log;
-
-import java.io.File;
-
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.EventCallback;
-
-public class GeckoService extends Service {
-
- private static final String LOGTAG = "GeckoService";
- private static final boolean DEBUG = false;
-
- private static final String INTENT_PROFILE_NAME = "org.mozilla.gecko.intent.PROFILE_NAME";
- private static final String INTENT_PROFILE_DIR = "org.mozilla.gecko.intent.PROFILE_DIR";
-
- private static final String INTENT_ACTION_UPDATE_ADDONS = "update-addons";
- private static final String INTENT_ACTION_CREATE_SERVICES = "create-services";
-
- private static final String INTENT_SERVICE_CATEGORY = "category";
- private static final String INTENT_SERVICE_DATA = "data";
-
- private static class EventListener implements NativeEventListener {
- @Override // NativeEventListener
- public void handleMessage(final String event,
- final NativeJSObject message,
- final EventCallback callback) {
- final Context context = GeckoAppShell.getApplicationContext();
- switch (event) {
- case "Gecko:ScheduleRun":
- if (DEBUG) {
- Log.d(LOGTAG, "Scheduling " + message.getString("action") +
- " @ " + message.getInt("interval") + "ms");
- }
-
- final Intent intent = getIntentForAction(context, message.getString("action"));
- final PendingIntent pendingIntent = PendingIntent.getService(
- context, /* requestCode */ 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
-
- final AlarmManager am = (AlarmManager)
- context.getSystemService(Context.ALARM_SERVICE);
- // Cancel any previous alarm and schedule a new one.
- am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
- message.getInt("trigger"),
- message.getInt("interval"),
- pendingIntent);
- break;
-
- default:
- throw new UnsupportedOperationException(event);
- }
- }
- }
-
- private static final EventListener EVENT_LISTENER = new EventListener();
-
- public static void register() {
- if (DEBUG) {
- Log.d(LOGTAG, "Registered listener");
- }
- EventDispatcher.getInstance().registerGeckoThreadListener(EVENT_LISTENER,
- "Gecko:ScheduleRun");
- }
-
- public static void unregister() {
- if (DEBUG) {
- Log.d(LOGTAG, "Unregistered listener");
- }
- EventDispatcher.getInstance().unregisterGeckoThreadListener(EVENT_LISTENER,
- "Gecko:ScheduleRun");
- }
-
- @Override // Service
- public void onCreate() {
- GeckoAppShell.ensureCrashHandling();
- GeckoThread.onResume();
- super.onCreate();
-
- if (DEBUG) {
- Log.d(LOGTAG, "Created");
- }
- }
-
- @Override // Service
- public void onDestroy() {
- GeckoThread.onPause();
-
- // We want to block here if we can, so we don't get killed when Gecko is in the
- // middle of handling onPause().
- if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
- GeckoThread.waitOnGecko();
- }
-
- if (DEBUG) {
- Log.d(LOGTAG, "Destroyed");
- }
- super.onDestroy();
- }
-
- private static Intent getIntentForAction(final Context context, final String action) {
- final Intent intent = new Intent(action, /* uri */ null, context, GeckoService.class);
- final GeckoProfile profile = GeckoThread.getActiveProfile();
- if (profile != null) {
- setIntentProfile(intent, profile.getName(), profile.getDir().getAbsolutePath());
- }
- return intent;
- }
-
- public static Intent getIntentToCreateServices(final Context context, final String category, final String data) {
- final Intent intent = getIntentForAction(context, INTENT_ACTION_CREATE_SERVICES);
- intent.putExtra(INTENT_SERVICE_CATEGORY, category);
- intent.putExtra(INTENT_SERVICE_DATA, data);
- return intent;
- }
-
- public static Intent getIntentToCreateServices(final Context context, final String category) {
- return getIntentToCreateServices(context, category, /* data */ null);
- }
-
- public static void setIntentProfile(final Intent intent, final String profileName,
- final String profileDir) {
- intent.putExtra(INTENT_PROFILE_NAME, profileName);
- intent.putExtra(INTENT_PROFILE_DIR, profileDir);
- }
-
- private int handleIntent(final Intent intent, final int startId) {
- if (DEBUG) {
- Log.d(LOGTAG, "Handling " + intent.getAction());
- }
-
- final String profileName = intent.getStringExtra(INTENT_PROFILE_NAME);
- final String profileDir = intent.getStringExtra(INTENT_PROFILE_DIR);
-
- if (profileName == null) {
- throw new IllegalArgumentException("Intent must specify profile.");
- }
-
- if (!GeckoThread.initWithProfile(profileName != null ? profileName : "",
- profileDir != null ? new File(profileDir) : null)) {
- Log.w(LOGTAG, "Ignoring due to profile mismatch: " +
- profileName + " [" + profileDir + ']');
-
- final GeckoProfile profile = GeckoThread.getActiveProfile();
- if (profile != null) {
- Log.w(LOGTAG, "Current profile is " + profile.getName() +
- " [" + profile.getDir().getAbsolutePath() + ']');
- }
- stopSelf(startId);
- return Service.START_NOT_STICKY;
- }
-
- GeckoThread.launch();
-
- switch (intent.getAction()) {
- case INTENT_ACTION_UPDATE_ADDONS:
- // Run the add-on update service. Because the service is automatically invoked
- // when loading Gecko, we don't have to do anything else here.
- break;
-
- case INTENT_ACTION_CREATE_SERVICES:
- final String category = intent.getStringExtra(INTENT_SERVICE_CATEGORY);
- final String data = intent.getStringExtra(INTENT_SERVICE_DATA);
-
- if (category == null) {
- break;
- }
- GeckoThread.createServices(category, data);
- break;
-
- default:
- Log.w(LOGTAG, "Unknown request: " + intent);
- }
-
- stopSelf(startId);
- return Service.START_NOT_STICKY;
- }
-
- @Override // Service
- public int onStartCommand(final Intent intent, final int flags, final int startId) {
- if (intent == null) {
- return Service.START_NOT_STICKY;
- }
- try {
- return handleIntent(intent, startId);
- } catch (final Throwable e) {
- Log.e(LOGTAG, "Cannot handle intent: " + intent, e);
- return Service.START_NOT_STICKY;
- }
- }
-
- @Override // Service
- public IBinder onBind(final Intent intent) {
- return null;
- }
-
- public static void startGecko(final GeckoProfile profile, final String args, final Context context) {
- if (GeckoThread.isLaunched()) {
- if (DEBUG) {
- Log.v(LOGTAG, "already launched");
- }
- return;
- }
-
- Handler handler = new Handler(Looper.getMainLooper());
- handler.post(new Runnable() {
- @Override
- public void run() {
- GeckoAppShell.ensureCrashHandling();
- GeckoAppShell.setApplicationContext(context);
- GeckoThread.onResume();
-
- GeckoThread.init(profile, args, null, false);
- GeckoThread.launch();
-
- if (DEBUG) {
- Log.v(LOGTAG, "warmed up (launched)");
- }
- }
- });
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoUpdateReceiver.java b/mobile/android/base/java/org/mozilla/gecko/GeckoUpdateReceiver.java
deleted file mode 100644
index f73c42e40..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoUpdateReceiver.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.updater.UpdateServiceHelper;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-
-public class GeckoUpdateReceiver extends BroadcastReceiver
-{
- @Override
- public void onReceive(Context context, Intent intent) {
- if (UpdateServiceHelper.ACTION_CHECK_UPDATE_RESULT.equals(intent.getAction())) {
- String result = intent.getStringExtra("result");
- if (GeckoAppShell.getGeckoInterface() != null && result != null) {
- GeckoAppShell.getGeckoInterface().notifyCheckUpdateResult(result);
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/GlobalHistory.java b/mobile/android/base/java/org/mozilla/gecko/GlobalHistory.java
deleted file mode 100644
index c1d9c4939..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/GlobalHistory.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import java.lang.ref.SoftReference;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.Queue;
-import java.util.Set;
-
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.reader.ReaderModeUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.Cursor;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.SystemClock;
-import android.util.Log;
-
-class GlobalHistory {
- private static final String LOGTAG = "GeckoGlobalHistory";
-
- public static final String EVENT_URI_AVAILABLE_IN_HISTORY = "URI_INSERTED_TO_HISTORY";
- public static final String EVENT_PARAM_URI = "uri";
-
- private static final String TELEMETRY_HISTOGRAM_ADD = "FENNEC_GLOBALHISTORY_ADD_MS";
- private static final String TELEMETRY_HISTOGRAM_UPDATE = "FENNEC_GLOBALHISTORY_UPDATE_MS";
- private static final String TELEMETRY_HISTOGRAM_BUILD_VISITED_LINK = "FENNEC_GLOBALHISTORY_VISITED_BUILD_MS";
-
- private static final GlobalHistory sInstance = new GlobalHistory();
-
- static GlobalHistory getInstance() {
- return sInstance;
- }
-
- // this is the delay between receiving a URI check request and processing it.
- // this allows batching together multiple requests and processing them together,
- // which is more efficient.
- private static final long BATCHING_DELAY_MS = 100;
-
- private final Handler mHandler; // a background thread on which we can process requests
-
- // Note: These fields are accessed through the NotificationRunnable inner class.
- final Queue<String> mPendingUris; // URIs that need to be checked
- SoftReference<Set<String>> mVisitedCache; // cache of the visited URI list
- boolean mProcessing; // = false // whether or not the runnable is queued/working
-
- private class NotifierRunnable implements Runnable {
- private final ContentResolver mContentResolver;
- private final BrowserDB mDB;
-
- public NotifierRunnable(final Context context) {
- mContentResolver = context.getContentResolver();
- mDB = BrowserDB.from(context);
- }
-
- @Override
- public void run() {
- Set<String> visitedSet = mVisitedCache.get();
- if (visitedSet == null) {
- // The cache was wiped. Repopulate it.
- Log.w(LOGTAG, "Rebuilding visited link set...");
- final long start = SystemClock.uptimeMillis();
- final Cursor c = mDB.getAllVisitedHistory(mContentResolver);
- if (c == null) {
- return;
- }
-
- try {
- visitedSet = new HashSet<String>();
- if (c.moveToFirst()) {
- do {
- visitedSet.add(c.getString(0));
- } while (c.moveToNext());
- }
- mVisitedCache = new SoftReference<Set<String>>(visitedSet);
- final long end = SystemClock.uptimeMillis();
- final long took = end - start;
- Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_BUILD_VISITED_LINK, (int) Math.min(took, Integer.MAX_VALUE));
- } finally {
- c.close();
- }
- }
-
- // This runs on the same handler thread as the checkUriVisited code,
- // so no synchronization is needed.
- while (true) {
- final String uri = mPendingUris.poll();
- if (uri == null) {
- break;
- }
-
- if (visitedSet.contains(uri)) {
- GeckoAppShell.notifyUriVisited(uri);
- }
- }
-
- mProcessing = false;
- }
- };
-
- private GlobalHistory() {
- mHandler = ThreadUtils.getBackgroundHandler();
- mPendingUris = new LinkedList<String>();
- mVisitedCache = new SoftReference<Set<String>>(null);
- }
-
- public void addToGeckoOnly(String uri) {
- Set<String> visitedSet = mVisitedCache.get();
- if (visitedSet != null) {
- visitedSet.add(uri);
- }
- GeckoAppShell.notifyUriVisited(uri);
- }
-
- public void add(final Context context, final BrowserDB db, String uri) {
- ThreadUtils.assertOnBackgroundThread();
- final long start = SystemClock.uptimeMillis();
-
- // stripAboutReaderUrl only removes about:reader if present, in all other cases the original string is returned
- final String uriToStore = ReaderModeUtils.stripAboutReaderUrl(uri);
-
- db.updateVisitedHistory(context.getContentResolver(), uriToStore);
-
- final long end = SystemClock.uptimeMillis();
- final long took = end - start;
- Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_ADD, (int) Math.min(took, Integer.MAX_VALUE));
- addToGeckoOnly(uriToStore);
- dispatchUriAvailableMessage(uri);
- }
-
- @SuppressWarnings("static-method")
- public void update(final ContentResolver cr, final BrowserDB db, String uri, String title) {
- ThreadUtils.assertOnBackgroundThread();
- final long start = SystemClock.uptimeMillis();
-
- final String uriToStore = ReaderModeUtils.stripAboutReaderUrl(uri);
-
- db.updateHistoryTitle(cr, uriToStore, title);
-
- final long end = SystemClock.uptimeMillis();
- final long took = end - start;
- Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_UPDATE, (int) Math.min(took, Integer.MAX_VALUE));
- }
-
- public void checkUriVisited(final String uri) {
- final String storedURI = ReaderModeUtils.stripAboutReaderUrl(uri);
-
- final NotifierRunnable runnable = new NotifierRunnable(GeckoAppShell.getContext());
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- // this runs on the same handler thread as the processing loop,
- // so no synchronization needed
- mPendingUris.add(storedURI);
- if (mProcessing) {
- // there's already a runnable queued up or working away, so
- // no need to post another
- return;
- }
- mProcessing = true;
- mHandler.postDelayed(runnable, BATCHING_DELAY_MS);
- }
- });
- }
-
- private void dispatchUriAvailableMessage(String uri) {
- final Bundle message = new Bundle();
- message.putString(EVENT_PARAM_URI, uri);
- EventDispatcher.getInstance().dispatch(EVENT_URI_AVAILABLE_IN_HISTORY, message);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/GlobalPageMetadata.java b/mobile/android/base/java/org/mozilla/gecko/GlobalPageMetadata.java
deleted file mode 100644
index d9d12962c..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/GlobalPageMetadata.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.content.ContentProviderClient;
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.annotation.VisibleForTesting;
-import android.text.TextUtils;
-import android.util.Log;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.util.BundleEventListener;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-/**
- * Provides access to metadata information about websites.
- *
- * While storing, in case of timing issues preventing us from looking up History GUID by a given uri,
- * we queue up metadata and wait for GlobalHistory to let us know history record is now available.
- *
- * TODO Bug 1313515: selection of metadata for a given uri/history_GUID
- *
- * @author grisha
- */
-/* package-local */ class GlobalPageMetadata implements BundleEventListener {
- private static final String LOG_TAG = "GeckoGlobalPageMetadata";
-
- private static final GlobalPageMetadata instance = new GlobalPageMetadata();
-
- private static final String KEY_HAS_IMAGE = "hasImage";
- private static final String KEY_METADATA_JSON = "metadataJSON";
-
- private static final int MAX_METADATA_QUEUE_SIZE = 15;
-
- private final Map<String, Bundle> queuedMetadata = Collections.synchronizedMap(new LimitedLinkedHashMap<String, Bundle>());
-
- public static GlobalPageMetadata getInstance() {
- return instance;
- }
-
- private static class LimitedLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
- private static final long serialVersionUID = 6359725112736360244L;
-
- @Override
- protected boolean removeEldestEntry(Entry<K, V> eldest) {
- if (size() > MAX_METADATA_QUEUE_SIZE) {
- Log.w(LOG_TAG, "Page metadata queue is full. Dropping oldest metadata.");
- return true;
- }
- return false;
- }
- }
-
- private GlobalPageMetadata() {}
-
- public void init() {
- EventDispatcher
- .getInstance()
- .registerBackgroundThreadListener(this, GlobalHistory.EVENT_URI_AVAILABLE_IN_HISTORY);
- }
-
- public void add(BrowserDB db, ContentProviderClient contentProviderClient, String uri, boolean hasImage, @NonNull String metadataJSON) {
- ThreadUtils.assertOnBackgroundThread();
-
- // NB: Other than checking that JSON is valid and trimming it,
- // we do not process metadataJSON in any way, trusting our source.
- doAddOrQueue(db, contentProviderClient, uri, hasImage, metadataJSON);
- }
-
- @VisibleForTesting
- /*package-local */ void doAddOrQueue(BrowserDB db, ContentProviderClient contentProviderClient, String uri, boolean hasImage, @NonNull String metadataJSON) {
- final String preparedMetadataJSON;
- try {
- preparedMetadataJSON = prepareJSON(metadataJSON);
- } catch (JSONException e) {
- Log.e(LOG_TAG, "Couldn't process metadata JSON", e);
- return;
- }
-
- // Don't bother queuing this if deletions fails to find a corresponding history record.
- // If we can't delete metadata because it didn't exist yet, that's OK.
- if (preparedMetadataJSON.equals("{}")) {
- final int deleted = db.deletePageMetadata(contentProviderClient, uri);
- // We could delete none if history record for uri isn't present.
- // We must delete one if history record for uri is present.
- if (deleted != 0 && deleted != 1) {
- throw new IllegalStateException("Deleted unexpected number of page metadata records: " + deleted);
- }
- return;
- }
-
- // If we could insert page metadata, we're done.
- if (db.insertPageMetadata(contentProviderClient, uri, hasImage, preparedMetadataJSON)) {
- return;
- }
-
- // Otherwise, we need to queue it for future insertion when history record is available.
- Bundle bundledMetadata = new Bundle();
- bundledMetadata.putBoolean(KEY_HAS_IMAGE, hasImage);
- bundledMetadata.putString(KEY_METADATA_JSON, preparedMetadataJSON);
- queuedMetadata.put(uri, bundledMetadata);
- }
-
- @VisibleForTesting
- /* package-local */ int getMetadataQueueSize() {
- return queuedMetadata.size();
- }
-
- @Override
- public void handleMessage(String event, Bundle message, EventCallback callback) {
- ThreadUtils.assertOnBackgroundThread();
-
- if (!GlobalHistory.EVENT_URI_AVAILABLE_IN_HISTORY.equals(event)) {
- return;
- }
-
- final String uri = message.getString(GlobalHistory.EVENT_PARAM_URI);
- if (TextUtils.isEmpty(uri)) {
- return;
- }
-
- final Bundle bundledMetadata;
- synchronized (queuedMetadata) {
- if (!queuedMetadata.containsKey(uri)) {
- return;
- }
-
- bundledMetadata = queuedMetadata.get(uri);
- queuedMetadata.remove(uri);
- }
-
- insertMetadataBundleForUri(uri, bundledMetadata);
- }
-
- private void insertMetadataBundleForUri(String uri, Bundle bundledMetadata) {
- final boolean hasImage = bundledMetadata.getBoolean(KEY_HAS_IMAGE);
- final String metadataJSON = bundledMetadata.getString(KEY_METADATA_JSON);
-
- // Acquire CPC, must be released in this function.
- final ContentProviderClient contentProviderClient = GeckoAppShell.getApplicationContext()
- .getContentResolver()
- .acquireContentProviderClient(BrowserContract.PageMetadata.CONTENT_URI);
-
- // Pre-conditions...
- if (contentProviderClient == null) {
- Log.e(LOG_TAG, "Couldn't acquire content provider client");
- return;
- }
-
- if (TextUtils.isEmpty(metadataJSON)) {
- Log.e(LOG_TAG, "Metadata bundle contained empty metadata json");
- return;
- }
-
- // Insert!
- try {
- add(
- BrowserDB.from(GeckoThread.getActiveProfile()),
- contentProviderClient,
- uri, hasImage, metadataJSON
- );
- } finally {
- contentProviderClient.release();
- }
- }
-
- private String prepareJSON(String json) throws JSONException {
- return (new JSONObject(json)).toString();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/GuestSession.java b/mobile/android/base/java/org/mozilla/gecko/GuestSession.java
deleted file mode 100644
index 69502f44a..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/GuestSession.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-package org.mozilla.gecko;
-
-import android.app.KeyguardManager;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.support.v4.app.NotificationCompat;
-import android.view.Window;
-import android.view.WindowManager;
-
-// Utility methods for entering/exiting guest mode.
-public final class GuestSession {
- private static final String LOGTAG = "GeckoGuestSession";
-
- public static final String NOTIFICATION_INTENT = "org.mozilla.gecko.GUEST_SESSION_INPROGRESS";
-
- private static PendingIntent getNotificationIntent(Context context) {
- Intent intent = new Intent(NOTIFICATION_INTENT);
- intent.setClassName(context, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
- return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- public static void showNotification(Context context) {
- final NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
- final Resources res = context.getResources();
- builder.setContentTitle(res.getString(R.string.guest_browsing_notification_title))
- .setContentText(res.getString(R.string.guest_browsing_notification_text))
- .setSmallIcon(R.drawable.alert_guest)
- .setOngoing(true)
- .setContentIntent(getNotificationIntent(context));
-
- final NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- manager.notify(R.id.guestNotification, builder.build());
- }
-
- public static void hideNotification(Context context) {
- final NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- manager.cancel(R.id.guestNotification);
- }
-
- public static void onNotificationIntentReceived(BrowserApp context) {
- context.showGuestModeDialog(BrowserApp.GuestModeDialog.LEAVING);
- }
-
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/IntentHelper.java b/mobile/android/base/java/org/mozilla/gecko/IntentHelper.java
deleted file mode 100644
index e2f34f926..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/IntentHelper.java
+++ /dev/null
@@ -1,599 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.overlays.ui.ShareDialog;
-import org.mozilla.gecko.util.ActivityResultHandler;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.JSONUtils;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.widget.ExternalIntentDuringPrivateBrowsingPromptFragment;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.net.Uri;
-import android.provider.Browser;
-import android.support.annotation.Nullable;
-import android.support.v4.app.FragmentActivity;
-import android.text.TextUtils;
-import android.util.Log;
-import android.webkit.MimeTypeMap;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URLEncoder;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Locale;
-
-public final class IntentHelper implements GeckoEventListener,
- NativeEventListener {
-
- private static final String LOGTAG = "GeckoIntentHelper";
- private static final String[] EVENTS = {
- "Intent:GetHandlers",
- "Intent:Open",
- "Intent:OpenForResult",
- };
-
- private static final String[] NATIVE_EVENTS = {
- "Intent:OpenNoHandler",
- };
-
- // via http://developer.android.com/distribute/tools/promote/linking.html
- private static String MARKET_INTENT_URI_PACKAGE_PREFIX = "market://details?id=";
- private static String EXTRA_BROWSER_FALLBACK_URL = "browser_fallback_url";
-
- /** A partial URI to an error page - the encoded error URI should be appended before loading. */
- private static String UNKNOWN_PROTOCOL_URI_PREFIX = "about:neterror?e=unknownProtocolFound&u=";
-
- private static IntentHelper instance;
-
- private final FragmentActivity activity;
-
- private IntentHelper(final FragmentActivity activity) {
- this.activity = activity;
- EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener) this, EVENTS);
- EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener) this, NATIVE_EVENTS);
- }
-
- public static IntentHelper init(final FragmentActivity activity) {
- if (instance == null) {
- instance = new IntentHelper(activity);
- } else {
- Log.w(LOGTAG, "IntentHelper.init() called twice, ignoring.");
- }
-
- return instance;
- }
-
- public static void destroy() {
- if (instance != null) {
- EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) instance, EVENTS);
- EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener) instance, NATIVE_EVENTS);
- instance = null;
- }
- }
-
- /**
- * Given the inputs to <code>getOpenURIIntent</code>, plus an optional
- * package name and class name, create and fire an intent to open the
- * provided URI. If a class name is specified but a package name is not,
- * we will default to using the current fennec package.
- *
- * @param targetURI the string spec of the URI to open.
- * @param mimeType an optional MIME type string.
- * @param packageName an optional app package name.
- * @param className an optional intent class name.
- * @param action an Android action specifier, such as
- * <code>Intent.ACTION_SEND</code>.
- * @param title the title to use in <code>ACTION_SEND</code> intents.
- * @param showPromptInPrivateBrowsing whether or not the user should be prompted when opening
- * this uri from private browsing. This should be true
- * when the user doesn't explicitly choose to open an an
- * external app (e.g. just clicked a link).
- * @return true if the activity started successfully or the user was prompted to open the
- * application; false otherwise.
- */
- public static boolean openUriExternal(String targetURI,
- String mimeType,
- String packageName,
- String className,
- String action,
- String title,
- final boolean showPromptInPrivateBrowsing) {
- final GeckoAppShell.GeckoInterface gi = GeckoAppShell.getGeckoInterface();
- final Context activityContext = gi != null ? gi.getActivity() : null;
- final Context context = activityContext != null ? activityContext : GeckoAppShell.getApplicationContext();
- final Intent intent = getOpenURIIntent(context, targetURI,
- mimeType, action, title);
-
- if (intent == null) {
- return false;
- }
-
- if (!TextUtils.isEmpty(className)) {
- if (!TextUtils.isEmpty(packageName)) {
- intent.setClassName(packageName, className);
- } else {
- // Default to using the fennec app context.
- intent.setClassName(context, className);
- }
- }
-
- if (!showPromptInPrivateBrowsing || activityContext == null) {
- if (activityContext == null) {
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- }
- return ActivityHandlerHelper.startIntentAndCatch(LOGTAG, context, intent);
- } else {
- // Ideally we retrieve the Activity from the calling args, rather than
- // statically, but since this method is called from Gecko and I'm
- // unfamiliar with that code, this is a simpler solution.
- final FragmentActivity fragmentActivity = (FragmentActivity) activityContext;
- return ExternalIntentDuringPrivateBrowsingPromptFragment.showDialogOrAndroidChooser(
- context, fragmentActivity.getSupportFragmentManager(), intent);
- }
- }
-
- public static boolean hasHandlersForIntent(Intent intent) {
- try {
- return !GeckoAppShell.queryIntentActivities(intent).isEmpty();
- } catch (Exception ex) {
- Log.e(LOGTAG, "Exception in hasHandlersForIntent");
- return false;
- }
- }
-
- public static String[] getHandlersForIntent(Intent intent) {
- final PackageManager pm = GeckoAppShell.getApplicationContext().getPackageManager();
- try {
- final List<ResolveInfo> list = GeckoAppShell.queryIntentActivities(intent);
-
- int numAttr = 4;
- final String[] ret = new String[list.size() * numAttr];
- for (int i = 0; i < list.size(); i++) {
- ResolveInfo resolveInfo = list.get(i);
- ret[i * numAttr] = resolveInfo.loadLabel(pm).toString();
- if (resolveInfo.isDefault)
- ret[i * numAttr + 1] = "default";
- else
- ret[i * numAttr + 1] = "";
- ret[i * numAttr + 2] = resolveInfo.activityInfo.applicationInfo.packageName;
- ret[i * numAttr + 3] = resolveInfo.activityInfo.name;
- }
- return ret;
- } catch (Exception ex) {
- Log.e(LOGTAG, "Exception in getHandlersForIntent");
- return new String[0];
- }
- }
-
- public static Intent getIntentForActionString(String aAction) {
- // Default to the view action if no other action as been specified.
- if (TextUtils.isEmpty(aAction)) {
- return new Intent(Intent.ACTION_VIEW);
- }
- return new Intent(aAction);
- }
-
- /**
- * Given a URI, a MIME type, and a title,
- * produce a share intent which can be used to query all activities
- * than can open the specified URI.
- *
- * @param context a <code>Context</code> instance.
- * @param targetURI the string spec of the URI to open.
- * @param mimeType an optional MIME type string.
- * @param title the title to use in <code>ACTION_SEND</code> intents.
- * @return an <code>Intent</code>, or <code>null</code> if none could be
- * produced.
- */
- public static Intent getShareIntent(final Context context,
- final String targetURI,
- final String mimeType,
- final String title) {
- Intent shareIntent = getIntentForActionString(Intent.ACTION_SEND);
- shareIntent.putExtra(Intent.EXTRA_TEXT, targetURI);
- shareIntent.putExtra(Intent.EXTRA_SUBJECT, title);
- shareIntent.putExtra(ShareDialog.INTENT_EXTRA_DEVICES_ONLY, true);
-
- // Note that EXTRA_TITLE is intended to be used for share dialog
- // titles. Common usage (e.g., Pocket) suggests that it's sometimes
- // interpreted as an alternate to EXTRA_SUBJECT, so we include it.
- shareIntent.putExtra(Intent.EXTRA_TITLE, title);
-
- if (mimeType != null && mimeType.length() > 0) {
- shareIntent.setType(mimeType);
- }
-
- return shareIntent;
- }
-
- /**
- * Given a URI, a MIME type, an Android intent "action", and a title,
- * produce an intent which can be used to start an activity to open
- * the specified URI.
- *
- * @param context a <code>Context</code> instance.
- * @param targetURI the string spec of the URI to open.
- * @param mimeType an optional MIME type string.
- * @param action an Android action specifier, such as
- * <code>Intent.ACTION_SEND</code>.
- * @param title the title to use in <code>ACTION_SEND</code> intents.
- * @return an <code>Intent</code>, or <code>null</code> if none could be
- * produced.
- */
- static Intent getOpenURIIntent(final Context context,
- final String targetURI,
- final String mimeType,
- final String action,
- final String title) {
-
- // The resultant chooser can return non-exported activities in 4.1 and earlier.
- // https://code.google.com/p/android/issues/detail?id=29535
- final Intent intent = getOpenURIIntentInner(context, targetURI, mimeType, action, title);
-
- if (intent != null) {
- // Some applications use this field to return to the same browser after processing the
- // Intent. While there is some danger (e.g. denial of service), other major browsers already
- // use it and so it's the norm.
- intent.putExtra(Browser.EXTRA_APPLICATION_ID, AppConstants.ANDROID_PACKAGE_NAME);
- }
-
- return intent;
- }
-
- private static Intent getOpenURIIntentInner(final Context context, final String targetURI,
- final String mimeType, final String action, final String title) {
-
- if (action.equalsIgnoreCase(Intent.ACTION_SEND)) {
- Intent shareIntent = getShareIntent(context, targetURI, mimeType, title);
- return Intent.createChooser(shareIntent,
- context.getResources().getString(R.string.share_title));
- }
-
- Uri uri = normalizeUriScheme(targetURI.indexOf(':') >= 0 ? Uri.parse(targetURI) : new Uri.Builder().scheme(targetURI).build());
- if (!TextUtils.isEmpty(mimeType)) {
- Intent intent = getIntentForActionString(action);
- intent.setDataAndType(uri, mimeType);
- return intent;
- }
-
- if (!GeckoAppShell.isUriSafeForScheme(uri)) {
- return null;
- }
-
- final String scheme = uri.getScheme();
- if ("intent".equals(scheme) || "android-app".equals(scheme)) {
- final Intent intent;
- try {
- intent = Intent.parseUri(targetURI, 0);
- } catch (final URISyntaxException e) {
- Log.e(LOGTAG, "Unable to parse URI - " + e);
- return null;
- }
-
- final Uri data = intent.getData();
- if (data != null && "file".equals(data.normalizeScheme().getScheme())) {
- Log.w(LOGTAG, "Blocked intent with \"file://\" data scheme.");
- return null;
- }
-
- // Only open applications which can accept arbitrary data from a browser.
- intent.addCategory(Intent.CATEGORY_BROWSABLE);
-
- // Prevent site from explicitly opening our internal activities, which can leak data.
- intent.setComponent(null);
- nullIntentSelector(intent);
-
- return intent;
- }
-
- // Compute our most likely intent, then check to see if there are any
- // custom handlers that would apply.
- // Start with the original URI. If we end up modifying it, we'll
- // overwrite it.
- final String extension = MimeTypeMap.getFileExtensionFromUrl(targetURI);
- final Intent intent = getIntentForActionString(action);
- intent.setData(uri);
-
- if ("file".equals(scheme)) {
- // Only set explicit mimeTypes on file://.
- final String mimeType2 = GeckoAppShell.getMimeTypeFromExtension(extension);
- intent.setType(mimeType2);
- return intent;
- }
-
- // Have a special handling for SMS based schemes, as the query parameters
- // are not extracted from the URI automatically.
- if (!"sms".equals(scheme) && !"smsto".equals(scheme) && !"mms".equals(scheme) && !"mmsto".equals(scheme)) {
- return intent;
- }
-
- final String query = uri.getEncodedQuery();
- if (TextUtils.isEmpty(query)) {
- return intent;
- }
-
- // It is common to see sms*/mms* uris on the web without '//', it is W3C standard not to have the slashes,
- // but android's Uri builder & Uri require the slashes and will interpret those without as malformed.
- String currentUri = uri.toString();
- String correctlyFormattedDataURIScheme = scheme + "://";
- if (!currentUri.contains(correctlyFormattedDataURIScheme)) {
- uri = Uri.parse(currentUri.replaceFirst(scheme + ":", correctlyFormattedDataURIScheme));
- }
-
- final String[] fields = query.split("&");
- boolean shouldUpdateIntent = false;
- String resultQuery = "";
- for (String field : fields) {
- if (field.startsWith("body=")) {
- final String body = Uri.decode(field.substring(5));
- intent.putExtra("sms_body", body);
- shouldUpdateIntent = true;
- } else if (field.startsWith("subject=")) {
- final String subject = Uri.decode(field.substring(8));
- intent.putExtra("subject", subject);
- shouldUpdateIntent = true;
- } else if (field.startsWith("cc=")) {
- final String ccNumber = Uri.decode(field.substring(3));
- String phoneNumber = uri.getAuthority();
- if (phoneNumber != null) {
- uri = uri.buildUpon().encodedAuthority(phoneNumber + ";" + ccNumber).build();
- }
- shouldUpdateIntent = true;
- } else {
- resultQuery = resultQuery.concat(resultQuery.length() > 0 ? "&" + field : field);
- }
- }
-
- if (!shouldUpdateIntent) {
- // No need to rewrite the URI, then.
- return intent;
- }
-
- // Form a new URI without the extracted fields in the query part, and
- // push that into the new Intent.
- final String newQuery = resultQuery.length() > 0 ? "?" + resultQuery : "";
- final Uri pruned = uri.buildUpon().encodedQuery(newQuery).build();
- intent.setData(pruned);
-
- return intent;
- }
-
- // We create a separate method to better encapsulate the @TargetApi use.
- @TargetApi(15)
- private static void nullIntentSelector(final Intent intent) {
- intent.setSelector(null);
- }
-
- /**
- * Return a <code>Uri</code> instance which is equivalent to <code>u</code>,
- * but with a guaranteed-lowercase scheme as if the API level 16 method
- * <code>u.normalizeScheme</code> had been called.
- *
- * @param u the <code>Uri</code> to normalize.
- * @return a <code>Uri</code>, which might be <code>u</code>.
- */
- private static Uri normalizeUriScheme(final Uri u) {
- final String scheme = u.getScheme();
- final String lower = scheme.toLowerCase(Locale.US);
- if (lower.equals(scheme)) {
- return u;
- }
-
- // Otherwise, return a new URI with a normalized scheme.
- return u.buildUpon().scheme(lower).build();
- }
-
- @Override
- public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) {
- if (event.equals("Intent:OpenNoHandler")) {
- openNoHandler(message, callback);
- }
- }
-
- @Override
- public void handleMessage(String event, JSONObject message) {
- try {
- if (event.equals("Intent:GetHandlers")) {
- getHandlers(message);
- } else if (event.equals("Intent:Open")) {
- open(message);
- } else if (event.equals("Intent:OpenForResult")) {
- openForResult(message);
- }
- } catch (JSONException e) {
- Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
- }
- }
-
- private void getHandlers(JSONObject message) throws JSONException {
- final Intent intent = getOpenURIIntent(activity,
- message.optString("url"),
- message.optString("mime"),
- message.optString("action"),
- message.optString("title"));
- final List<String> appList = Arrays.asList(getHandlersForIntent(intent));
-
- final JSONObject response = new JSONObject();
- response.put("apps", new JSONArray(appList));
- EventDispatcher.sendResponse(message, response);
- }
-
- private void open(JSONObject message) throws JSONException {
- openUriExternal(message.optString("url"),
- message.optString("mime"),
- message.optString("packageName"),
- message.optString("className"),
- message.optString("action"),
- message.optString("title"), false);
- }
-
- private void openForResult(final JSONObject message) throws JSONException {
- Intent intent = getOpenURIIntent(activity,
- message.optString("url"),
- message.optString("mime"),
- message.optString("action"),
- message.optString("title"));
- intent.setClassName(message.optString("packageName"), message.optString("className"));
- intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-
- final ResultHandler handler = new ResultHandler(message);
- try {
- ActivityHandlerHelper.startIntentForActivity(activity, intent, handler);
- } catch (SecurityException e) {
- Log.w(LOGTAG, "Forbidden to launch activity.", e);
- }
- }
-
- /**
- * Opens a URI without any valid handlers on device. In the best case, a package is specified
- * and we can bring the user directly to the application page in an app market. If a package is
- * not specified and there is a fallback url in the intent extras, we open that url. If neither
- * is present, we alert the user that we were unable to open the link.
- *
- * @param msg A message with the uri with no handlers as the value for the "uri" key
- * @param callback A callback that will be called with success & no params if Java loads a page, or with error and
- * the uri to load if Java does not load a page
- */
- private void openNoHandler(final NativeJSObject msg, final EventCallback callback) {
- final String uri = msg.getString("uri");
-
- if (TextUtils.isEmpty(uri)) {
- Log.w(LOGTAG, "Received empty URL - loading about:neterror");
- callback.sendError(getUnknownProtocolErrorPageUri(""));
- return;
- }
-
- final Intent intent;
- try {
- // TODO (bug 1173626): This will not handle android-app uris on non 5.1 devices.
- intent = Intent.parseUri(uri, 0);
- } catch (final URISyntaxException e) {
- String errorUri;
- try {
- errorUri = getUnknownProtocolErrorPageUri(URLEncoder.encode(uri, "UTF-8"));
- } catch (final UnsupportedEncodingException encodingE) {
- errorUri = getUnknownProtocolErrorPageUri("");
- }
-
- // Don't log the exception to prevent leaking URIs.
- Log.w(LOGTAG, "Unable to parse Intent URI - loading about:neterror");
- callback.sendError(errorUri);
- return;
- }
-
- // For this flow, we follow Chrome's lead:
- // https://developer.chrome.com/multidevice/android/intents
- final String fallbackUrl = intent.getStringExtra(EXTRA_BROWSER_FALLBACK_URL);
- if (isFallbackUrlValid(fallbackUrl)) {
- // Opens the page in JS.
- callback.sendError(fallbackUrl);
-
- } else if (intent.getPackage() != null) {
- // Note on alternative flows: we could get the intent package from a component, however, for
- // security reasons, components are ignored when opening URIs (bug 1168998) so we should
- // ignore it here too.
- //
- // Our old flow used to prompt the user to search for their app in the market by scheme and
- // while this could help the user find a new app, there is not always a correlation in
- // scheme to application name and we could end up steering the user wrong (potentially to
- // malicious software). Better to leave that one alone.
- final String marketUri = MARKET_INTENT_URI_PACKAGE_PREFIX + intent.getPackage();
- final Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(marketUri));
- marketIntent.addCategory(Intent.CATEGORY_BROWSABLE);
- marketIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
- // (Bug 1192436) We don't know if marketIntent matches any Activities (e.g. non-Play
- // Store devices). If it doesn't, clicking the link will cause no action to occur.
- ExternalIntentDuringPrivateBrowsingPromptFragment.showDialogOrAndroidChooser(
- activity, activity.getSupportFragmentManager(), marketIntent);
- callback.sendSuccess(null);
-
- } else {
- // We return the error page here, but it will only be shown if we think the load did
- // not come from clicking a link. Chrome does not show error pages in that case, and
- // many websites have catered to this behavior. For example, the site might set a timeout and load a play
- // store url for their app if the intent link fails to load, i.e. the app is not installed.
- // These work-arounds would often end with our users seeing about:neterror instead of the intended experience.
- // While I feel showing about:neterror is a better solution for users (when not hacked around),
- // we should match the status quo for the good of our users.
- //
- // Don't log the URI to prevent leaking it.
- Log.w(LOGTAG, "Unable to open URI, maybe showing neterror");
- callback.sendError(getUnknownProtocolErrorPageUri(intent.getData().toString()));
- }
- }
-
- private static boolean isFallbackUrlValid(@Nullable final String fallbackUrl) {
- if (fallbackUrl == null) {
- return false;
- }
-
- try {
- final String anyCaseScheme = new URI(fallbackUrl).getScheme();
- final String scheme = (anyCaseScheme == null) ? null : anyCaseScheme.toLowerCase(Locale.US);
- if ("http".equals(scheme) || "https".equals(scheme)) {
- return true;
- } else {
- Log.w(LOGTAG, "Fallback URI uses unsupported scheme: " + scheme + ". Try http or https.");
- }
- } catch (final URISyntaxException e) {
- // Do not include Exception to avoid leaking uris.
- Log.w(LOGTAG, "URISyntaxException parsing fallback URI");
- }
- return false;
- }
-
- /**
- * Returns an about:neterror uri with the unknownProtocolFound text as a parameter.
- * @param encodedUri The encoded uri. While the page does not open correctly without specifying
- * a uri parameter, it happily accepts the empty String so this argument may
- * be the empty String.
- */
- private String getUnknownProtocolErrorPageUri(final String encodedUri) {
- return UNKNOWN_PROTOCOL_URI_PREFIX + encodedUri;
- }
-
- private static class ResultHandler implements ActivityResultHandler {
- private final JSONObject message;
-
- public ResultHandler(JSONObject message) {
- this.message = message;
- }
-
- @Override
- public void onActivityResult(int resultCode, Intent data) {
- JSONObject response = new JSONObject();
- try {
- if (data != null) {
- if (data.getExtras() != null) {
- response.put("extras", JSONUtils.bundleToJSON(data.getExtras()));
- }
- if (data.getData() != null) {
- response.put("uri", data.getData().toString());
- }
- }
- response.put("resultCode", resultCode);
- } catch (JSONException e) {
- Log.w(LOGTAG, "Error building JSON response.", e);
- }
- EventDispatcher.sendResponse(message, response);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/LauncherActivity.java b/mobile/android/base/java/org/mozilla/gecko/LauncherActivity.java
deleted file mode 100644
index 4de8fa423..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/LauncherActivity.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.customtabs.CustomTabsIntent;
-
-import org.mozilla.gecko.customtabs.CustomTabsActivity;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.mozglue.SafeIntent;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-import org.mozilla.gecko.tabqueue.TabQueueHelper;
-import org.mozilla.gecko.tabqueue.TabQueueService;
-
-/**
- * Activity that receives incoming Intents and dispatches them to the appropriate activities (e.g. browser, custom tabs, web app).
- */
-public class LauncherActivity extends Activity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- GeckoAppShell.ensureCrashHandling();
-
- final SafeIntent safeIntent = new SafeIntent(getIntent());
-
- // If it's not a view intent, it won't be a custom tabs intent either. Just launch!
- if (!isViewIntentWithURL(safeIntent)) {
- dispatchNormalIntent();
-
- // Is this a custom tabs intent, and are custom tabs enabled?
- } else if (AppConstants.MOZ_ANDROID_CUSTOM_TABS && isCustomTabsIntent(safeIntent)
- && isCustomTabsEnabled()) {
- dispatchCustomTabsIntent();
-
- // Can we dispatch this VIEW action intent to the tab queue service?
- } else if (!safeIntent.getBooleanExtra(BrowserContract.SKIP_TAB_QUEUE_FLAG, false)
- && TabQueueHelper.TAB_QUEUE_ENABLED
- && TabQueueHelper.isTabQueueEnabled(this)) {
- dispatchTabQueueIntent();
-
- // Dispatch this VIEW action intent to the browser.
- } else {
- dispatchNormalIntent();
- }
-
- finish();
- }
-
- /**
- * Launch tab queue service to display overlay.
- */
- private void dispatchTabQueueIntent() {
- Intent intent = new Intent(getIntent());
- intent.setClass(getApplicationContext(), TabQueueService.class);
- startService(intent);
- }
-
- /**
- * Launch the browser activity.
- */
- private void dispatchNormalIntent() {
- Intent intent = new Intent(getIntent());
- intent.setClassName(getApplicationContext(), AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
-
- filterFlags(intent);
-
- startActivity(intent);
- }
-
- private void dispatchCustomTabsIntent() {
- Intent intent = new Intent(getIntent());
- intent.setClassName(getApplicationContext(), CustomTabsActivity.class.getName());
-
- filterFlags(intent);
-
- startActivity(intent);
- }
-
- private static void filterFlags(Intent intent) {
- // Explicitly remove the new task and clear task flags (Our browser activity is a single
- // task activity and we never want to start a second task here). See bug 1280112.
- intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_CLEAR_TASK);
-
- // LauncherActivity is started with the "exclude from recents" flag (set in manifest). We do
- // not want to propagate this flag from the launcher activity to the browser.
- intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
- }
-
- private static boolean isViewIntentWithURL(@NonNull final SafeIntent safeIntent) {
- return Intent.ACTION_VIEW.equals(safeIntent.getAction())
- && safeIntent.getDataString() != null;
- }
-
- private static boolean isCustomTabsIntent(@NonNull final SafeIntent safeIntent) {
- return isViewIntentWithURL(safeIntent)
- && safeIntent.hasExtra(CustomTabsIntent.EXTRA_SESSION);
- }
-
- private boolean isCustomTabsEnabled() {
- return GeckoSharedPrefs.forApp(this).getBoolean(GeckoPreferences.PREFS_CUSTOM_TABS, false);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/LocaleManager.java b/mobile/android/base/java/org/mozilla/gecko/LocaleManager.java
deleted file mode 100644
index 795caa925..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/LocaleManager.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import java.util.Locale;
-
-import android.content.Context;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-
-/**
- * Implement this interface to provide Fennec's locale switching functionality.
- *
- * The LocaleManager is responsible for persisting and applying selected locales,
- * and correcting configurations after Android has changed them.
- */
-public interface LocaleManager {
- void initialize(Context context);
-
- /**
- * @return true if locale switching is enabled.
- */
- boolean isEnabled();
- Locale getCurrentLocale(Context context);
- String getAndApplyPersistedLocale(Context context);
- void correctLocale(Context context, Resources resources, Configuration newConfig);
- void updateConfiguration(Context context, Locale locale);
- String setSelectedLocale(Context context, String localeCode);
- boolean systemLocaleDidChange();
- void resetToSystemLocale(Context context);
-
- /**
- * Call this in your onConfigurationChanged handler. This method is expected
- * to do the appropriate thing: if the user has selected a locale, it
- * corrects the incoming configuration; if not, it signals the new locale to
- * use.
- */
- Locale onSystemConfigurationChanged(Context context, Resources resources, Configuration configuration, Locale currentActivityLocale);
- String getFallbackLocaleTag();
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/Locales.java b/mobile/android/base/java/org/mozilla/gecko/Locales.java
deleted file mode 100644
index e030b95e9..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/Locales.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import java.lang.reflect.Method;
-import java.util.Locale;
-
-import org.mozilla.gecko.LocaleManager;
-
-import android.app.Activity;
-import android.content.Context;
-import android.os.Bundle;
-import android.os.StrictMode;
-import android.support.v4.app.FragmentActivity;
-import android.support.v7.app.AppCompatActivity;
-
-/**
- * This is a helper class to do typical locale switching operations without
- * hitting StrictMode errors or adding boilerplate to common activity
- * subclasses.
- *
- * Either call {@link Locales#initializeLocale(Context)} in your
- * <code>onCreate</code> method, or inherit from
- * <code>LocaleAwareFragmentActivity</code> or <code>LocaleAwareActivity</code>.
- */
-public class Locales {
- public static LocaleManager getLocaleManager() {
- try {
- final Class<?> clazz = Class.forName("org.mozilla.gecko.BrowserLocaleManager");
- final Method getInstance = clazz.getMethod("getInstance");
- final LocaleManager localeManager = (LocaleManager) getInstance.invoke(null);
- return localeManager;
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
-
- public static void initializeLocale(Context context) {
- final LocaleManager localeManager = getLocaleManager();
- final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
- StrictMode.allowThreadDiskWrites();
- try {
- localeManager.getAndApplyPersistedLocale(context);
- } finally {
- StrictMode.setThreadPolicy(savedPolicy);
- }
- }
-
- public static abstract class LocaleAwareAppCompatActivity extends AppCompatActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- Locales.initializeLocale(getApplicationContext());
- super.onCreate(savedInstanceState);
- }
-
- }
- public static abstract class LocaleAwareFragmentActivity extends FragmentActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- Locales.initializeLocale(getApplicationContext());
- super.onCreate(savedInstanceState);
- }
- }
-
- public static abstract class LocaleAwareActivity extends Activity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- Locales.initializeLocale(getApplicationContext());
- super.onCreate(savedInstanceState);
- }
- }
-
- /**
- * Sometimes we want just the language for a locale, not the entire language
- * tag. But Java's .getLanguage method is wrong.
- *
- * This method is equivalent to the first part of
- * {@link Locales#getLanguageTag(Locale)}.
- *
- * @return a language string, such as "he" for the Hebrew locales.
- */
- public static String getLanguage(final Locale locale) {
- // Can, but should never be, an empty string.
- final String language = locale.getLanguage();
-
- // Modernize certain language codes.
- if (language.equals("iw")) {
- return "he";
- }
-
- if (language.equals("in")) {
- return "id";
- }
-
- if (language.equals("ji")) {
- return "yi";
- }
-
- return language;
- }
-
- /**
- * Gecko uses locale codes like "es-ES", whereas a Java {@link Locale}
- * stringifies as "es_ES".
- *
- * This method approximates the Java 7 method
- * <code>Locale#toLanguageTag()</code>.
- *
- * @return a locale string suitable for passing to Gecko.
- */
- public static String getLanguageTag(final Locale locale) {
- // If this were Java 7:
- // return locale.toLanguageTag();
-
- final String language = getLanguage(locale);
- final String country = locale.getCountry(); // Can be an empty string.
- if (country.equals("")) {
- return language;
- }
- return language + "-" + country;
- }
-
- public static Locale parseLocaleCode(final String localeCode) {
- int index;
- if ((index = localeCode.indexOf('-')) != -1 ||
- (index = localeCode.indexOf('_')) != -1) {
- final String langCode = localeCode.substring(0, index);
- final String countryCode = localeCode.substring(index + 1);
- return new Locale(langCode, countryCode);
- }
-
- return new Locale(localeCode);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/MediaCastingBar.java b/mobile/android/base/java/org/mozilla/gecko/MediaCastingBar.java
deleted file mode 100644
index bd109058c..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/MediaCastingBar.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import org.json.JSONObject;
-
-import android.content.Context;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.ImageButton;
-import android.widget.RelativeLayout;
-import android.widget.TextView;
-
-public class MediaCastingBar extends RelativeLayout implements View.OnClickListener, GeckoEventListener {
- private static final String LOGTAG = "GeckoMediaCastingBar";
-
- private TextView mCastingTo;
- private ImageButton mMediaPlay;
- private ImageButton mMediaPause;
- private ImageButton mMediaStop;
-
- private boolean mInflated;
-
- public MediaCastingBar(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
- "Casting:Started",
- "Casting:Paused",
- "Casting:Playing",
- "Casting:Stopped");
- }
-
- public void inflateContent() {
- LayoutInflater inflater = LayoutInflater.from(getContext());
- View content = inflater.inflate(R.layout.media_casting, this);
-
- mMediaPlay = (ImageButton) content.findViewById(R.id.media_play);
- mMediaPlay.setOnClickListener(this);
- mMediaPause = (ImageButton) content.findViewById(R.id.media_pause);
- mMediaPause.setOnClickListener(this);
- mMediaStop = (ImageButton) content.findViewById(R.id.media_stop);
- mMediaStop.setOnClickListener(this);
-
- mCastingTo = (TextView) content.findViewById(R.id.media_sending_to);
-
- // Capture clicks on the rest of the view to prevent them from
- // leaking into other views positioned below.
- content.setOnClickListener(this);
-
- mInflated = true;
- }
-
- public void show() {
- if (!mInflated)
- inflateContent();
-
- setVisibility(VISIBLE);
- }
-
- public void hide() {
- setVisibility(GONE);
- }
-
- public void onDestroy() {
- GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
- "Casting:Started",
- "Casting:Paused",
- "Casting:Playing",
- "Casting:Stopped");
- }
-
- // View.OnClickListener implementation
- @Override
- public void onClick(View v) {
- final int viewId = v.getId();
-
- if (viewId == R.id.media_play) {
- GeckoAppShell.notifyObservers("Casting:Play", "");
- mMediaPlay.setVisibility(GONE);
- mMediaPause.setVisibility(VISIBLE);
- } else if (viewId == R.id.media_pause) {
- GeckoAppShell.notifyObservers("Casting:Pause", "");
- mMediaPause.setVisibility(GONE);
- mMediaPlay.setVisibility(VISIBLE);
- } else if (viewId == R.id.media_stop) {
- GeckoAppShell.notifyObservers("Casting:Stop", "");
- }
- }
-
- // GeckoEventListener implementation
- @Override
- public void handleMessage(final String event, final JSONObject message) {
- final String device = message.optString("device");
-
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- if (event.equals("Casting:Started")) {
- show();
- if (!TextUtils.isEmpty(device)) {
- mCastingTo.setText(device);
- } else {
- // Should not happen
- mCastingTo.setText("");
- Log.d(LOGTAG, "Device name is empty.");
- }
- mMediaPlay.setVisibility(GONE);
- mMediaPause.setVisibility(VISIBLE);
- } else if (event.equals("Casting:Paused")) {
- mMediaPause.setVisibility(GONE);
- mMediaPlay.setVisibility(VISIBLE);
- } else if (event.equals("Casting:Playing")) {
- mMediaPlay.setVisibility(GONE);
- mMediaPause.setVisibility(VISIBLE);
- } else if (event.equals("Casting:Stopped")) {
- hide();
- }
- }
- });
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java b/mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java
deleted file mode 100644
index fc0ce82cf..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java
+++ /dev/null
@@ -1,323 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.os.Bundle;
-import android.support.v4.app.Fragment;
-import android.support.v7.media.MediaControlIntent;
-import android.support.v7.media.MediaRouteSelector;
-import android.support.v7.media.MediaRouter;
-import android.support.v7.media.MediaRouter.RouteInfo;
-import android.util.Log;
-
-import com.google.android.gms.cast.CastMediaControlIntent;
-
-import org.json.JSONObject;
-import org.mozilla.gecko.annotation.JNITarget;
-import org.mozilla.gecko.annotation.ReflectionTarget;
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
-
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-
-/**
- * Manages a list of GeckoMediaPlayers methods (i.e. Chromecast/Miracast). Routes messages
- * from Gecko to the correct caster based on the id of the display
- */
-public class MediaPlayerManager extends Fragment implements NativeEventListener {
- /**
- * Create a new instance of DetailsFragment, initialized to
- * show the text at 'index'.
- */
-
- private static MediaPlayerManager instance = null;
-
- @ReflectionTarget
- public static MediaPlayerManager getInstance() {
- if (instance != null) {
- return instance;
- }
- if (Versions.feature17Plus) {
- instance = (MediaPlayerManager) new PresentationMediaPlayerManager();
- } else {
- instance = new MediaPlayerManager();
- }
- return instance;
- }
-
- private static final String LOGTAG = "GeckoMediaPlayerManager";
- protected boolean isPresentationMode = false; // Used to prevent mirroring when Presentation API is used.
-
- @ReflectionTarget
- public static final String MEDIA_PLAYER_TAG = "MPManagerFragment";
-
- private static final boolean SHOW_DEBUG = false;
- // Simplified debugging interfaces
- private static void debug(String msg, Exception e) {
- if (SHOW_DEBUG) {
- Log.e(LOGTAG, msg, e);
- }
- }
-
- private static void debug(String msg) {
- if (SHOW_DEBUG) {
- Log.d(LOGTAG, msg);
- }
- }
-
- protected MediaRouter mediaRouter = null;
- protected final Map<String, GeckoMediaPlayer> players = new HashMap<String, GeckoMediaPlayer>();
- protected final Map<String, GeckoPresentationDisplay> displays = new HashMap<String, GeckoPresentationDisplay>(); // used for Presentation API
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
- "MediaPlayer:Load",
- "MediaPlayer:Start",
- "MediaPlayer:Stop",
- "MediaPlayer:Play",
- "MediaPlayer:Pause",
- "MediaPlayer:End",
- "MediaPlayer:Mirror",
- "MediaPlayer:Message",
- "AndroidCastDevice:Start",
- "AndroidCastDevice:Stop",
- "AndroidCastDevice:SyncDevice");
- }
-
- @Override
- @JNITarget
- public void onDestroy() {
- super.onDestroy();
- GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
- "MediaPlayer:Load",
- "MediaPlayer:Start",
- "MediaPlayer:Stop",
- "MediaPlayer:Play",
- "MediaPlayer:Pause",
- "MediaPlayer:End",
- "MediaPlayer:Mirror",
- "MediaPlayer:Message",
- "AndroidCastDevice:Start",
- "AndroidCastDevice:Stop",
- "AndroidCastDevice:SyncDevice");
- }
-
- // GeckoEventListener implementation
- @Override
- public void handleMessage(String event, final NativeJSObject message, final EventCallback callback) {
- debug(event);
- if (event.startsWith("MediaPlayer:")) {
- final GeckoMediaPlayer player = players.get(message.getString("id"));
- if (player == null) {
- Log.e(LOGTAG, "Couldn't find a player for this id: " + message.getString("id") + " for message: " + event);
- if (callback != null) {
- callback.sendError(null);
- }
- return;
- }
-
- if ("MediaPlayer:Play".equals(event)) {
- player.play(callback);
- } else if ("MediaPlayer:Start".equals(event)) {
- player.start(callback);
- } else if ("MediaPlayer:Stop".equals(event)) {
- player.stop(callback);
- } else if ("MediaPlayer:Pause".equals(event)) {
- player.pause(callback);
- } else if ("MediaPlayer:End".equals(event)) {
- player.end(callback);
- } else if ("MediaPlayer:Mirror".equals(event)) {
- player.mirror(callback);
- } else if ("MediaPlayer:Message".equals(event) && message.has("data")) {
- player.message(message.getString("data"), callback);
- } else if ("MediaPlayer:Load".equals(event)) {
- final String url = message.optString("source", "");
- final String type = message.optString("type", "video/mp4");
- final String title = message.optString("title", "");
- player.load(title, url, type, callback);
- }
- }
-
- if (event.startsWith("AndroidCastDevice:")) {
- if ("AndroidCastDevice:Start".equals(event)) {
- final GeckoPresentationDisplay display = displays.get(message.getString("id"));
- if (display == null) {
- Log.e(LOGTAG, "Couldn't find a display for this id: " + message.getString("id") + " for message: " + event);
- return;
- }
- display.start(callback);
- } else if ("AndroidCastDevice:Stop".equals(event)) {
- final GeckoPresentationDisplay display = displays.get(message.getString("id"));
- if (display == null) {
- Log.e(LOGTAG, "Couldn't find a display for this id: " + message.getString("id") + " for message: " + event);
- return;
- }
- display.stop(callback);
- } else if ("AndroidCastDevice:SyncDevice".equals(event)) {
- for (Map.Entry<String, GeckoPresentationDisplay> entry : displays.entrySet()) {
- GeckoPresentationDisplay display = entry.getValue();
- JSONObject json = display.toJSON();
- if (json == null) {
- break;
- }
- GeckoAppShell.notifyObservers("AndroidCastDevice:Added", json.toString());
- }
- }
- }
- }
-
- private final MediaRouter.Callback callback =
- new MediaRouter.Callback() {
- @Override
- public void onRouteRemoved(MediaRouter router, RouteInfo route) {
- debug("onRouteRemoved: route=" + route);
-
- // Remove from media player list.
- players.remove(route.getId());
- GeckoAppShell.notifyObservers("MediaPlayer:Removed", route.getId());
- updatePresentation();
-
- // Remove from presentation display list.
- displays.remove(route.getId());
- GeckoAppShell.notifyObservers("AndroidCastDevice:Removed", route.getId());
- }
-
- @SuppressWarnings("unused")
- public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo route) {
- updatePresentation();
- }
-
- // These methods aren't used by the support version Media Router
- @SuppressWarnings("unused")
- public void onRouteUnselected(MediaRouter router, int type, RouteInfo route) {
- updatePresentation();
- }
-
- @Override
- public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo route) {
- updatePresentation();
- }
-
- @Override
- public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) {
- }
-
- @Override
- public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo route) {
- debug("onRouteAdded: route=" + route);
- final GeckoMediaPlayer player = getMediaPlayerForRoute(route);
- saveAndNotifyOfPlayer("MediaPlayer:Added", route, player);
- updatePresentation();
-
- final GeckoPresentationDisplay display = getPresentationDisplayForRoute(route);
- saveAndNotifyOfDisplay("AndroidCastDevice:Added", route, display);
- }
-
- @Override
- public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) {
- debug("onRouteChanged: route=" + route);
- final GeckoMediaPlayer player = players.get(route.getId());
- saveAndNotifyOfPlayer("MediaPlayer:Changed", route, player);
- updatePresentation();
-
- final GeckoPresentationDisplay display = displays.get(route.getId());
- saveAndNotifyOfDisplay("AndroidCastDevice:Changed", route, display);
- }
-
- private void saveAndNotifyOfPlayer(final String eventName,
- MediaRouter.RouteInfo route,
- final GeckoMediaPlayer player) {
- if (player == null) {
- return;
- }
-
- final JSONObject json = player.toJSON();
- if (json == null) {
- return;
- }
-
- players.put(route.getId(), player);
- GeckoAppShell.notifyObservers(eventName, json.toString());
- }
-
- private void saveAndNotifyOfDisplay(final String eventName,
- MediaRouter.RouteInfo route,
- final GeckoPresentationDisplay display) {
- if (display == null) {
- return;
- }
-
- final JSONObject json = display.toJSON();
- if (json == null) {
- return;
- }
-
- displays.put(route.getId(), display);
- GeckoAppShell.notifyObservers(eventName, json.toString());
- }
- };
-
- private GeckoMediaPlayer getMediaPlayerForRoute(MediaRouter.RouteInfo route) {
- try {
- if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
- return new ChromeCastPlayer(getActivity(), route);
- }
- } catch (Exception ex) {
- debug("Error handling presentation", ex);
- }
-
- return null;
- }
-
- private GeckoPresentationDisplay getPresentationDisplayForRoute(MediaRouter.RouteInfo route) {
- try {
- if (route.supportsControlCategory(CastMediaControlIntent.categoryForCast(ChromeCastDisplay.REMOTE_DISPLAY_APP_ID))) {
- return new ChromeCastDisplay(getActivity(), route);
- }
- } catch (Exception ex) {
- debug("Error handling presentation", ex);
- }
- return null;
- }
-
- @Override
- public void onPause() {
- super.onPause();
- mediaRouter.removeCallback(callback);
- mediaRouter = null;
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- // The mediaRouter shouldn't exist here, but this is a nice safety check.
- if (mediaRouter != null) {
- return;
- }
-
- mediaRouter = MediaRouter.getInstance(getActivity());
- final MediaRouteSelector selectorBuilder = new MediaRouteSelector.Builder()
- .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)
- .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
- .addControlCategory(CastMediaControlIntent.categoryForCast(ChromeCastPlayer.MIRROR_RECEIVER_APP_ID))
- .addControlCategory(CastMediaControlIntent.categoryForCast(ChromeCastDisplay.REMOTE_DISPLAY_APP_ID))
- .build();
- mediaRouter.addCallback(selectorBuilder, callback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
- }
-
- public void setPresentationMode(boolean isPresentationMode) {
- this.isPresentationMode = isPresentationMode;
- }
-
- protected void updatePresentation() { /* Overridden in sub-classes. */ }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/MemoryMonitor.java b/mobile/android/base/java/org/mozilla/gecko/MemoryMonitor.java
deleted file mode 100644
index 94ca761b9..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/MemoryMonitor.java
+++ /dev/null
@@ -1,279 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.BrowserProvider;
-import org.mozilla.gecko.home.ImageLoader;
-import org.mozilla.gecko.icons.storage.MemoryStorage;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.content.BroadcastReceiver;
-import android.content.ComponentCallbacks2;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.support.v4.content.LocalBroadcastManager;
-import android.util.Log;
-
-/**
- * This is a utility class to keep track of how much memory and disk-space pressure
- * the system is under. It receives input from GeckoActivity via the onLowMemory() and
- * onTrimMemory() functions, and also listens for some system intents related to
- * disk-space notifications. Internally it will track how much memory and disk pressure
- * the system is under, and perform various actions to help alleviate the pressure.
- *
- * Note that since there is no notification for when the system has lots of free memory
- * again, this class also assumes that, over time, the system will free up memory. This
- * assumption is implemented using a timer that slowly lowers the internal memory
- * pressure state if no new low-memory notifications are received.
- *
- * Synchronization note: MemoryMonitor contains an inner class PressureDecrementer. Both
- * of these classes may be accessed from various threads, and have both been designed to
- * be thread-safe. In terms of lock ordering, code holding the PressureDecrementer lock
- * is allowed to pick up the MemoryMonitor lock, but not vice-versa.
- */
-class MemoryMonitor extends BroadcastReceiver {
- private static final String LOGTAG = "GeckoMemoryMonitor";
- private static final String ACTION_MEMORY_DUMP = "org.mozilla.gecko.MEMORY_DUMP";
- private static final String ACTION_FORCE_PRESSURE = "org.mozilla.gecko.FORCE_MEMORY_PRESSURE";
-
- // Memory pressure levels. Keep these in sync with those in AndroidJavaWrappers.h
- private static final int MEMORY_PRESSURE_NONE = 0;
- private static final int MEMORY_PRESSURE_CLEANUP = 1;
- private static final int MEMORY_PRESSURE_LOW = 2;
- private static final int MEMORY_PRESSURE_MEDIUM = 3;
- private static final int MEMORY_PRESSURE_HIGH = 4;
-
- private static final MemoryMonitor sInstance = new MemoryMonitor();
-
- static MemoryMonitor getInstance() {
- return sInstance;
- }
-
- private Context mAppContext;
- private final PressureDecrementer mPressureDecrementer;
- private int mMemoryPressure; // Synchronized access only.
- private volatile boolean mStoragePressure; // Accessed via UI thread intent, background runnables.
- private boolean mInited;
-
- private MemoryMonitor() {
- mPressureDecrementer = new PressureDecrementer();
- mMemoryPressure = MEMORY_PRESSURE_NONE;
- }
-
- public void init(final Context context) {
- if (mInited) {
- return;
- }
-
- mAppContext = context.getApplicationContext();
- IntentFilter filter = new IntentFilter();
- filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
- filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
- filter.addAction(ACTION_MEMORY_DUMP);
- filter.addAction(ACTION_FORCE_PRESSURE);
- mAppContext.registerReceiver(this, filter);
- mInited = true;
- }
-
- public void onLowMemory() {
- Log.d(LOGTAG, "onLowMemory() notification received");
- if (increaseMemoryPressure(MEMORY_PRESSURE_HIGH)) {
- // We need to wait on Gecko here, because if we haven't reduced
- // memory usage enough when we return from this, Android will kill us.
- if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
- GeckoThread.waitOnGecko();
- }
- }
- }
-
- public void onTrimMemory(int level) {
- Log.d(LOGTAG, "onTrimMemory() notification received with level " + level);
- if (level == ComponentCallbacks2.TRIM_MEMORY_COMPLETE) {
- // We seem to get this just by entering the task switcher or hitting the home button.
- // Seems bogus, because we are the foreground app, or at least not at the end of the LRU list.
- // Just ignore it, and if there is a real memory pressure event (CRITICAL, MODERATE, etc),
- // we'll respond appropriately.
- return;
- }
-
- switch (level) {
- case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
- case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
- // TRIM_MEMORY_MODERATE is the highest level we'll respond to while backgrounded
- increaseMemoryPressure(MEMORY_PRESSURE_HIGH);
- break;
- case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
- increaseMemoryPressure(MEMORY_PRESSURE_MEDIUM);
- break;
- case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
- increaseMemoryPressure(MEMORY_PRESSURE_LOW);
- break;
- case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
- case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
- increaseMemoryPressure(MEMORY_PRESSURE_CLEANUP);
- break;
- default:
- Log.d(LOGTAG, "Unhandled onTrimMemory() level " + level);
- break;
- }
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(intent.getAction())) {
- Log.d(LOGTAG, "Device storage is low");
- mStoragePressure = true;
- ThreadUtils.postToBackgroundThread(new StorageReducer(context));
- } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intent.getAction())) {
- Log.d(LOGTAG, "Device storage is ok");
- mStoragePressure = false;
- } else if (ACTION_MEMORY_DUMP.equals(intent.getAction())) {
- String label = intent.getStringExtra("label");
- if (label == null) {
- label = "default";
- }
- GeckoAppShell.notifyObservers("Memory:Dump", label);
- } else if (ACTION_FORCE_PRESSURE.equals(intent.getAction())) {
- increaseMemoryPressure(MEMORY_PRESSURE_HIGH);
- }
- }
-
- @WrapForJNI(calledFrom = "ui")
- private static native void dispatchMemoryPressure();
-
- private boolean increaseMemoryPressure(int level) {
- int oldLevel;
- synchronized (this) {
- // bump up our level if we're not already higher
- if (mMemoryPressure > level) {
- return false;
- }
- oldLevel = mMemoryPressure;
- mMemoryPressure = level;
- }
-
- Log.d(LOGTAG, "increasing memory pressure to " + level);
-
- // since we don't get notifications for when memory pressure is off,
- // we schedule our own timer to slowly back off the memory pressure level.
- // note that this will reset the time to next decrement if the decrementer
- // is already running, which is the desired behaviour because we just got
- // a new low-mem notification.
- mPressureDecrementer.start();
-
- if (oldLevel == level) {
- // if we're not going to a higher level we probably don't
- // need to run another round of the same memory reductions
- // we did on the last memory pressure increase.
- return false;
- }
-
- // TODO hook in memory-reduction stuff for different levels here
- if (level >= MEMORY_PRESSURE_MEDIUM) {
- //Only send medium or higher events because that's all that is used right now
- if (GeckoThread.isRunning()) {
- dispatchMemoryPressure();
- }
-
- MemoryStorage.get().evictAll();
- ImageLoader.clearLruCache();
- LocalBroadcastManager.getInstance(mAppContext)
- .sendBroadcast(new Intent(BrowserProvider.ACTION_SHRINK_MEMORY));
- }
- return true;
- }
-
- /**
- * Thread-safe due to mStoragePressure's volatility.
- */
- boolean isUnderStoragePressure() {
- return mStoragePressure;
- }
-
- private boolean decreaseMemoryPressure() {
- int newLevel;
- synchronized (this) {
- if (mMemoryPressure <= 0) {
- return false;
- }
-
- newLevel = --mMemoryPressure;
- }
- Log.d(LOGTAG, "Decreased memory pressure to " + newLevel);
-
- return true;
- }
-
- class PressureDecrementer implements Runnable {
- private static final int DECREMENT_DELAY = 5 * 60 * 1000; // 5 minutes
-
- private boolean mPosted;
-
- synchronized void start() {
- if (mPosted) {
- // cancel the old one before scheduling a new one
- ThreadUtils.getBackgroundHandler().removeCallbacks(this);
- }
- ThreadUtils.getBackgroundHandler().postDelayed(this, DECREMENT_DELAY);
- mPosted = true;
- }
-
- @Override
- public synchronized void run() {
- if (!decreaseMemoryPressure()) {
- // done decrementing, bail out
- mPosted = false;
- return;
- }
-
- // need to keep decrementing
- ThreadUtils.getBackgroundHandler().postDelayed(this, DECREMENT_DELAY);
- }
- }
-
- private static class StorageReducer implements Runnable {
- private final Context mContext;
- private final BrowserDB mDB;
-
- public StorageReducer(final Context context) {
- this.mContext = context;
- // Since this may be called while Fennec is in the background, we don't want to risk accidentally
- // using the wrong context. If the profile we get is a guest profile, use the default profile instead.
- GeckoProfile profile = GeckoProfile.get(mContext);
- if (profile.inGuestMode()) {
- // If it was the guest profile, switch to the default one.
- profile = GeckoProfile.get(mContext, GeckoProfile.DEFAULT_PROFILE);
- }
-
- mDB = BrowserDB.from(profile);
- }
-
- @Override
- public void run() {
- // this might get run right on startup, if so wait 10 seconds and try again
- if (!GeckoThread.isRunning()) {
- ThreadUtils.getBackgroundHandler().postDelayed(this, 10000);
- return;
- }
-
- if (!MemoryMonitor.getInstance().isUnderStoragePressure()) {
- // Pressure is off, so we can abort.
- return;
- }
-
- final ContentResolver cr = mContext.getContentResolver();
- mDB.expireHistory(cr, BrowserContract.ExpirePriority.AGGRESSIVE);
- mDB.removeThumbnails(cr);
-
- // TODO: drop or shrink disk caches
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/MotionEventInterceptor.java b/mobile/android/base/java/org/mozilla/gecko/MotionEventInterceptor.java
deleted file mode 100644
index 814c09995..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/MotionEventInterceptor.java
+++ /dev/null
@@ -1,13 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.view.MotionEvent;
-import android.view.View;
-
-public interface MotionEventInterceptor {
- public boolean onInterceptMotionEvent(View view, MotionEvent event);
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/PackageReplacedReceiver.java b/mobile/android/base/java/org/mozilla/gecko/PackageReplacedReceiver.java
deleted file mode 100644
index 37dd8c304..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/PackageReplacedReceiver.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-
-import org.mozilla.gecko.mozglue.GeckoLoader;
-
-/**
- * This broadcast receiver receives ACTION_MY_PACKAGE_REPLACED broadcasts and
- * starts procedures that should run after the APK has been updated.
- */
-public class PackageReplacedReceiver extends BroadcastReceiver {
- public static final String ACTION_MY_PACKAGE_REPLACED = "android.intent.action.MY_PACKAGE_REPLACED";
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent == null || !ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())) {
- // This is not the broadcast we are looking for.
- return;
- }
-
- // Extract Gecko libs to allow them to be loaded from cache on startup.
- extractGeckoLibs(context);
- }
-
- private static void extractGeckoLibs(final Context context) {
- final String resourcePath = context.getPackageResourcePath();
- GeckoLoader.loadMozGlue(context);
- GeckoLoader.extractGeckoLibs(context, resourcePath);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/PresentationMediaPlayerManager.java b/mobile/android/base/java/org/mozilla/gecko/PresentationMediaPlayerManager.java
deleted file mode 100644
index e44096489..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/PresentationMediaPlayerManager.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.annotation.TargetApi;
-import android.app.Presentation;
-import android.content.Context;
-import android.os.Bundle;
-import android.support.v7.media.MediaRouter;
-import android.util.Log;
-import android.view.Display;
-import android.view.Surface;
-import android.view.SurfaceHolder;
-import android.view.SurfaceView;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-
-import org.mozilla.gecko.AppConstants.Versions;
-
-import org.mozilla.gecko.annotation.WrapForJNI;
-
-/**
- * A MediaPlayerManager with API 17+ Presentation support.
- */
-@TargetApi(17)
-public class PresentationMediaPlayerManager extends MediaPlayerManager {
-
- private static final String LOGTAG = "Gecko" + PresentationMediaPlayerManager.class.getSimpleName();
-
- private GeckoPresentation presentation;
-
- public PresentationMediaPlayerManager() {
- if (!Versions.feature17Plus) {
- throw new IllegalStateException(PresentationMediaPlayerManager.class.getSimpleName() +
- " does not support < API 17");
- }
- }
-
- @Override
- public void onStop() {
- super.onStop();
- if (presentation != null) {
- presentation.dismiss();
- presentation = null;
- }
- }
-
- @Override
- protected void updatePresentation() {
- if (mediaRouter == null) {
- return;
- }
-
- if (isPresentationMode) {
- return;
- }
-
- MediaRouter.RouteInfo route = mediaRouter.getSelectedRoute();
- Display display = route != null ? route.getPresentationDisplay() : null;
-
- if (display != null) {
- if ((presentation != null) && (presentation.getDisplay() != display)) {
- presentation.dismiss();
- presentation = null;
- }
-
- if (presentation == null) {
- final GeckoView geckoView = (GeckoView) getActivity().findViewById(R.id.layer_view);
- presentation = new GeckoPresentation(getActivity(), display, geckoView);
-
- try {
- presentation.show();
- } catch (WindowManager.InvalidDisplayException ex) {
- Log.w(LOGTAG, "Couldn't show presentation! Display was removed in "
- + "the meantime.", ex);
- presentation = null;
- }
- }
- } else if (presentation != null) {
- presentation.dismiss();
- presentation = null;
- }
- }
-
- @WrapForJNI(calledFrom = "ui")
- /* protected */ static native void invalidateAndScheduleComposite(GeckoView geckoView);
-
- @WrapForJNI(calledFrom = "ui")
- /* protected */ static native void addPresentationSurface(GeckoView geckoView, Surface surface);
-
- @WrapForJNI(calledFrom = "ui")
- /* protected */ static native void removePresentationSurface();
-
- private static final class GeckoPresentation extends Presentation {
- private SurfaceView mView;
- private GeckoView mGeckoView;
-
- public GeckoPresentation(Context context, Display display, GeckoView geckoView) {
- super(context, display);
-
- mGeckoView = geckoView;
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- mView = new SurfaceView(getContext());
- setContentView(mView, new ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT));
- mView.getHolder().addCallback(new SurfaceListener(mGeckoView));
- }
- }
-
- private static final class SurfaceListener implements SurfaceHolder.Callback {
- private GeckoView mGeckoView;
-
- public SurfaceListener(GeckoView geckoView) {
- mGeckoView = geckoView;
- }
-
- @Override
- public void surfaceChanged(SurfaceHolder holder, int format, int width,
- int height) {
- // Surface changed so force a composite
- if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
- invalidateAndScheduleComposite(mGeckoView);
- }
- }
-
- @Override
- public void surfaceCreated(SurfaceHolder holder) {
- if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
- addPresentationSurface(mGeckoView, holder.getSurface());
- }
- }
-
- @Override
- public void surfaceDestroyed(SurfaceHolder holder) {
- if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
- removePresentationSurface();
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/PresentationView.java b/mobile/android/base/java/org/mozilla/gecko/PresentationView.java
deleted file mode 100644
index 3e5b5ffb3..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/PresentationView.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * vim: ts=4 sw=4 expandtab:
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.GeckoThread;
-import org.mozilla.gecko.GeckoView;
-import org.mozilla.gecko.ScreenManagerHelper;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
-
-public class PresentationView extends GeckoView {
- private static final String LOGTAG = "PresentationView";
- private static final String presentationViewURI = "chrome://browser/content/PresentationView.xul";
-
- public PresentationView(Context context, String deviceId, int screenId) {
- super(context);
- this.chromeURI = presentationViewURI + "#" + deviceId;
- this.screenId = screenId;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/PrintHelper.java b/mobile/android/base/java/org/mozilla/gecko/PrintHelper.java
deleted file mode 100644
index 077b2d29b..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/PrintHelper.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.util.GeckoRequest;
-import org.mozilla.gecko.util.IOUtils;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.InputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.os.CancellationSignal;
-import android.os.ParcelFileDescriptor;
-import android.print.PrintAttributes;
-import android.print.PrintDocumentAdapter;
-import android.print.PrintDocumentAdapter.LayoutResultCallback;
-import android.print.PrintDocumentAdapter.WriteResultCallback;
-import android.print.PrintDocumentInfo;
-import android.print.PrintManager;
-import android.print.PageRange;
-import android.util.Log;
-
-public class PrintHelper {
- private static final String LOGTAG = "GeckoPrintUtils";
-
- public static void printPDF(final Context context) {
- GeckoAppShell.sendRequestToGecko(new GeckoRequest("Print:PDF", new JSONObject()) {
- @Override
- public void onResponse(NativeJSObject nativeJSObject) {
- final String filePath = nativeJSObject.getString("file");
- final String title = nativeJSObject.getString("title");
- finish(context, filePath, title);
- }
-
- @Override
- public void onError(NativeJSObject error) {
- // Gecko didn't respond due to state change, javascript error, etc.
- Log.d(LOGTAG, "No response from Gecko on request to generate a PDF");
- }
-
- private void finish(final Context context, final String filePath, final String title) {
- PrintManager printManager = (PrintManager) context.getSystemService(Context.PRINT_SERVICE);
- String jobName = title;
-
- // The adapter methods are all called on the UI thread by the PrintManager. Put the heavyweight code
- // in onWrite on the background thread.
- PrintDocumentAdapter pda = new PrintDocumentAdapter() {
- @Override
- public void onWrite(final PageRange[] pages, final ParcelFileDescriptor destination, final CancellationSignal cancellationSignal, final WriteResultCallback callback) {
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- InputStream input = null;
- OutputStream output = null;
-
- try {
- File pdfFile = new File(filePath);
- input = new FileInputStream(pdfFile);
- output = new FileOutputStream(destination.getFileDescriptor());
-
- byte[] buf = new byte[8192];
- int bytesRead;
- while ((bytesRead = input.read(buf)) > 0) {
- output.write(buf, 0, bytesRead);
- }
-
- callback.onWriteFinished(new PageRange[] { PageRange.ALL_PAGES });
- } catch (FileNotFoundException ee) {
- Log.d(LOGTAG, "Unable to find the temporary PDF file.");
- } catch (IOException ioe) {
- Log.e(LOGTAG, "IOException while transferring temporary PDF file: ", ioe);
- } finally {
- IOUtils.safeStreamClose(input);
- IOUtils.safeStreamClose(output);
- }
- }
- });
- }
-
- @Override
- public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes, CancellationSignal cancellationSignal, LayoutResultCallback callback, Bundle extras) {
- if (cancellationSignal.isCanceled()) {
- callback.onLayoutCancelled();
- return;
- }
-
- PrintDocumentInfo pdi = new PrintDocumentInfo.Builder(filePath).setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT).build();
- callback.onLayoutFinished(pdi, true);
- }
-
- @Override
- public void onFinish() {
- // Remove the temporary file when the printing system is finished.
- try {
- File pdfFile = new File(filePath);
- pdfFile.delete();
- } catch (NullPointerException npe) {
- // Silence the exception. We only want to delete a real file. We don't
- // care if the file doesn't exist.
- }
- }
- };
-
- printManager.print(jobName, pda, null);
- }
- });
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/PrivateTab.java b/mobile/android/base/java/org/mozilla/gecko/PrivateTab.java
deleted file mode 100644
index 39b6899d3..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/PrivateTab.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.content.Context;
-
-import org.json.JSONObject;
-import org.mozilla.gecko.db.BrowserDB;
-
-public class PrivateTab extends Tab {
- public PrivateTab(Context context, int id, String url, boolean external, int parentId, String title) {
- super(context, id, url, external, parentId, title);
- }
-
- @Override
- protected void saveThumbnailToDB(final BrowserDB db) {}
-
- @Override
- public void setMetadata(JSONObject metadata) {}
-
- @Override
- public boolean isPrivate() {
- return true;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/RemoteClientsDialogFragment.java b/mobile/android/base/java/org/mozilla/gecko/RemoteClientsDialogFragment.java
deleted file mode 100644
index b4aee9370..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/RemoteClientsDialogFragment.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.mozilla.gecko.db.RemoteClient;
-
-import android.app.AlertDialog;
-import android.app.AlertDialog.Builder;
-import android.app.Dialog;
-import android.content.DialogInterface;
-import android.os.Bundle;
-import android.support.v4.app.DialogFragment;
-import android.support.v4.app.Fragment;
-import android.util.SparseBooleanArray;
-
-/**
- * A dialog fragment that displays a list of remote clients.
- * <p>
- * The dialog allows both single (one tap) and multiple (checkbox) selection.
- * The dialog's results are communicated via the {@link RemoteClientsListener}
- * interface. Either the dialog fragment's <i>target fragment</i> (see
- * {@link Fragment#setTargetFragment(Fragment, int)}), or the containing
- * <i>activity</i>, must implement that interface. See
- * {@link #notifyListener(List)} for details.
- */
-public class RemoteClientsDialogFragment extends DialogFragment {
- private static final String KEY_TITLE = "title";
- private static final String KEY_CHOICE_MODE = "choice_mode";
- private static final String KEY_POSITIVE_BUTTON_TEXT = "positive_button_text";
- private static final String KEY_CLIENTS = "clients";
-
- public interface RemoteClientsListener {
- // Always called on the main UI thread.
- public void onClients(List<RemoteClient> clients);
- }
-
- public enum ChoiceMode {
- SINGLE,
- MULTIPLE,
- }
-
- public static RemoteClientsDialogFragment newInstance(String title, String positiveButtonText, ChoiceMode choiceMode, ArrayList<RemoteClient> clients) {
- final RemoteClientsDialogFragment dialog = new RemoteClientsDialogFragment();
- final Bundle args = new Bundle();
- args.putString(KEY_TITLE, title);
- args.putString(KEY_POSITIVE_BUTTON_TEXT, positiveButtonText);
- args.putInt(KEY_CHOICE_MODE, choiceMode.ordinal());
- args.putParcelableArrayList(KEY_CLIENTS, clients);
- dialog.setArguments(args);
- return dialog;
- }
-
- public RemoteClientsDialogFragment() {
- // Empty constructor is required for DialogFragment.
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
-
- GeckoApplication.watchReference(getActivity(), this);
- }
-
- protected void notifyListener(List<RemoteClient> clients) {
- RemoteClientsListener listener;
- try {
- listener = (RemoteClientsListener) getTargetFragment();
- } catch (ClassCastException e) {
- try {
- listener = (RemoteClientsListener) getActivity();
- } catch (ClassCastException f) {
- throw new ClassCastException(getTargetFragment() + " or " + getActivity()
- + " must implement RemoteClientsListener");
- }
- }
- listener.onClients(clients);
- }
-
- @Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- final String title = getArguments().getString(KEY_TITLE);
- final String positiveButtonText = getArguments().getString(KEY_POSITIVE_BUTTON_TEXT);
- final ChoiceMode choiceMode = ChoiceMode.values()[getArguments().getInt(KEY_CHOICE_MODE)];
- final ArrayList<RemoteClient> clients = getArguments().getParcelableArrayList(KEY_CLIENTS);
-
- final Builder builder = new AlertDialog.Builder(getActivity());
- builder.setTitle(title);
-
- final String[] clientNames = new String[clients.size()];
- for (int i = 0; i < clients.size(); i++) {
- clientNames[i] = clients.get(i).name;
- }
-
- if (choiceMode == ChoiceMode.MULTIPLE) {
- builder.setMultiChoiceItems(clientNames, null, null);
- builder.setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialogInterface, int which) {
- if (which != Dialog.BUTTON_POSITIVE) {
- return;
- }
-
- final AlertDialog dialog = (AlertDialog) dialogInterface;
- final SparseBooleanArray checkedItemPositions = dialog.getListView().getCheckedItemPositions();
- final ArrayList<RemoteClient> checked = new ArrayList<RemoteClient>();
- for (int i = 0; i < clients.size(); i++) {
- if (checkedItemPositions.get(i)) {
- checked.add(clients.get(i));
- }
- }
- notifyListener(checked);
- }
- });
- } else {
- builder.setItems(clientNames, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int index) {
- final ArrayList<RemoteClient> checked = new ArrayList<RemoteClient>();
- checked.add(clients.get(index));
- notifyListener(checked);
- }
- });
- }
-
- return builder.create();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/RemotePresentationService.java b/mobile/android/base/java/org/mozilla/gecko/RemotePresentationService.java
deleted file mode 100644
index b5a5527c9..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/RemotePresentationService.java
+++ /dev/null
@@ -1,150 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * vim: ts=4 sw=4 expandtab:
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.json.JSONObject;
-import org.json.JSONException;
-
-import org.json.JSONObject;
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.PresentationView;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.ScreenManagerHelper;
-import org.mozilla.gecko.annotation.JNITarget;
-import org.mozilla.gecko.annotation.ReflectionTarget;
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.gfx.LayerView;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
-
-import com.google.android.gms.cast.CastMediaControlIntent;
-import com.google.android.gms.cast.CastPresentation;
-import com.google.android.gms.cast.CastRemoteDisplayLocalService;
-import com.google.android.gms.common.ConnectionResult;
-import com.google.android.gms.common.GooglePlayServicesUtil;
-
-import android.app.Activity;
-import android.content.Context;
-import android.os.Bundle;
-import android.support.v4.app.Fragment;
-import android.support.v7.media.MediaControlIntent;
-import android.support.v7.media.MediaRouteSelector;
-import android.support.v7.media.MediaRouter.RouteInfo;
-import android.support.v7.media.MediaRouter;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.view.Display;
-import android.view.ViewGroup.LayoutParams;
-import android.view.WindowManager;
-import android.widget.RelativeLayout;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/*
- * Service to keep the remote display running even when the app goes into the background
- */
-public class RemotePresentationService extends CastRemoteDisplayLocalService {
-
- private static final String LOGTAG = "RemotePresentationService";
- private CastPresentation presentation;
- private String deviceId;
- private int screenId;
-
- public void setDeviceId(String deviceId) {
- this.deviceId = deviceId;
- }
-
- public String getDeviceId() {
- return deviceId;
- }
-
- @Override
- public void onCreatePresentation(Display display) {
- createPresentation();
- }
-
- @Override
- public void onDismissPresentation() {
- dismissPresentation();
- }
-
- private void dismissPresentation() {
- if (presentation != null) {
- presentation.dismiss();
- presentation = null;
- ScreenManagerHelper.removeDisplay(screenId);
- MediaPlayerManager.getInstance().setPresentationMode(false);
- }
- }
-
- private void createPresentation() {
- dismissPresentation();
-
- MediaPlayerManager.getInstance().setPresentationMode(true);
-
- DisplayMetrics metrics = new DisplayMetrics();
- getDisplay().getMetrics(metrics);
- screenId = ScreenManagerHelper.addDisplay(ScreenManagerHelper.DISPLAY_VIRTUAL,
- metrics.widthPixels,
- metrics.heightPixels,
- metrics.density);
-
- VirtualPresentation virtualPresentation = new VirtualPresentation(this, getDisplay());
- virtualPresentation.setDeviceId(deviceId);
- virtualPresentation.setScreenId(screenId);
- presentation = (CastPresentation) virtualPresentation;
-
- try {
- presentation.show();
- } catch (WindowManager.InvalidDisplayException ex) {
- Log.e(LOGTAG, "Unable to show presentation, display was removed.", ex);
- dismissPresentation();
- }
- }
-}
-
-class VirtualPresentation extends CastPresentation {
- private final String LOGTAG = "VirtualPresentation";
- private RelativeLayout layout;
- private PresentationView view;
- private String deviceId;
- private int screenId;
-
- public VirtualPresentation(Context context, Display display) {
- super(context, display);
- }
-
- public void setDeviceId(String deviceId) { this.deviceId = deviceId; }
- public void setScreenId(int screenId) { this.screenId = screenId; }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- /*
- * NOTICE: The context get from getContext() is different to the context
- * of the application. Presentaion has its own context to get correct
- * resources.
- */
-
- // Create new PresentationView
- view = new PresentationView(getContext(), deviceId, screenId);
- view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
- LayoutParams.MATCH_PARENT));
-
- // Create new layout to put the GeckoView
- layout = new RelativeLayout(getContext());
- layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
- LayoutParams.MATCH_PARENT));
- layout.addView(view);
-
- setContentView(layout);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/Restarter.java b/mobile/android/base/java/org/mozilla/gecko/Restarter.java
deleted file mode 100644
index b049f7627..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/Restarter.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.app.Service;
-import android.content.Intent;
-import android.os.IBinder;
-import android.os.Process;
-import android.util.Log;
-
-public class Restarter extends Service {
- private static final String LOGTAG = "GeckoRestarter";
-
- private void doRestart(Intent intent) {
- final int oldProc = intent.getIntExtra("pid", -1);
- if (oldProc < 0) {
- return;
- }
-
- Process.killProcess(oldProc);
- Log.d(LOGTAG, "Killed " + oldProc);
- try {
- Thread.sleep(100);
- } catch (final InterruptedException e) {
- }
-
- final Intent restartIntent = (Intent)intent.getParcelableExtra(Intent.EXTRA_INTENT);
- restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- .putExtra("didRestart", true)
- .setClassName(getApplicationContext(),
- AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
- startActivity(restartIntent);
- Log.d(LOGTAG, "Launched " + restartIntent);
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- doRestart(intent);
- stopSelf(startId);
- return Service.START_NOT_STICKY;
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/ScreenManagerHelper.java b/mobile/android/base/java/org/mozilla/gecko/ScreenManagerHelper.java
deleted file mode 100644
index 5cb404ce8..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/ScreenManagerHelper.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * vim: ts=4 sw=4 expandtab:
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.annotation.WrapForJNI;
-
-class ScreenManagerHelper {
-
- /**
- * The following display types use the same definition in nsIScreen.idl
- */
- final static int DISPLAY_PRIMARY = 0; // primary screen
- final static int DISPLAY_EXTERNAL = 1; // wired displays, such as HDMI, DisplayPort, etc.
- final static int DISPLAY_VIRTUAL = 2; // wireless displays, such as Chromecast, WiFi-Display, etc.
-
- /**
- * Add a new nsScreen when a new display in Android is available.
- *
- * @param displayType the display type of the nsScreen would be added
- * @param width the width of the new nsScreen
- * @param height the height of the new nsScreen
- * @param density the density of the new nsScreen
- *
- * @return return the ID of the added nsScreen
- */
- @WrapForJNI
- public native static int addDisplay(int displayType,
- int width,
- int height,
- float density);
-
- /**
- * Remove the nsScreen by the specific screen ID.
- *
- * @param screenId the ID of the screen would be removed.
- */
- @WrapForJNI
- public native static void removeDisplay(int screenId);
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/ScreenshotObserver.java b/mobile/android/base/java/org/mozilla/gecko/ScreenshotObserver.java
deleted file mode 100644
index 64f101e51..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/ScreenshotObserver.java
+++ /dev/null
@@ -1,146 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.permissions.Permissions;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.Manifest;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.net.Uri;
-import android.provider.MediaStore;
-import android.util.Log;
-
-public class ScreenshotObserver {
- private static final String LOGTAG = "GeckoScreenshotObserver";
- public Context context;
-
- /**
- * Listener for screenshot changes.
- */
- public interface OnScreenshotListener {
- /**
- * This callback is executed on the UI thread.
- */
- public void onScreenshotTaken(String data, String title);
- }
-
- private OnScreenshotListener listener;
-
- public ScreenshotObserver() {
- }
-
- public void setListener(Context context, OnScreenshotListener listener) {
- this.context = context;
- this.listener = listener;
- }
-
- private MediaObserver mediaObserver;
- private String[] mediaProjections = new String[] {
- MediaStore.Images.ImageColumns.DATA,
- MediaStore.Images.ImageColumns.DISPLAY_NAME,
- MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
- MediaStore.Images.ImageColumns.DATE_TAKEN,
- MediaStore.Images.ImageColumns.TITLE
- };
-
- /**
- * Start ScreenshotObserver if this device is supported and all required runtime permissions
- * have been granted by the user. Calling this method will not prompt for permissions.
- */
- public void start() {
- Permissions.from(context)
- .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
- .doNotPrompt()
- .run(startObserverRunnable());
- }
-
- private Runnable startObserverRunnable() {
- return new Runnable() {
- @Override
- public void run() {
- try {
- if (mediaObserver == null) {
- mediaObserver = new MediaObserver();
- context.getContentResolver().registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, false, mediaObserver);
- }
- } catch (Exception e) {
- Log.e(LOGTAG, "Failure to start watching media: ", e);
- }
- }
- };
- }
-
- public void stop() {
- if (mediaObserver == null) {
- return;
- }
-
- try {
- context.getContentResolver().unregisterContentObserver(mediaObserver);
- mediaObserver = null;
- } catch (Exception e) {
- Log.e(LOGTAG, "Failure to stop watching media: ", e);
- }
- }
-
- public void onMediaChange(final Uri uri) {
- // Make sure we are on not on the main thread.
- final ContentResolver cr = context.getContentResolver();
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- // Find the most recent image added to the MediaStore and see if it's a screenshot.
- final Cursor cursor = cr.query(uri, mediaProjections, null, null, MediaStore.Images.ImageColumns.DATE_ADDED + " DESC LIMIT 1");
- try {
- if (cursor == null) {
- return;
- }
-
- while (cursor.moveToNext()) {
- String data = cursor.getString(0);
- Log.i(LOGTAG, "data: " + data);
- String display = cursor.getString(1);
- Log.i(LOGTAG, "display: " + display);
- String album = cursor.getString(2);
- Log.i(LOGTAG, "album: " + album);
- long date = cursor.getLong(3);
- String title = cursor.getString(4);
- Log.i(LOGTAG, "title: " + title);
- if (album != null && album.toLowerCase().contains("screenshot")) {
- if (listener != null) {
- listener.onScreenshotTaken(data, title);
- break;
- }
- }
- }
- } catch (Exception e) {
- Log.e(LOGTAG, "Failure to process media change: ", e);
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- }
- });
- }
-
- private class MediaObserver extends ContentObserver {
- public MediaObserver() {
- super(null);
- }
-
- @Override
- public void onChange(boolean selfChange) {
- super.onChange(selfChange);
- onMediaChange(MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/SessionParser.java b/mobile/android/base/java/org/mozilla/gecko/SessionParser.java
deleted file mode 100644
index d29aaadc7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/SessionParser.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * ***** BEGIN LICENSE BLOCK *****
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/.
- *
- * ***** END LICENSE BLOCK ***** */
-
-package org.mozilla.gecko;
-
-import java.util.LinkedList;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.util.Log;
-
-public abstract class SessionParser {
- private static final String LOGTAG = "GeckoSessionParser";
-
- public class SessionTab {
- final private String mTitle;
- final private String mUrl;
- final private JSONObject mTabObject;
- private boolean mIsSelected;
-
- private SessionTab(String title, String url, boolean isSelected, JSONObject tabObject) {
- mTitle = title;
- mUrl = url;
- mIsSelected = isSelected;
- mTabObject = tabObject;
- }
-
- public String getTitle() {
- return mTitle;
- }
-
- public String getUrl() {
- return mUrl;
- }
-
- public boolean isSelected() {
- return mIsSelected;
- }
-
- public JSONObject getTabObject() {
- return mTabObject;
- }
-
- /**
- * Is this tab pointing to about:home and does not contain any other history?
- */
- public boolean isAboutHomeWithoutHistory() {
- JSONArray entries = mTabObject.optJSONArray("entries");
- return entries != null && entries.length() == 1 && AboutPages.isAboutHome(mUrl);
- }
- };
-
- abstract public void onTabRead(SessionTab tab);
-
- /**
- * Placeholder method that must be overloaded to handle closedTabs while parsing session data.
- *
- * @param closedTabs, JSONArray of recently closed tab entries.
- * @throws JSONException
- */
- public void onClosedTabsRead(final JSONArray closedTabs) throws JSONException {
- }
-
- /**
- * Parses the provided session store data and calls onTabRead for each tab that has been found.
- *
- * @param sessionStrings One or more strings containing session store data.
- * @return False if any of the session strings provided didn't contain valid session store data.
- */
- public boolean parse(String... sessionStrings) {
- final LinkedList<SessionTab> sessionTabs = new LinkedList<SessionTab>();
- int totalCount = 0;
- int selectedIndex = -1;
- try {
- for (String sessionString : sessionStrings) {
- final JSONArray windowsArray = new JSONObject(sessionString).getJSONArray("windows");
- if (windowsArray.length() == 0) {
- // Session json can be empty if the user has opted out of session restore.
- Log.d(LOGTAG, "Session restore file is empty, no session entries found.");
- continue;
- }
-
- final JSONObject window = windowsArray.getJSONObject(0);
- final JSONArray tabs = window.getJSONArray("tabs");
- final int optSelected = window.optInt("selected", -1);
- final JSONArray closedTabs = window.optJSONArray("closedTabs");
- if (closedTabs != null) {
- onClosedTabsRead(closedTabs);
- }
-
- for (int i = 0; i < tabs.length(); i++) {
- final JSONObject tab = tabs.getJSONObject(i);
- final int index = tab.getInt("index");
- final JSONArray entries = tab.getJSONArray("entries");
- if (index < 1 || entries.length() < index) {
- Log.w(LOGTAG, "Session entries and index don't agree.");
- continue;
- }
- final JSONObject entry = entries.getJSONObject(index - 1);
- final String url = entry.getString("url");
-
- String title = entry.optString("title");
- if (title.length() == 0) {
- title = url;
- }
-
- totalCount++;
- boolean selected = false;
- if (optSelected == i + 1) {
- selected = true;
- selectedIndex = totalCount;
- }
- sessionTabs.add(new SessionTab(title, url, selected, tab));
- }
- }
- } catch (JSONException e) {
- Log.e(LOGTAG, "JSON error", e);
- return false;
- }
-
- // If no selected index was found, select the first tab.
- if (selectedIndex == -1 && sessionTabs.size() > 0) {
- sessionTabs.getFirst().mIsSelected = true;
- }
-
- for (SessionTab tab : sessionTabs) {
- onTabRead(tab);
- }
-
- return true;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/SharedPreferencesHelper.java b/mobile/android/base/java/org/mozilla/gecko/SharedPreferencesHelper.java
deleted file mode 100644
index 1066da079..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/SharedPreferencesHelper.java
+++ /dev/null
@@ -1,311 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
-import android.util.Log;
-
-import java.util.Map;
-import java.util.HashMap;
-
-/**
- * Helper class to get, set, and observe Android Shared Preferences.
- */
-public final class SharedPreferencesHelper
- implements GeckoEventListener
-{
- public static final String LOGTAG = "GeckoAndSharedPrefs";
-
- // Calculate this once, at initialization. isLoggable is too expensive to
- // have in-line in each log call.
- private static final boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
-
- private enum Scope {
- APP("app"),
- PROFILE("profile"),
- GLOBAL("global");
-
- public final String key;
-
- private Scope(String key) {
- this.key = key;
- }
-
- public static Scope forKey(String key) {
- for (Scope scope : values()) {
- if (scope.key.equals(key)) {
- return scope;
- }
- }
-
- throw new IllegalStateException("SharedPreferences scope must be valid.");
- }
- }
-
- protected final Context mContext;
-
- // mListeners is not synchronized because it is only updated in
- // handleObserve, which is called from Gecko serially.
- protected final Map<String, SharedPreferences.OnSharedPreferenceChangeListener> mListeners;
-
- public SharedPreferencesHelper(Context context) {
- mContext = context;
-
- mListeners = new HashMap<String, SharedPreferences.OnSharedPreferenceChangeListener>();
-
- EventDispatcher dispatcher = GeckoApp.getEventDispatcher();
- if (dispatcher == null) {
- Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException());
- return;
- }
- dispatcher.registerGeckoThreadListener(this,
- "SharedPreferences:Set",
- "SharedPreferences:Get",
- "SharedPreferences:Observe");
- }
-
- public synchronized void uninit() {
- EventDispatcher dispatcher = GeckoApp.getEventDispatcher();
- if (dispatcher == null) {
- Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException());
- return;
- }
- dispatcher.unregisterGeckoThreadListener(this,
- "SharedPreferences:Set",
- "SharedPreferences:Get",
- "SharedPreferences:Observe");
- }
-
- private SharedPreferences getSharedPreferences(JSONObject message) throws JSONException {
- final Scope scope = Scope.forKey(message.getString("scope"));
- switch (scope) {
- case APP:
- return GeckoSharedPrefs.forApp(mContext);
- case PROFILE:
- final String profileName = message.optString("profileName", null);
- if (profileName == null) {
- return GeckoSharedPrefs.forProfile(mContext);
- } else {
- return GeckoSharedPrefs.forProfileName(mContext, profileName);
- }
- case GLOBAL:
- final String branch = message.optString("branch", null);
- if (branch == null) {
- return PreferenceManager.getDefaultSharedPreferences(mContext);
- } else {
- return mContext.getSharedPreferences(branch, Context.MODE_PRIVATE);
- }
- }
-
- return null;
- }
-
- private String getBranch(Scope scope, String profileName, String branch) {
- switch (scope) {
- case APP:
- return GeckoSharedPrefs.APP_PREFS_NAME;
- case PROFILE:
- if (profileName == null) {
- profileName = GeckoProfile.get(mContext).getName();
- }
-
- return GeckoSharedPrefs.PROFILE_PREFS_NAME_PREFIX + profileName;
- case GLOBAL:
- return branch;
- }
-
- return null;
- }
-
- /**
- * Set many SharedPreferences in Android.
- *
- * message.branch must exist, and should be a String SharedPreferences
- * branch name, or null for the default branch.
- * message.preferences should be an array of preferences. Each preference
- * must include a String name, a String type in ["bool", "int", "string"],
- * and an Object value.
- */
- private void handleSet(JSONObject message) throws JSONException {
- SharedPreferences.Editor editor = getSharedPreferences(message).edit();
-
- JSONArray jsonPrefs = message.getJSONArray("preferences");
-
- for (int i = 0; i < jsonPrefs.length(); i++) {
- JSONObject pref = jsonPrefs.getJSONObject(i);
- String name = pref.getString("name");
- String type = pref.getString("type");
- if ("bool".equals(type)) {
- editor.putBoolean(name, pref.getBoolean("value"));
- } else if ("int".equals(type)) {
- editor.putInt(name, pref.getInt("value"));
- } else if ("string".equals(type)) {
- editor.putString(name, pref.getString("value"));
- } else {
- Log.w(LOGTAG, "Unknown pref value type [" + type + "] for pref [" + name + "]");
- }
- editor.apply();
- }
- }
-
- /**
- * Get many SharedPreferences from Android.
- *
- * message.branch must exist, and should be a String SharedPreferences
- * branch name, or null for the default branch.
- * message.preferences should be an array of preferences. Each preference
- * must include a String name, and a String type in ["bool", "int",
- * "string"].
- */
- private JSONArray handleGet(JSONObject message) throws JSONException {
- SharedPreferences prefs = getSharedPreferences(message);
- JSONArray jsonPrefs = message.getJSONArray("preferences");
- JSONArray jsonValues = new JSONArray();
-
- for (int i = 0; i < jsonPrefs.length(); i++) {
- JSONObject pref = jsonPrefs.getJSONObject(i);
- String name = pref.getString("name");
- String type = pref.getString("type");
- JSONObject jsonValue = new JSONObject();
- jsonValue.put("name", name);
- jsonValue.put("type", type);
- try {
- if ("bool".equals(type)) {
- boolean value = prefs.getBoolean(name, false);
- jsonValue.put("value", value);
- } else if ("int".equals(type)) {
- int value = prefs.getInt(name, 0);
- jsonValue.put("value", value);
- } else if ("string".equals(type)) {
- String value = prefs.getString(name, "");
- jsonValue.put("value", value);
- } else {
- Log.w(LOGTAG, "Unknown pref value type [" + type + "] for pref [" + name + "]");
- }
- } catch (ClassCastException e) {
- // Thrown if there is a preference with the given name that is
- // not the right type.
- Log.w(LOGTAG, "Wrong pref value type [" + type + "] for pref [" + name + "]");
- }
- jsonValues.put(jsonValue);
- }
-
- return jsonValues;
- }
-
- private static class ChangeListener
- implements SharedPreferences.OnSharedPreferenceChangeListener {
- public final Scope scope;
- public final String branch;
- public final String profileName;
-
- public ChangeListener(final Scope scope, final String branch, final String profileName) {
- this.scope = scope;
- this.branch = branch;
- this.profileName = profileName;
- }
-
- @Override
- public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
- if (logVerbose) {
- Log.v(LOGTAG, "Got onSharedPreferenceChanged");
- }
- try {
- final JSONObject msg = new JSONObject();
- msg.put("scope", this.scope.key);
- msg.put("branch", this.branch);
- msg.put("profileName", this.profileName);
- msg.put("key", key);
-
- // Truly, this is awful, but the API impedance is strong: there
- // is no way to get a single untyped value from a
- // SharedPreferences instance.
- msg.put("value", sharedPreferences.getAll().get(key));
-
- GeckoAppShell.notifyObservers("SharedPreferences:Changed", msg.toString());
- } catch (JSONException e) {
- Log.e(LOGTAG, "Got exception creating JSON object", e);
- return;
- }
- }
- }
-
- /**
- * Register or unregister a SharedPreferences.OnSharedPreferenceChangeListener.
- *
- * message.branch must exist, and should be a String SharedPreferences
- * branch name, or null for the default branch.
- * message.enable should be a boolean: true to enable listening, false to
- * disable listening.
- */
- private void handleObserve(JSONObject message) throws JSONException {
- final SharedPreferences prefs = getSharedPreferences(message);
- final boolean enable = message.getBoolean("enable");
-
- final Scope scope = Scope.forKey(message.getString("scope"));
- final String profileName = message.optString("profileName", null);
- final String branch = getBranch(scope, profileName, message.optString("branch", null));
-
- if (branch == null) {
- Log.e(LOGTAG, "No branch specified for SharedPreference:Observe; aborting.");
- return;
- }
-
- // mListeners is only modified in this one observer, which is called
- // from Gecko serially.
- if (enable && !this.mListeners.containsKey(branch)) {
- SharedPreferences.OnSharedPreferenceChangeListener listener
- = new ChangeListener(scope, branch, profileName);
- this.mListeners.put(branch, listener);
- prefs.registerOnSharedPreferenceChangeListener(listener);
- }
- if (!enable && this.mListeners.containsKey(branch)) {
- SharedPreferences.OnSharedPreferenceChangeListener listener
- = this.mListeners.remove(branch);
- prefs.unregisterOnSharedPreferenceChangeListener(listener);
- }
- }
-
- @Override
- public void handleMessage(String event, JSONObject message) {
- // Everything here is synchronous and serial, so we need not worry about
- // overwriting an in-progress response.
- try {
- if (event.equals("SharedPreferences:Set")) {
- if (logVerbose) {
- Log.v(LOGTAG, "Got SharedPreferences:Set message.");
- }
- handleSet(message);
- } else if (event.equals("SharedPreferences:Get")) {
- if (logVerbose) {
- Log.v(LOGTAG, "Got SharedPreferences:Get message.");
- }
- JSONObject obj = new JSONObject();
- obj.put("values", handleGet(message));
- EventDispatcher.sendResponse(message, obj);
- } else if (event.equals("SharedPreferences:Observe")) {
- if (logVerbose) {
- Log.v(LOGTAG, "Got SharedPreferences:Observe message.");
- }
- handleObserve(message);
- } else {
- Log.e(LOGTAG, "SharedPreferencesHelper got unexpected message " + event);
- return;
- }
- } catch (JSONException e) {
- Log.e(LOGTAG, "Got exception in handleMessage handling event " + event, e);
- return;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/SiteIdentity.java b/mobile/android/base/java/org/mozilla/gecko/SiteIdentity.java
deleted file mode 100644
index e39d25dd8..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/SiteIdentity.java
+++ /dev/null
@@ -1,249 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.json.JSONObject;
-
-import android.text.TextUtils;
-
-public class SiteIdentity {
- private final String LOGTAG = "GeckoSiteIdentity";
- private SecurityMode mSecurityMode;
- private boolean mSecure;
- private MixedMode mMixedModeActive;
- private MixedMode mMixedModeDisplay;
- private TrackingMode mTrackingMode;
- private String mHost;
- private String mOwner;
- private String mSupplemental;
- private String mCountry;
- private String mVerifier;
- private String mOrigin;
-
- // The order of the items here relate to image levels in
- // site_security_level.xml
- public enum SecurityMode {
- UNKNOWN("unknown"),
- IDENTIFIED("identified"),
- VERIFIED("verified"),
- CHROMEUI("chromeUI");
-
- private final String mId;
-
- private SecurityMode(String id) {
- mId = id;
- }
-
- public static SecurityMode fromString(String id) {
- if (id == null) {
- throw new IllegalArgumentException("Can't convert null String to SiteIdentity");
- }
-
- for (SecurityMode mode : SecurityMode.values()) {
- if (TextUtils.equals(mode.mId, id)) {
- return mode;
- }
- }
-
- throw new IllegalArgumentException("Could not convert String id to SiteIdentity");
- }
-
- @Override
- public String toString() {
- return mId;
- }
- }
-
- // The order of the items here relate to image levels in
- // site_security_level.xml
- public enum MixedMode {
- UNKNOWN("unknown"),
- MIXED_CONTENT_BLOCKED("blocked"),
- MIXED_CONTENT_LOADED("loaded");
-
- private final String mId;
-
- private MixedMode(String id) {
- mId = id;
- }
-
- public static MixedMode fromString(String id) {
- if (id == null) {
- throw new IllegalArgumentException("Can't convert null String to MixedMode");
- }
-
- for (MixedMode mode : MixedMode.values()) {
- if (TextUtils.equals(mode.mId, id.toLowerCase())) {
- return mode;
- }
- }
-
- throw new IllegalArgumentException("Could not convert String id to MixedMode");
- }
-
- @Override
- public String toString() {
- return mId;
- }
- }
-
- // The order of the items here relate to image levels in
- // site_security_level.xml
- public enum TrackingMode {
- UNKNOWN("unknown"),
- TRACKING_CONTENT_BLOCKED("tracking_content_blocked"),
- TRACKING_CONTENT_LOADED("tracking_content_loaded");
-
- private final String mId;
-
- private TrackingMode(String id) {
- mId = id;
- }
-
- public static TrackingMode fromString(String id) {
- if (id == null) {
- throw new IllegalArgumentException("Can't convert null String to TrackingMode");
- }
-
- for (TrackingMode mode : TrackingMode.values()) {
- if (TextUtils.equals(mode.mId, id.toLowerCase())) {
- return mode;
- }
- }
-
- throw new IllegalArgumentException("Could not convert String id to TrackingMode");
- }
-
- @Override
- public String toString() {
- return mId;
- }
- }
-
- public SiteIdentity() {
- reset();
- }
-
- public void resetIdentity() {
- mSecurityMode = SecurityMode.UNKNOWN;
- mOrigin = null;
- mHost = null;
- mOwner = null;
- mSupplemental = null;
- mCountry = null;
- mVerifier = null;
- mSecure = false;
- }
-
- public void reset() {
- resetIdentity();
- mMixedModeActive = MixedMode.UNKNOWN;
- mMixedModeDisplay = MixedMode.UNKNOWN;
- mTrackingMode = TrackingMode.UNKNOWN;
- }
-
- void update(JSONObject identityData) {
- if (identityData == null) {
- reset();
- return;
- }
-
- try {
- JSONObject mode = identityData.getJSONObject("mode");
-
- try {
- mMixedModeDisplay = MixedMode.fromString(mode.getString("mixed_display"));
- } catch (Exception e) {
- mMixedModeDisplay = MixedMode.UNKNOWN;
- }
-
- try {
- mMixedModeActive = MixedMode.fromString(mode.getString("mixed_active"));
- } catch (Exception e) {
- mMixedModeActive = MixedMode.UNKNOWN;
- }
-
- try {
- mTrackingMode = TrackingMode.fromString(mode.getString("tracking"));
- } catch (Exception e) {
- mTrackingMode = TrackingMode.UNKNOWN;
- }
-
- try {
- mSecurityMode = SecurityMode.fromString(mode.getString("identity"));
- } catch (Exception e) {
- resetIdentity();
- return;
- }
-
- try {
- mOrigin = identityData.getString("origin");
- mHost = identityData.optString("host", null);
- mOwner = identityData.optString("owner", null);
- mSupplemental = identityData.optString("supplemental", null);
- mCountry = identityData.optString("country", null);
- mVerifier = identityData.optString("verifier", null);
- mSecure = identityData.optBoolean("secure", false);
- } catch (Exception e) {
- resetIdentity();
- }
- } catch (Exception e) {
- reset();
- }
- }
-
- public SecurityMode getSecurityMode() {
- return mSecurityMode;
- }
-
- public String getOrigin() {
- return mOrigin;
- }
-
- public String getHost() {
- return mHost;
- }
-
- public String getOwner() {
- return mOwner;
- }
-
- public boolean hasOwner() {
- return !TextUtils.isEmpty(mOwner);
- }
-
- public String getSupplemental() {
- return mSupplemental;
- }
-
- public String getCountry() {
- return mCountry;
- }
-
- public boolean hasCountry() {
- return !TextUtils.isEmpty(mCountry);
- }
-
- public String getVerifier() {
- return mVerifier;
- }
-
- public boolean isSecure() {
- return mSecure;
- }
-
- public MixedMode getMixedModeActive() {
- return mMixedModeActive;
- }
-
- public MixedMode getMixedModeDisplay() {
- return mMixedModeDisplay;
- }
-
- public TrackingMode getTrackingMode() {
- return mTrackingMode;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/SnackbarBuilder.java b/mobile/android/base/java/org/mozilla/gecko/SnackbarBuilder.java
deleted file mode 100644
index 3283e7c37..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/SnackbarBuilder.java
+++ /dev/null
@@ -1,257 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.NativeJSObject;
-
-import android.app.Activity;
-import android.graphics.Color;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.InsetDrawable;
-import android.support.annotation.StringRes;
-import android.support.design.widget.Snackbar;
-import android.support.v4.content.ContextCompat;
-import android.text.TextUtils;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.View;
-import android.widget.TextView;
-
-import java.lang.ref.WeakReference;
-
-/**
- * Helper class for creating and dismissing snackbars. Use this class to guarantee a consistent style and behavior
- * across the app.
- */
-public class SnackbarBuilder {
- /**
- * Combined interface for handling all callbacks from a snackbar because anonymous classes can only extend one
- * interface or class.
- */
- public static abstract class SnackbarCallback extends Snackbar.Callback implements View.OnClickListener {}
- public static final String LOGTAG = "GeckoSnackbarBuilder";
-
- /**
- * SnackbarCallback implementation for delegating snackbar events to an EventCallback.
- */
- private static class SnackbarEventCallback extends SnackbarCallback {
- private EventCallback callback;
-
- public SnackbarEventCallback(EventCallback callback) {
- this.callback = callback;
- }
-
- @Override
- public synchronized void onClick(View view) {
- if (callback == null) {
- return;
- }
-
- callback.sendSuccess(null);
- callback = null; // Releasing reference. We only want to execute the callback once.
- }
-
- @Override
- public synchronized void onDismissed(Snackbar snackbar, int event) {
- if (callback == null || event == Snackbar.Callback.DISMISS_EVENT_ACTION) {
- return;
- }
-
- callback.sendError(null);
- callback = null; // Releasing reference. We only want to execute the callback once.
- }
- }
-
- private static final Object currentSnackbarLock = new Object();
- private static WeakReference<Snackbar> currentSnackbar = new WeakReference<>(null); // Guarded by 'currentSnackbarLock'
-
- private final Activity activity;
- private String message;
- private int duration;
- private String action;
- private SnackbarCallback callback;
- private Drawable icon;
- private Integer backgroundColor;
- private Integer actionColor;
-
- /**
- * @param activity Activity to show the snackbar in.
- */
- private SnackbarBuilder(final Activity activity) {
- this.activity = activity;
- }
-
- public static SnackbarBuilder builder(final Activity activity) {
- return new SnackbarBuilder(activity);
- }
-
- /**
- * @param message The text to show. Can be formatted text.
- */
- public SnackbarBuilder message(final String message) {
- this.message = message;
- return this;
- }
-
- /**
- * @param id The id of the string resource to show. Can be formatted text.
- */
- public SnackbarBuilder message(@StringRes final int id) {
- message = activity.getResources().getString(id);
- return this;
- }
-
- /**
- * @param duration How long to display the message.
- */
- public SnackbarBuilder duration(final int duration) {
- this.duration = duration;
- return this;
- }
-
- /**
- * @param action Action text to display.
- */
- public SnackbarBuilder action(final String action) {
- this.action = action;
- return this;
- }
-
- /**
- * @param id The id of the string resource for the action text to display.
- */
- public SnackbarBuilder action(@StringRes final int id) {
- action = activity.getResources().getString(id);
- return this;
- }
-
- /**
- * @param callback Callback to be invoked when the action is clicked or the snackbar is dismissed.
- */
- public SnackbarBuilder callback(final SnackbarCallback callback) {
- this.callback = callback;
- return this;
- }
-
- /**
- * @param callback Callback to be invoked when the action is clicked or the snackbar is dismissed.
- */
- public SnackbarBuilder callback(final EventCallback callback) {
- this.callback = new SnackbarEventCallback(callback);
- return this;
- }
-
- /**
- * @param icon Icon to be displayed with the snackbar text.
- */
- public SnackbarBuilder icon(final Drawable icon) {
- this.icon = icon;
- return this;
- }
-
- /**
- * @param backgroundColor Snackbar background color.
- */
- public SnackbarBuilder backgroundColor(final Integer backgroundColor) {
- this.backgroundColor = backgroundColor;
- return this;
- }
-
- /**
- * @param actionColor Action text color.
- */
- public SnackbarBuilder actionColor(final Integer actionColor) {
- this.actionColor = actionColor;
- return this;
- }
-
- /**
- * @param object Populate the builder with data from a Gecko Snackbar:Show event.
- */
- public SnackbarBuilder fromEvent(final NativeJSObject object) {
- message = object.getString("message");
- duration = object.getInt("duration");
-
- if (object.has("backgroundColor")) {
- final String providedColor = object.getString("backgroundColor");
- try {
- backgroundColor = Color.parseColor(providedColor);
- } catch (IllegalArgumentException e) {
- Log.w(LOGTAG, "Failed to parse color string: " + providedColor);
- }
- }
-
- NativeJSObject actionObject = object.optObject("action", null);
- if (actionObject != null) {
- action = actionObject.optString("label", null);
- }
- return this;
- }
-
- public void buildAndShow() {
- final View parentView = findBestParentView(activity);
- final Snackbar snackbar = Snackbar.make(parentView, message, duration);
-
- if (callback != null && !TextUtils.isEmpty(action)) {
- snackbar.setAction(action, callback);
- if (actionColor == null) {
- snackbar.setActionTextColor(ContextCompat.getColor(activity, R.color.fennec_ui_orange));
- } else {
- snackbar.setActionTextColor(actionColor);
- }
- snackbar.setCallback(callback);
- }
-
- if (icon != null) {
- int leftPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, activity.getResources().getDisplayMetrics());
-
- final InsetDrawable paddedIcon = new InsetDrawable(icon, 0, 0, leftPadding, 0);
-
- paddedIcon.setBounds(0, 0, leftPadding + icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
-
- TextView textView = (TextView) snackbar.getView().findViewById(android.support.design.R.id.snackbar_text);
- textView.setCompoundDrawables(paddedIcon, null, null, null);
- }
-
- if (backgroundColor != null) {
- snackbar.getView().setBackgroundColor(backgroundColor);
- }
-
- snackbar.show();
-
- synchronized (currentSnackbarLock) {
- currentSnackbar = new WeakReference<>(snackbar);
- }
- }
-
- /**
- * Dismiss the currently visible snackbar.
- */
- public static void dismissCurrentSnackbar() {
- synchronized (currentSnackbarLock) {
- final Snackbar snackbar = currentSnackbar.get();
- if (snackbar != null && snackbar.isShown()) {
- snackbar.dismiss();
- }
- }
- }
-
- /**
- * Find the best parent view to hold the Snackbar's view. The Snackbar implementation of the support
- * library will use this view to walk up the view tree to find an actual suitable parent (if needed).
- */
- private static View findBestParentView(Activity activity) {
- if (activity instanceof GeckoApp) {
- final View view = activity.findViewById(R.id.root_layout);
- if (view != null) {
- return view;
- }
- }
-
- return activity.findViewById(android.R.id.content);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/SuggestClient.java b/mobile/android/base/java/org/mozilla/gecko/SuggestClient.java
deleted file mode 100644
index e43bbef1f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/SuggestClient.java
+++ /dev/null
@@ -1,142 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import java.io.BufferedInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.net.URLEncoder;
-import java.util.ArrayList;
-
-import org.json.JSONArray;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.util.HardwareUtils;
-
-import android.content.Context;
-import android.text.TextUtils;
-import android.util.Log;
-import org.mozilla.gecko.util.NetworkUtils;
-
-/**
- * Use network-based search suggestions.
- */
-public class SuggestClient {
- private static final String LOGTAG = "GeckoSuggestClient";
-
- // This should go through GeckoInterface to get the UA, but the search activity
- // doesn't use a GeckoView yet. Until it does, get the UA directly.
- private static final String USER_AGENT = HardwareUtils.isTablet() ?
- AppConstants.USER_AGENT_FENNEC_TABLET : AppConstants.USER_AGENT_FENNEC_MOBILE;
-
- private final Context mContext;
- private final int mTimeout;
-
- // should contain the string "__searchTerms__", which is replaced with the query
- private final String mSuggestTemplate;
-
- // the maximum number of suggestions to return
- private final int mMaxResults;
-
- // used by robocop for testing
- private final boolean mCheckNetwork;
-
- // used to make suggestions appear instantly after opt-in
- private String mPrevQuery;
- private ArrayList<String> mPrevResults;
-
- @RobocopTarget
- public SuggestClient(Context context, String suggestTemplate, int timeout, int maxResults, boolean checkNetwork) {
- mContext = context;
- mMaxResults = maxResults;
- mSuggestTemplate = suggestTemplate;
- mTimeout = timeout;
- mCheckNetwork = checkNetwork;
- }
-
- public String getSuggestTemplate() {
- return mSuggestTemplate;
- }
-
- /**
- * Queries for a given search term and returns an ArrayList of suggestions.
- */
- public ArrayList<String> query(String query) {
- if (query.equals(mPrevQuery))
- return mPrevResults;
-
- ArrayList<String> suggestions = new ArrayList<String>();
- if (TextUtils.isEmpty(mSuggestTemplate) || TextUtils.isEmpty(query)) {
- return suggestions;
- }
-
- if (!NetworkUtils.isConnected(mContext) && mCheckNetwork) {
- Log.i(LOGTAG, "Not connected to network");
- return suggestions;
- }
-
- try {
- String encoded = URLEncoder.encode(query, "UTF-8");
- String suggestUri = mSuggestTemplate.replace("__searchTerms__", encoded);
-
- URL url = new URL(suggestUri);
- String json = null;
- HttpURLConnection urlConnection = null;
- InputStream in = null;
- try {
- urlConnection = (HttpURLConnection) url.openConnection();
- urlConnection.setConnectTimeout(mTimeout);
- urlConnection.setRequestProperty("User-Agent", USER_AGENT);
- in = new BufferedInputStream(urlConnection.getInputStream());
- json = convertStreamToString(in);
- } finally {
- if (urlConnection != null)
- urlConnection.disconnect();
- if (in != null) {
- try {
- in.close();
- } catch (IOException e) {
- Log.e(LOGTAG, "error", e);
- }
- }
- }
-
- if (json != null) {
- /*
- * Sample result:
- * ["foo",["food network","foothill college","foot locker",...]]
- */
- JSONArray results = new JSONArray(json);
- JSONArray jsonSuggestions = results.getJSONArray(1);
-
- int added = 0;
- for (int i = 0; (i < jsonSuggestions.length()) && (added < mMaxResults); i++) {
- String suggestion = jsonSuggestions.getString(i);
- if (!suggestion.equalsIgnoreCase(query)) {
- suggestions.add(suggestion);
- added++;
- }
- }
- } else {
- Log.e(LOGTAG, "Suggestion query failed");
- }
- } catch (Exception e) {
- Log.e(LOGTAG, "Error", e);
- }
-
- mPrevQuery = query;
- mPrevResults = suggestions;
- return suggestions;
- }
-
- private String convertStreamToString(java.io.InputStream is) {
- try {
- return new java.util.Scanner(is).useDelimiter("\\A").next();
- } catch (java.util.NoSuchElementException e) {
- return "";
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/Tab.java b/mobile/android/base/java/org/mozilla/gecko/Tab.java
deleted file mode 100644
index 6010a3dd9..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/Tab.java
+++ /dev/null
@@ -1,843 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.Future;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.db.URLMetadata;
-import org.mozilla.gecko.gfx.BitmapUtils;
-import org.mozilla.gecko.icons.IconCallback;
-import org.mozilla.gecko.icons.IconDescriptor;
-import org.mozilla.gecko.icons.IconRequestBuilder;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.icons.Icons;
-import org.mozilla.gecko.reader.ReaderModeUtils;
-import org.mozilla.gecko.reader.ReadingListHelper;
-import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.widget.SiteLogins;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.drawable.BitmapDrawable;
-import android.os.Build;
-import android.os.Bundle;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.View;
-
-public class Tab {
- private static final String LOGTAG = "GeckoTab";
-
- private static Pattern sColorPattern;
- private final int mId;
- private final BrowserDB mDB;
- private long mLastUsed;
- private String mUrl;
- private String mBaseDomain;
- private String mUserRequested; // The original url requested. May be typed by the user or sent by an extneral app for example.
- private String mTitle;
- private Bitmap mFavicon;
- private String mFaviconUrl;
- private String mApplicationId; // Intended to be null after explicit user action.
-
- private IconRequestBuilder mIconRequestBuilder;
- private Future<IconResponse> mRunningIconRequest;
-
- private boolean mHasFeeds;
- private boolean mHasOpenSearch;
- private final SiteIdentity mSiteIdentity;
- private SiteLogins mSiteLogins;
- private BitmapDrawable mThumbnail;
- private final int mParentId;
- // Indicates the url was loaded from a source external to the app. This will be cleared
- // when the user explicitly loads a new url (e.g. clicking a link is not explicit).
- private final boolean mExternal;
- private boolean mBookmark;
- private int mFaviconLoadId;
- private String mContentType;
- private boolean mHasTouchListeners;
- private final ArrayList<View> mPluginViews;
- private int mState;
- private Bitmap mThumbnailBitmap;
- private boolean mDesktopMode;
- private boolean mEnteringReaderMode;
- private final Context mAppContext;
- private ErrorType mErrorType = ErrorType.NONE;
- private volatile int mLoadProgress;
- private volatile int mRecordingCount;
- private volatile boolean mIsAudioPlaying;
- private volatile boolean mIsMediaPlaying;
- private String mMostRecentHomePanel;
- private boolean mShouldShowToolbarWithoutAnimationOnFirstSelection;
-
- /*
- * Bundle containing restore data for the panel referenced in mMostRecentHomePanel. This can be
- * e.g. the most recent folder for the bookmarks panel, or any other state that should be
- * persisted. This is then used e.g. when returning to homepanels via history.
- */
- private Bundle mMostRecentHomePanelData;
-
- private int mHistoryIndex;
- private int mHistorySize;
- private boolean mCanDoBack;
- private boolean mCanDoForward;
-
- private boolean mIsEditing;
- private final TabEditingState mEditingState = new TabEditingState();
-
- // Will be true when tab is loaded from cache while device was offline.
- private boolean mLoadedFromCache;
-
- public static final int STATE_DELAYED = 0;
- public static final int STATE_LOADING = 1;
- public static final int STATE_SUCCESS = 2;
- public static final int STATE_ERROR = 3;
-
- public static final int LOAD_PROGRESS_INIT = 10;
- public static final int LOAD_PROGRESS_START = 20;
- public static final int LOAD_PROGRESS_LOCATION_CHANGE = 60;
- public static final int LOAD_PROGRESS_LOADED = 80;
- public static final int LOAD_PROGRESS_STOP = 100;
-
- public enum ErrorType {
- CERT_ERROR, // Pages with certificate problems
- BLOCKED, // Pages blocked for phishing or malware warnings
- NET_ERROR, // All other types of error
- NONE // Non error pages
- }
-
- public Tab(Context context, int id, String url, boolean external, int parentId, String title) {
- mAppContext = context.getApplicationContext();
- mDB = BrowserDB.from(context);
- mId = id;
- mUrl = url;
- mBaseDomain = "";
- mUserRequested = "";
- mExternal = external;
- mParentId = parentId;
- mTitle = title == null ? "" : title;
- mSiteIdentity = new SiteIdentity();
- mHistoryIndex = -1;
- mContentType = "";
- mPluginViews = new ArrayList<View>();
- mState = shouldShowProgress(url) ? STATE_LOADING : STATE_SUCCESS;
- mLoadProgress = LOAD_PROGRESS_INIT;
- mIconRequestBuilder = Icons.with(mAppContext).pageUrl(mUrl);
-
- updateBookmark();
- }
-
- private ContentResolver getContentResolver() {
- return mAppContext.getContentResolver();
- }
-
- public void onDestroy() {
- Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.CLOSED);
- }
-
- @RobocopTarget
- public int getId() {
- return mId;
- }
-
- public synchronized void onChange() {
- mLastUsed = System.currentTimeMillis();
- }
-
- public synchronized long getLastUsed() {
- return mLastUsed;
- }
-
- public int getParentId() {
- return mParentId;
- }
-
- // may be null if user-entered query hasn't yet been resolved to a URI
- public synchronized String getURL() {
- return mUrl;
- }
-
- // mUserRequested should never be null, but it may be an empty string
- public synchronized String getUserRequested() {
- return mUserRequested;
- }
-
- // mTitle should never be null, but it may be an empty string
- public synchronized String getTitle() {
- return mTitle;
- }
-
- public String getDisplayTitle() {
- if (mTitle != null && mTitle.length() > 0) {
- return mTitle;
- }
-
- return mUrl;
- }
-
- /**
- * Returns the base domain of the loaded uri. Note that if the page is
- * a Reader mode uri, the base domain returned is that of the original uri.
- */
- public String getBaseDomain() {
- return mBaseDomain;
- }
-
- public Bitmap getFavicon() {
- return mFavicon;
- }
-
- protected String getApplicationId() {
- return mApplicationId;
- }
-
- protected void setApplicationId(final String applicationId) {
- mApplicationId = applicationId;
- }
-
- public BitmapDrawable getThumbnail() {
- return mThumbnail;
- }
-
- public String getMostRecentHomePanel() {
- return mMostRecentHomePanel;
- }
-
- public Bundle getMostRecentHomePanelData() {
- return mMostRecentHomePanelData;
- }
-
- public void setMostRecentHomePanel(String panelId) {
- mMostRecentHomePanel = panelId;
- mMostRecentHomePanelData = null;
- }
-
- public void setMostRecentHomePanelData(Bundle data) {
- mMostRecentHomePanelData = data;
- }
-
- public Bitmap getThumbnailBitmap(int width, int height) {
- if (mThumbnailBitmap != null) {
- // Bug 787318 - Honeycomb has a bug with bitmap caching, we can't
- // reuse the bitmap there.
- boolean honeycomb = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB
- && Build.VERSION.SDK_INT <= Build.VERSION_CODES.HONEYCOMB_MR2);
- boolean sizeChange = mThumbnailBitmap.getWidth() != width
- || mThumbnailBitmap.getHeight() != height;
- if (honeycomb || sizeChange) {
- mThumbnailBitmap = null;
- }
- }
-
- if (mThumbnailBitmap == null) {
- Bitmap.Config config = (GeckoAppShell.getScreenDepth() == 24) ?
- Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
- mThumbnailBitmap = Bitmap.createBitmap(width, height, config);
- }
-
- return mThumbnailBitmap;
- }
-
- public void updateThumbnail(final Bitmap b, final ThumbnailHelper.CachePolicy cachePolicy) {
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- if (b != null) {
- try {
- mThumbnail = new BitmapDrawable(mAppContext.getResources(), b);
- if (mState == Tab.STATE_SUCCESS && cachePolicy == ThumbnailHelper.CachePolicy.STORE) {
- saveThumbnailToDB(mDB);
- } else {
- // If the page failed to load, or requested that we not cache info about it, clear any previous
- // thumbnails we've stored.
- clearThumbnailFromDB(mDB);
- }
- } catch (OutOfMemoryError oom) {
- Log.w(LOGTAG, "Unable to create/scale bitmap.", oom);
- mThumbnail = null;
- }
- } else {
- mThumbnail = null;
- }
-
- Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.THUMBNAIL);
- }
- });
- }
-
- public synchronized String getFaviconURL() {
- return mFaviconUrl;
- }
-
- public boolean hasFeeds() {
- return mHasFeeds;
- }
-
- public boolean hasOpenSearch() {
- return mHasOpenSearch;
- }
-
- public boolean hasLoadedFromCache() {
- return mLoadedFromCache;
- }
-
- public SiteIdentity getSiteIdentity() {
- return mSiteIdentity;
- }
-
- public void resetSiteIdentity() {
- if (mSiteIdentity != null) {
- mSiteIdentity.reset();
- Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.SECURITY_CHANGE);
- }
- }
-
- public SiteLogins getSiteLogins() {
- return mSiteLogins;
- }
-
- public boolean isBookmark() {
- return mBookmark;
- }
-
- public boolean isExternal() {
- return mExternal;
- }
-
- public synchronized void updateURL(String url) {
- if (url != null && url.length() > 0) {
- mUrl = url;
- }
- }
-
- public synchronized void updateUserRequested(String userRequested) {
- mUserRequested = userRequested;
- }
-
- public void setErrorType(String type) {
- if ("blocked".equals(type))
- setErrorType(ErrorType.BLOCKED);
- else if ("certerror".equals(type))
- setErrorType(ErrorType.CERT_ERROR);
- else if ("neterror".equals(type))
- setErrorType(ErrorType.NET_ERROR);
- else
- setErrorType(ErrorType.NONE);
- }
-
- public void setErrorType(ErrorType type) {
- mErrorType = type;
- }
-
- public void setMetadata(JSONObject metadata) {
- if (metadata == null) {
- return;
- }
-
- final ContentResolver cr = mAppContext.getContentResolver();
- final URLMetadata urlMetadata = mDB.getURLMetadata();
-
- final Map<String, Object> data = urlMetadata.fromJSON(metadata);
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- urlMetadata.save(cr, data);
- }
- });
- }
-
- public ErrorType getErrorType() {
- return mErrorType;
- }
-
- public void setContentType(String contentType) {
- mContentType = (contentType == null) ? "" : contentType;
- }
-
- public String getContentType() {
- return mContentType;
- }
-
- public int getHistoryIndex() {
- return mHistoryIndex;
- }
-
- public int getHistorySize() {
- return mHistorySize;
- }
-
- public synchronized void updateTitle(String title) {
- // Keep the title unchanged while entering reader mode.
- if (mEnteringReaderMode) {
- return;
- }
-
- // If there was a title, but it hasn't changed, do nothing.
- if (mTitle != null &&
- TextUtils.equals(mTitle, title)) {
- return;
- }
-
- mTitle = (title == null ? "" : title);
- Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.TITLE);
- }
-
- public void setState(int state) {
- mState = state;
-
- if (mState != Tab.STATE_LOADING)
- mEnteringReaderMode = false;
- }
-
- public int getState() {
- return mState;
- }
-
- public void setHasTouchListeners(boolean aValue) {
- mHasTouchListeners = aValue;
- }
-
- public boolean getHasTouchListeners() {
- return mHasTouchListeners;
- }
-
- public synchronized void addFavicon(String faviconURL, int faviconSize, String mimeType) {
- mIconRequestBuilder
- .icon(IconDescriptor.createFavicon(faviconURL, faviconSize, mimeType))
- .deferBuild();
- }
-
- public synchronized void addTouchicon(String iconUrl, int faviconSize, String mimeType) {
- mIconRequestBuilder
- .icon(IconDescriptor.createTouchicon(iconUrl, faviconSize, mimeType))
- .deferBuild();
- }
-
- public void loadFavicon() {
- // Static Favicons never change
- if (AboutPages.isBuiltinIconPage(mUrl) && mFavicon != null) {
- return;
- }
-
- mRunningIconRequest = mIconRequestBuilder
- .build()
- .execute(new IconCallback() {
- @Override
- public void onIconResponse(IconResponse response) {
- mFavicon = response.getBitmap();
-
- Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.FAVICON);
- }
- });
- }
-
- public synchronized void clearFavicon() {
- // Cancel any ongoing favicon load (if we never finished downloading the old favicon before
- // we changed page).
- if (mRunningIconRequest != null) {
- mRunningIconRequest.cancel(true);
- }
-
- // Keep the favicon unchanged while entering reader mode
- if (mEnteringReaderMode)
- return;
-
- mFavicon = null;
- mFaviconUrl = null;
- }
-
- public void setHasFeeds(boolean hasFeeds) {
- mHasFeeds = hasFeeds;
- }
-
- public void setHasOpenSearch(boolean hasOpenSearch) {
- mHasOpenSearch = hasOpenSearch;
- }
-
- public void setLoadedFromCache(boolean loadedFromCache) {
- mLoadedFromCache = loadedFromCache;
- }
-
- public void updateIdentityData(JSONObject identityData) {
- mSiteIdentity.update(identityData);
- }
-
- public void setSiteLogins(SiteLogins siteLogins) {
- mSiteLogins = siteLogins;
- }
-
- void updateBookmark() {
- if (getURL() == null) {
- return;
- }
-
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- final String url = getURL();
- if (url == null) {
- return;
- }
- final String pageUrl = ReaderModeUtils.stripAboutReaderUrl(url);
-
- mBookmark = mDB.isBookmark(getContentResolver(), pageUrl);
- Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.MENU_UPDATED);
- }
- });
- }
-
- public void addBookmark() {
- final String url = getURL();
- if (url == null) {
- return;
- }
-
- final String pageUrl = ReaderModeUtils.stripAboutReaderUrl(getURL());
-
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- mDB.addBookmark(getContentResolver(), mTitle, pageUrl);
- Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.BOOKMARK_ADDED);
- }
- });
-
- if (AboutPages.isAboutReader(url)) {
- ReadingListHelper.cacheReaderItem(pageUrl, mId, mAppContext);
- }
- }
-
- public void removeBookmark() {
- final String url = getURL();
- if (url == null) {
- return;
- }
-
- final String pageUrl = ReaderModeUtils.stripAboutReaderUrl(getURL());
-
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- mDB.removeBookmarksWithURL(getContentResolver(), pageUrl);
- Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.BOOKMARK_REMOVED);
- }
- });
-
- // We need to ensure we remove readercached items here - we could have switched out of readermode
- // before unbookmarking, so we don't necessarily have an about:reader URL here.
- ReadingListHelper.removeCachedReaderItem(pageUrl, mAppContext);
- }
-
- public boolean isEnteringReaderMode() {
- return mEnteringReaderMode;
- }
-
- public void doReload(boolean bypassCache) {
- GeckoAppShell.notifyObservers("Session:Reload", "{\"bypassCache\":" + String.valueOf(bypassCache) + "}");
- }
-
- // Our version of nsSHistory::GetCanGoBack
- public boolean canDoBack() {
- return mCanDoBack;
- }
-
- public boolean doBack() {
- if (!canDoBack())
- return false;
-
- GeckoAppShell.notifyObservers("Session:Back", "");
- return true;
- }
-
- public void doStop() {
- GeckoAppShell.notifyObservers("Session:Stop", "");
- }
-
- // Our version of nsSHistory::GetCanGoForward
- public boolean canDoForward() {
- return mCanDoForward;
- }
-
- public boolean doForward() {
- if (!canDoForward())
- return false;
-
- GeckoAppShell.notifyObservers("Session:Forward", "");
- return true;
- }
-
- void handleLocationChange(JSONObject message) throws JSONException {
- final String uri = message.getString("uri");
- final String oldUrl = getURL();
- final boolean sameDocument = message.getBoolean("sameDocument");
- mEnteringReaderMode = ReaderModeUtils.isEnteringReaderMode(oldUrl, uri);
- mHistoryIndex = message.getInt("historyIndex");
- mHistorySize = message.getInt("historySize");
- mCanDoBack = message.getBoolean("canGoBack");
- mCanDoForward = message.getBoolean("canGoForward");
-
- if (!TextUtils.equals(oldUrl, uri)) {
- updateURL(uri);
- updateBookmark();
- if (!sameDocument) {
- // We can unconditionally clear the favicon and title here: we
- // already filtered both cases in which this was a (pseudo-)
- // spurious location change, so we're definitely loading a new
- // page.
- clearFavicon();
-
- // Start to build a new request to load a favicon.
- mIconRequestBuilder = Icons.with(mAppContext)
- .pageUrl(uri);
-
- // Load local static Favicons immediately
- if (AboutPages.isBuiltinIconPage(uri)) {
- loadFavicon();
- }
-
- updateTitle(null);
- }
- }
-
- if (sameDocument) {
- // We can get a location change event for the same document with an anchor tag
- // Notify listeners so that buttons like back or forward will update themselves
- Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.LOCATION_CHANGE, oldUrl);
- return;
- }
-
- setContentType(message.getString("contentType"));
- updateUserRequested(message.getString("userRequested"));
- mBaseDomain = message.optString("baseDomain");
-
- setHasFeeds(false);
- setHasOpenSearch(false);
- mSiteIdentity.reset();
- setSiteLogins(null);
- setHasTouchListeners(false);
- setErrorType(ErrorType.NONE);
- setLoadProgressIfLoading(LOAD_PROGRESS_LOCATION_CHANGE);
-
- Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.LOCATION_CHANGE, oldUrl);
- }
-
- private static boolean shouldShowProgress(final String url) {
- return !AboutPages.isAboutPage(url);
- }
-
- void handleDocumentStart(boolean restoring, String url) {
- setLoadProgress(LOAD_PROGRESS_START);
- setState((!restoring && shouldShowProgress(url)) ? STATE_LOADING : STATE_SUCCESS);
- mSiteIdentity.reset();
- }
-
- void handleDocumentStop(boolean success) {
- setState(success ? STATE_SUCCESS : STATE_ERROR);
-
- final String oldURL = getURL();
- final Tab tab = this;
- tab.setLoadProgress(LOAD_PROGRESS_STOP);
-
- ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() {
- @Override
- public void run() {
- // tab.getURL() may return null
- if (!TextUtils.equals(oldURL, getURL()))
- return;
-
- ThumbnailHelper.getInstance().getAndProcessThumbnailFor(tab);
- }
- }, 500);
- }
-
- void handleContentLoaded() {
- setLoadProgressIfLoading(LOAD_PROGRESS_LOADED);
- }
-
- protected void saveThumbnailToDB(final BrowserDB db) {
- final BitmapDrawable thumbnail = mThumbnail;
- if (thumbnail == null) {
- return;
- }
-
- try {
- final String url = getURL();
- if (url == null) {
- return;
- }
-
- db.updateThumbnailForUrl(getContentResolver(), url, thumbnail);
- } catch (Exception e) {
- // ignore
- }
- }
-
- public void loadThumbnailFromDB(final BrowserDB db) {
- try {
- final String url = getURL();
- if (url == null) {
- return;
- }
-
- byte[] thumbnail = db.getThumbnailForUrl(getContentResolver(), url);
- if (thumbnail == null) {
- return;
- }
-
- Bitmap bitmap = BitmapUtils.decodeByteArray(thumbnail);
- mThumbnail = new BitmapDrawable(mAppContext.getResources(), bitmap);
-
- Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.THUMBNAIL);
- } catch (Exception e) {
- // ignore
- }
- }
-
- private void clearThumbnailFromDB(final BrowserDB db) {
- try {
- final String url = getURL();
- if (url == null) {
- return;
- }
-
- // Passing in a null thumbnail will delete the stored thumbnail for this url
- db.updateThumbnailForUrl(getContentResolver(), url, null);
- } catch (Exception e) {
- // ignore
- }
- }
-
- public void addPluginView(View view) {
- mPluginViews.add(view);
- }
-
- public void removePluginView(View view) {
- mPluginViews.remove(view);
- }
-
- public View[] getPluginViews() {
- return mPluginViews.toArray(new View[mPluginViews.size()]);
- }
-
- public void setDesktopMode(boolean enabled) {
- mDesktopMode = enabled;
- }
-
- public boolean getDesktopMode() {
- return mDesktopMode;
- }
-
- public boolean isPrivate() {
- return false;
- }
-
- /**
- * Sets the tab load progress to the given percentage.
- *
- * @param progressPercentage Percentage to set progress to (0-100)
- */
- void setLoadProgress(int progressPercentage) {
- mLoadProgress = progressPercentage;
- }
-
- /**
- * Sets the tab load progress to the given percentage only if the tab is
- * currently loading.
- *
- * about:neterror can trigger a STOP before other page load events (bug
- * 976426), so any post-START events should make sure the page is loading
- * before updating progress.
- *
- * @param progressPercentage Percentage to set progress to (0-100)
- */
- void setLoadProgressIfLoading(int progressPercentage) {
- if (getState() == STATE_LOADING) {
- setLoadProgress(progressPercentage);
- }
- }
-
- /**
- * Gets the tab load progress percentage.
- *
- * @return Current progress percentage
- */
- public int getLoadProgress() {
- return mLoadProgress;
- }
-
- public void setRecording(boolean isRecording) {
- if (isRecording) {
- mRecordingCount++;
- } else {
- mRecordingCount--;
- }
- }
-
- public boolean isRecording() {
- return mRecordingCount > 0;
- }
-
- /**
- * The "MediaPlaying" is used for controling media control interface and
- * means the tab has playing media.
- *
- * @param isMediaPlaying the tab has any playing media or not
- */
- public void setIsMediaPlaying(boolean isMediaPlaying) {
- mIsMediaPlaying = isMediaPlaying;
- }
-
- public boolean isMediaPlaying() {
- return mIsMediaPlaying;
- }
-
- /**
- * The "AudioPlaying" is used for showing the tab sound indicator and means
- * the tab has playing media and the media is audible.
- *
- * @param isAudioPlaying the tab has any audible playing media or not
- */
- public void setIsAudioPlaying(boolean isAudioPlaying) {
- mIsAudioPlaying = isAudioPlaying;
- }
-
- public boolean isAudioPlaying() {
- return mIsAudioPlaying;
- }
-
- public boolean isEditing() {
- return mIsEditing;
- }
-
- public void setIsEditing(final boolean isEditing) {
- this.mIsEditing = isEditing;
- }
-
- public TabEditingState getEditingState() {
- return mEditingState;
- }
-
- public void setShouldShowToolbarWithoutAnimationOnFirstSelection(final boolean shouldShowWithoutAnimation) {
- mShouldShowToolbarWithoutAnimationOnFirstSelection = shouldShowWithoutAnimation;
- }
-
- public boolean getShouldShowToolbarWithoutAnimationOnFirstSelection() {
- return mShouldShowToolbarWithoutAnimationOnFirstSelection;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/Tabs.java b/mobile/android/base/java/org/mozilla/gecko/Tabs.java
deleted file mode 100644
index c7e024fe0..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/Tabs.java
+++ /dev/null
@@ -1,1021 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import android.support.annotation.Nullable;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import org.mozilla.gecko.annotation.JNITarget;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.gfx.LayerView;
-import org.mozilla.gecko.mozglue.SafeIntent;
-import org.mozilla.gecko.notifications.WhatsNewReceiver;
-import org.mozilla.gecko.reader.ReaderModeUtils;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.accounts.Account;
-import android.accounts.AccountManager;
-import android.accounts.OnAccountsUpdateListener;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.database.sqlite.SQLiteException;
-import android.graphics.Color;
-import android.net.Uri;
-import android.os.Handler;
-import android.provider.Browser;
-import android.support.v4.content.ContextCompat;
-import android.util.Log;
-
-public class Tabs implements GeckoEventListener {
- private static final String LOGTAG = "GeckoTabs";
-
- // mOrder and mTabs are always of the same cardinality, and contain the same values.
- private final CopyOnWriteArrayList<Tab> mOrder = new CopyOnWriteArrayList<Tab>();
-
- // All writes to mSelectedTab must be synchronized on the Tabs instance.
- // In general, it's preferred to always use selectTab()).
- private volatile Tab mSelectedTab;
-
- // All accesses to mTabs must be synchronized on the Tabs instance.
- private final HashMap<Integer, Tab> mTabs = new HashMap<Integer, Tab>();
-
- private AccountManager mAccountManager;
- private OnAccountsUpdateListener mAccountListener;
-
- public static final int LOADURL_NONE = 0;
- public static final int LOADURL_NEW_TAB = 1 << 0;
- public static final int LOADURL_USER_ENTERED = 1 << 1;
- public static final int LOADURL_PRIVATE = 1 << 2;
- public static final int LOADURL_PINNED = 1 << 3;
- public static final int LOADURL_DELAY_LOAD = 1 << 4;
- public static final int LOADURL_DESKTOP = 1 << 5;
- public static final int LOADURL_BACKGROUND = 1 << 6;
- /** Indicates the url has been specified by a source external to the app. */
- public static final int LOADURL_EXTERNAL = 1 << 7;
- /** Indicates the tab is the first shown after Firefox is hidden and restored. */
- public static final int LOADURL_FIRST_AFTER_ACTIVITY_UNHIDDEN = 1 << 8;
-
- private static final long PERSIST_TABS_AFTER_MILLISECONDS = 1000 * 2;
-
- public static final int INVALID_TAB_ID = -1;
-
- private static final AtomicInteger sTabId = new AtomicInteger(0);
- private volatile boolean mInitialTabsAdded;
-
- private Context mAppContext;
- private LayerView mLayerView;
- private ContentObserver mBookmarksContentObserver;
- private PersistTabsRunnable mPersistTabsRunnable;
- private int mPrivateClearColor;
-
- private static class PersistTabsRunnable implements Runnable {
- private final BrowserDB db;
- private final Context context;
- private final Iterable<Tab> tabs;
-
- public PersistTabsRunnable(final Context context, Iterable<Tab> tabsInOrder) {
- this.context = context;
- this.db = BrowserDB.from(context);
- this.tabs = tabsInOrder;
- }
-
- @Override
- public void run() {
- try {
- db.getTabsAccessor().persistLocalTabs(context.getContentResolver(), tabs);
- } catch (SQLiteException e) {
- Log.w(LOGTAG, "Error persisting local tabs", e);
- }
- }
- };
-
- private Tabs() {
- EventDispatcher.getInstance().registerGeckoThreadListener(this,
- "Tab:Added",
- "Tab:Close",
- "Tab:Select",
- "Content:LocationChange",
- "Content:SecurityChange",
- "Content:StateChange",
- "Content:LoadError",
- "Content:PageShow",
- "DOMTitleChanged",
- "Link:Favicon",
- "Link:Touchicon",
- "Link:Feed",
- "Link:OpenSearch",
- "DesktopMode:Changed",
- "Tab:StreamStart",
- "Tab:StreamStop",
- "Tab:AudioPlayingChange",
- "Tab:MediaPlaybackChange");
-
- mPrivateClearColor = Color.RED;
-
- }
-
- public synchronized void attachToContext(Context context, LayerView layerView) {
- final Context appContext = context.getApplicationContext();
- if (mAppContext == appContext) {
- return;
- }
-
- if (mAppContext != null) {
- // This should never happen.
- Log.w(LOGTAG, "The application context has changed!");
- }
-
- mAppContext = appContext;
- mLayerView = layerView;
- mPrivateClearColor = ContextCompat.getColor(context, R.color.tabs_tray_grey_pressed);
- mAccountManager = AccountManager.get(appContext);
-
- mAccountListener = new OnAccountsUpdateListener() {
- @Override
- public void onAccountsUpdated(Account[] accounts) {
- queuePersistAllTabs();
- }
- };
-
- // The listener will run on the background thread (see 2nd argument).
- mAccountManager.addOnAccountsUpdatedListener(mAccountListener, ThreadUtils.getBackgroundHandler(), false);
-
- if (mBookmarksContentObserver != null) {
- // It's safe to use the db here since we aren't doing any I/O.
- final GeckoProfile profile = GeckoProfile.get(context);
- BrowserDB.from(profile).registerBookmarkObserver(getContentResolver(), mBookmarksContentObserver);
- }
- }
-
- /**
- * Gets the tab count corresponding to the private state of the selected
- * tab.
- *
- * If the selected tab is a non-private tab, this will return the number of
- * non-private tabs; likewise, if this is a private tab, this will return
- * the number of private tabs.
- *
- * @return the number of tabs in the current private state
- */
- public synchronized int getDisplayCount() {
- // Once mSelectedTab is non-null, it cannot be null for the remainder
- // of the object's lifetime.
- boolean getPrivate = mSelectedTab != null && mSelectedTab.isPrivate();
- int count = 0;
- for (Tab tab : mOrder) {
- if (tab.isPrivate() == getPrivate) {
- count++;
- }
- }
- return count;
- }
-
- public int isOpen(String url) {
- for (Tab tab : mOrder) {
- if (tab.getURL().equals(url)) {
- return tab.getId();
- }
- }
- return -1;
- }
-
- // Must be synchronized to avoid racing on mBookmarksContentObserver.
- private void lazyRegisterBookmarkObserver() {
- if (mBookmarksContentObserver == null) {
- mBookmarksContentObserver = new ContentObserver(null) {
- @Override
- public void onChange(boolean selfChange) {
- for (Tab tab : mOrder) {
- tab.updateBookmark();
- }
- }
- };
-
- // It's safe to use the db here since we aren't doing any I/O.
- final GeckoProfile profile = GeckoProfile.get(mAppContext);
- BrowserDB.from(profile).registerBookmarkObserver(getContentResolver(), mBookmarksContentObserver);
- }
- }
-
- private Tab addTab(int id, String url, boolean external, int parentId, String title, boolean isPrivate, int tabIndex) {
- final Tab tab = isPrivate ? new PrivateTab(mAppContext, id, url, external, parentId, title) :
- new Tab(mAppContext, id, url, external, parentId, title);
- synchronized (this) {
- lazyRegisterBookmarkObserver();
- mTabs.put(id, tab);
-
- if (tabIndex > -1) {
- mOrder.add(tabIndex, tab);
- } else {
- mOrder.add(tab);
- }
- }
-
- // Suppress the ADDED event to prevent animation of tabs created via session restore.
- if (mInitialTabsAdded) {
- notifyListeners(tab, TabEvents.ADDED,
- Integer.toString(getPrivacySpecificTabIndex(tabIndex, isPrivate)));
- }
-
- return tab;
- }
-
- // Return the index, among those tabs whose privacy setting matches isPrivate, of the tab at
- // position index in mOrder. Returns -1, for "new last tab", when index is -1.
- private int getPrivacySpecificTabIndex(int index, boolean isPrivate) {
- int privacySpecificIndex = -1;
- for (int i = 0; i <= index; i++) {
- final Tab tab = mOrder.get(i);
- if (tab.isPrivate() == isPrivate) {
- privacySpecificIndex++;
- }
- }
- return privacySpecificIndex;
- }
-
- public synchronized void removeTab(int id) {
- if (mTabs.containsKey(id)) {
- Tab tab = getTab(id);
- mOrder.remove(tab);
- mTabs.remove(id);
- }
- }
-
- public synchronized Tab selectTab(int id) {
- if (!mTabs.containsKey(id))
- return null;
-
- final Tab oldTab = getSelectedTab();
- final Tab tab = mTabs.get(id);
-
- // This avoids a NPE below, but callers need to be careful to
- // handle this case.
- if (tab == null || oldTab == tab) {
- return tab;
- }
-
- mSelectedTab = tab;
- notifyListeners(tab, TabEvents.SELECTED);
-
- if (mLayerView != null) {
- mLayerView.setClearColor(getTabColor(tab));
- }
-
- if (oldTab != null) {
- notifyListeners(oldTab, TabEvents.UNSELECTED);
- }
-
- // Pass a message to Gecko to update tab state in BrowserApp.
- GeckoAppShell.notifyObservers("Tab:Selected", String.valueOf(tab.getId()));
- return tab;
- }
-
- public synchronized boolean selectLastTab() {
- if (mOrder.isEmpty()) {
- return false;
- }
-
- selectTab(mOrder.get(mOrder.size() - 1).getId());
- return true;
- }
-
- private int getIndexOf(Tab tab) {
- return mOrder.lastIndexOf(tab);
- }
-
- private Tab getNextTabFrom(Tab tab, boolean getPrivate) {
- int numTabs = mOrder.size();
- int index = getIndexOf(tab);
- for (int i = index + 1; i < numTabs; i++) {
- Tab next = mOrder.get(i);
- if (next.isPrivate() == getPrivate) {
- return next;
- }
- }
- return null;
- }
-
- private Tab getPreviousTabFrom(Tab tab, boolean getPrivate) {
- int index = getIndexOf(tab);
- for (int i = index - 1; i >= 0; i--) {
- Tab prev = mOrder.get(i);
- if (prev.isPrivate() == getPrivate) {
- return prev;
- }
- }
- return null;
- }
-
- /**
- * Gets the selected tab.
- *
- * The selected tab can be null if we're doing a session restore after a
- * crash and Gecko isn't ready yet.
- *
- * @return the selected tab, or null if no tabs exist
- */
- @Nullable
- public Tab getSelectedTab() {
- return mSelectedTab;
- }
-
- public boolean isSelectedTab(Tab tab) {
- return tab != null && tab == mSelectedTab;
- }
-
- public boolean isSelectedTabId(int tabId) {
- final Tab selected = mSelectedTab;
- return selected != null && selected.getId() == tabId;
- }
-
- @RobocopTarget
- public synchronized Tab getTab(int id) {
- if (id == -1)
- return null;
-
- if (mTabs.size() == 0)
- return null;
-
- if (!mTabs.containsKey(id))
- return null;
-
- return mTabs.get(id);
- }
-
- public synchronized Tab getTabForApplicationId(final String applicationId) {
- if (applicationId == null) {
- return null;
- }
-
- for (final Tab tab : mOrder) {
- if (applicationId.equals(tab.getApplicationId())) {
- return tab;
- }
- }
-
- return null;
- }
-
- /** Close tab and then select the default next tab */
- @RobocopTarget
- public synchronized void closeTab(Tab tab) {
- closeTab(tab, getNextTab(tab));
- }
-
- public synchronized void closeTab(Tab tab, Tab nextTab) {
- closeTab(tab, nextTab, false);
- }
-
- public synchronized void closeTab(Tab tab, boolean showUndoToast) {
- closeTab(tab, getNextTab(tab), showUndoToast);
- }
-
- /** Close tab and then select nextTab */
- public synchronized void closeTab(final Tab tab, Tab nextTab, boolean showUndoToast) {
- if (tab == null)
- return;
-
- int tabId = tab.getId();
- removeTab(tabId);
-
- if (nextTab == null) {
- nextTab = loadUrl(AboutPages.HOME, LOADURL_NEW_TAB);
- }
-
- selectTab(nextTab.getId());
-
- tab.onDestroy();
-
- final JSONObject args = new JSONObject();
- try {
- args.put("tabId", String.valueOf(tabId));
- args.put("showUndoToast", showUndoToast);
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error building Tab:Closed arguments: " + e);
- }
-
- // Pass a message to Gecko to update tab state in BrowserApp
- GeckoAppShell.notifyObservers("Tab:Closed", args.toString());
- }
-
- /** Return the tab that will be selected by default after this one is closed */
- public Tab getNextTab(Tab tab) {
- Tab selectedTab = getSelectedTab();
- if (selectedTab != tab)
- return selectedTab;
-
- boolean getPrivate = tab.isPrivate();
- Tab nextTab = getNextTabFrom(tab, getPrivate);
- if (nextTab == null)
- nextTab = getPreviousTabFrom(tab, getPrivate);
- if (nextTab == null && getPrivate) {
- // If there are no private tabs remaining, get the last normal tab
- Tab lastTab = mOrder.get(mOrder.size() - 1);
- if (!lastTab.isPrivate()) {
- nextTab = lastTab;
- } else {
- nextTab = getPreviousTabFrom(lastTab, false);
- }
- }
-
- Tab parent = getTab(tab.getParentId());
- if (parent != null) {
- // If the next tab is a sibling, switch to it. Otherwise go back to the parent.
- if (nextTab != null && nextTab.getParentId() == tab.getParentId())
- return nextTab;
- else
- return parent;
- }
- return nextTab;
- }
-
- public Iterable<Tab> getTabsInOrder() {
- return mOrder;
- }
-
- /**
- * @return the current GeckoApp instance, or throws if
- * we aren't correctly initialized.
- */
- private synchronized Context getAppContext() {
- if (mAppContext == null) {
- throw new IllegalStateException("Tabs not initialized with a GeckoApp instance.");
- }
- return mAppContext;
- }
-
- public ContentResolver getContentResolver() {
- return getAppContext().getContentResolver();
- }
-
- // Make Tabs a singleton class.
- private static class TabsInstanceHolder {
- private static final Tabs INSTANCE = new Tabs();
- }
-
- @RobocopTarget
- public static Tabs getInstance() {
- return Tabs.TabsInstanceHolder.INSTANCE;
- }
-
- // GeckoEventListener implementation
- @Override
- public void handleMessage(String event, JSONObject message) {
- Log.d(LOGTAG, "handleMessage: " + event);
- try {
- // All other events handled below should contain a tabID property
- int id = message.getInt("tabID");
- Tab tab = getTab(id);
-
- // "Tab:Added" is a special case because tab will be null if the tab was just added
- if (event.equals("Tab:Added")) {
- String url = message.isNull("uri") ? null : message.getString("uri");
-
- if (message.getBoolean("cancelEditMode")) {
- final Tab oldTab = getSelectedTab();
- if (oldTab != null) {
- oldTab.setIsEditing(false);
- }
- }
-
- if (message.getBoolean("stub")) {
- if (tab == null) {
- // Tab was already closed; abort
- return;
- }
- } else {
- tab = addTab(id, url, message.getBoolean("external"),
- message.getInt("parentId"),
- message.getString("title"),
- message.getBoolean("isPrivate"),
- message.getInt("tabIndex"));
- // If we added the tab as a stub, we should have already
- // selected it, so ignore this flag for stubbed tabs.
- if (message.getBoolean("selected"))
- selectTab(id);
- }
-
- if (message.getBoolean("delayLoad"))
- tab.setState(Tab.STATE_DELAYED);
- if (message.getBoolean("desktopMode"))
- tab.setDesktopMode(true);
- return;
- }
-
- // Tab was already closed; abort
- if (tab == null)
- return;
-
- if (event.equals("Tab:Close")) {
- closeTab(tab);
- } else if (event.equals("Tab:Select")) {
- selectTab(tab.getId());
- } else if (event.equals("Content:LocationChange")) {
- tab.handleLocationChange(message);
- } else if (event.equals("Content:SecurityChange")) {
- tab.updateIdentityData(message.getJSONObject("identity"));
- notifyListeners(tab, TabEvents.SECURITY_CHANGE);
- } else if (event.equals("Content:StateChange")) {
- int state = message.getInt("state");
- if ((state & GeckoAppShell.WPL_STATE_IS_NETWORK) != 0) {
- if ((state & GeckoAppShell.WPL_STATE_START) != 0) {
- boolean restoring = message.getBoolean("restoring");
- tab.handleDocumentStart(restoring, message.getString("uri"));
- notifyListeners(tab, Tabs.TabEvents.START);
- } else if ((state & GeckoAppShell.WPL_STATE_STOP) != 0) {
- tab.handleDocumentStop(message.getBoolean("success"));
- notifyListeners(tab, Tabs.TabEvents.STOP);
- }
- }
- } else if (event.equals("Content:LoadError")) {
- tab.handleContentLoaded();
- notifyListeners(tab, Tabs.TabEvents.LOAD_ERROR);
- } else if (event.equals("Content:PageShow")) {
- tab.setLoadedFromCache(message.getBoolean("fromCache"));
- tab.updateUserRequested(message.getString("userRequested"));
- notifyListeners(tab, TabEvents.PAGE_SHOW);
- } else if (event.equals("DOMTitleChanged")) {
- tab.updateTitle(message.getString("title"));
- } else if (event.equals("Link:Favicon")) {
- // Add the favicon to the set of available icons for this tab.
-
- tab.addFavicon(message.getString("href"), message.getInt("size"), message.getString("mime"));
-
- // Load the favicon. If the tab is still loading, we actually do the load once the
- // page has loaded, in an attempt to prevent the favicon load from having a
- // detrimental effect on page load time.
- if (tab.getState() != Tab.STATE_LOADING) {
- tab.loadFavicon();
- }
- } else if (event.equals("Link:Touchicon")) {
- tab.addTouchicon(message.getString("href"), message.getInt("size"), message.getString("mime"));
- } else if (event.equals("Link:Feed")) {
- tab.setHasFeeds(true);
- notifyListeners(tab, TabEvents.LINK_FEED);
- } else if (event.equals("Link:OpenSearch")) {
- boolean visible = message.getBoolean("visible");
- tab.setHasOpenSearch(visible);
- } else if (event.equals("DesktopMode:Changed")) {
- tab.setDesktopMode(message.getBoolean("desktopMode"));
- notifyListeners(tab, TabEvents.DESKTOP_MODE_CHANGE);
- } else if (event.equals("Tab:StreamStart")) {
- tab.setRecording(true);
- notifyListeners(tab, TabEvents.RECORDING_CHANGE);
- } else if (event.equals("Tab:StreamStop")) {
- tab.setRecording(false);
- notifyListeners(tab, TabEvents.RECORDING_CHANGE);
- } else if (event.equals("Tab:AudioPlayingChange")) {
- tab.setIsAudioPlaying(message.getBoolean("isAudioPlaying"));
- notifyListeners(tab, TabEvents.AUDIO_PLAYING_CHANGE);
- } else if (event.equals("Tab:MediaPlaybackChange")) {
- final String status = message.getString("status");
- if (status.equals("resume")) {
- notifyListeners(tab, TabEvents.MEDIA_PLAYING_RESUME);
- } else {
- tab.setIsMediaPlaying(status.equals("start"));
- notifyListeners(tab, TabEvents.MEDIA_PLAYING_CHANGE);
- }
- }
-
- } catch (Exception e) {
- Log.w(LOGTAG, "handleMessage threw for " + event, e);
- }
- }
-
- public void refreshThumbnails() {
- final BrowserDB db = BrowserDB.from(mAppContext);
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- for (final Tab tab : mOrder) {
- if (tab.getThumbnail() == null) {
- tab.loadThumbnailFromDB(db);
- }
- }
- }
- });
- }
-
- public interface OnTabsChangedListener {
- void onTabChanged(Tab tab, TabEvents msg, String data);
- }
-
- private static final List<OnTabsChangedListener> TABS_CHANGED_LISTENERS = new CopyOnWriteArrayList<OnTabsChangedListener>();
-
- public static void registerOnTabsChangedListener(OnTabsChangedListener listener) {
- TABS_CHANGED_LISTENERS.add(listener);
- }
-
- public static void unregisterOnTabsChangedListener(OnTabsChangedListener listener) {
- TABS_CHANGED_LISTENERS.remove(listener);
- }
-
- public enum TabEvents {
- CLOSED,
- START,
- LOADED,
- LOAD_ERROR,
- STOP,
- FAVICON,
- THUMBNAIL,
- TITLE,
- SELECTED,
- UNSELECTED,
- ADDED,
- RESTORED,
- LOCATION_CHANGE,
- MENU_UPDATED,
- PAGE_SHOW,
- LINK_FEED,
- SECURITY_CHANGE,
- DESKTOP_MODE_CHANGE,
- RECORDING_CHANGE,
- BOOKMARK_ADDED,
- BOOKMARK_REMOVED,
- AUDIO_PLAYING_CHANGE,
- OPENED_FROM_TABS_TRAY,
- MEDIA_PLAYING_CHANGE,
- MEDIA_PLAYING_RESUME
- }
-
- public void notifyListeners(Tab tab, TabEvents msg) {
- notifyListeners(tab, msg, "");
- }
-
- public void notifyListeners(final Tab tab, final TabEvents msg, final String data) {
- if (tab == null &&
- msg != TabEvents.RESTORED) {
- throw new IllegalArgumentException("onTabChanged:" + msg + " must specify a tab.");
- }
-
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- onTabChanged(tab, msg, data);
-
- if (TABS_CHANGED_LISTENERS.isEmpty()) {
- return;
- }
-
- Iterator<OnTabsChangedListener> items = TABS_CHANGED_LISTENERS.iterator();
- while (items.hasNext()) {
- items.next().onTabChanged(tab, msg, data);
- }
- }
- });
- }
-
- private void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
- switch (msg) {
- // We want the tab record to have an accurate favicon, so queue
- // the persisting of tabs when it changes.
- case FAVICON:
- case LOCATION_CHANGE:
- queuePersistAllTabs();
- break;
- case RESTORED:
- mInitialTabsAdded = true;
- break;
-
- // When one tab is deselected, another one is always selected, so only
- // queue a single persist operation. When tabs are added/closed, they
- // are also selected/unselected, so it would be redundant to also listen
- // for ADDED/CLOSED events.
- case SELECTED:
- if (mLayerView != null) {
- mLayerView.setSurfaceBackgroundColor(getTabColor(tab));
- mLayerView.setPaintState(LayerView.PAINT_START);
- }
- queuePersistAllTabs();
- case UNSELECTED:
- tab.onChange();
- break;
- default:
- break;
- }
- }
-
- /**
- * Queues a request to persist tabs after PERSIST_TABS_AFTER_MILLISECONDS
- * milliseconds have elapsed. If any existing requests are already queued then
- * those requests are removed.
- */
- private void queuePersistAllTabs() {
- final Handler backgroundHandler = ThreadUtils.getBackgroundHandler();
-
- // Note: Its safe to modify the runnable here because all of the callers are on the same thread.
- if (mPersistTabsRunnable != null) {
- backgroundHandler.removeCallbacks(mPersistTabsRunnable);
- mPersistTabsRunnable = null;
- }
-
- mPersistTabsRunnable = new PersistTabsRunnable(mAppContext, getTabsInOrder());
- backgroundHandler.postDelayed(mPersistTabsRunnable, PERSIST_TABS_AFTER_MILLISECONDS);
- }
-
- /**
- * Looks for an open tab with the given URL.
- * @param url the URL of the tab we're looking for
- *
- * @return first Tab with the given URL, or null if there is no such tab.
- */
- public Tab getFirstTabForUrl(String url) {
- return getFirstTabForUrlHelper(url, null);
- }
-
- /**
- * Looks for an open tab with the given URL and private state.
- * @param url the URL of the tab we're looking for
- * @param isPrivate if true, only look for tabs that are private. if false,
- * only look for tabs that are non-private.
- *
- * @return first Tab with the given URL, or null if there is no such tab.
- */
- public Tab getFirstTabForUrl(String url, boolean isPrivate) {
- return getFirstTabForUrlHelper(url, isPrivate);
- }
-
- private Tab getFirstTabForUrlHelper(String url, Boolean isPrivate) {
- if (url == null) {
- return null;
- }
-
- for (Tab tab : mOrder) {
- if (isPrivate != null && isPrivate != tab.isPrivate()) {
- continue;
- }
- if (url.equals(tab.getURL())) {
- return tab;
- }
- }
-
- return null;
- }
-
- /**
- * Looks for a reader mode enabled open tab with the given URL and private
- * state.
- *
- * @param url
- * The URL of the tab we're looking for. The url parameter can be
- * the actual article URL or the reader mode article URL.
- * @param isPrivate
- * If true, only look for tabs that are private. If false, only
- * look for tabs that are not private.
- *
- * @return The first Tab with the given URL, or null if there is no such
- * tab.
- */
- public Tab getFirstReaderTabForUrl(String url, boolean isPrivate) {
- if (url == null) {
- return null;
- }
-
- url = ReaderModeUtils.stripAboutReaderUrl(url);
-
- for (Tab tab : mOrder) {
- if (isPrivate != tab.isPrivate()) {
- continue;
- }
- String tabUrl = tab.getURL();
- if (AboutPages.isAboutReader(tabUrl)) {
- tabUrl = ReaderModeUtils.stripAboutReaderUrl(tabUrl);
- if (url.equals(tabUrl)) {
- return tab;
- }
- }
- }
-
- return null;
- }
-
- /**
- * Loads a tab with the given URL in the currently selected tab.
- *
- * @param url URL of page to load, or search term used if searchEngine is given
- */
- @RobocopTarget
- public Tab loadUrl(String url) {
- return loadUrl(url, LOADURL_NONE);
- }
-
- /**
- * Loads a tab with the given URL.
- *
- * @param url URL of page to load, or search term used if searchEngine is given
- * @param flags flags used to load tab
- *
- * @return the Tab if a new one was created; null otherwise
- */
- @RobocopTarget
- public Tab loadUrl(String url, int flags) {
- return loadUrl(url, null, -1, null, flags);
- }
-
- public Tab loadUrlWithIntentExtras(final String url, final SafeIntent intent, final int flags) {
- // We can't directly create a listener to tell when the user taps on the "What's new"
- // notification, so we use this intent handling as a signal that they tapped the notification.
- if (intent.getBooleanExtra(WhatsNewReceiver.EXTRA_WHATSNEW_NOTIFICATION, false)) {
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION,
- WhatsNewReceiver.EXTRA_WHATSNEW_NOTIFICATION);
- }
-
- // Note: we don't get the URL from the intent so the calling
- // method has the opportunity to change the URL if applicable.
- return loadUrl(url, null, -1, intent, flags);
- }
-
- public Tab loadUrl(final String url, final String searchEngine, final int parentId, final int flags) {
- return loadUrl(url, searchEngine, parentId, null, flags);
- }
-
- /**
- * Loads a tab with the given URL.
- *
- * @param url URL of page to load, or search term used if searchEngine is given
- * @param searchEngine if given, the search engine with this name is used
- * to search for the url string; if null, the URL is loaded directly
- * @param parentId ID of this tab's parent, or -1 if it has no parent
- * @param intent an intent whose extras are used to modify the request
- * @param flags flags used to load tab
- *
- * @return the Tab if a new one was created; null otherwise
- */
- public Tab loadUrl(final String url, final String searchEngine, final int parentId,
- final SafeIntent intent, final int flags) {
- JSONObject args = new JSONObject();
- Tab tabToSelect = null;
- boolean delayLoad = (flags & LOADURL_DELAY_LOAD) != 0;
-
- // delayLoad implies background tab
- boolean background = delayLoad || (flags & LOADURL_BACKGROUND) != 0;
-
- try {
- boolean isPrivate = (flags & LOADURL_PRIVATE) != 0;
- boolean userEntered = (flags & LOADURL_USER_ENTERED) != 0;
- boolean desktopMode = (flags & LOADURL_DESKTOP) != 0;
- boolean external = (flags & LOADURL_EXTERNAL) != 0;
- final boolean isFirstShownAfterActivityUnhidden = (flags & LOADURL_FIRST_AFTER_ACTIVITY_UNHIDDEN) != 0;
-
- args.put("url", url);
- args.put("engine", searchEngine);
- args.put("parentId", parentId);
- args.put("userEntered", userEntered);
- args.put("isPrivate", isPrivate);
- args.put("pinned", (flags & LOADURL_PINNED) != 0);
- args.put("desktopMode", desktopMode);
-
- final boolean needsNewTab;
- final String applicationId = (intent == null) ? null :
- intent.getStringExtra(Browser.EXTRA_APPLICATION_ID);
- if (applicationId == null) {
- needsNewTab = (flags & LOADURL_NEW_TAB) != 0;
- } else {
- // If you modify this code, be careful that intent != null.
- final boolean extraCreateNewTab = intent.getBooleanExtra(Browser.EXTRA_CREATE_NEW_TAB, false);
- final Tab applicationTab = getTabForApplicationId(applicationId);
- if (applicationTab == null || extraCreateNewTab) {
- needsNewTab = true;
- } else {
- needsNewTab = false;
- delayLoad = false;
- background = false;
-
- tabToSelect = applicationTab;
- final int tabToSelectId = tabToSelect.getId();
- args.put("tabID", tabToSelectId);
-
- // This must be called before the "Tab:Load" event is sent. I think addTab gets
- // away with it because having "newTab" == true causes the selected tab to be
- // updated in JS for the "Tab:Load" event but "newTab" is false in our case.
- // This makes me think the other selectTab is not necessary (bug 1160673).
- //
- // Note: that makes the later call redundant but selectTab exits early so I'm
- // fine not adding the complex logic to avoid calling it again.
- selectTab(tabToSelect.getId());
- }
- }
-
- args.put("newTab", needsNewTab);
- args.put("delayLoad", delayLoad);
- args.put("selected", !background);
-
- if (needsNewTab) {
- int tabId = getNextTabId();
- args.put("tabID", tabId);
-
- // The URL is updated for the tab once Gecko responds with the
- // Tab:Added message. We can preliminarily set the tab's URL as
- // long as it's a valid URI.
- String tabUrl = (url != null && Uri.parse(url).getScheme() != null) ? url : null;
-
- // Add the new tab to the end of the tab order.
- final int tabIndex = -1;
-
- tabToSelect = addTab(tabId, tabUrl, external, parentId, url, isPrivate, tabIndex);
- tabToSelect.setDesktopMode(desktopMode);
- tabToSelect.setApplicationId(applicationId);
- if (isFirstShownAfterActivityUnhidden) {
- // We just opened Firefox so we want to show
- // the toolbar but not animate it to avoid jank.
- tabToSelect.setShouldShowToolbarWithoutAnimationOnFirstSelection(true);
- }
- }
- } catch (Exception e) {
- Log.w(LOGTAG, "Error building JSON arguments for loadUrl.", e);
- }
-
- GeckoAppShell.notifyObservers("Tab:Load", args.toString());
-
- if (tabToSelect == null) {
- return null;
- }
-
- if (!delayLoad && !background) {
- selectTab(tabToSelect.getId());
- }
-
- // Load favicon instantly for about:home page because it's already cached
- if (AboutPages.isBuiltinIconPage(url)) {
- tabToSelect.loadFavicon();
- }
-
- return tabToSelect;
- }
-
- public Tab addTab() {
- return loadUrl(AboutPages.HOME, Tabs.LOADURL_NEW_TAB);
- }
-
- public Tab addPrivateTab() {
- return loadUrl(AboutPages.PRIVATEBROWSING, Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_PRIVATE);
- }
-
- /**
- * Open the url as a new tab, and mark the selected tab as its "parent".
- *
- * If the url is already open in a tab, the existing tab is selected.
- * Use this for tabs opened by the browser chrome, so users can press the
- * "Back" button to return to the previous tab.
- *
- * This method will open a new private tab if the currently selected tab
- * is also private.
- *
- * @param url URL of page to load
- */
- public void loadUrlInTab(String url) {
- Iterable<Tab> tabs = getTabsInOrder();
- for (Tab tab : tabs) {
- if (url.equals(tab.getURL())) {
- selectTab(tab.getId());
- return;
- }
- }
-
- // getSelectedTab() can return null if no tab has been created yet
- // (i.e., we're restoring a session after a crash). In these cases,
- // don't mark any tabs as a parent.
- int parentId = -1;
- int flags = LOADURL_NEW_TAB;
-
- final Tab selectedTab = getSelectedTab();
- if (selectedTab != null) {
- parentId = selectedTab.getId();
- if (selectedTab.isPrivate()) {
- flags = flags | LOADURL_PRIVATE;
- }
- }
-
- loadUrl(url, null, parentId, flags);
- }
-
- /**
- * Gets the next tab ID.
- */
- @JNITarget
- public static int getNextTabId() {
- return sTabId.getAndIncrement();
- }
-
- private int getTabColor(Tab tab) {
- if (tab != null) {
- return tab.isPrivate() ? mPrivateClearColor : Color.WHITE;
- }
-
- return Color.WHITE;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/Telemetry.java b/mobile/android/base/java/org/mozilla/gecko/Telemetry.java
deleted file mode 100644
index 342445bf2..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/Telemetry.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.TelemetryContract.Event;
-import org.mozilla.gecko.TelemetryContract.Method;
-import org.mozilla.gecko.TelemetryContract.Reason;
-import org.mozilla.gecko.TelemetryContract.Session;
-
-import android.os.SystemClock;
-import android.util.Log;
-
-/**
- * All telemetry times are relative to one of two clocks:
- *
- * * Real time since the device was booted, including deep sleep. Use this
- * as a substitute for wall clock.
- * * Uptime since the device was booted, excluding deep sleep. Use this to
- * avoid timing a user activity when their phone is in their pocket!
- *
- * The majority of methods in this class are defined in terms of real time.
- */
-@RobocopTarget
-public class Telemetry {
- private static final String LOGTAG = "Telemetry";
-
- @WrapForJNI(stubName = "AddHistogram", dispatchTo = "gecko")
- private static native void nativeAddHistogram(String name, int value);
- @WrapForJNI(stubName = "AddKeyedHistogram", dispatchTo = "gecko")
- private static native void nativeAddKeyedHistogram(String name, String key, int value);
- @WrapForJNI(stubName = "StartUISession", dispatchTo = "gecko")
- private static native void nativeStartUiSession(String name, long timestamp);
- @WrapForJNI(stubName = "StopUISession", dispatchTo = "gecko")
- private static native void nativeStopUiSession(String name, String reason, long timestamp);
- @WrapForJNI(stubName = "AddUIEvent", dispatchTo = "gecko")
- private static native void nativeAddUiEvent(String action, String method,
- long timestamp, String extras);
-
- public static long uptime() {
- return SystemClock.uptimeMillis();
- }
-
- public static long realtime() {
- return SystemClock.elapsedRealtime();
- }
-
- // Define new histograms in:
- // toolkit/components/telemetry/Histograms.json
- public static void addToHistogram(String name, int value) {
- if (GeckoThread.isRunning()) {
- nativeAddHistogram(name, value);
- } else {
- GeckoThread.queueNativeCall(Telemetry.class, "nativeAddHistogram",
- String.class, name, value);
- }
- }
-
- public static void addToKeyedHistogram(String name, String key, int value) {
- if (GeckoThread.isRunning()) {
- nativeAddKeyedHistogram(name, key, value);
- } else {
- GeckoThread.queueNativeCall(Telemetry.class, "nativeAddKeyedHistogram",
- String.class, name, String.class, key, value);
- }
- }
-
- public abstract static class Timer {
- private final long mStartTime;
- private final String mName;
-
- private volatile boolean mHasFinished;
- private volatile long mElapsed = -1;
-
- protected abstract long now();
-
- public Timer(String name) {
- mName = name;
- mStartTime = now();
- }
-
- public void cancel() {
- mHasFinished = true;
- }
-
- public long getElapsed() {
- return mElapsed;
- }
-
- public void stop() {
- // Only the first stop counts.
- if (mHasFinished) {
- return;
- }
-
- mHasFinished = true;
-
- final long elapsed = now() - mStartTime;
- if (elapsed < 0) {
- Log.e(LOGTAG, "Current time less than start time -- clock shenanigans?");
- return;
- }
-
- mElapsed = elapsed;
- if (elapsed > Integer.MAX_VALUE) {
- Log.e(LOGTAG, "Duration of " + elapsed + "ms is too great to add to histogram.");
- return;
- }
-
- addToHistogram(mName, (int) (elapsed));
- }
- }
-
- public static class RealtimeTimer extends Timer {
- public RealtimeTimer(String name) {
- super(name);
- }
-
- @Override
- protected long now() {
- return Telemetry.realtime();
- }
- }
-
- public static class UptimeTimer extends Timer {
- public UptimeTimer(String name) {
- super(name);
- }
-
- @Override
- protected long now() {
- return Telemetry.uptime();
- }
- }
-
- public static void startUISession(final Session session, final String sessionNameSuffix) {
- final String sessionName = getSessionName(session, sessionNameSuffix);
-
- Log.d(LOGTAG, "StartUISession: " + sessionName);
- if (GeckoThread.isRunning()) {
- nativeStartUiSession(sessionName, realtime());
- } else {
- GeckoThread.queueNativeCall(Telemetry.class, "nativeStartUiSession",
- String.class, sessionName, realtime());
- }
- }
-
- public static void startUISession(final Session session) {
- startUISession(session, null);
- }
-
- public static void stopUISession(final Session session, final String sessionNameSuffix,
- final Reason reason) {
- final String sessionName = getSessionName(session, sessionNameSuffix);
-
- Log.d(LOGTAG, "StopUISession: " + sessionName + ", reason=" + reason);
- if (GeckoThread.isRunning()) {
- nativeStopUiSession(sessionName, reason.toString(), realtime());
- } else {
- GeckoThread.queueNativeCall(Telemetry.class, "nativeStopUiSession",
- String.class, sessionName,
- String.class, reason.toString(), realtime());
- }
- }
-
- public static void stopUISession(final Session session, final Reason reason) {
- stopUISession(session, null, reason);
- }
-
- public static void stopUISession(final Session session, final String sessionNameSuffix) {
- stopUISession(session, sessionNameSuffix, Reason.NONE);
- }
-
- public static void stopUISession(final Session session) {
- stopUISession(session, null, Reason.NONE);
- }
-
- private static String getSessionName(final Session session, final String sessionNameSuffix) {
- if (sessionNameSuffix != null) {
- return session.toString() + ":" + sessionNameSuffix;
- } else {
- return session.toString();
- }
- }
-
- /**
- * @param method A non-null method (if null is desired, consider using Method.NONE)
- */
- private static void sendUIEvent(final String eventName, final Method method,
- final long timestamp, final String extras) {
- if (method == null) {
- throw new IllegalArgumentException("Expected non-null method - use Method.NONE?");
- }
-
- if (!AppConstants.RELEASE_OR_BETA) {
- final String logString = "SendUIEvent: event = " + eventName + " method = " + method + " timestamp = " +
- timestamp + " extras = " + extras;
- Log.d(LOGTAG, logString);
- }
- if (GeckoThread.isRunning()) {
- nativeAddUiEvent(eventName, method.toString(), timestamp, extras);
- } else {
- GeckoThread.queueNativeCall(Telemetry.class, "nativeAddUiEvent",
- String.class, eventName, String.class, method.toString(),
- timestamp, String.class, extras);
- }
- }
-
- public static void sendUIEvent(final Event event, final Method method, final long timestamp,
- final String extras) {
- sendUIEvent(event.toString(), method, timestamp, extras);
- }
-
- public static void sendUIEvent(final Event event, final Method method, final long timestamp) {
- sendUIEvent(event, method, timestamp, null);
- }
-
- public static void sendUIEvent(final Event event, final Method method, final String extras) {
- sendUIEvent(event, method, realtime(), extras);
- }
-
- public static void sendUIEvent(final Event event, final Method method) {
- sendUIEvent(event, method, realtime(), null);
- }
-
- public static void sendUIEvent(final Event event) {
- sendUIEvent(event, Method.NONE, realtime(), null);
- }
-
- /**
- * Sends a UIEvent with the given status appended to the event name.
- *
- * This method is a slight bend of the Telemetry framework so chances
- * are that you don't want to use this: please think really hard before you do.
- *
- * Intended for use with data policy notifications.
- */
- public static void sendUIEvent(final Event event, final boolean eventStatus) {
- final String eventName = event + ":" + eventStatus;
- sendUIEvent(eventName, Method.NONE, realtime(), null);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/TelemetryContract.java b/mobile/android/base/java/org/mozilla/gecko/TelemetryContract.java
deleted file mode 100644
index 0c2051a9d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/TelemetryContract.java
+++ /dev/null
@@ -1,307 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-
-/**
- * Holds data definitions for our UI Telemetry implementation.
- *
- * Note that enum values of "_TEST*" are reserved for testing and
- * should not be changed without changing the associated tests.
- *
- * See mobile/android/base/docs/index.rst for a full dictionary.
- */
-@RobocopTarget
-public interface TelemetryContract {
-
- /**
- * Holds event names. Intended for use with
- * Telemetry.sendUIEvent() as the "action" parameter.
- *
- * Please keep this list sorted.
- */
- public enum Event {
- // Generic action, usually for tracking menu and toolbar actions.
- ACTION("action.1"),
-
- // Cancel a state, action, etc.
- CANCEL("cancel.1"),
-
- // Start casting a video.
- // Note: Only used in JavaScript for now, but here for completeness.
- CAST("cast.1"),
-
- // Editing an item.
- EDIT("edit.1"),
-
- // Launching (opening) an external application.
- // Note: Only used in JavaScript for now, but here for completeness.
- LAUNCH("launch.1"),
-
- // Loading a URL.
- LOAD_URL("loadurl.1"),
-
- LOCALE_BROWSER_RESET("locale.browser.reset.1"),
- LOCALE_BROWSER_SELECTED("locale.browser.selected.1"),
- LOCALE_BROWSER_UNSELECTED("locale.browser.unselected.1"),
-
- // Hide a built-in home panel.
- PANEL_HIDE("panel.hide.1"),
-
- // Move a home panel up or down.
- PANEL_MOVE("panel.move.1"),
-
- // Remove a custom home panel.
- PANEL_REMOVE("panel.remove.1"),
-
- // Set default home panel.
- PANEL_SET_DEFAULT("panel.setdefault.1"),
-
- // Show a hidden built-in home panel.
- PANEL_SHOW("panel.show.1"),
-
- // Pinning an item.
- PIN("pin.1"),
-
- // Outcome of data policy notification: can be true or false.
- POLICY_NOTIFICATION_SUCCESS("policynotification.success.1"),
-
- // Sanitizing private data.
- SANITIZE("sanitize.1"),
-
- // Saving a resource (reader, bookmark, etc) for viewing later.
- SAVE("save.1"),
-
- // Perform a search -- currently used when starting a search in the search activity.
- SEARCH("search.1"),
-
- // Remove a search engine.
- SEARCH_REMOVE("search.remove.1"),
-
- // Restore default search engines.
- SEARCH_RESTORE_DEFAULTS("search.restoredefaults.1"),
-
- // Set default search engine.
- SEARCH_SET_DEFAULT("search.setdefault.1"),
-
- // Sharing content.
- SHARE("share.1"),
-
- // Show a UI element.
- SHOW("show.1"),
-
- // Undoing a user action.
- // Note: Only used in JavaScript for now, but here for completeness.
- UNDO("undo.1"),
-
- // Unpinning an item.
- UNPIN("unpin.1"),
-
- // Stop holding a resource (reader, bookmark, etc) for viewing later.
- UNSAVE("unsave.1"),
-
- // When the user performs actions on the in-content network error page.
- NETERROR("neterror.1"),
-
- // VALUES BELOW THIS LINE ARE EXCLUSIVE TO TESTING.
- _TEST1("_test_event_1.1"),
- _TEST2("_test_event_2.1"),
- _TEST3("_test_event_3.1"),
- _TEST4("_test_event_4.1"),
- ;
-
- private final String string;
-
- Event(final String string) {
- this.string = string;
- }
-
- @Override
- public String toString() {
- return string;
- }
- }
-
- /**
- * Holds event methods. Intended for use in
- * Telemetry.sendUIEvent() as the "method" parameter.
- *
- * Please keep this list sorted.
- */
- public enum Method {
- // Action triggered from the action bar (including the toolbar).
- ACTIONBAR("actionbar"),
-
- // Action triggered by hitting the Android back button.
- BACK("back"),
-
- // Action triggered from a button.
- BUTTON("button"),
-
- // Action taken from a content page -- for example, a search results web page.
- CONTENT("content"),
-
- // Action occurred via a context menu.
- CONTEXT_MENU("contextmenu"),
-
- // Action triggered from a dialog.
- DIALOG("dialog"),
-
- // Action triggered from a doorhanger popup prompt.
- DOORHANGER("doorhanger"),
-
- // Action triggered from a view grid item, like a thumbnail.
- GRID_ITEM("griditem"),
-
- // Action occurred via an intent.
- INTENT("intent"),
-
- // Action occurred via a homescreen launcher.
- HOMESCREEN("homescreen"),
-
- // Action triggered from a list.
- LIST("list"),
-
- // Action triggered from a view list item, like a row of a list.
- LIST_ITEM("listitem"),
-
- // Action occurred via the main menu.
- MENU("menu"),
-
- // No method is specified.
- NONE(null),
-
- // Action triggered from a notification in the Android notification bar.
- NOTIFICATION("notification"),
-
- // Action triggered from a pageaction in the URLBar.
- // Note: Only used in JavaScript for now, but here for completeness.
- PAGEACTION("pageaction"),
-
- // Action triggered from one of a series of views, such as ViewPager.
- PANEL("panel"),
-
- // Action triggered by a background service / automatic system making a decision.
- SERVICE("service"),
-
- // Action triggered from a settings screen.
- SETTINGS("settings"),
-
- // Actions triggered from the share overlay.
- SHARE_OVERLAY("shareoverlay"),
-
- // Action triggered from a suggestion provided to the user.
- SUGGESTION("suggestion"),
-
- // Action triggered from an OS system action.
- SYSTEM("system"),
-
- // Action triggered from a SuperToast.
- // Note: Only used in JavaScript for now, but here for completeness.
- TOAST("toast"),
-
- // Action triggerred by pressing a SearchWidget button
- WIDGET("widget"),
-
- // VALUES BELOW THIS LINE ARE EXCLUSIVE TO TESTING.
- _TEST1("_test_method_1"),
- _TEST2("_test_method_2"),
- ;
-
- private final String string;
-
- Method(final String string) {
- this.string = string;
- }
-
- @Override
- public String toString() {
- return string;
- }
- }
-
- /**
- * Holds session names. Intended for use with
- * Telemetry.startUISession() as the "sessionName" parameter.
- *
- * Please keep this list sorted.
- */
- public enum Session {
- // Awesomescreen (including frecency search) is active.
- AWESOMESCREEN("awesomescreen.1"),
-
- // Used to tag experiments being run.
- EXPERIMENT("experiment.1"),
-
- // Started the very first time we believe the application has been launched.
- FIRSTRUN("firstrun.1"),
-
- // Awesomescreen frecency search is active.
- FRECENCY("frecency.1"),
-
- // Started when a user enters a given home panel.
- // Session name is dynamic, encoded as "homepanel.1:<panel_id>"
- HOME_PANEL("homepanel.1"),
-
- // Started when a Reader viewer becomes active in the foreground.
- // Note: Only used in JavaScript for now, but here for completeness.
- READER("reader.1"),
-
- // Started when the search activity launches.
- SEARCH_ACTIVITY("searchactivity.1"),
-
- // Settings activity is active.
- SETTINGS("settings.1"),
-
- // VALUES BELOW THIS LINE ARE EXCLUSIVE TO TESTING.
- _TEST_STARTED_TWICE("_test_session_started_twice.1"),
- _TEST_STOPPED_TWICE("_test_session_stopped_twice.1"),
- ;
-
- private final String string;
-
- Session(final String string) {
- this.string = string;
- }
-
- @Override
- public String toString() {
- return string;
- }
- }
-
- /**
- * Holds reasons for stopping a session. Intended for use in
- * Telemetry.stopUISession() as the "reason" parameter.
- *
- * Please keep this list sorted.
- */
- public enum Reason {
- // Changes were committed.
- COMMIT("commit"),
-
- // No reason is specified.
- NONE(null),
-
- // VALUES BELOW THIS LINE ARE EXCLUSIVE TO TESTING.
- _TEST1("_test_reason_1"),
- _TEST2("_test_reason_2"),
- _TEST_IGNORED("_test_reason_ignored"),
- ;
-
- private final String string;
-
- Reason(final String string) {
- this.string = string;
- }
-
- @Override
- public String toString() {
- return string;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/ThumbnailHelper.java b/mobile/android/base/java/org/mozilla/gecko/ThumbnailHelper.java
deleted file mode 100644
index 3a7012431..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/ThumbnailHelper.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.gfx.BitmapUtils;
-import org.mozilla.gecko.util.ResourceDrawableUtils;
-import org.mozilla.gecko.mozglue.DirectBufferAllocator;
-
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.util.Log;
-
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-
-/**
- * Helper class to generate thumbnails for tabs.
- * Internally, a queue of pending thumbnails is maintained in mPendingThumbnails.
- * The head of the queue is the thumbnail that is currently being processed; upon
- * completion of the current thumbnail the next one is automatically processed.
- * Changes to the thumbnail width are stashed in mPendingWidth and the change is
- * applied between thumbnail processing. This allows a single thumbnail buffer to
- * be used for all thumbnails.
- */
-public final class ThumbnailHelper {
- private static final boolean DEBUG = false;
- private static final String LOGTAG = "GeckoThumbnailHelper";
-
- public static final float TABS_PANEL_THUMBNAIL_ASPECT_RATIO = 0.8333333f;
- public static final float TOP_SITES_THUMBNAIL_ASPECT_RATIO = 0.571428571f; // this is a 4:7 ratio (as per UX decision)
- public static final float THUMBNAIL_ASPECT_RATIO;
-
- static {
- // As we only want to generate one thumbnail for each tab, we calculate the
- // largest aspect ratio required and create the thumbnail based off that.
- // Any views with a smaller aspect ratio will use a cropped version of the
- // same image.
- THUMBNAIL_ASPECT_RATIO = Math.max(TABS_PANEL_THUMBNAIL_ASPECT_RATIO, TOP_SITES_THUMBNAIL_ASPECT_RATIO);
- }
-
- public enum CachePolicy {
- STORE,
- NO_STORE
- }
-
- // static singleton stuff
-
- private static ThumbnailHelper sInstance;
-
- public static synchronized ThumbnailHelper getInstance() {
- if (sInstance == null) {
- sInstance = new ThumbnailHelper();
- }
- return sInstance;
- }
-
- // instance stuff
-
- private final ArrayList<Tab> mPendingThumbnails; // synchronized access only
- private volatile int mPendingWidth;
- private int mWidth;
- private int mHeight;
- private ByteBuffer mBuffer;
-
- private ThumbnailHelper() {
- final Resources res = GeckoAppShell.getContext().getResources();
-
- mPendingThumbnails = new ArrayList<>();
- try {
- mPendingWidth = (int) res.getDimension(R.dimen.tab_thumbnail_width);
- } catch (Resources.NotFoundException nfe) {
- }
- mWidth = -1;
- mHeight = -1;
- }
-
- public void getAndProcessThumbnailFor(final int tabId, final ResourceDrawableUtils.BitmapLoader loader) {
- final Tab tab = Tabs.getInstance().getTab(tabId);
- if (tab != null) {
- getAndProcessThumbnailFor(tab, loader);
- }
- }
-
- public void getAndProcessThumbnailFor(final Tab tab, final ResourceDrawableUtils.BitmapLoader loader) {
- ResourceDrawableUtils.runOnBitmapFoundOnUiThread(loader, tab.getThumbnail());
-
- Tabs.registerOnTabsChangedListener(new Tabs.OnTabsChangedListener() {
- @Override
- public void onTabChanged(final Tab t, final Tabs.TabEvents msg, final String data) {
- if (tab != t || msg != Tabs.TabEvents.THUMBNAIL) {
- return;
- }
- Tabs.unregisterOnTabsChangedListener(this);
- ResourceDrawableUtils.runOnBitmapFoundOnUiThread(loader, t.getThumbnail());
- }
- });
- getAndProcessThumbnailFor(tab);
- }
-
- public void getAndProcessThumbnailFor(Tab tab) {
- if (AboutPages.isAboutHome(tab.getURL()) || AboutPages.isAboutPrivateBrowsing(tab.getURL())) {
- tab.updateThumbnail(null, CachePolicy.NO_STORE);
- return;
- }
-
- synchronized (mPendingThumbnails) {
- if (mPendingThumbnails.lastIndexOf(tab) > 0) {
- // This tab is already in the queue, so don't add it again.
- // Note that if this tab is only at the *head* of the queue,
- // (i.e. mPendingThumbnails.lastIndexOf(tab) == 0) then we do
- // add it again because it may have already been thumbnailed
- // and now we need to do it again.
- return;
- }
-
- mPendingThumbnails.add(tab);
- if (mPendingThumbnails.size() > 1) {
- // Some thumbnail was already being processed, so wait
- // for that to be done.
- return;
- }
-
- requestThumbnailLocked(tab);
- }
- }
-
- public void setThumbnailWidth(int width) {
- // Check inverted for safety: Bug 803299 Comment 34.
- if (GeckoAppShell.getScreenDepth() == 24) {
- mPendingWidth = width;
- } else {
- // Bug 776906: on 16-bit screens we need to ensure an even width.
- mPendingWidth = (width + 1) & (~1);
- }
- }
-
- private void updateThumbnailSizeLocked() {
- // Apply any pending width updates.
- mWidth = mPendingWidth;
- mHeight = Math.round(mWidth * THUMBNAIL_ASPECT_RATIO);
-
- int pixelSize = (GeckoAppShell.getScreenDepth() == 24) ? 4 : 2;
- int capacity = mWidth * mHeight * pixelSize;
- if (DEBUG) {
- Log.d(LOGTAG, "Using new thumbnail size: " + capacity +
- " (width " + mWidth + " - height " + mHeight + ")");
- }
- if (mBuffer == null || mBuffer.capacity() != capacity) {
- if (mBuffer != null) {
- mBuffer = DirectBufferAllocator.free(mBuffer);
- }
- try {
- mBuffer = DirectBufferAllocator.allocate(capacity);
- } catch (IllegalArgumentException iae) {
- Log.w(LOGTAG, iae.toString());
- } catch (OutOfMemoryError oom) {
- Log.w(LOGTAG, "Unable to allocate thumbnail buffer of capacity " + capacity);
- }
- // If we hit an error above, mBuffer will be pointing to null, so we are in a sane state.
- }
- }
-
- private void requestThumbnailLocked(Tab tab) {
- updateThumbnailSizeLocked();
-
- if (mBuffer == null) {
- // Buffer allocation may have failed. In this case we can't send the
- // event requesting the screenshot which means we won't get back a response
- // and so our queue will grow unboundedly. Handle this scenario by clearing
- // the queue (no point trying more thumbnailing right now since we're likely
- // low on memory). We will try again normally on the next call to
- // getAndProcessThumbnailFor which will hopefully be when we have more free memory.
- mPendingThumbnails.clear();
- return;
- }
-
- if (DEBUG) {
- Log.d(LOGTAG, "Sending thumbnail event: " + mWidth + ", " + mHeight);
- }
- requestThumbnailLocked(mBuffer, tab, tab.getId(), mWidth, mHeight);
- }
-
- @WrapForJNI(stubName = "RequestThumbnail", dispatchTo = "proxy")
- private static native void requestThumbnailLocked(ByteBuffer data, Tab tab, int tabId,
- int width, int height);
-
- /* This method is invoked by JNI once the thumbnail data is ready. */
- @WrapForJNI(calledFrom = "gecko")
- private static void notifyThumbnail(final ByteBuffer data, final Tab tab,
- final boolean success, final boolean shouldStore) {
- final ThumbnailHelper helper = ThumbnailHelper.getInstance();
- if (success) {
- helper.handleThumbnailData(
- tab, data, shouldStore ? CachePolicy.STORE : CachePolicy.NO_STORE);
- }
- helper.processNextThumbnail();
- }
-
- private void processNextThumbnail() {
- synchronized (mPendingThumbnails) {
- if (mPendingThumbnails.isEmpty()) {
- return;
- }
-
- mPendingThumbnails.remove(0);
-
- if (!mPendingThumbnails.isEmpty()) {
- requestThumbnailLocked(mPendingThumbnails.get(0));
- }
- }
- }
-
- private void handleThumbnailData(Tab tab, ByteBuffer data, CachePolicy cachePolicy) {
- if (DEBUG) {
- Log.d(LOGTAG, "handleThumbnailData: " + data.capacity());
- }
- if (data != mBuffer) {
- // This should never happen, but log it and recover gracefully
- Log.e(LOGTAG, "handleThumbnailData called with an unexpected ByteBuffer!");
- }
-
- processThumbnailData(tab, data, cachePolicy);
- }
-
- private void processThumbnailData(Tab tab, ByteBuffer data, CachePolicy cachePolicy) {
- Bitmap b = tab.getThumbnailBitmap(mWidth, mHeight);
- data.position(0);
- b.copyPixelsFromBuffer(data);
- setTabThumbnail(tab, b, null, cachePolicy);
- }
-
- private void setTabThumbnail(Tab tab, Bitmap bitmap, byte[] compressed, CachePolicy cachePolicy) {
- if (bitmap == null) {
- if (compressed == null) {
- Log.w(LOGTAG, "setTabThumbnail: one of bitmap or compressed must be non-null!");
- return;
- }
- bitmap = BitmapUtils.decodeByteArray(compressed);
- }
- tab.updateThumbnail(bitmap, cachePolicy);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/ZoomedView.java b/mobile/android/base/java/org/mozilla/gecko/ZoomedView.java
deleted file mode 100644
index c0c9307dc..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/ZoomedView.java
+++ /dev/null
@@ -1,838 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.animation.ViewHelper;
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
-import org.mozilla.gecko.gfx.LayerView;
-import org.mozilla.gecko.gfx.PanZoomController;
-import org.mozilla.gecko.gfx.PointUtils;
-import org.mozilla.gecko.mozglue.DirectBufferAllocator;
-import org.mozilla.gecko.PrefsHelper;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.content.res.Configuration;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.BitmapShader;
-import android.graphics.Canvas;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.Matrix;
-import android.graphics.Paint;
-import android.graphics.Point;
-import android.graphics.PointF;
-import android.graphics.RectF;
-import android.graphics.Shader;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewTreeObserver;
-import android.view.animation.Animation;
-import android.view.animation.Animation.AnimationListener;
-import android.view.animation.OvershootInterpolator;
-import android.view.animation.ScaleAnimation;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.RelativeLayout;
-import android.widget.TextView;
-
-import java.nio.ByteBuffer;
-import java.text.DecimalFormat;
-
-public class ZoomedView extends FrameLayout implements LayerView.DynamicToolbarListener,
- LayerView.ZoomedViewListener, GeckoEventListener {
- private static final String LOGTAG = "Gecko" + ZoomedView.class.getSimpleName();
-
- private static final float[] ZOOM_FACTORS_LIST = {2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 1.5f};
- private static final int W_CAPTURED_VIEW_IN_PERCENT = 50;
- private static final int H_CAPTURED_VIEW_IN_PERCENT = 50;
- private static final int MINIMUM_DELAY_BETWEEN_TWO_RENDER_CALLS_NS = 1000000;
- private static final int DELAY_BEFORE_NEXT_RENDER_REQUEST_MS = 2000;
- private static final int OPENING_ANIMATION_DURATION_MS = 250;
- private static final int CLOSING_ANIMATION_DURATION_MS = 150;
- private static final float OVERSHOOT_INTERPOLATOR_TENSION = 1.5f;
-
- private float zoomFactor;
- private int currentZoomFactorIndex;
- private boolean isSimplifiedUI;
- private int defaultZoomFactor;
- private PrefsHelper.PrefHandler prefObserver;
-
- private ImageView zoomedImageView;
- private LayerView layerView;
- private int viewWidth;
- private int viewHeight; // Only the zoomed view height, no toolbar, no shadow ...
- private int viewContainerWidth;
- private int viewContainerHeight; // Zoomed view height with toolbar and other elements like shadow, ...
- private int containterSize; // shadow, margin, ...
- private Point lastPosition;
- private boolean shouldSetVisibleOnUpdate;
- private boolean isBlockedFromAppearing; // Prevent the display of the zoomedview while FormAssistantPopup is visible
- private PointF returnValue;
- private final PointF animationStart;
- private ImageView closeButton;
- private TextView changeZoomFactorButton;
- private boolean toolbarOnTop;
- private float offsetDueToToolBarPosition;
- private int toolbarHeight;
- private int cornerRadius;
- private float dynamicToolbarOverlap;
-
- private boolean stopUpdateView;
-
- private int lastOrientation;
-
- private ByteBuffer buffer;
- private Runnable requestRenderRunnable;
- private long startTimeReRender;
- private long lastStartTimeReRender;
-
- private ZoomedViewTouchListener touchListener;
-
- private enum StartPointUpdate {
- GECKO_POSITION, CENTER, NO_CHANGE
- }
-
- private class RoundedBitmapDrawable extends BitmapDrawable {
- private Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
- final float cornerRadius;
- final boolean squareOnTopOfDrawable;
-
- RoundedBitmapDrawable(Resources res, Bitmap bitmap, boolean squareOnTop, int radius) {
- super(res, bitmap);
- squareOnTopOfDrawable = squareOnTop;
- final BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP,
- Shader.TileMode.CLAMP);
- paint.setAntiAlias(true);
- paint.setShader(shader);
- cornerRadius = radius;
- }
-
- @Override
- public void draw(Canvas canvas) {
- int height = getBounds().height();
- int width = getBounds().width();
- RectF rect = new RectF(0.0f, 0.0f, width, height);
- canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint);
-
- //draw rectangles over the corners we want to be square
- if (squareOnTopOfDrawable) {
- canvas.drawRect(0, 0, cornerRadius, cornerRadius, paint);
- canvas.drawRect(width - cornerRadius, 0, width, cornerRadius, paint);
- } else {
- canvas.drawRect(0, height - cornerRadius, cornerRadius, height, paint);
- canvas.drawRect(width - cornerRadius, height - cornerRadius, width, height, paint);
- }
- }
- }
-
- private class ZoomedViewTouchListener implements View.OnTouchListener {
- private float originRawX;
- private float originRawY;
- private boolean dragged;
- private MotionEvent actionDownEvent;
-
- @Override
- public boolean onTouch(View view, MotionEvent event) {
- if (layerView == null) {
- return false;
- }
-
- switch (event.getAction()) {
- case MotionEvent.ACTION_MOVE:
- if (moveZoomedView(event)) {
- dragged = true;
- }
- break;
-
- case MotionEvent.ACTION_UP:
- if (dragged) {
- dragged = false;
- } else {
- if (isClickInZoomedView(event.getY())) {
- GeckoAppShell.notifyObservers("Gesture:ClickInZoomedView", "");
- layerView.dispatchTouchEvent(actionDownEvent);
- actionDownEvent.recycle();
- PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(event.getX(), event.getY());
- // the LayerView expects the coordinates relative to the window, not the surface, so we need
- // to adjust that here.
- convertedPosition.y += layerView.getSurfaceTranslation();
- MotionEvent e = MotionEvent.obtain(event.getDownTime(), event.getEventTime(),
- MotionEvent.ACTION_UP, convertedPosition.x, convertedPosition.y,
- event.getMetaState());
- layerView.dispatchTouchEvent(e);
- e.recycle();
- }
- }
- break;
-
- case MotionEvent.ACTION_DOWN:
- dragged = false;
- originRawX = event.getRawX();
- originRawY = event.getRawY();
- PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(event.getX(), event.getY());
- // the LayerView expects the coordinates relative to the window, not the surface, so we need
- // to adjust that here.
- convertedPosition.y += layerView.getSurfaceTranslation();
- actionDownEvent = MotionEvent.obtain(event.getDownTime(), event.getEventTime(),
- MotionEvent.ACTION_DOWN, convertedPosition.x, convertedPosition.y,
- event.getMetaState());
- break;
- }
- return true;
- }
-
- private boolean isClickInZoomedView(float y) {
- return ((toolbarOnTop && y > toolbarHeight) ||
- (!toolbarOnTop && y < ZoomedView.this.viewHeight));
- }
-
- private boolean moveZoomedView(MotionEvent event) {
- RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) ZoomedView.this.getLayoutParams();
- if ((!dragged) && (Math.abs((int) (event.getRawX() - originRawX)) < PanZoomController.CLICK_THRESHOLD)
- && (Math.abs((int) (event.getRawY() - originRawY)) < PanZoomController.CLICK_THRESHOLD)) {
- // When the user just touches the screen ACTION_MOVE can be detected for a very small delta on position.
- // In this case, the move is ignored if the delta is lower than 1 unit.
- return false;
- }
-
- float newLeftMargin = params.leftMargin + event.getRawX() - originRawX;
- float newTopMargin = params.topMargin + event.getRawY() - originRawY;
- ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
- ZoomedView.this.moveZoomedView(metrics, newLeftMargin, newTopMargin, StartPointUpdate.CENTER);
- originRawX = event.getRawX();
- originRawY = event.getRawY();
- return true;
- }
- }
-
- public ZoomedView(Context context) {
- this(context, null, 0);
- }
-
- public ZoomedView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public ZoomedView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- isSimplifiedUI = true;
- isBlockedFromAppearing = false;
- getPrefs();
- currentZoomFactorIndex = 0;
- returnValue = new PointF();
- animationStart = new PointF();
- requestRenderRunnable = new Runnable() {
- @Override
- public void run() {
- requestZoomedViewRender();
- }
- };
- touchListener = new ZoomedViewTouchListener();
- GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
- "Gesture:clusteredLinksClicked", "Window:Resize", "Content:LocationChange",
- "Gesture:CloseZoomedView", "Browser:ZoomToPageWidth", "Browser:ZoomToRect",
- "FormAssist:AutoComplete", "FormAssist:Hide");
- }
-
- void destroy() {
- if (prefObserver != null) {
- PrefsHelper.removeObserver(prefObserver);
- prefObserver = null;
- }
- ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable);
- GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
- "Gesture:clusteredLinksClicked", "Window:Resize", "Content:LocationChange",
- "Gesture:CloseZoomedView", "Browser:ZoomToPageWidth", "Browser:ZoomToRect",
- "FormAssist:AutoComplete", "FormAssist:Hide");
- }
-
- // This method (onFinishInflate) is called only when the zoomed view class is used inside
- // an xml structure <org.mozilla.gecko.ZoomedView ...
- // It won't be called if the class is used from java code like "new ZoomedView(context);"
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- closeButton = (ImageView) findViewById(R.id.dialog_close);
- changeZoomFactorButton = (TextView) findViewById(R.id.change_zoom_factor);
- zoomedImageView = (ImageView) findViewById(R.id.zoomed_image_view);
-
- updateUI();
-
- toolbarHeight = getResources().getDimensionPixelSize(R.dimen.zoomed_view_toolbar_height);
- containterSize = getResources().getDimensionPixelSize(R.dimen.drawable_dropshadow_size);
- cornerRadius = getResources().getDimensionPixelSize(R.dimen.standard_corner_radius);
-
- moveToolbar(true);
- }
-
- private void setListeners() {
- closeButton.setOnClickListener(new View.OnClickListener() {
- public void onClick(View view) {
- stopZoomDisplay(true);
- }
- });
-
- changeZoomFactorButton.setOnTouchListener(new OnTouchListener() {
- public boolean onTouch(View v, MotionEvent event) {
-
- if (event.getAction() == MotionEvent.ACTION_UP) {
- if (event.getX() >= (changeZoomFactorButton.getLeft() + changeZoomFactorButton.getWidth() / 2)) {
- changeZoomFactor(true);
- } else {
- changeZoomFactor(false);
- }
- }
- return true;
- }
- });
-
- setOnTouchListener(touchListener);
- }
-
- private void removeListeners() {
- closeButton.setOnClickListener(null);
-
- changeZoomFactorButton.setOnTouchListener(null);
-
- setOnTouchListener(null);
- }
- /*
- * Convert a click from ZoomedView. Return the position of the click in the
- * LayerView
- */
- private PointF getUnzoomedPositionFromPointInZoomedView(float x, float y) {
- ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
- final float parentWidth = metrics.getWidth();
- final float parentHeight = metrics.getHeight();
- RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams();
-
- // The number of unzoomed content pixels that can be displayed in the
- // zoomed area.
- float visibleContentPixels = viewWidth / zoomFactor;
- // The offset in content pixels of the leftmost zoomed pixel from the
- // layerview's left edge when the zoomed view is moved to the right as
- // far as it can go.
- float maxContentOffset = parentWidth - visibleContentPixels;
- // The maximum offset in screen pixels that the zoomed view can have
- float maxZoomedViewOffset = parentWidth - viewContainerWidth;
-
- // The above values allow us to compute the term
- // maxContentOffset / maxZoomedViewOffset
- // which is the number of content pixels that we should move over by
- // for every screen pixel that the zoomed view is moved over by.
- // This allows a smooth transition from when the zoomed view is at the
- // leftmost extent to when it is at the rightmost extent.
-
- // This is the offset in content pixels of the leftmost zoomed pixel
- // visible in the zoomed view. This value is relative to the layerview
- // edge.
- float zoomedContentOffset = ((float)params.leftMargin) * maxContentOffset / maxZoomedViewOffset;
- returnValue.x = (int)(zoomedContentOffset + (x / zoomFactor));
-
- // Same comments here vertically
- visibleContentPixels = viewHeight / zoomFactor;
- maxContentOffset = parentHeight - visibleContentPixels;
- maxZoomedViewOffset = parentHeight - (viewContainerHeight - toolbarHeight);
- float zoomedAreaOffset = (float)params.topMargin + offsetDueToToolBarPosition - layerView.getSurfaceTranslation();
- zoomedContentOffset = zoomedAreaOffset * maxContentOffset / maxZoomedViewOffset;
- returnValue.y = (int)(zoomedContentOffset + ((y - offsetDueToToolBarPosition) / zoomFactor));
-
- return returnValue;
- }
-
- /*
- * A touch point (x,y) occurs in LayerView, this point should be displayed
- * in the center of the zoomed view. The returned point is the position of
- * the Top-Left zoomed view point on the screen device
- */
- private PointF getZoomedViewTopLeftPositionFromTouchPosition(float x, float y) {
- ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
- final float parentWidth = metrics.getWidth();
- final float parentHeight = metrics.getHeight();
-
- // See comments in getUnzoomedPositionFromPointInZoomedView, but the
- // transformations here are largely the reverse of that function.
-
- float visibleContentPixels = viewWidth / zoomFactor;
- float maxContentOffset = parentWidth - visibleContentPixels;
- float maxZoomedViewOffset = parentWidth - viewContainerWidth;
- float contentPixelOffset = x - (visibleContentPixels / 2.0f);
- returnValue.x = (int)(contentPixelOffset * (maxZoomedViewOffset / maxContentOffset));
-
- visibleContentPixels = viewHeight / zoomFactor;
- maxContentOffset = parentHeight - visibleContentPixels;
- maxZoomedViewOffset = parentHeight - (viewContainerHeight - toolbarHeight);
- contentPixelOffset = y - (visibleContentPixels / 2.0f);
- float unscaledViewOffset = layerView.getSurfaceTranslation() - offsetDueToToolBarPosition;
- returnValue.y = (int)((contentPixelOffset * (maxZoomedViewOffset / maxContentOffset)) + unscaledViewOffset);
-
- return returnValue;
- }
-
- private void moveZoomedView(ImmutableViewportMetrics metrics, float newLeftMargin, float newTopMargin,
- StartPointUpdate animateStartPoint) {
- RelativeLayout.LayoutParams newLayoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
- newLayoutParams.leftMargin = (int) newLeftMargin;
- newLayoutParams.topMargin = (int) newTopMargin;
- int topMarginMin = (int)(layerView.getSurfaceTranslation() + dynamicToolbarOverlap);
- int topMarginMax = layerView.getHeight() - viewContainerHeight;
- int leftMarginMin = 0;
- int leftMarginMax = layerView.getWidth() - viewContainerWidth;
-
- if (newTopMargin < topMarginMin) {
- newLayoutParams.topMargin = topMarginMin;
- } else if (newTopMargin > topMarginMax) {
- newLayoutParams.topMargin = topMarginMax;
- }
-
- if (newLeftMargin < leftMarginMin) {
- newLayoutParams.leftMargin = leftMarginMin;
- } else if (newLeftMargin > leftMarginMax) {
- newLayoutParams.leftMargin = leftMarginMax;
- }
-
- if (newLayoutParams.topMargin < topMarginMin + 1) {
- moveToolbar(false);
- } else if (newLayoutParams.topMargin > topMarginMax - 1) {
- moveToolbar(true);
- }
-
- if (animateStartPoint == StartPointUpdate.GECKO_POSITION) {
- // Before this point, the animationStart point is relative to the layerView.
- // The value is initialized in startZoomDisplay using the click point position coming from Gecko.
- // The position of the zoomed view is now calculated, so the position of the animation
- // can now be correctly set relative to the zoomed view
- animationStart.x = animationStart.x - newLayoutParams.leftMargin;
- animationStart.y = animationStart.y - newLayoutParams.topMargin;
- } else if (animateStartPoint == StartPointUpdate.CENTER) {
- // At this point, the animationStart point is no more valid probably because
- // the zoomed view has been moved by the user.
- // In this case, the animationStart point is set to the center point of the zoomed view.
- PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(viewContainerWidth / 2, viewContainerHeight / 2);
- animationStart.x = convertedPosition.x - newLayoutParams.leftMargin;
- animationStart.y = convertedPosition.y - newLayoutParams.topMargin;
- }
-
- setLayoutParams(newLayoutParams);
- PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(0, offsetDueToToolBarPosition);
- lastPosition = PointUtils.round(convertedPosition);
- requestZoomedViewRender();
- }
-
- private void moveToolbar(boolean moveTop) {
- if (toolbarOnTop == moveTop) {
- return;
- }
- toolbarOnTop = moveTop;
- if (toolbarOnTop) {
- offsetDueToToolBarPosition = toolbarHeight;
- } else {
- offsetDueToToolBarPosition = 0;
- }
-
- RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) zoomedImageView.getLayoutParams();
- RelativeLayout.LayoutParams pChangeZoomFactorButton = (RelativeLayout.LayoutParams) changeZoomFactorButton.getLayoutParams();
- RelativeLayout.LayoutParams pCloseButton = (RelativeLayout.LayoutParams) closeButton.getLayoutParams();
-
- if (moveTop) {
- p.addRule(RelativeLayout.BELOW, R.id.change_zoom_factor);
- pChangeZoomFactorButton.addRule(RelativeLayout.BELOW, 0);
- pCloseButton.addRule(RelativeLayout.BELOW, 0);
- } else {
- p.addRule(RelativeLayout.BELOW, 0);
- pChangeZoomFactorButton.addRule(RelativeLayout.BELOW, R.id.zoomed_image_view);
- pCloseButton.addRule(RelativeLayout.BELOW, R.id.zoomed_image_view);
- }
- pChangeZoomFactorButton.addRule(RelativeLayout.ALIGN_LEFT, R.id.zoomed_image_view);
- pCloseButton.addRule(RelativeLayout.ALIGN_RIGHT, R.id.zoomed_image_view);
- zoomedImageView.setLayoutParams(p);
- changeZoomFactorButton.setLayoutParams(pChangeZoomFactorButton);
- closeButton.setLayoutParams(pCloseButton);
- }
-
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- // In case of orientation change, the zoomed view update is stopped until the orientation change
- // is completed. At this time, the function onMetricsChanged is called and the
- // zoomed view update is restarted again.
- if (lastOrientation != newConfig.orientation) {
- shouldBlockUpdate(true);
- lastOrientation = newConfig.orientation;
- }
- }
-
- private void refreshZoomedViewSize(ImmutableViewportMetrics viewport) {
- if (layerView == null) {
- return;
- }
-
- RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams();
- setCapturedSize(viewport);
- moveZoomedView(viewport, params.leftMargin, params.topMargin, StartPointUpdate.NO_CHANGE);
- }
-
- private void setCapturedSize(ImmutableViewportMetrics metrics) {
- float parentMinSize = Math.min(metrics.getWidth(), metrics.getHeight());
- viewWidth = (int) ((parentMinSize * W_CAPTURED_VIEW_IN_PERCENT / (zoomFactor * 100.0)) * zoomFactor);
- viewHeight = (int) ((parentMinSize * H_CAPTURED_VIEW_IN_PERCENT / (zoomFactor * 100.0)) * zoomFactor);
- viewContainerHeight = viewHeight + toolbarHeight +
- 2 * containterSize; // Top and bottom shadows
- viewContainerWidth = viewWidth +
- 2 * containterSize; // Right and left shadows
- // Display in zoomedview is corrupted when width is an odd number
- // More details about this issue here: bug 776906 comment 11
- viewWidth &= ~0x1;
- }
-
- private void shouldBlockUpdate(boolean shouldBlockUpdate) {
- stopUpdateView = shouldBlockUpdate;
- }
-
- private Bitmap.Config getBitmapConfig() {
- return (GeckoAppShell.getScreenDepth() == 24) ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
- }
-
- private void updateUI() {
- // onFinishInflate is not yet completed, the update of the UI will be done later
- if (changeZoomFactorButton == null) {
- return;
- }
- if (isSimplifiedUI) {
- changeZoomFactorButton.setVisibility(View.INVISIBLE);
- } else {
- setTextInZoomFactorButton(zoomFactor);
- changeZoomFactorButton.setVisibility(View.VISIBLE);
- }
- }
-
- private void getPrefs() {
- prefObserver = new PrefsHelper.PrefHandlerBase() {
- @Override
- public void prefValue(String pref, boolean simplified) {
- isSimplifiedUI = simplified;
- if (simplified) {
- zoomFactor = (float) defaultZoomFactor;
- } else {
- zoomFactor = ZOOM_FACTORS_LIST[currentZoomFactorIndex];
- }
- updateUI();
- }
-
- @Override
- public void prefValue(String pref, int defaultZoomFactorFromSettings) {
- defaultZoomFactor = defaultZoomFactorFromSettings;
- if (isSimplifiedUI) {
- zoomFactor = (float) defaultZoomFactor;
- } else {
- zoomFactor = ZOOM_FACTORS_LIST[currentZoomFactorIndex];
- }
- updateUI();
- }
- };
- PrefsHelper.addObserver(new String[] { "ui.zoomedview.simplified",
- "ui.zoomedview.defaultZoomFactor" },
- prefObserver);
- }
-
- private void startZoomDisplay(LayerView aLayerView, final int leftFromGecko, final int topFromGecko) {
- if (isBlockedFromAppearing) {
- return;
- }
- if (layerView == null) {
- layerView = aLayerView;
- layerView.addZoomedViewListener(this);
- layerView.getDynamicToolbarAnimator().addTranslationListener(this);
- ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
- setCapturedSize(metrics);
- }
- startTimeReRender = 0;
- shouldSetVisibleOnUpdate = true;
-
- ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
- // At this point, the start point is relative to the layerView.
- // Later, it will be converted relative to the zoomed view as soon as
- // the position of the zoomed view will be calculated.
- animationStart.x = (float) leftFromGecko * metrics.zoomFactor;
- animationStart.y = (float) topFromGecko * metrics.zoomFactor + layerView.getSurfaceTranslation();
-
- moveUsingGeckoPosition(leftFromGecko, topFromGecko);
- }
-
- public void stopZoomDisplay(boolean withAnimation) {
- // If "startZoomDisplay" is running and not totally completed (Gecko thread is still
- // running and "showZoomedView" has not yet been called), the zoomed view will be
- // displayed after this call and it should not.
- // Force the stop of the zoomed view, changing the shouldSetVisibleOnUpdate flag
- // before the test of the visibility.
- shouldSetVisibleOnUpdate = false;
- if (getVisibility() == View.VISIBLE) {
- hideZoomedView(withAnimation);
- ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable);
- if (layerView != null) {
- layerView.getDynamicToolbarAnimator().removeTranslationListener(this);
- layerView.removeZoomedViewListener(this);
- layerView = null;
- }
- }
- }
-
- private void changeZoomFactor(boolean zoomIn) {
- if (zoomIn && currentZoomFactorIndex < ZOOM_FACTORS_LIST.length - 1) {
- currentZoomFactorIndex++;
- } else if (zoomIn && currentZoomFactorIndex >= ZOOM_FACTORS_LIST.length - 1) {
- currentZoomFactorIndex = 0;
- } else if (!zoomIn && currentZoomFactorIndex > 0) {
- currentZoomFactorIndex--;
- } else {
- currentZoomFactorIndex = ZOOM_FACTORS_LIST.length - 1;
- }
- zoomFactor = ZOOM_FACTORS_LIST[currentZoomFactorIndex];
-
- ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
- refreshZoomedViewSize(metrics);
- setTextInZoomFactorButton(zoomFactor);
- }
-
- private void setTextInZoomFactorButton(float zoom) {
- final String percentageValue = Integer.toString((int) (100 * zoom));
- changeZoomFactorButton.setText("- " + getResources().getString(R.string.percent, percentageValue) + " +");
- }
-
- @Override
- public void handleMessage(final String event, final JSONObject message) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- try {
- if (event.equals("Gesture:clusteredLinksClicked")) {
- final JSONObject clickPosition = message.getJSONObject("clickPosition");
- int left = clickPosition.getInt("x");
- int top = clickPosition.getInt("y");
- // Start to display inside the zoomedView
- LayerView geckoAppLayerView = GeckoAppShell.getLayerView();
- if (geckoAppLayerView != null) {
- startZoomDisplay(geckoAppLayerView, left, top);
- }
- } else if (event.equals("Window:Resize")) {
- ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
- refreshZoomedViewSize(metrics);
- } else if (event.equals("Content:LocationChange")) {
- stopZoomDisplay(false);
- } else if (event.equals("Gesture:CloseZoomedView") ||
- event.equals("Browser:ZoomToPageWidth") ||
- event.equals("Browser:ZoomToRect")) {
- stopZoomDisplay(true);
- } else if (event.equals("FormAssist:AutoComplete")) {
- isBlockedFromAppearing = true;
- stopZoomDisplay(true);
- } else if (event.equals("FormAssist:Hide")) {
- isBlockedFromAppearing = false;
- }
- } catch (JSONException e) {
- Log.e(LOGTAG, "JSON exception", e);
- }
- }
- });
- }
-
- private void moveUsingGeckoPosition(int leftFromGecko, int topFromGecko) {
- ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
- final float parentHeight = metrics.getHeight();
- // moveToolbar is called before getZoomedViewTopLeftPositionFromTouchPosition in order to
- // correctly center vertically the zoomed area
- moveToolbar((topFromGecko * metrics.zoomFactor > parentHeight / 2));
- PointF convertedPosition = getZoomedViewTopLeftPositionFromTouchPosition((leftFromGecko * metrics.zoomFactor),
- (topFromGecko * metrics.zoomFactor));
- moveZoomedView(metrics, convertedPosition.x, convertedPosition.y, StartPointUpdate.GECKO_POSITION);
- }
-
- @Override
- public void onTranslationChanged(float aToolbarTranslation, float aLayerViewTranslation) {
- ThreadUtils.assertOnUiThread();
- if (layerView != null) {
- dynamicToolbarOverlap = aLayerViewTranslation - aToolbarTranslation;
- refreshZoomedViewSize(layerView.getViewportMetrics());
- }
- }
-
- @Override
- public void onMetricsChanged(final ImmutableViewportMetrics viewport) {
- // It can be called from a Gecko thread (forceViewportMetrics in GeckoLayerClient).
- // Post to UI Thread to avoid Exception:
- // "Only the original thread that created a view hierarchy can touch its views."
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- shouldBlockUpdate(false);
- refreshZoomedViewSize(viewport);
- }
- });
- }
-
- @Override
- public void onPanZoomStopped() {
- }
-
- @Override
- public void updateView(ByteBuffer data) {
- final Bitmap sb3 = Bitmap.createBitmap(viewWidth, viewHeight, getBitmapConfig());
- if (sb3 != null) {
- data.rewind();
- try {
- sb3.copyPixelsFromBuffer(data);
- } catch (Exception iae) {
- Log.w(LOGTAG, iae.toString());
- }
- if (zoomedImageView != null) {
- RoundedBitmapDrawable ob3 = new RoundedBitmapDrawable(getResources(), sb3, toolbarOnTop, cornerRadius);
- zoomedImageView.setImageDrawable(ob3);
- }
- }
- if (shouldSetVisibleOnUpdate) {
- this.showZoomedView();
- }
- lastStartTimeReRender = startTimeReRender;
- startTimeReRender = 0;
- }
-
- private void showZoomedView() {
- // no animation if the zoomed view is already visible
- if (getVisibility() != View.VISIBLE) {
- final Animation anim = new ScaleAnimation(
- 0f, 1f, // Start and end values for the X axis scaling
- 0f, 1f, // Start and end values for the Y axis scaling
- Animation.ABSOLUTE, animationStart.x, // Pivot point of X scaling
- Animation.ABSOLUTE, animationStart.y); // Pivot point of Y scaling
- anim.setFillAfter(true); // Needed to keep the result of the animation
- anim.setDuration(OPENING_ANIMATION_DURATION_MS);
- anim.setInterpolator(new OvershootInterpolator(OVERSHOOT_INTERPOLATOR_TENSION));
- anim.setAnimationListener(new AnimationListener() {
- public void onAnimationEnd(Animation animation) {
- setListeners();
- }
- public void onAnimationRepeat(Animation animation) {
- }
- public void onAnimationStart(Animation animation) {
- removeListeners();
- }
- });
- setAnimation(anim);
- }
- setVisibility(View.VISIBLE);
- shouldSetVisibleOnUpdate = false;
- }
-
- private void hideZoomedView(boolean withAnimation) {
- if (withAnimation) {
- final Animation anim = new ScaleAnimation(
- 1f, 0f, // Start and end values for the X axis scaling
- 1f, 0f, // Start and end values for the Y axis scaling
- Animation.ABSOLUTE, animationStart.x, // Pivot point of X scaling
- Animation.ABSOLUTE, animationStart.y); // Pivot point of Y scaling
- anim.setFillAfter(true); // Needed to keep the result of the animation
- anim.setDuration(CLOSING_ANIMATION_DURATION_MS);
- anim.setAnimationListener(new AnimationListener() {
- public void onAnimationEnd(Animation animation) {
- }
- public void onAnimationRepeat(Animation animation) {
- }
- public void onAnimationStart(Animation animation) {
- removeListeners();
- }
- });
- setAnimation(anim);
- } else {
- removeListeners();
- setAnimation(null);
- }
- setVisibility(View.GONE);
- shouldSetVisibleOnUpdate = false;
- }
-
- private void updateBufferSize() {
- int pixelSize = (GeckoAppShell.getScreenDepth() == 24) ? 4 : 2;
- int capacity = viewWidth * viewHeight * pixelSize;
- if (buffer == null || buffer.capacity() != capacity) {
- buffer = DirectBufferAllocator.free(buffer);
- buffer = DirectBufferAllocator.allocate(capacity);
- }
- }
-
- private boolean isRendering() {
- return (startTimeReRender != 0);
- }
-
- private boolean renderFrequencyTooHigh() {
- return ((System.nanoTime() - lastStartTimeReRender) < MINIMUM_DELAY_BETWEEN_TWO_RENDER_CALLS_NS);
- }
-
- @WrapForJNI(dispatchTo = "gecko")
- private static native void requestZoomedViewData(ByteBuffer buffer, int tabId,
- int xPos, int yPos, int width,
- int height, float scale);
-
- @Override
- public void requestZoomedViewRender() {
- if (stopUpdateView) {
- return;
- }
- // remove pending runnable
- ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable);
-
- // "requestZoomedViewRender" can be called very often by Gecko (endDrawing in LayerRender) without
- // any thing changed in the zoomed area (useless calls from the "zoomed area" point of view).
- // "requestZoomedViewRender" can take time to re-render the zoomed view, it depends of the complexity
- // of the html on this area.
- // To avoid to slow down the application, the 2 following cases are tested:
-
- // 1- Last render is still running, plan another render later.
- if (isRendering()) {
- // post a new runnable DELAY_BEFORE_NEXT_RENDER_REQUEST_MS later
- // We need to post with a delay to be sure that the last call to requestZoomedViewRender will be done.
- // For a static html page WITHOUT any animation/video, there is a last call to endDrawing and we need to make
- // the zoomed render on this last call.
- ThreadUtils.postDelayedToUiThread(requestRenderRunnable, DELAY_BEFORE_NEXT_RENDER_REQUEST_MS);
- return;
- }
-
- // 2- Current render occurs too early, plan another render later.
- if (renderFrequencyTooHigh()) {
- // post a new runnable DELAY_BEFORE_NEXT_RENDER_REQUEST_MS later
- // We need to post with a delay to be sure that the last call to requestZoomedViewRender will be done.
- // For a page WITH animation/video, the animation/video can be stopped, and we need to make
- // the zoomed render on this last call.
- ThreadUtils.postDelayedToUiThread(requestRenderRunnable, DELAY_BEFORE_NEXT_RENDER_REQUEST_MS);
- return;
- }
-
- startTimeReRender = System.nanoTime();
- // Allocate the buffer if it's the first call.
- // Change the buffer size if it's not the right size.
- updateBufferSize();
-
- int tabId = Tabs.getInstance().getSelectedTab().getId();
-
- ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
- PointF origin = metrics.getOrigin();
-
- final int xPos = (int)origin.x + lastPosition.x;
- final int yPos = (int)origin.y + lastPosition.y;
-
- requestZoomedViewData(buffer, tabId, xPos, yPos, viewWidth, viewHeight,
- zoomFactor * metrics.zoomFactor);
- }
-
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/activitystream/ActivityStream.java b/mobile/android/base/java/org/mozilla/gecko/activitystream/ActivityStream.java
deleted file mode 100644
index d1c3f5916..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/activitystream/ActivityStream.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.activitystream;
-
-import android.content.Context;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.text.TextUtils;
-
-import com.keepsafe.switchboard.SwitchBoard;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.Experiments;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-import org.mozilla.gecko.util.StringUtils;
-import org.mozilla.gecko.util.publicsuffix.PublicSuffix;
-
-import java.util.Arrays;
-import java.util.List;
-
-public class ActivityStream {
- /**
- * List of undesired prefixes for labels based on a URL.
- *
- * This list is by no means complete and is based on those sources:
- * - https://gist.github.com/nchapman/36502ad115e8825d522a66549971a3f0
- * - https://github.com/mozilla/activity-stream/issues/1311
- */
- private static final List<String> UNDESIRED_LABEL_PREFIXES = Arrays.asList(
- "index.",
- "home."
- );
-
- /**
- * Undesired labels for labels based on a URL.
- *
- * This list is by no means complete and is based on those sources:
- * - https://gist.github.com/nchapman/36502ad115e8825d522a66549971a3f0
- * - https://github.com/mozilla/activity-stream/issues/1311
- */
- private static final List<String> UNDESIRED_LABELS = Arrays.asList(
- "render",
- "login",
- "edit"
- );
-
- public static boolean isEnabled(Context context) {
- if (!isUserEligible(context)) {
- // If the user is not eligible then disable activity stream. Even if it has been
- // enabled before.
- return false;
- }
-
- return GeckoSharedPrefs.forApp(context)
- .getBoolean(GeckoPreferences.PREFS_ACTIVITY_STREAM, false);
- }
-
- /**
- * Is the user eligible to use activity stream or should we hide it from settings etc.?
- */
- public static boolean isUserEligible(Context context) {
- if (AppConstants.MOZ_ANDROID_ACTIVITY_STREAM) {
- // If the build flag is enabled then just show the option to the user.
- return true;
- }
-
- if (AppConstants.NIGHTLY_BUILD && SwitchBoard.isInExperiment(context, Experiments.ACTIVITY_STREAM)) {
- // If this is a nightly build and the user is part of the activity stream experiment then
- // the option should be visible too. The experiment is limited to Nightly too but I want
- // to make really sure that this isn't riding the trains accidentally.
- return true;
- }
-
- // For everyone else activity stream is not available yet.
- return false;
- }
-
- /**
- * Query whether we want to display Activity Stream as a Home Panel (within the HomePager),
- * or as a HomePager replacement.
- */
- public static boolean isHomePanel() {
- return true;
- }
-
- /**
- * Extract a label from a URL to use in Activity Stream.
- *
- * This method implements the proposal from this desktop AS issue:
- * https://github.com/mozilla/activity-stream/issues/1311
- *
- * @param usePath Use the path of the URL to extract a label (if suitable)
- */
- public static void extractLabel(final Context context, final String url, final boolean usePath, final LabelCallback callback) {
- new AsyncTask<Void, Void, String>() {
- @Override
- protected String doInBackground(Void... params) {
- if (TextUtils.isEmpty(url)) {
- return "";
- }
-
- final Uri uri = Uri.parse(url);
-
- // Use last path segment if suitable
- if (usePath) {
- final String segment = uri.getLastPathSegment();
- if (!TextUtils.isEmpty(segment)
- && !UNDESIRED_LABELS.contains(segment)
- && !segment.matches("^[0-9]+$")) {
-
- boolean hasUndesiredPrefix = false;
- for (int i = 0; i < UNDESIRED_LABEL_PREFIXES.size(); i++) {
- if (segment.startsWith(UNDESIRED_LABEL_PREFIXES.get(i))) {
- hasUndesiredPrefix = true;
- break;
- }
- }
-
- if (!hasUndesiredPrefix) {
- return segment;
- }
- }
- }
-
- // If no usable path segment was found then use the host without public suffix and common subdomains
- final String host = uri.getHost();
- if (TextUtils.isEmpty(host)) {
- return url;
- }
-
- return StringUtils.stripCommonSubdomains(
- PublicSuffix.stripPublicSuffix(context, host));
- }
-
- @Override
- protected void onPostExecute(String label) {
- callback.onLabelExtracted(label);
- }
- }.execute();
- }
-
- public abstract static class LabelCallback {
- public abstract void onLabelExtracted(String label);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/adjust/AdjustBrowserAppDelegate.java b/mobile/android/base/java/org/mozilla/gecko/adjust/AdjustBrowserAppDelegate.java
deleted file mode 100644
index aee0bba63..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/adjust/AdjustBrowserAppDelegate.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package org.mozilla.gecko.adjust;
-
-import android.content.SharedPreferences;
-import android.os.Bundle;
-
-import org.mozilla.gecko.AdjustConstants;
-import org.mozilla.gecko.BrowserApp;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.delegates.BrowserAppDelegate;
-import org.mozilla.gecko.mozglue.SafeIntent;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-import org.mozilla.gecko.util.IntentUtils;
-
-public class AdjustBrowserAppDelegate extends BrowserAppDelegate {
- private final AdjustHelperInterface adjustHelper;
- private final AttributionHelperListener attributionHelperListener;
-
- public AdjustBrowserAppDelegate(AttributionHelperListener attributionHelperListener) {
- this.adjustHelper = AdjustConstants.getAdjustHelper();
- this.attributionHelperListener = attributionHelperListener;
- }
-
- @Override
- public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {
- adjustHelper.onCreate(browserApp,
- AdjustConstants.MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN,
- attributionHelperListener);
-
- final boolean isInAutomation = IntentUtils.getIsInAutomationFromEnvironment(
- new SafeIntent(browserApp.getIntent()));
-
- final SharedPreferences prefs = GeckoSharedPrefs.forApp(browserApp);
-
- // Adjust stores enabled state so this is only necessary because users may have set
- // their data preferences before this feature was implemented and we need to respect
- // those before upload can occur in Adjust.onResume.
- adjustHelper.setEnabled(!isInAutomation
- && prefs.getBoolean(GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true));
- }
-
- @Override
- public void onResume(BrowserApp browserApp) {
- // Needed for Adjust to get accurate session measurements
- adjustHelper.onResume();
- }
-
- @Override
- public void onPause(BrowserApp browserApp) {
- // Needed for Adjust to get accurate session measurements
- adjustHelper.onPause();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/adjust/AdjustHelper.java b/mobile/android/base/java/org/mozilla/gecko/adjust/AdjustHelper.java
deleted file mode 100644
index 19399e735..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/adjust/AdjustHelper.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.adjust;
-
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-
-import com.adjust.sdk.Adjust;
-import com.adjust.sdk.AdjustAttribution;
-import com.adjust.sdk.AdjustConfig;
-import com.adjust.sdk.AdjustReferrerReceiver;
-import com.adjust.sdk.LogLevel;
-import com.adjust.sdk.OnAttributionChangedListener;
-
-import org.mozilla.gecko.AppConstants;
-
-public class AdjustHelper implements AdjustHelperInterface, OnAttributionChangedListener {
-
- private static final String LOGTAG = AdjustHelper.class.getSimpleName();
- private AttributionHelperListener attributionListener;
-
- public void onCreate(final Context context, final String maybeAppToken, final AttributionHelperListener listener) {
- final String environment;
- final LogLevel logLevel;
- if (AppConstants.MOZILLA_OFFICIAL) {
- environment = AdjustConfig.ENVIRONMENT_PRODUCTION;
- logLevel = LogLevel.WARN;
- } else {
- environment = AdjustConfig.ENVIRONMENT_SANDBOX;
- logLevel = LogLevel.VERBOSE;
- }
- if (maybeAppToken == null) {
- // We've got install tracking turned on -- we better have a token!
- throw new IllegalArgumentException("maybeAppToken must not be null");
- }
- attributionListener = listener;
- AdjustConfig config = new AdjustConfig(context, maybeAppToken, environment);
- config.setLogLevel(logLevel);
- config.setOnAttributionChangedListener(this);
- Adjust.onCreate(config);
- }
-
- public void onPause() {
- Adjust.onPause();
- }
-
- public void onResume() {
- Adjust.onResume();
- }
-
- public void setEnabled(final boolean isEnabled) {
- Adjust.setEnabled(isEnabled);
- }
-
- public void onReceive(final Context context, final Intent intent) {
- new AdjustReferrerReceiver().onReceive(context, intent);
- }
-
- @Override
- public void onAttributionChanged(AdjustAttribution attribution) {
- if (attributionListener == null) {
- throw new IllegalStateException("Expected non-null attribution listener.");
- }
-
- if (attribution == null) {
- Log.e(LOGTAG, "Adjust attribution is null; skipping campaign id retrieval.");
- return;
- }
- attributionListener.onCampaignIdChanged(attribution.campaign);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/adjust/AdjustHelperInterface.java b/mobile/android/base/java/org/mozilla/gecko/adjust/AdjustHelperInterface.java
deleted file mode 100644
index aeb7b4334..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/adjust/AdjustHelperInterface.java
+++ /dev/null
@@ -1,22 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.adjust;
-
-import android.content.Context;
-import android.content.Intent;
-
-public interface AdjustHelperInterface {
- /**
- * Register the Application with the Adjust SDK.
- * @param appToken the (secret!) Adjust SDK per-application token to register with; may be null.
- */
- void onCreate(final Context context, final String appToken, final AttributionHelperListener listener);
- void onPause();
- void onResume();
-
- void setEnabled(final boolean isEnabled);
- void onReceive(final Context context, final Intent intent);
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/adjust/AttributionHelperListener.java b/mobile/android/base/java/org/mozilla/gecko/adjust/AttributionHelperListener.java
deleted file mode 100644
index 6dadd2261..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/adjust/AttributionHelperListener.java
+++ /dev/null
@@ -1,17 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.adjust;
-
-/**
- * Because of how our build module dependencies are structured, we aren't able to use
- * the {@link com.adjust.sdk.OnAttributionChangedListener} directly outside of {@link AdjustHelper}.
- * If the Adjust SDK is enabled, this listener should be notified when {@link com.adjust.sdk.OnAttributionChangedListener}
- * is fired (i.e. this listener would be daisy-chained to the Adjust one). The listener also
- * inherits thread-safety from GeckoSharedPrefs which is used to store the campaign ID.
- */
-public interface AttributionHelperListener {
- void onCampaignIdChanged(String campaignId);
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/adjust/StubAdjustHelper.java b/mobile/android/base/java/org/mozilla/gecko/adjust/StubAdjustHelper.java
deleted file mode 100644
index ddfed84bd..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/adjust/StubAdjustHelper.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.adjust;
-
-import android.content.Context;
-import android.content.Intent;
-
-public class StubAdjustHelper implements AdjustHelperInterface {
- public void onCreate(final Context context, final String appToken, final AttributionHelperListener listener) {
- // Do nothing.
- }
-
- public void onPause() {
- // Do nothing.
- }
-
- public void onResume() {
- // Do nothing.
- }
-
- public void setEnabled(final boolean isEnabled) {
- // Do nothing.
- }
-
- public void onReceive(final Context context, final Intent intent) {
- // Do nothing.
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/animation/AnimationUtils.java b/mobile/android/base/java/org/mozilla/gecko/animation/AnimationUtils.java
deleted file mode 100644
index 63e8e168e..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/animation/AnimationUtils.java
+++ /dev/null
@@ -1,21 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-
-package org.mozilla.gecko.animation;
-
-import android.content.Context;
-
-public class AnimationUtils {
- private static long mShortDuration = -1;
-
- public static long getShortDuration(Context context) {
- if (mShortDuration < 0) {
- mShortDuration = context.getResources().getInteger(android.R.integer.config_shortAnimTime);
- }
- return mShortDuration;
- }
-}
-
diff --git a/mobile/android/base/java/org/mozilla/gecko/animation/HeightChangeAnimation.java b/mobile/android/base/java/org/mozilla/gecko/animation/HeightChangeAnimation.java
deleted file mode 100644
index bf8007bbf..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/animation/HeightChangeAnimation.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.animation;
-
-import android.view.View;
-import android.view.animation.Animation;
-import android.view.animation.Transformation;
-
-public class HeightChangeAnimation extends Animation {
- int mFromHeight;
- int mToHeight;
- View mView;
-
- public HeightChangeAnimation(View view, int fromHeight, int toHeight) {
- mView = view;
- mFromHeight = fromHeight;
- mToHeight = toHeight;
- }
-
- @Override
- protected void applyTransformation(float interpolatedTime, Transformation t) {
- mView.getLayoutParams().height = Math.round((mFromHeight * (1 - interpolatedTime)) + (mToHeight * interpolatedTime));
- mView.requestLayout();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/animation/PropertyAnimator.java b/mobile/android/base/java/org/mozilla/gecko/animation/PropertyAnimator.java
deleted file mode 100644
index dc2403bbd..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/animation/PropertyAnimator.java
+++ /dev/null
@@ -1,342 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.animation;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.mozilla.gecko.AppConstants.Versions;
-
-import android.os.Handler;
-import android.support.v4.view.ViewCompat;
-import android.view.Choreographer;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
-import android.view.animation.AnimationUtils;
-import android.view.animation.DecelerateInterpolator;
-import android.view.animation.Interpolator;
-
-public class PropertyAnimator implements Runnable {
- private static final String LOGTAG = "GeckoPropertyAnimator";
-
- public static enum Property {
- ALPHA,
- TRANSLATION_X,
- TRANSLATION_Y,
- SCROLL_X,
- SCROLL_Y,
- WIDTH,
- HEIGHT
- }
-
- private class ElementHolder {
- View view;
- Property property;
- float from;
- float to;
- }
-
- public static interface PropertyAnimationListener {
- public void onPropertyAnimationStart();
- public void onPropertyAnimationEnd();
- }
-
- private final Interpolator mInterpolator;
- private long mStartTime;
- private final long mDuration;
- private final float mDurationReciprocal;
- private final List<ElementHolder> mElementsList;
- private List<PropertyAnimationListener> mListeners;
- FramePoster mFramePoster;
- private boolean mUseHardwareLayer;
-
- public PropertyAnimator(long duration) {
- this(duration, new DecelerateInterpolator());
- }
-
- public PropertyAnimator(long duration, Interpolator interpolator) {
- mDuration = duration;
- mDurationReciprocal = 1.0f / mDuration;
- mInterpolator = interpolator;
- mElementsList = new ArrayList<ElementHolder>();
- mFramePoster = FramePoster.create(this);
- mUseHardwareLayer = true;
- }
-
- public void setUseHardwareLayer(boolean useHardwareLayer) {
- mUseHardwareLayer = useHardwareLayer;
- }
-
- public void attach(View view, Property property, float to) {
- ElementHolder element = new ElementHolder();
-
- element.view = view;
- element.property = property;
- element.to = to;
-
- mElementsList.add(element);
- }
-
- public void addPropertyAnimationListener(PropertyAnimationListener listener) {
- if (mListeners == null) {
- mListeners = new ArrayList<PropertyAnimationListener>();
- }
-
- mListeners.add(listener);
- }
-
- public long getDuration() {
- return mDuration;
- }
-
- public long getRemainingTime() {
- int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);
- return mDuration - timePassed;
- }
-
- @Override
- public void run() {
- int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);
- if (timePassed >= mDuration) {
- stop();
- return;
- }
-
- float interpolation = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
-
- for (ElementHolder element : mElementsList) {
- float delta = element.from + ((element.to - element.from) * interpolation);
- invalidate(element, delta);
- }
-
- mFramePoster.postNextAnimationFrame();
- }
-
- public void start() {
- if (mDuration == 0) {
- return;
- }
-
- mStartTime = AnimationUtils.currentAnimationTimeMillis();
-
- // Fix the from value based on current position and property
- for (ElementHolder element : mElementsList) {
- if (element.property == Property.ALPHA)
- element.from = ViewHelper.getAlpha(element.view);
- else if (element.property == Property.TRANSLATION_Y)
- element.from = ViewHelper.getTranslationY(element.view);
- else if (element.property == Property.TRANSLATION_X)
- element.from = ViewHelper.getTranslationX(element.view);
- else if (element.property == Property.SCROLL_Y)
- element.from = ViewHelper.getScrollY(element.view);
- else if (element.property == Property.SCROLL_X)
- element.from = ViewHelper.getScrollX(element.view);
- else if (element.property == Property.WIDTH)
- element.from = ViewHelper.getWidth(element.view);
- else if (element.property == Property.HEIGHT)
- element.from = ViewHelper.getHeight(element.view);
-
- ViewCompat.setHasTransientState(element.view, true);
-
- if (shouldEnableHardwareLayer(element))
- element.view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
- else
- element.view.setDrawingCacheEnabled(true);
- }
-
- // Get ViewTreeObserver from any of the participant views
- // in the animation.
- final ViewTreeObserver treeObserver;
- if (mElementsList.size() > 0) {
- treeObserver = mElementsList.get(0).view.getViewTreeObserver();
- } else {
- treeObserver = null;
- }
-
- final ViewTreeObserver.OnPreDrawListener preDrawListener = new ViewTreeObserver.OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- if (treeObserver.isAlive()) {
- treeObserver.removeOnPreDrawListener(this);
- }
-
- mFramePoster.postFirstAnimationFrame();
- return true;
- }
- };
-
- // Try to start animation after any on-going layout round
- // in the current view tree. OnPreDrawListener seems broken
- // on pre-Honeycomb devices, start animation immediatelly
- // in this case.
- if (treeObserver != null && treeObserver.isAlive()) {
- treeObserver.addOnPreDrawListener(preDrawListener);
- } else {
- mFramePoster.postFirstAnimationFrame();
- }
-
- if (mListeners != null) {
- for (PropertyAnimationListener listener : mListeners) {
- listener.onPropertyAnimationStart();
- }
- }
- }
-
- /**
- * Stop the animation, optionally snapping to the end position.
- * onPropertyAnimationEnd is only called when snapping to the end position.
- */
- public void stop(boolean snapToEndPosition) {
- mFramePoster.cancelAnimationFrame();
-
- // Make sure to snap to the end position.
- for (ElementHolder element : mElementsList) {
- if (snapToEndPosition)
- invalidate(element, element.to);
-
- ViewCompat.setHasTransientState(element.view, false);
-
- if (shouldEnableHardwareLayer(element)) {
- element.view.setLayerType(View.LAYER_TYPE_NONE, null);
- } else {
- element.view.setDrawingCacheEnabled(false);
- }
- }
-
- mElementsList.clear();
-
- if (mListeners != null) {
- if (snapToEndPosition) {
- for (PropertyAnimationListener listener : mListeners) {
- listener.onPropertyAnimationEnd();
- }
- }
-
- mListeners.clear();
- mListeners = null;
- }
- }
-
- public void stop() {
- stop(true);
- }
-
- private boolean shouldEnableHardwareLayer(ElementHolder element) {
- if (!mUseHardwareLayer) {
- return false;
- }
-
- if (!(element.view instanceof ViewGroup)) {
- return false;
- }
-
- if (element.property == Property.ALPHA ||
- element.property == Property.TRANSLATION_Y ||
- element.property == Property.TRANSLATION_X) {
- return true;
- }
-
- return false;
- }
-
- private void invalidate(final ElementHolder element, final float delta) {
- final View view = element.view;
-
- // check to see if the view was detached between the check above and this code
- // getting run on the UI thread.
- if (view.getHandler() == null)
- return;
-
- if (element.property == Property.ALPHA)
- ViewHelper.setAlpha(element.view, delta);
- else if (element.property == Property.TRANSLATION_Y)
- ViewHelper.setTranslationY(element.view, delta);
- else if (element.property == Property.TRANSLATION_X)
- ViewHelper.setTranslationX(element.view, delta);
- else if (element.property == Property.SCROLL_Y)
- ViewHelper.scrollTo(element.view, ViewHelper.getScrollX(element.view), (int) delta);
- else if (element.property == Property.SCROLL_X)
- ViewHelper.scrollTo(element.view, (int) delta, ViewHelper.getScrollY(element.view));
- else if (element.property == Property.WIDTH)
- ViewHelper.setWidth(element.view, (int) delta);
- else if (element.property == Property.HEIGHT)
- ViewHelper.setHeight(element.view, (int) delta);
- }
-
- private static abstract class FramePoster {
- public static FramePoster create(Runnable r) {
- if (Versions.feature16Plus) {
- return new FramePosterPostJB(r);
- }
-
- return new FramePosterPreJB(r);
- }
-
- public abstract void postFirstAnimationFrame();
- public abstract void postNextAnimationFrame();
- public abstract void cancelAnimationFrame();
- }
-
- private static class FramePosterPreJB extends FramePoster {
- // Default refresh rate in ms.
- private static final int INTERVAL = 10;
-
- private final Handler mHandler;
- private final Runnable mRunnable;
-
- public FramePosterPreJB(Runnable r) {
- mHandler = new Handler();
- mRunnable = r;
- }
-
- @Override
- public void postFirstAnimationFrame() {
- mHandler.post(mRunnable);
- }
-
- @Override
- public void postNextAnimationFrame() {
- mHandler.postDelayed(mRunnable, INTERVAL);
- }
-
- @Override
- public void cancelAnimationFrame() {
- mHandler.removeCallbacks(mRunnable);
- }
- }
-
- private static class FramePosterPostJB extends FramePoster {
- private final Choreographer mChoreographer;
- private final Choreographer.FrameCallback mCallback;
-
- public FramePosterPostJB(final Runnable r) {
- mChoreographer = Choreographer.getInstance();
-
- mCallback = new Choreographer.FrameCallback() {
- @Override
- public void doFrame(long frameTimeNanos) {
- r.run();
- }
- };
- }
-
- @Override
- public void postFirstAnimationFrame() {
- postNextAnimationFrame();
- }
-
- @Override
- public void postNextAnimationFrame() {
- mChoreographer.postFrameCallback(mCallback);
- }
-
- @Override
- public void cancelAnimationFrame() {
- mChoreographer.removeFrameCallback(mCallback);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/animation/Rotate3DAnimation.java b/mobile/android/base/java/org/mozilla/gecko/animation/Rotate3DAnimation.java
deleted file mode 100644
index 7e8377f55..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/animation/Rotate3DAnimation.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright (C) 2007 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.mozilla.gecko.animation;
-
-import android.view.animation.Animation;
-import android.view.animation.Transformation;
-
-import android.graphics.Camera;
-import android.graphics.Matrix;
-
-/**
- * An animation that rotates the view on the Y axis between two specified angles.
- * This animation also adds a translation on the Z axis (depth) to improve the effect.
- */
-public class Rotate3DAnimation extends Animation {
- private final float mFromDegrees;
- private final float mToDegrees;
-
- private final float mCenterX;
- private final float mCenterY;
-
- private final float mDepthZ;
- private final boolean mReverse;
- private Camera mCamera;
-
- private int mWidth = 1;
- private int mHeight = 1;
-
- /**
- * Creates a new 3D rotation on the Y axis. The rotation is defined by its
- * start angle and its end angle. Both angles are in degrees. The rotation
- * is performed around a center point on the 2D space, defined by a pair
- * of X and Y coordinates, called centerX and centerY. When the animation
- * starts, a translation on the Z axis (depth) is performed. The length
- * of the translation can be specified, as well as whether the translation
- * should be reversed in time.
- *
- * @param fromDegrees the start angle of the 3D rotation
- * @param toDegrees the end angle of the 3D rotation
- * @param centerX the X center of the 3D rotation
- * @param centerY the Y center of the 3D rotation
- * @param reverse true if the translation should be reversed, false otherwise
- */
- public Rotate3DAnimation(float fromDegrees, float toDegrees,
- float centerX, float centerY, float depthZ, boolean reverse) {
- mFromDegrees = fromDegrees;
- mToDegrees = toDegrees;
- mCenterX = centerX;
- mCenterY = centerY;
- mDepthZ = depthZ;
- mReverse = reverse;
- }
-
- @Override
- public void initialize(int width, int height, int parentWidth, int parentHeight) {
- super.initialize(width, height, parentWidth, parentHeight);
- mCamera = new Camera();
- mWidth = width;
- mHeight = height;
- }
-
- @Override
- protected void applyTransformation(float interpolatedTime, Transformation t) {
- final float fromDegrees = mFromDegrees;
- float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);
-
- final Camera camera = mCamera;
- final Matrix matrix = t.getMatrix();
-
- camera.save();
- if (mReverse) {
- camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);
- } else {
- camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime));
- }
- camera.rotateX(degrees);
- camera.getMatrix(matrix);
- camera.restore();
-
- matrix.preTranslate(-mCenterX * mWidth, -mCenterY * mHeight);
- matrix.postTranslate(mCenterX * mWidth, mCenterY * mHeight);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/animation/ViewHelper.java b/mobile/android/base/java/org/mozilla/gecko/animation/ViewHelper.java
deleted file mode 100644
index 3ea2e8437..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/animation/ViewHelper.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.animation;
-
-import android.view.View;
-import android.view.ViewGroup;
-
-public final class ViewHelper {
- private ViewHelper() {
- }
-
- public static float getTranslationX(View view) {
- if (view != null) {
- return view.getTranslationX();
- }
-
- return 0;
- }
-
- public static void setTranslationX(View view, float translationX) {
- if (view != null) {
- view.setTranslationX(translationX);
- }
- }
-
- public static float getTranslationY(View view) {
- if (view != null) {
- return view.getTranslationY();
- }
-
- return 0;
- }
-
- public static void setTranslationY(View view, float translationY) {
- if (view != null) {
- view.setTranslationY(translationY);
- }
- }
-
- public static float getAlpha(View view) {
- if (view != null) {
- return view.getAlpha();
- }
-
- return 1;
- }
-
- public static void setAlpha(View view, float alpha) {
- if (view != null) {
- view.setAlpha(alpha);
- }
- }
-
- public static int getWidth(View view) {
- if (view != null) {
- return view.getWidth();
- }
-
- return 0;
- }
-
- public static void setWidth(View view, int width) {
- if (view != null) {
- ViewGroup.LayoutParams lp = view.getLayoutParams();
- lp.width = width;
- view.setLayoutParams(lp);
- }
- }
-
- public static int getHeight(View view) {
- if (view != null) {
- return view.getHeight();
- }
-
- return 0;
- }
-
- public static void setHeight(View view, int height) {
- if (view != null) {
- ViewGroup.LayoutParams lp = view.getLayoutParams();
- lp.height = height;
- view.setLayoutParams(lp);
- }
- }
-
- public static int getScrollX(View view) {
- if (view != null) {
- return view.getScrollX();
- }
-
- return 0;
- }
-
- public static int getScrollY(View view) {
- if (view != null) {
- return view.getScrollY();
- }
-
- return 0;
- }
-
- public static void scrollTo(View view, int scrollX, int scrollY) {
- if (view != null) {
- view.scrollTo(scrollX, scrollY);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/cleanup/FileCleanupController.java b/mobile/android/base/java/org/mozilla/gecko/cleanup/FileCleanupController.java
deleted file mode 100644
index 447b837e8..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/cleanup/FileCleanupController.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, you can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-package org.mozilla.gecko.cleanup;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.support.annotation.VisibleForTesting;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Encapsulates the code to run the {@link FileCleanupService}. Call
- * {@link #startIfReady(Context, SharedPreferences, String)} to start the clean-up.
- *
- * Note: for simplicity, the current implementation does not cache which
- * files have been cleaned up and will attempt to delete the same files
- * each time it is run. If the file deletion list grows large, consider
- * keeping a cache.
- */
-public class FileCleanupController {
-
- private static final long MILLIS_BETWEEN_CLEANUPS = TimeUnit.DAYS.toMillis(7);
- @VisibleForTesting static final String PREF_LAST_CLEANUP_MILLIS = "cleanup.lastFileCleanupMillis";
-
- // These will be prepended with the path of the profile we're cleaning up.
- private static final String[] PROFILE_FILES_TO_CLEANUP = new String[] {
- "health.db",
- "health.db-journal",
- "health.db-shm",
- "health.db-wal",
- };
-
- /**
- * Starts the clean-up if it's time to clean-up, otherwise returns. For simplicity,
- * it does not schedule the cleanup for some point in the future - this method will
- * have to be called again (i.e. polled) in order to run the clean-up service.
- *
- * @param context Context of the calling {@link android.app.Activity}
- * @param sharedPrefs The {@link SharedPreferences} instance to store the controller state to
- * @param profilePath The path to the profile the service should clean-up files from
- */
- public static void startIfReady(final Context context, final SharedPreferences sharedPrefs, final String profilePath) {
- if (!isCleanupReady(sharedPrefs)) {
- return;
- }
-
- recordCleanupScheduled(sharedPrefs);
-
- final Intent fileCleanupIntent = new Intent(context, FileCleanupService.class);
- fileCleanupIntent.setAction(FileCleanupService.ACTION_DELETE_FILES);
- fileCleanupIntent.putExtra(FileCleanupService.EXTRA_FILE_PATHS_TO_DELETE, getFilesToCleanup(profilePath + "/"));
- context.startService(fileCleanupIntent);
- }
-
- private static boolean isCleanupReady(final SharedPreferences sharedPrefs) {
- final long lastCleanupMillis = sharedPrefs.getLong(PREF_LAST_CLEANUP_MILLIS, -1);
- return lastCleanupMillis + MILLIS_BETWEEN_CLEANUPS < System.currentTimeMillis();
- }
-
- private static void recordCleanupScheduled(final SharedPreferences sharedPrefs) {
- final SharedPreferences.Editor editor = sharedPrefs.edit();
- editor.putLong(PREF_LAST_CLEANUP_MILLIS, System.currentTimeMillis()).apply();
- }
-
- @VisibleForTesting
- static ArrayList<String> getFilesToCleanup(final String profilePath) {
- final ArrayList<String> out = new ArrayList<>(PROFILE_FILES_TO_CLEANUP.length);
- for (final String path : PROFILE_FILES_TO_CLEANUP) {
- // Append a file separator, just in-case the caller didn't include one.
- out.add(profilePath + File.separator + path);
- }
- return out;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/cleanup/FileCleanupService.java b/mobile/android/base/java/org/mozilla/gecko/cleanup/FileCleanupService.java
deleted file mode 100644
index 76aff733a..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/cleanup/FileCleanupService.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, you can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-package org.mozilla.gecko.cleanup;
-
-import android.app.IntentService;
-import android.content.Intent;
-import android.util.Log;
-
-import java.io.File;
-import java.util.ArrayList;
-
-/**
- * An IntentService to delete files.
- *
- * It takes an {@link ArrayList} of String file paths to delete via the extra
- * {@link #EXTRA_FILE_PATHS_TO_DELETE}. If these file paths are directories, they will
- * not be traversed recursively and will only be deleted if empty. This is to avoid accidentally
- * trashing a users' profile if a folder is accidentally listed.
- *
- * An IntentService was chosen because:
- * * It generally won't be killed when the Activity is
- * * (unlike HandlerThread) The system handles scheduling, prioritizing,
- * and shutting down the underlying background thread
- * * (unlike an existing background thread) We don't block our background operations
- * for this, which doesn't directly affect the user.
- *
- * The major trade-off is that this Service is very dangerous if it's exported... so don't do that!
- */
-public class FileCleanupService extends IntentService {
- private static final String LOGTAG = "Gecko" + FileCleanupService.class.getSimpleName();
- private static final String WORKER_THREAD_NAME = LOGTAG + "Worker";
-
- public static final String ACTION_DELETE_FILES = "org.mozilla.gecko.intent.action.DELETE_FILES";
- public static final String EXTRA_FILE_PATHS_TO_DELETE = "org.mozilla.gecko.file_paths_to_delete";
-
- public FileCleanupService() {
- super(WORKER_THREAD_NAME);
-
- // We're likely to get scheduled again - let's wait until then in order to avoid:
- // * The coding complexity of re-running this
- // * Consuming system resources: we were probably killed for resource conservation purposes
- setIntentRedelivery(false);
- }
-
- @Override
- protected void onHandleIntent(final Intent intent) {
- if (!isIntentValid(intent)) {
- return;
- }
-
- final ArrayList<String> filesToDelete = intent.getStringArrayListExtra(EXTRA_FILE_PATHS_TO_DELETE);
- for (final String path : filesToDelete) {
- final File file = new File(path);
- file.delete();
- }
- }
-
- private static boolean isIntentValid(final Intent intent) {
- if (intent == null) {
- Log.w(LOGTAG, "Received null intent");
- return false;
- }
-
- if (!intent.getAction().equals(ACTION_DELETE_FILES)) {
- Log.w(LOGTAG, "Received unknown intent action: " + intent.getAction());
- return false;
- }
-
- if (!intent.hasExtra(EXTRA_FILE_PATHS_TO_DELETE)) {
- Log.w(LOGTAG, "Received intent with no files extra");
- return false;
- }
-
- return true;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
deleted file mode 100644
index b1bf567b0..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
+++ /dev/null
@@ -1,177 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.customtabs;
-
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.support.v7.app.ActionBar;
-import android.support.v7.widget.Toolbar;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.Window;
-import android.view.WindowManager;
-import android.widget.TextView;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.util.ColorUtil;
-import org.mozilla.gecko.util.GeckoRequest;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import java.lang.reflect.Field;
-
-import static android.support.customtabs.CustomTabsIntent.EXTRA_TOOLBAR_COLOR;
-
-public class CustomTabsActivity extends GeckoApp implements Tabs.OnTabsChangedListener {
- private static final String LOGTAG = "CustomTabsActivity";
- private static final String SAVED_TOOLBAR_COLOR = "SavedToolbarColor";
- private static final String SAVED_TOOLBAR_TITLE = "SavedToolbarTitle";
- private static final int NO_COLOR = -1;
- private Toolbar toolbar;
-
- private ActionBar actionBar;
- private int tabId = -1;
- private boolean useDomainTitle = true;
-
- private int toolbarColor;
- private String toolbarTitle;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- if (savedInstanceState != null) {
- toolbarColor = savedInstanceState.getInt(SAVED_TOOLBAR_COLOR, NO_COLOR);
- toolbarTitle = savedInstanceState.getString(SAVED_TOOLBAR_TITLE, AppConstants.MOZ_APP_BASENAME);
- } else {
- toolbarColor = NO_COLOR;
- toolbarTitle = AppConstants.MOZ_APP_BASENAME;
- }
-
- Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
- updateActionBarWithToolbar(toolbar);
- try {
- // Since we don't create the Toolbar's TextView ourselves, this seems
- // to be the only way of changing the ellipsize setting.
- Field f = toolbar.getClass().getDeclaredField("mTitleTextView");
- f.setAccessible(true);
- TextView textView = (TextView) f.get(toolbar);
- textView.setEllipsize(TextUtils.TruncateAt.START);
- } catch (Exception e) {
- // If we can't ellipsize at the start of the title, we shouldn't display the host
- // so as to avoid displaying a misleadingly truncated host.
- Log.w(LOGTAG, "Failed to get Toolbar TextView, using default title.");
- useDomainTitle = false;
- }
- actionBar = getSupportActionBar();
- actionBar.setTitle(toolbarTitle);
- updateToolbarColor(toolbar);
-
- toolbar.setNavigationOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- onBackPressed();
- }
- });
-
- Tabs.registerOnTabsChangedListener(this);
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- Tabs.unregisterOnTabsChangedListener(this);
- }
-
- @Override
- public int getLayout() {
- return R.layout.customtabs_activity;
- }
-
- @Override
- protected void onDone() {
- finish();
- }
-
- @Override
- public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
- if (tab == null) {
- return;
- }
-
- if (tabId >= 0 && tab.getId() != tabId) {
- return;
- }
-
- if (msg == Tabs.TabEvents.LOCATION_CHANGE) {
- tabId = tab.getId();
- final Uri uri = Uri.parse(tab.getURL());
- String title = null;
- if (uri != null) {
- title = uri.getHost();
- }
- if (!useDomainTitle || title == null || title.isEmpty()) {
- toolbarTitle = AppConstants.MOZ_APP_BASENAME;
- } else {
- toolbarTitle = title;
- }
- actionBar.setTitle(toolbarTitle);
- }
- }
-
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
-
- outState.putInt(SAVED_TOOLBAR_COLOR, toolbarColor);
- outState.putString(SAVED_TOOLBAR_TITLE, toolbarTitle);
- }
-
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- finish();
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- private void updateActionBarWithToolbar(final Toolbar toolbar) {
- setSupportActionBar(toolbar);
- final ActionBar ab = getSupportActionBar();
- if (ab != null) {
- ab.setDisplayHomeAsUpEnabled(true);
- }
- }
-
- private void updateToolbarColor(final Toolbar toolbar) {
- if (toolbarColor == NO_COLOR) {
- final int color = getIntent().getIntExtra(EXTRA_TOOLBAR_COLOR, NO_COLOR);
- if (color == NO_COLOR) {
- return;
- }
- toolbarColor = color;
- }
-
- final int titleTextColor = ColorUtil.getReadableTextColor(toolbarColor);
-
- toolbar.setBackgroundColor(toolbarColor);
- toolbar.setTitleTextColor(titleTextColor);
- final Window window = getWindow();
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
- window.setStatusBarColor(ColorUtil.darken(toolbarColor, 0.25));
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/customtabs/GeckoCustomTabsService.java b/mobile/android/base/java/org/mozilla/gecko/customtabs/GeckoCustomTabsService.java
deleted file mode 100644
index 7960f7832..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/customtabs/GeckoCustomTabsService.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.customtabs;
-
-import android.net.Uri;
-import android.os.Bundle;
-import android.support.customtabs.CustomTabsService;
-import android.support.customtabs.CustomTabsSessionToken;
-import android.util.Log;
-
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.GeckoService;
-
-import java.util.List;
-
-/**
- * Custom tabs service external, third-party apps connect to.
- */
-public class GeckoCustomTabsService extends CustomTabsService {
- private static final String LOGTAG = "GeckoCustomTabsService";
- private static final boolean DEBUG = false;
-
- @Override
- protected boolean updateVisuals(CustomTabsSessionToken sessionToken, Bundle bundle) {
- Log.v(LOGTAG, "updateVisuals()");
-
- return false;
- }
-
- @Override
- protected boolean warmup(long flags) {
- if (DEBUG) {
- Log.v(LOGTAG, "warming up...");
- }
-
- GeckoService.startGecko(GeckoProfile.initFromArgs(this, null), null, getApplicationContext());
-
- return true;
- }
-
- @Override
- protected boolean newSession(CustomTabsSessionToken sessionToken) {
- Log.v(LOGTAG, "newSession()");
-
- // Pretend session has been started
- return true;
- }
-
- @Override
- protected boolean mayLaunchUrl(CustomTabsSessionToken sessionToken, Uri uri, Bundle bundle, List<Bundle> list) {
- Log.v(LOGTAG, "mayLaunchUrl()");
-
- return false;
- }
-
- @Override
- protected Bundle extraCommand(String commandName, Bundle bundle) {
- Log.v(LOGTAG, "extraCommand()");
-
- return null;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/AbstractPerProfileDatabaseProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/AbstractPerProfileDatabaseProvider.java
deleted file mode 100644
index 2e056cc1e..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/AbstractPerProfileDatabaseProvider.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.net.Uri;
-
-/**
- * The base class for ContentProviders that wish to use a different DB
- * for each profile.
- *
- * This class has logic shared between ordinary per-profile CPs and
- * those that wish to share DB connections between CPs.
- */
-public abstract class AbstractPerProfileDatabaseProvider extends AbstractTransactionalProvider {
-
- /**
- * Extend this to provide access to your own map of shared databases. This
- * is a method so that your subclass doesn't collide with others!
- */
- protected abstract PerProfileDatabases<? extends SQLiteOpenHelper> getDatabases();
-
- /*
- * Fetches a readable database based on the profile indicated in the
- * passed URI. If the URI does not contain a profile param, the default profile
- * is used.
- *
- * @param uri content URI optionally indicating the profile of the user
- * @return instance of a readable SQLiteDatabase
- */
- @Override
- protected SQLiteDatabase getReadableDatabase(Uri uri) {
- String profile = null;
- if (uri != null) {
- profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
- }
-
- return getDatabases().getDatabaseHelperForProfile(profile, isTest(uri)).getReadableDatabase();
- }
-
- /*
- * Fetches a writable database based on the profile indicated in the
- * passed URI. If the URI does not contain a profile param, the default profile
- * is used
- *
- * @param uri content URI optionally indicating the profile of the user
- * @return instance of a writable SQLiteDatabase
- */
- @Override
- protected SQLiteDatabase getWritableDatabase(Uri uri) {
- String profile = null;
- if (uri != null) {
- profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
- }
-
- return getDatabases().getDatabaseHelperForProfile(profile, isTest(uri)).getWritableDatabase();
- }
-
- protected SQLiteDatabase getWritableDatabaseForProfile(String profile, boolean isTest) {
- return getDatabases().getDatabaseHelperForProfile(profile, isTest).getWritableDatabase();
- }
-
- /**
- * This method should ONLY be used for testing purposes.
- *
- * @param uri content URI optionally indicating the profile of the user
- * @return instance of a writable SQLiteDatabase
- */
- @Override
- @RobocopTarget
- public SQLiteDatabase getWritableDatabaseForTesting(Uri uri) {
- return getWritableDatabase(uri);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/AbstractTransactionalProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/AbstractTransactionalProvider.java
deleted file mode 100644
index 7e289b76f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/AbstractTransactionalProvider.java
+++ /dev/null
@@ -1,328 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import org.mozilla.gecko.AppConstants.Versions;
-
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.database.SQLException;
-import android.database.sqlite.SQLiteDatabase;
-import android.net.Uri;
-import android.text.TextUtils;
-import android.util.Log;
-
-/**
- * This abstract class exists to capture some of the transaction-handling
- * commonalities in Fennec's DB layer.
- *
- * In particular, this abstracts DB access, batching, and a particular
- * transaction approach.
- *
- * That approach is: subclasses implement the abstract methods
- * {@link #insertInTransaction(android.net.Uri, android.content.ContentValues)},
- * {@link #deleteInTransaction(android.net.Uri, String, String[])}, and
- * {@link #updateInTransaction(android.net.Uri, android.content.ContentValues, String, String[])}.
- *
- * These are all called expecting a transaction to be established, so failed
- * modifications can be rolled-back, and work batched.
- *
- * If no transaction is established, that's not a problem. Transaction nesting
- * can be avoided by using {@link #beginWrite(SQLiteDatabase)}.
- *
- * The decision of when to begin a transaction is left to the subclasses,
- * primarily to avoid the pattern of a transaction being begun, a read occurring,
- * and then a write being necessary. This lock upgrade can result in SQLITE_BUSY,
- * which we don't handle well. Better to avoid starting a transaction too soon!
- *
- * You are probably interested in some subclasses:
- *
- * * {@link AbstractPerProfileDatabaseProvider} provides a simple abstraction for
- * querying databases that are stored in the user's profile directory.
- * * {@link PerProfileDatabaseProvider} is a simple version that only allows a
- * single ContentProvider to access each per-profile database.
- * * {@link SharedBrowserDatabaseProvider} is an example of a per-profile provider
- * that allows for multiple providers to safely work with the same databases.
- */
-@SuppressWarnings("javadoc")
-public abstract class AbstractTransactionalProvider extends ContentProvider {
- private static final String LOGTAG = "GeckoTransProvider";
-
- private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
- private static final boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
-
- protected abstract SQLiteDatabase getReadableDatabase(Uri uri);
- protected abstract SQLiteDatabase getWritableDatabase(Uri uri);
-
- public abstract SQLiteDatabase getWritableDatabaseForTesting(Uri uri);
-
- protected abstract Uri insertInTransaction(Uri uri, ContentValues values);
- protected abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
- protected abstract int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs);
-
- /**
- * Track whether we're in a batch operation.
- *
- * When we're in a batch operation, individual write steps won't even try
- * to start a transaction... and neither will they attempt to finish one.
- *
- * Set this to <code>Boolean.TRUE</code> when you're entering a batch --
- * a section of code in which {@link ContentProvider} methods will be
- * called, but nested transactions should not be started. Callers are
- * responsible for beginning and ending the enclosing transaction, and
- * for setting this to <code>Boolean.FALSE</code> when done.
- *
- * This is a ThreadLocal separate from `db.inTransaction` because batched
- * operations start transactions independent of individual ContentProvider
- * operations. This doesn't work well with the entire concept of this
- * abstract class -- that is, automatically beginning and ending transactions
- * for each insert/delete/update operation -- and doing so without
- * causing arbitrary nesting requires external tracking.
- *
- * Note that beginWrite takes a DB argument, but we don't differentiate
- * between databases in this tracking flag. If your ContentProvider manages
- * multiple database transactions within the same thread, you'll need to
- * amend this scheme -- but then, you're already doing some serious wizardry,
- * so rock on.
- */
- final ThreadLocal<Boolean> isInBatchOperation = new ThreadLocal<Boolean>();
-
- private boolean isInBatch() {
- final Boolean isInBatch = isInBatchOperation.get();
- if (isInBatch == null) {
- return false;
- }
-
- return isInBatch;
- }
-
- /**
- * If we're not currently in a transaction, and we should be, start one.
- */
- protected void beginWrite(final SQLiteDatabase db) {
- if (isInBatch()) {
- trace("Not bothering with an intermediate write transaction: inside batch operation.");
- return;
- }
-
- if (!db.inTransaction()) {
- trace("beginWrite: beginning transaction.");
- db.beginTransaction();
- }
- }
-
- /**
- * If we're not in a batch, but we are in a write transaction, mark it as
- * successful.
- */
- protected void markWriteSuccessful(final SQLiteDatabase db) {
- if (isInBatch()) {
- trace("Not marking write successful: inside batch operation.");
- return;
- }
-
- if (db.inTransaction()) {
- trace("Marking write transaction successful.");
- db.setTransactionSuccessful();
- }
- }
-
- /**
- * If we're not in a batch, but we are in a write transaction,
- * end it.
- *
- * @see PerProfileDatabaseProvider#markWriteSuccessful(SQLiteDatabase)
- */
- protected void endWrite(final SQLiteDatabase db) {
- if (isInBatch()) {
- trace("Not ending write: inside batch operation.");
- return;
- }
-
- if (db.inTransaction()) {
- trace("endWrite: ending transaction.");
- db.endTransaction();
- }
- }
-
- protected void beginBatch(final SQLiteDatabase db) {
- trace("Beginning batch.");
- isInBatchOperation.set(Boolean.TRUE);
- db.beginTransaction();
- }
-
- protected void markBatchSuccessful(final SQLiteDatabase db) {
- if (isInBatch()) {
- trace("Marking batch successful.");
- db.setTransactionSuccessful();
- return;
- }
- Log.w(LOGTAG, "Unexpectedly asked to mark batch successful, but not in batch!");
- throw new IllegalStateException("Not in batch.");
- }
-
- protected void endBatch(final SQLiteDatabase db) {
- trace("Ending batch.");
- db.endTransaction();
- isInBatchOperation.set(Boolean.FALSE);
- }
-
- @Override
- public int delete(Uri uri, String selection, String[] selectionArgs) {
- trace("Calling delete on URI: " + uri + ", " + selection + ", " + selectionArgs);
-
- final SQLiteDatabase db = getWritableDatabase(uri);
- int deleted = 0;
-
- try {
- deleted = deleteInTransaction(uri, selection, selectionArgs);
- markWriteSuccessful(db);
- } finally {
- endWrite(db);
- }
-
- if (deleted > 0) {
- final boolean shouldSyncToNetwork = !isCallerSync(uri);
- getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
- }
-
- return deleted;
- }
-
- @Override
- public Uri insert(Uri uri, ContentValues values) {
- trace("Calling insert on URI: " + uri);
-
- final SQLiteDatabase db = getWritableDatabase(uri);
- Uri result = null;
- try {
- result = insertInTransaction(uri, values);
- markWriteSuccessful(db);
- } catch (SQLException sqle) {
- Log.e(LOGTAG, "exception in DB operation", sqle);
- } catch (UnsupportedOperationException uoe) {
- Log.e(LOGTAG, "don't know how to perform that insert", uoe);
- } finally {
- endWrite(db);
- }
-
- if (result != null) {
- final boolean shouldSyncToNetwork = !isCallerSync(uri);
- getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
- }
-
- return result;
- }
-
- @Override
- public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
- trace("Calling update on URI: " + uri + ", " + selection + ", " + selectionArgs);
-
- final SQLiteDatabase db = getWritableDatabase(uri);
- int updated = 0;
-
- try {
- updated = updateInTransaction(uri, values, selection,
- selectionArgs);
- markWriteSuccessful(db);
- } finally {
- endWrite(db);
- }
-
- if (updated > 0) {
- final boolean shouldSyncToNetwork = !isCallerSync(uri);
- getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
- }
-
- return updated;
- }
-
- @Override
- public int bulkInsert(Uri uri, ContentValues[] values) {
- if (values == null) {
- return 0;
- }
-
- int numValues = values.length;
- int successes = 0;
-
- final SQLiteDatabase db = getWritableDatabase(uri);
-
- debug("bulkInsert: explicitly starting transaction.");
- beginBatch(db);
-
- try {
- for (int i = 0; i < numValues; i++) {
- insertInTransaction(uri, values[i]);
- successes++;
- }
- trace("Flushing DB bulkinsert...");
- markBatchSuccessful(db);
- } finally {
- debug("bulkInsert: explicitly ending transaction.");
- endBatch(db);
- }
-
- if (successes > 0) {
- final boolean shouldSyncToNetwork = !isCallerSync(uri);
- getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
- }
-
- return successes;
- }
-
- /**
- * Indicates whether a query should include deleted fields
- * based on the URI.
- * @param uri query URI
- */
- protected static boolean shouldShowDeleted(Uri uri) {
- String showDeleted = uri.getQueryParameter(BrowserContract.PARAM_SHOW_DELETED);
- return !TextUtils.isEmpty(showDeleted);
- }
-
- /**
- * Indicates whether an insertion should be made if a record doesn't
- * exist, based on the URI.
- * @param uri query URI
- */
- protected static boolean shouldUpdateOrInsert(Uri uri) {
- String insertIfNeeded = uri.getQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED);
- return Boolean.parseBoolean(insertIfNeeded);
- }
-
- /**
- * Indicates whether query is a test based on the URI.
- * @param uri query URI
- */
- protected static boolean isTest(Uri uri) {
- if (uri == null) {
- return false;
- }
- String isTest = uri.getQueryParameter(BrowserContract.PARAM_IS_TEST);
- return !TextUtils.isEmpty(isTest);
- }
-
- /**
- * Return true of the query is from Firefox Sync.
- * @param uri query URI
- */
- protected static boolean isCallerSync(Uri uri) {
- String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
- return !TextUtils.isEmpty(isSync);
- }
-
- protected static void trace(String message) {
- if (logVerbose) {
- Log.v(LOGTAG, message);
- }
- }
-
- protected static void debug(String message) {
- if (logDebug) {
- Log.d(LOGTAG, message);
- }
- }
-} \ No newline at end of file
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/BaseTable.java b/mobile/android/base/java/org/mozilla/gecko/db/BaseTable.java
deleted file mode 100644
index 418d547ed..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/BaseTable.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.net.Uri;
-import android.util.Log;
-
-// BaseTable provides a basic implementation of a Table for tables that don't require advanced operations during
-// insert, delete, update, or query operations. Implementors must still provide onCreate and onUpgrade operations.
-public abstract class BaseTable implements Table {
- private static final String LOGTAG = "GeckoBaseTable";
-
- private static final boolean DEBUG = false;
-
- protected static void log(String msg) {
- if (DEBUG) {
- Log.i(LOGTAG, msg);
- }
- }
-
- // Table implementation
- @Override
- public Table.ContentProviderInfo[] getContentProviderInfo() {
- return new Table.ContentProviderInfo[0];
- }
-
- // Returns the name of the table to modify/query
- protected abstract String getTable();
-
- // Table implementation
- @Override
- public Cursor query(SQLiteDatabase db, Uri uri, int dbId, String[] columns, String selection, String[] selectionArgs, String sortOrder, String groupBy, String limit) {
- Cursor c = db.query(getTable(), columns, selection, selectionArgs, groupBy, null, sortOrder, limit);
- log("query " + columns + " in " + selection + " = " + c);
- return c;
- }
-
- @Override
- public int update(SQLiteDatabase db, Uri uri, int dbId, ContentValues values, String selection, String[] selectionArgs) {
- int updated = db.updateWithOnConflict(getTable(), values, selection, selectionArgs, SQLiteDatabase.CONFLICT_REPLACE);
- log("update " + values + " in " + selection + " = " + updated);
- return updated;
- }
-
- @Override
- public long insert(SQLiteDatabase db, Uri uri, int dbId, ContentValues values) {
- long inserted = db.insertOrThrow(getTable(), null, values);
- log("insert " + values + " = " + inserted);
- return inserted;
- }
-
- @Override
- public int delete(SQLiteDatabase db, Uri uri, int dbId, String selection, String[] selectionArgs) {
- int deleted = db.delete(getTable(), selection, selectionArgs);
- log("delete " + selection + " = " + deleted);
- return deleted;
- }
-};
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java b/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java
deleted file mode 100644
index 51c8d964f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java
+++ /dev/null
@@ -1,785 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import org.mozilla.gecko.AppConstants;
-
-import android.net.Uri;
-import android.support.annotation.NonNull;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-
-@RobocopTarget
-public class BrowserContract {
- public static final String AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.browser";
- public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
-
- public static final String PASSWORDS_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.passwords";
- public static final Uri PASSWORDS_AUTHORITY_URI = Uri.parse("content://" + PASSWORDS_AUTHORITY);
-
- public static final String FORM_HISTORY_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.formhistory";
- public static final Uri FORM_HISTORY_AUTHORITY_URI = Uri.parse("content://" + FORM_HISTORY_AUTHORITY);
-
- public static final String TABS_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.tabs";
- public static final Uri TABS_AUTHORITY_URI = Uri.parse("content://" + TABS_AUTHORITY);
-
- public static final String HOME_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.home";
- public static final Uri HOME_AUTHORITY_URI = Uri.parse("content://" + HOME_AUTHORITY);
-
- public static final String PROFILES_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".profiles";
- public static final Uri PROFILES_AUTHORITY_URI = Uri.parse("content://" + PROFILES_AUTHORITY);
-
- public static final String READING_LIST_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.readinglist";
- public static final Uri READING_LIST_AUTHORITY_URI = Uri.parse("content://" + READING_LIST_AUTHORITY);
-
- public static final String SEARCH_HISTORY_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.searchhistory";
- public static final Uri SEARCH_HISTORY_AUTHORITY_URI = Uri.parse("content://" + SEARCH_HISTORY_AUTHORITY);
-
- public static final String LOGINS_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.logins";
- public static final Uri LOGINS_AUTHORITY_URI = Uri.parse("content://" + LOGINS_AUTHORITY);
-
- public static final String PARAM_PROFILE = "profile";
- public static final String PARAM_PROFILE_PATH = "profilePath";
- public static final String PARAM_LIMIT = "limit";
- public static final String PARAM_SUGGESTEDSITES_LIMIT = "suggestedsites_limit";
- public static final String PARAM_TOPSITES_DISABLE_PINNED = "topsites_disable_pinned";
- public static final String PARAM_IS_SYNC = "sync";
- public static final String PARAM_SHOW_DELETED = "show_deleted";
- public static final String PARAM_IS_TEST = "test";
- public static final String PARAM_INSERT_IF_NEEDED = "insert_if_needed";
- public static final String PARAM_INCREMENT_VISITS = "increment_visits";
- public static final String PARAM_INCREMENT_REMOTE_AGGREGATES = "increment_remote_aggregates";
- public static final String PARAM_EXPIRE_PRIORITY = "priority";
- public static final String PARAM_DATASET_ID = "dataset_id";
- public static final String PARAM_GROUP_BY = "group_by";
-
- static public enum ExpirePriority {
- NORMAL,
- AGGRESSIVE
- }
-
- /**
- * Produces a SQL expression used for sorting results of the "combined" view by frecency.
- * Combines remote and local frecency calculations, weighting local visits much heavier.
- *
- * @param includesBookmarks When URL is bookmarked, should we give it bonus frecency points?
- * @param ascending Indicates if sorting order ascending
- * @return Combined frecency sorting expression
- */
- static public String getCombinedFrecencySortOrder(boolean includesBookmarks, boolean ascending) {
- final long now = System.currentTimeMillis();
- StringBuilder order = new StringBuilder(getRemoteFrecencySQL(now) + " + " + getLocalFrecencySQL(now));
-
- if (includesBookmarks) {
- order.insert(0, "(CASE WHEN " + Combined.BOOKMARK_ID + " > -1 THEN 100 ELSE 0 END) + ");
- }
-
- order.append(ascending ? " ASC" : " DESC");
- return order.toString();
- }
-
- /**
- * See Bug 1265525 for details (explanation + graphs) on how Remote frecency compares to Local frecency for different
- * combinations of visits count and age.
- *
- * @param now Base time in milliseconds for age calculation
- * @return remote frecency SQL calculation
- */
- static public String getRemoteFrecencySQL(final long now) {
- return getFrecencyCalculation(now, 1, 110, Combined.REMOTE_VISITS_COUNT, Combined.REMOTE_DATE_LAST_VISITED);
- }
-
- /**
- * Local frecency SQL calculation. Note higher scale factor and squared visit count which achieve
- * visits generated locally being much preferred over remote visits.
- * See Bug 1265525 for details (explanation + comparison graphs).
- *
- * @param now Base time in milliseconds for age calculation
- * @return local frecency SQL calculation
- */
- static public String getLocalFrecencySQL(final long now) {
- String visitCountExpr = "(" + Combined.LOCAL_VISITS_COUNT + " + 2)";
- visitCountExpr = visitCountExpr + " * " + visitCountExpr;
-
- return getFrecencyCalculation(now, 2, 225, visitCountExpr, Combined.LOCAL_DATE_LAST_VISITED);
- }
-
- /**
- * Our version of frecency is computed by scaling the number of visits by a multiplier
- * that approximates Gaussian decay, based on how long ago the entry was last visited.
- * Since we're limited by the math we can do with sqlite, we're calculating this
- * approximation using the Cauchy distribution: multiplier = scale_const / (age^2 + scale_const).
- * For example, with 15 as our scale parameter, we get a scale constant 15^2 = 225. Then:
- * frecencyScore = numVisits * max(1, 100 * 225 / (age*age + 225)). (See bug 704977)
- *
- * @param now Base time in milliseconds for age calculation
- * @param minFrecency Minimum allowed frecency value
- * @param multiplier Scale constant
- * @param visitCountExpr Expression which will produce a visit count
- * @param lastVisitExpr Expression which will produce "last-visited" timestamp
- * @return Frecency SQL calculation
- */
- static public String getFrecencyCalculation(final long now, final int minFrecency, final int multiplier, @NonNull final String visitCountExpr, @NonNull final String lastVisitExpr) {
- final long nowInMicroseconds = now * 1000;
- final long microsecondsPerDay = 86400000000L;
- final String ageExpr = "(" + nowInMicroseconds + " - " + lastVisitExpr + ") / " + microsecondsPerDay;
-
- return visitCountExpr + " * MAX(" + minFrecency + ", 100 * " + multiplier + " / (" + ageExpr + " * " + ageExpr + " + " + multiplier + "))";
- }
-
- @RobocopTarget
- public interface CommonColumns {
- public static final String _ID = "_id";
- }
-
- @RobocopTarget
- public interface DateSyncColumns {
- public static final String DATE_CREATED = "created";
- public static final String DATE_MODIFIED = "modified";
- }
-
- @RobocopTarget
- public interface SyncColumns extends DateSyncColumns {
- public static final String GUID = "guid";
- public static final String IS_DELETED = "deleted";
- }
-
- @RobocopTarget
- public interface URLColumns {
- public static final String URL = "url";
- public static final String TITLE = "title";
- }
-
- @RobocopTarget
- public interface FaviconColumns {
- public static final String FAVICON = "favicon";
- public static final String FAVICON_ID = "favicon_id";
- public static final String FAVICON_URL = "favicon_url";
- }
-
- @RobocopTarget
- public interface HistoryColumns {
- public static final String DATE_LAST_VISITED = "date";
- public static final String VISITS = "visits";
- // Aggregates used to speed up top sites and search frecency-powered queries
- public static final String LOCAL_VISITS = "visits_local";
- public static final String REMOTE_VISITS = "visits_remote";
- public static final String LOCAL_DATE_LAST_VISITED = "date_local";
- public static final String REMOTE_DATE_LAST_VISITED = "date_remote";
- }
-
- @RobocopTarget
- public interface VisitsColumns {
- public static final String HISTORY_GUID = "history_guid";
- public static final String VISIT_TYPE = "visit_type";
- public static final String DATE_VISITED = "date";
- // Used to distinguish between visits that were generated locally vs those that came in from Sync.
- // Since we don't track "origin clientID" for visits, this is the best we can do for now.
- public static final String IS_LOCAL = "is_local";
- }
-
- public interface PageMetadataColumns {
- public static final String HISTORY_GUID = "history_guid";
- public static final String DATE_CREATED = "created";
- public static final String HAS_IMAGE = "has_image";
- public static final String JSON = "json";
- }
-
- public interface DeletedColumns {
- public static final String ID = "id";
- public static final String GUID = "guid";
- public static final String TIME_DELETED = "timeDeleted";
- }
-
- @RobocopTarget
- public static final class Favicons implements CommonColumns, DateSyncColumns {
- private Favicons() {}
-
- public static final String TABLE_NAME = "favicons";
-
- public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "favicons");
-
- public static final String URL = "url";
- public static final String DATA = "data";
- public static final String PAGE_URL = "page_url";
- }
-
- @RobocopTarget
- public static final class Thumbnails implements CommonColumns {
- private Thumbnails() {}
-
- public static final String TABLE_NAME = "thumbnails";
-
- public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "thumbnails");
-
- public static final String URL = "url";
- public static final String DATA = "data";
- }
-
- public static final class Profiles {
- private Profiles() {}
- public static final String NAME = "name";
- public static final String PATH = "path";
- }
-
- @RobocopTarget
- public static final class Bookmarks implements CommonColumns, URLColumns, FaviconColumns, SyncColumns {
- private Bookmarks() {}
-
- public static final String TABLE_NAME = "bookmarks";
-
- public static final String VIEW_WITH_FAVICONS = "bookmarks_with_favicons";
-
- public static final String VIEW_WITH_ANNOTATIONS = "bookmarks_with_annotations";
-
- public static final int FIXED_ROOT_ID = 0;
- public static final int FAKE_DESKTOP_FOLDER_ID = -1;
- public static final int FIXED_READING_LIST_ID = -2;
- public static final int FIXED_PINNED_LIST_ID = -3;
- public static final int FIXED_SCREENSHOT_FOLDER_ID = -4;
- public static final int FAKE_READINGLIST_SMARTFOLDER_ID = -5;
-
- /**
- * This ID and the following negative IDs are reserved for bookmarks from Android's partner
- * bookmark provider.
- */
- public static final long FAKE_PARTNER_BOOKMARKS_START = -1000;
-
- public static final String MOBILE_FOLDER_GUID = "mobile";
- public static final String PLACES_FOLDER_GUID = "places";
- public static final String MENU_FOLDER_GUID = "menu";
- public static final String TAGS_FOLDER_GUID = "tags";
- public static final String TOOLBAR_FOLDER_GUID = "toolbar";
- public static final String UNFILED_FOLDER_GUID = "unfiled";
- public static final String FAKE_DESKTOP_FOLDER_GUID = "desktop";
- public static final String PINNED_FOLDER_GUID = "pinned";
- public static final String SCREENSHOT_FOLDER_GUID = "screenshots";
- public static final String FAKE_READINGLIST_SMARTFOLDER_GUID = "readinglist";
-
- public static final int TYPE_FOLDER = 0;
- public static final int TYPE_BOOKMARK = 1;
- public static final int TYPE_SEPARATOR = 2;
- public static final int TYPE_LIVEMARK = 3;
- public static final int TYPE_QUERY = 4;
-
- public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "bookmarks");
- public static final Uri PARENTS_CONTENT_URI = Uri.withAppendedPath(CONTENT_URI, "parents");
- // Hacky API for bulk-updating positions. Bug 728783.
- public static final Uri POSITIONS_CONTENT_URI = Uri.withAppendedPath(CONTENT_URI, "positions");
- public static final long DEFAULT_POSITION = Long.MIN_VALUE;
-
- public static final String CONTENT_TYPE = "vnd.android.cursor.dir/bookmark";
- public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/bookmark";
- public static final String TYPE = "type";
- public static final String PARENT = "parent";
- public static final String POSITION = "position";
- public static final String TAGS = "tags";
- public static final String DESCRIPTION = "description";
- public static final String KEYWORD = "keyword";
-
- public static final String ANNOTATION_KEY = "annotation_key";
- public static final String ANNOTATION_VALUE = "annotation_value";
- }
-
- @RobocopTarget
- public static final class History implements CommonColumns, URLColumns, HistoryColumns, FaviconColumns, SyncColumns {
- private History() {}
-
- public static final String TABLE_NAME = "history";
-
- public static final String VIEW_WITH_FAVICONS = "history_with_favicons";
-
- public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "history");
- public static final Uri CONTENT_OLD_URI = Uri.withAppendedPath(AUTHORITY_URI, "history/old");
- public static final String CONTENT_TYPE = "vnd.android.cursor.dir/browser-history";
- public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/browser-history";
- }
-
- @RobocopTarget
- public static final class Visits implements CommonColumns, VisitsColumns {
- private Visits() {}
-
- public static final String TABLE_NAME = "visits";
-
- public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "visits");
-
- public static final int VISIT_IS_LOCAL = 1;
- public static final int VISIT_IS_REMOTE = 0;
- }
-
- // Combined bookmarks and history
- @RobocopTarget
- public static final class Combined implements CommonColumns, URLColumns, HistoryColumns, FaviconColumns {
- private Combined() {}
-
- public static final String VIEW_NAME = "combined";
-
- public static final String VIEW_WITH_FAVICONS = "combined_with_favicons";
-
- public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "combined");
-
- public static final String BOOKMARK_ID = "bookmark_id";
- public static final String HISTORY_ID = "history_id";
-
- public static final String REMOTE_VISITS_COUNT = "remoteVisitCount";
- public static final String REMOTE_DATE_LAST_VISITED = "remoteDateLastVisited";
-
- public static final String LOCAL_VISITS_COUNT = "localVisitCount";
- public static final String LOCAL_DATE_LAST_VISITED = "localDateLastVisited";
- }
-
- public static final class Schema {
- private Schema() {}
- public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "schema");
-
- public static final String VERSION = "version";
- }
-
- public static final class Passwords {
- private Passwords() {}
- public static final Uri CONTENT_URI = Uri.withAppendedPath(PASSWORDS_AUTHORITY_URI, "passwords");
- public static final String CONTENT_TYPE = "vnd.android.cursor.dir/passwords";
-
- public static final String ID = "id";
- public static final String HOSTNAME = "hostname";
- public static final String HTTP_REALM = "httpRealm";
- public static final String FORM_SUBMIT_URL = "formSubmitURL";
- public static final String USERNAME_FIELD = "usernameField";
- public static final String PASSWORD_FIELD = "passwordField";
- public static final String ENCRYPTED_USERNAME = "encryptedUsername";
- public static final String ENCRYPTED_PASSWORD = "encryptedPassword";
- public static final String ENC_TYPE = "encType";
- public static final String TIME_CREATED = "timeCreated";
- public static final String TIME_LAST_USED = "timeLastUsed";
- public static final String TIME_PASSWORD_CHANGED = "timePasswordChanged";
- public static final String TIMES_USED = "timesUsed";
- public static final String GUID = "guid";
-
- // This needs to be kept in sync with the types defined in toolkit/components/passwordmgr/nsILoginManagerCrypto.idl#45
- public static final int ENCTYPE_SDR = 1;
- }
-
- public static final class DeletedPasswords implements DeletedColumns {
- private DeletedPasswords() {}
- public static final String CONTENT_TYPE = "vnd.android.cursor.dir/deleted-passwords";
- public static final Uri CONTENT_URI = Uri.withAppendedPath(PASSWORDS_AUTHORITY_URI, "deleted-passwords");
- }
-
- @RobocopTarget
- public static final class GeckoDisabledHosts {
- private GeckoDisabledHosts() {}
- public static final String CONTENT_TYPE = "vnd.android.cursor.dir/disabled-hosts";
- public static final Uri CONTENT_URI = Uri.withAppendedPath(PASSWORDS_AUTHORITY_URI, "disabled-hosts");
-
- public static final String HOSTNAME = "hostname";
- }
-
- public static final class FormHistory {
- private FormHistory() {}
- public static final Uri CONTENT_URI = Uri.withAppendedPath(FORM_HISTORY_AUTHORITY_URI, "formhistory");
- public static final String CONTENT_TYPE = "vnd.android.cursor.dir/formhistory";
-
- public static final String ID = "id";
- public static final String FIELD_NAME = "fieldname";
- public static final String VALUE = "value";
- public static final String TIMES_USED = "timesUsed";
- public static final String FIRST_USED = "firstUsed";
- public static final String LAST_USED = "lastUsed";
- public static final String GUID = "guid";
- }
-
- public static final class DeletedFormHistory implements DeletedColumns {
- private DeletedFormHistory() {}
- public static final Uri CONTENT_URI = Uri.withAppendedPath(FORM_HISTORY_AUTHORITY_URI, "deleted-formhistory");
- public static final String CONTENT_TYPE = "vnd.android.cursor.dir/deleted-formhistory";
- }
-
- @RobocopTarget
- public static final class Tabs implements CommonColumns {
- private Tabs() {}
- public static final String TABLE_NAME = "tabs";
-
- public static final Uri CONTENT_URI = Uri.withAppendedPath(TABS_AUTHORITY_URI, "tabs");
- public static final String CONTENT_TYPE = "vnd.android.cursor.dir/tab";
- public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/tab";
-
- // Title of the tab.
- public static final String TITLE = "title";
-
- // Topmost URL from the history array. Allows processing of this tab without
- // parsing that array.
- public static final String URL = "url";
-
- // Sync-assigned GUID for client device. NULL for local tabs.
- public static final String CLIENT_GUID = "client_guid";
-
- // JSON-encoded array of history URL strings, from most recent to least recent.
- public static final String HISTORY = "history";
-
- // Favicon URL for the tab's topmost history entry.
- public static final String FAVICON = "favicon";
-
- // Last used time of the tab.
- public static final String LAST_USED = "last_used";
-
- // Position of the tab. 0 represents foreground.
- public static final String POSITION = "position";
- }
-
- public static final class Clients implements CommonColumns {
- private Clients() {}
- public static final Uri CONTENT_RECENCY_URI = Uri.withAppendedPath(TABS_AUTHORITY_URI, "clients_recency");
- public static final Uri CONTENT_URI = Uri.withAppendedPath(TABS_AUTHORITY_URI, "clients");
- public static final String CONTENT_TYPE = "vnd.android.cursor.dir/client";
- public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/client";
-
- // Client-provided name string. Could conceivably be null.
- public static final String NAME = "name";
-
- // Sync-assigned GUID for client device. NULL for local tabs.
- public static final String GUID = "guid";
-
- // Last modified time for the client's tab record. For remote records, a server
- // timestamp provided by Sync during insertion.
- public static final String LAST_MODIFIED = "last_modified";
-
- public static final String DEVICE_TYPE = "device_type";
- }
-
- // Data storage for dynamic panels on about:home
- @RobocopTarget
- public static final class HomeItems implements CommonColumns {
- private HomeItems() {}
- public static final Uri CONTENT_FAKE_URI = Uri.withAppendedPath(HOME_AUTHORITY_URI, "items/fake");
- public static final Uri CONTENT_URI = Uri.withAppendedPath(HOME_AUTHORITY_URI, "items");
-
- public static final String CONTENT_TYPE = "vnd.android.cursor.dir/homeitem";
- public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/homeitem";
-
- public static final String DATASET_ID = "dataset_id";
- public static final String URL = "url";
- public static final String TITLE = "title";
- public static final String DESCRIPTION = "description";
- public static final String IMAGE_URL = "image_url";
- public static final String BACKGROUND_COLOR = "background_color";
- public static final String BACKGROUND_URL = "background_url";
- public static final String CREATED = "created";
- public static final String FILTER = "filter";
-
- public static final String[] DEFAULT_PROJECTION =
- new String[] { _ID, DATASET_ID, URL, TITLE, DESCRIPTION, IMAGE_URL, BACKGROUND_COLOR, BACKGROUND_URL, FILTER };
- }
-
- @RobocopTarget
- public static final class ReadingListItems implements CommonColumns, URLColumns {
- public static final String EXCERPT = "excerpt";
- public static final String CLIENT_LAST_MODIFIED = "client_last_modified";
- public static final String GUID = "guid";
- public static final String SERVER_LAST_MODIFIED = "last_modified";
- public static final String SERVER_STORED_ON = "stored_on";
- public static final String ADDED_ON = "added_on";
- public static final String MARKED_READ_ON = "marked_read_on";
- public static final String IS_DELETED = "is_deleted";
- public static final String IS_ARCHIVED = "is_archived";
- public static final String IS_UNREAD = "is_unread";
- public static final String IS_ARTICLE = "is_article";
- public static final String IS_FAVORITE = "is_favorite";
- public static final String RESOLVED_URL = "resolved_url";
- public static final String RESOLVED_TITLE = "resolved_title";
- public static final String ADDED_BY = "added_by";
- public static final String MARKED_READ_BY = "marked_read_by";
- public static final String WORD_COUNT = "word_count";
- public static final String READ_POSITION = "read_position";
- public static final String CONTENT_STATUS = "content_status";
-
- public static final String SYNC_STATUS = "sync_status";
- public static final String SYNC_CHANGE_FLAGS = "sync_change_flags";
-
- private ReadingListItems() {}
- public static final Uri CONTENT_URI = Uri.withAppendedPath(READING_LIST_AUTHORITY_URI, "items");
-
- public static final String CONTENT_TYPE = "vnd.android.cursor.dir/readinglistitem";
- public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/readinglistitem";
-
- // CONTENT_STATUS represents the result of an attempt to fetch content for the reading list item.
- public static final int STATUS_UNFETCHED = 0;
- public static final int STATUS_FETCH_FAILED_TEMPORARY = 1;
- public static final int STATUS_FETCH_FAILED_PERMANENT = 2;
- public static final int STATUS_FETCH_FAILED_UNSUPPORTED_FORMAT = 3;
- public static final int STATUS_FETCHED_ARTICLE = 4;
-
- // See https://github.com/mozilla-services/readinglist/wiki/Client-phases for how this is expected to work.
- //
- // If an item is SYNCED, it doesn't need to be uploaded.
- //
- // If its status is NEW, the entire record should be uploaded.
- //
- // If DELETED, the record should be deleted. A record can only move into this state from SYNCED; NEW records
- // are deleted immediately.
- //
-
- public static final int SYNC_STATUS_SYNCED = 0;
- public static final int SYNC_STATUS_NEW = 1; // Upload everything.
- public static final int SYNC_STATUS_DELETED = 2; // Delete the record from the server.
- public static final int SYNC_STATUS_MODIFIED = 3; // Consult SYNC_CHANGE_FLAGS.
-
- // SYNC_CHANGE_FLAG represents the sets of fields that need to be uploaded.
- // If its status is only UNREAD_CHANGED (and maybe FAVORITE_CHANGED?), then it can easily be uploaded
- // in a fire-and-forget manner. This change can never conflict.
- //
- // If its status is RESOLVED, then one or more of the content-oriented fields has changed, and a full
- // upload of those fields should occur. These can result in conflicts.
- //
- // Note that these are flags; they should be considered together when deciding on a course of action.
- //
- // These flags are meaningless for records in any state other than SYNCED. They can be safely altered in
- // other states (to avoid having to query to pre-fill a ContentValues), but should be ignored.
- public static final int SYNC_CHANGE_NONE = 0;
- public static final int SYNC_CHANGE_UNREAD_CHANGED = 1 << 0; // => marked_read_{on,by}, is_unread
- public static final int SYNC_CHANGE_FAVORITE_CHANGED = 1 << 1; // => is_favorite
- public static final int SYNC_CHANGE_RESOLVED = 1 << 2; // => is_article, resolved_{url,title}, excerpt, word_count
-
-
- public static final String DEFAULT_SORT_ORDER = CLIENT_LAST_MODIFIED + " DESC";
- public static final String[] DEFAULT_PROJECTION = new String[] { _ID, URL, TITLE, EXCERPT, WORD_COUNT, IS_UNREAD };
-
- // Minimum fields required to create a reading list item.
- public static final String[] REQUIRED_FIELDS = { ReadingListItems.URL, ReadingListItems.TITLE };
-
- // All fields that might be mapped from the DB into a record object.
- public static final String[] ALL_FIELDS = {
- CommonColumns._ID,
- URLColumns.URL,
- URLColumns.TITLE,
- EXCERPT,
- CLIENT_LAST_MODIFIED,
- GUID,
- SERVER_LAST_MODIFIED,
- SERVER_STORED_ON,
- ADDED_ON,
- MARKED_READ_ON,
- IS_DELETED,
- IS_ARCHIVED,
- IS_UNREAD,
- IS_ARTICLE,
- IS_FAVORITE,
- RESOLVED_URL,
- RESOLVED_TITLE,
- ADDED_BY,
- MARKED_READ_BY,
- WORD_COUNT,
- READ_POSITION,
- CONTENT_STATUS,
-
- SYNC_STATUS,
- SYNC_CHANGE_FLAGS,
- };
-
- public static final String TABLE_NAME = "reading_list";
- }
-
- @RobocopTarget
- public static final class TopSites implements CommonColumns, URLColumns {
- private TopSites() {}
-
- public static final int TYPE_BLANK = 0;
- public static final int TYPE_TOP = 1;
- public static final int TYPE_PINNED = 2;
- public static final int TYPE_SUGGESTED = 3;
-
- public static final String BOOKMARK_ID = "bookmark_id";
- public static final String HISTORY_ID = "history_id";
- public static final String TYPE = "type";
-
- public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "topsites");
- }
-
- public static final class Highlights {
- public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "highlights");
-
- public static final String DATE = "date";
- }
-
- @RobocopTarget
- public static final class SearchHistory implements CommonColumns, HistoryColumns {
- private SearchHistory() {}
-
- public static final String CONTENT_TYPE = "vnd.android.cursor.dir/searchhistory";
- public static final String QUERY = "query";
- public static final String DATE = "date";
- public static final String TABLE_NAME = "searchhistory";
-
- public static final Uri CONTENT_URI = Uri.withAppendedPath(SEARCH_HISTORY_AUTHORITY_URI, "searchhistory");
- }
-
- @RobocopTarget
- public static final class SuggestedSites implements CommonColumns, URLColumns {
- private SuggestedSites() {}
-
- public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "suggestedsites");
- }
-
- public static final class ActivityStreamBlocklist implements CommonColumns {
- private ActivityStreamBlocklist() {}
-
- public static final String TABLE_NAME = "activity_stream_blocklist";
- public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, TABLE_NAME);
-
- public static final String URL = "url";
- public static final String CREATED = "created";
- }
-
- @RobocopTarget
- public static final class UrlAnnotations implements CommonColumns, DateSyncColumns {
- private UrlAnnotations() {}
-
- public static final String TABLE_NAME = "urlannotations";
- public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, TABLE_NAME);
-
- public static final String URL = "url";
- public static final String KEY = "key";
- public static final String VALUE = "value";
- public static final String SYNC_STATUS = "sync_status";
-
- public enum Key {
- // We use a parameter, rather than name(), as defensive coding: we can't let the
- // enum name change because we've already stored values into the DB.
- SCREENSHOT ("screenshot"),
-
- /**
- * This key maps URLs to its feeds.
- *
- * Key: feed
- * Value: URL of feed
- */
- FEED("feed"),
-
- /**
- * This key maps URLs of feeds to an object describing the feed.
- *
- * Key: feed_subscription
- * Value: JSON object describing feed
- */
- FEED_SUBSCRIPTION("feed_subscription"),
-
- /**
- * Indicates that this URL (if stored as a bookmark) should be opened into reader view.
- *
- * Key: reader_view
- * Value: String "true" to indicate that we would like to open into reader view.
- */
- READER_VIEW("reader_view"),
-
- /**
- * Indicator that the user interacted with the URL in regards to home screen shortcuts.
- *
- * Key: home_screen_shortcut
- * Value: True: User created an home screen shortcut for this URL
- * False: User declined to create a shortcut for this URL
- */
- HOME_SCREEN_SHORTCUT("home_screen_shortcut");
-
- private final String dbValue;
-
- Key(final String dbValue) { this.dbValue = dbValue; }
- public String getDbValue() { return dbValue; }
- }
-
- public enum SyncStatus {
- // We use a parameter, rather than ordinal(), as defensive coding: we can't let the
- // ordinal values change because we've already stored values into the DB.
- NEW (0);
-
- // Value stored into the database for this column.
- private final int dbValue;
-
- SyncStatus(final int dbValue) {
- this.dbValue = dbValue;
- }
-
- public int getDBValue() { return dbValue; }
- }
-
- /**
- * Value used to indicate that a reader view item is saved. We use the
- */
- public static final String READER_VIEW_SAVED_VALUE = "true";
- }
-
- public static final class Numbers {
- private Numbers() {}
-
- public static final String TABLE_NAME = "numbers";
-
- public static final String POSITION = "position";
-
- public static final int MAX_VALUE = 50;
- }
-
- @RobocopTarget
- public static final class Logins implements CommonColumns {
- private Logins() {}
-
- public static final Uri CONTENT_URI = Uri.withAppendedPath(LOGINS_AUTHORITY_URI, "logins");
- public static final String CONTENT_TYPE = "vnd.android.cursor.dir/logins";
- public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/logins";
- public static final String TABLE_LOGINS = "logins";
-
- public static final String HOSTNAME = "hostname";
- public static final String HTTP_REALM = "httpRealm";
- public static final String FORM_SUBMIT_URL = "formSubmitURL";
- public static final String USERNAME_FIELD = "usernameField";
- public static final String PASSWORD_FIELD = "passwordField";
- public static final String ENCRYPTED_USERNAME = "encryptedUsername";
- public static final String ENCRYPTED_PASSWORD = "encryptedPassword";
- public static final String ENC_TYPE = "encType";
- public static final String TIME_CREATED = "timeCreated";
- public static final String TIME_LAST_USED = "timeLastUsed";
- public static final String TIME_PASSWORD_CHANGED = "timePasswordChanged";
- public static final String TIMES_USED = "timesUsed";
- public static final String GUID = "guid";
- }
-
- @RobocopTarget
- public static final class DeletedLogins implements CommonColumns {
- private DeletedLogins() {}
-
- public static final Uri CONTENT_URI = Uri.withAppendedPath(LOGINS_AUTHORITY_URI, "deleted-logins");
- public static final String CONTENT_TYPE = "vnd.android.cursor.dir/deleted-logins";
- public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/deleted-logins";
- public static final String TABLE_DELETED_LOGINS = "deleted_logins";
-
- public static final String GUID = "guid";
- public static final String TIME_DELETED = "timeDeleted";
- }
-
- @RobocopTarget
- public static final class LoginsDisabledHosts implements CommonColumns {
- private LoginsDisabledHosts() {}
-
- public static final Uri CONTENT_URI = Uri.withAppendedPath(LOGINS_AUTHORITY_URI, "logins-disabled-hosts");
- public static final String CONTENT_TYPE = "vnd.android.cursor.dir/logins-disabled-hosts";
- public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/logins-disabled-hosts";
- public static final String TABLE_DISABLED_HOSTS = "logins_disabled_hosts";
-
- public static final String HOSTNAME = "hostname";
- }
-
- @RobocopTarget
- public static final class PageMetadata implements CommonColumns, PageMetadataColumns {
- private PageMetadata() {}
-
- public static final String TABLE_NAME = "page_metadata";
- public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "page_metadata");
- }
-
- // We refer to the service by name to decouple services from the rest of the code base.
- public static final String TAB_RECEIVED_SERVICE_CLASS_NAME = "org.mozilla.gecko.tabqueue.TabReceivedService";
-
- public static final String SKIP_TAB_QUEUE_FLAG = "skip_tab_queue";
-
- public static final String EXTRA_CLIENT_GUID = "org.mozilla.gecko.extra.CLIENT_ID";
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
deleted file mode 100644
index 4219e45b1..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
+++ /dev/null
@@ -1,205 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import java.io.File;
-import java.util.Collection;
-import java.util.EnumSet;
-import java.util.List;
-
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.db.BrowserContract.ExpirePriority;
-import org.mozilla.gecko.distribution.Distribution;
-import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
-
-import android.content.ContentProviderClient;
-import android.content.ContentProviderOperation;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.graphics.drawable.BitmapDrawable;
-import android.support.v4.content.CursorLoader;
-
-/**
- * Interface for interactions with all databases. If you want an instance
- * that implements this, you should go through GeckoProfile. E.g.,
- * <code>BrowserDB.from(context)</code>.
- */
-public abstract class BrowserDB {
- public static enum FilterFlags {
- EXCLUDE_PINNED_SITES
- }
-
- public abstract Searches getSearches();
- public abstract TabsAccessor getTabsAccessor();
- public abstract URLMetadata getURLMetadata();
- @RobocopTarget public abstract UrlAnnotations getUrlAnnotations();
-
- /**
- * Add default bookmarks to the database.
- * Takes an offset; returns a new offset.
- */
- public abstract int addDefaultBookmarks(Context context, ContentResolver cr, int offset);
-
- /**
- * Add bookmarks from the provided distribution.
- * Takes an offset; returns a new offset.
- */
- public abstract int addDistributionBookmarks(ContentResolver cr, Distribution distribution, int offset);
-
- /**
- * Invalidate cached data.
- */
- public abstract void invalidate();
-
- public abstract int getCount(ContentResolver cr, String database);
-
- /**
- * @return a cursor representing the contents of the DB filtered according to the arguments.
- * Can return <code>null</code>. <code>CursorLoader</code> will handle this correctly.
- */
- public abstract Cursor filter(ContentResolver cr, CharSequence constraint,
- int limit, EnumSet<BrowserDB.FilterFlags> flags);
-
- /**
- * @return a cursor over top sites (high-ranking bookmarks and history).
- * Can return <code>null</code>.
- * Returns no more than <code>limit</code> results.
- * Suggested sites will be limited to being within the first <code>suggestedRangeLimit</code> results.
- */
- public abstract Cursor getTopSites(ContentResolver cr, int suggestedRangeLimit, int limit);
-
- public abstract CursorLoader getActivityStreamTopSites(Context context, int limit);
-
- public abstract void updateVisitedHistory(ContentResolver cr, String uri);
-
- public abstract void updateHistoryTitle(ContentResolver cr, String uri, String title);
-
- /**
- * Can return <code>null</code>.
- */
- public abstract Cursor getAllVisitedHistory(ContentResolver cr);
-
- /**
- * Can return <code>null</code>.
- */
- public abstract Cursor getRecentHistory(ContentResolver cr, int limit);
-
- public abstract Cursor getHistoryForURL(ContentResolver cr, String uri);
-
- public abstract Cursor getRecentHistoryBetweenTime(ContentResolver cr, int historyLimit, long start, long end);
-
- public abstract long getPrePathLastVisitedTimeMilliseconds(ContentResolver cr, String prePath);
-
- public abstract void expireHistory(ContentResolver cr, ExpirePriority priority);
-
- public abstract void removeHistoryEntry(ContentResolver cr, String url);
-
- public abstract void clearHistory(ContentResolver cr, boolean clearSearchHistory);
-
-
- public abstract String getUrlForKeyword(ContentResolver cr, String keyword);
-
- public abstract boolean isBookmark(ContentResolver cr, String uri);
- public abstract boolean addBookmark(ContentResolver cr, String title, String uri);
- public abstract Cursor getBookmarkForUrl(ContentResolver cr, String url);
- public abstract Cursor getBookmarksForPartialUrl(ContentResolver cr, String partialUrl);
- public abstract void removeBookmarksWithURL(ContentResolver cr, String uri);
- public abstract void registerBookmarkObserver(ContentResolver cr, ContentObserver observer);
- public abstract void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword);
- public abstract boolean hasBookmarkWithGuid(ContentResolver cr, String guid);
-
- public abstract boolean insertPageMetadata(ContentProviderClient contentProviderClient, String pageUrl, boolean hasImage, String metadataJSON);
- public abstract int deletePageMetadata(ContentProviderClient contentProviderClient, String pageUrl);
- /**
- * Can return <code>null</code>.
- */
- public abstract Cursor getBookmarksInFolder(ContentResolver cr, long folderId);
-
- public abstract int getBookmarkCountForFolder(ContentResolver cr, long folderId);
-
- /**
- * Get the favicon from the database, if any, associated with the given favicon URL. (That is,
- * the URL of the actual favicon image, not the URL of the page with which the favicon is associated.)
- * @param cr The ContentResolver to use.
- * @param faviconURL The URL of the favicon to fetch from the database.
- * @return The decoded Bitmap from the database, if any. null if none is stored.
- */
- public abstract LoadFaviconResult getFaviconForUrl(Context context, ContentResolver cr, String faviconURL);
-
- /**
- * Try to find a usable favicon URL in the history or bookmarks table.
- */
- public abstract String getFaviconURLFromPageURL(ContentResolver cr, String uri);
-
- public abstract byte[] getThumbnailForUrl(ContentResolver cr, String uri);
- public abstract void updateThumbnailForUrl(ContentResolver cr, String uri, BitmapDrawable thumbnail);
-
- /**
- * Query for non-null thumbnails matching the provided <code>urls</code>.
- * The returned cursor will have no more than, but possibly fewer than,
- * the requested number of thumbnails.
- *
- * Returns null if the provided list of URLs is empty or null.
- */
- public abstract Cursor getThumbnailsForUrls(ContentResolver cr,
- List<String> urls);
-
- public abstract void removeThumbnails(ContentResolver cr);
-
- // Utility function for updating existing history using batch operations
- public abstract void updateHistoryInBatch(ContentResolver cr,
- Collection<ContentProviderOperation> operations, String url,
- String title, long date, int visits);
-
- public abstract void updateBookmarkInBatch(ContentResolver cr,
- Collection<ContentProviderOperation> operations, String url,
- String title, String guid, long parent, long added, long modified,
- long position, String keyword, int type);
-
- public abstract void pinSite(ContentResolver cr, String url, String title, int position);
- public abstract void unpinSite(ContentResolver cr, int position);
-
- public abstract boolean hideSuggestedSite(String url);
- public abstract void setSuggestedSites(SuggestedSites suggestedSites);
- public abstract SuggestedSites getSuggestedSites();
- public abstract boolean hasSuggestedImageUrl(String url);
- public abstract String getSuggestedImageUrlForUrl(String url);
- public abstract int getSuggestedBackgroundColorForUrl(String url);
-
- /**
- * Obtain a set of links for highlights from bookmarks and history.
- *
- * @param context The context to load the cursor.
- * @param limit Maximum number of results to return.
- */
- public abstract CursorLoader getHighlights(Context context, int limit);
-
- /**
- * Block a page from the highlights list.
- *
- * @param url The page URL. Only pages exactly matching this URL will be blocked.
- */
- public abstract void blockActivityStreamSite(ContentResolver cr, String url);
-
- public static BrowserDB from(final Context context) {
- return from(GeckoProfile.get(context));
- }
-
- public static BrowserDB from(final GeckoProfile profile) {
- synchronized (profile.getLock()) {
- BrowserDB db = (BrowserDB) profile.getData();
- if (db != null) {
- return db;
- }
-
- db = new LocalBrowserDB(profile.getName());
- profile.setData(db);
- return db;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java
deleted file mode 100644
index f823d9060..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java
+++ /dev/null
@@ -1,2237 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import java.io.File;
-import java.io.UnsupportedEncodingException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import org.mozilla.apache.commons.codec.binary.Base32;
-import org.json.simple.JSONArray;
-import org.json.simple.JSONObject;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.db.BrowserContract.ActivityStreamBlocklist;
-import org.mozilla.gecko.db.BrowserContract.Bookmarks;
-import org.mozilla.gecko.db.BrowserContract.Combined;
-import org.mozilla.gecko.db.BrowserContract.Favicons;
-import org.mozilla.gecko.db.BrowserContract.History;
-import org.mozilla.gecko.db.BrowserContract.Visits;
-import org.mozilla.gecko.db.BrowserContract.PageMetadata;
-import org.mozilla.gecko.db.BrowserContract.Numbers;
-import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
-import org.mozilla.gecko.db.BrowserContract.SearchHistory;
-import org.mozilla.gecko.db.BrowserContract.Thumbnails;
-import org.mozilla.gecko.db.BrowserContract.UrlAnnotations;
-import org.mozilla.gecko.fxa.FirefoxAccounts;
-import org.mozilla.gecko.reader.SavedReaderViewHelper;
-import org.mozilla.gecko.sync.Utils;
-import org.mozilla.gecko.sync.repositories.android.RepoUtils;
-import org.mozilla.gecko.util.FileUtils;
-
-import static org.mozilla.gecko.db.DBUtils.qualifyColumn;
-
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.DatabaseUtils;
-import android.database.SQLException;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteException;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.database.sqlite.SQLiteStatement;
-import android.net.Uri;
-import android.os.Build;
-import android.util.Log;
-
-
-// public for robocop testing
-public final class BrowserDatabaseHelper extends SQLiteOpenHelper {
- private static final String LOGTAG = "GeckoBrowserDBHelper";
-
- // Replace the Bug number below with your Bug that is conducting a DB upgrade, as to force a merge conflict with any
- // other patches that require a DB upgrade.
- public static final int DATABASE_VERSION = 36; // Bug 1301717
- public static final String DATABASE_NAME = "browser.db";
-
- final protected Context mContext;
-
- static final String TABLE_BOOKMARKS = Bookmarks.TABLE_NAME;
- static final String TABLE_HISTORY = History.TABLE_NAME;
- static final String TABLE_VISITS = Visits.TABLE_NAME;
- static final String TABLE_PAGE_METADATA = PageMetadata.TABLE_NAME;
- static final String TABLE_FAVICONS = Favicons.TABLE_NAME;
- static final String TABLE_THUMBNAILS = Thumbnails.TABLE_NAME;
- static final String TABLE_READING_LIST = ReadingListItems.TABLE_NAME;
- static final String TABLE_TABS = TabsProvider.TABLE_TABS;
- static final String TABLE_CLIENTS = TabsProvider.TABLE_CLIENTS;
- static final String TABLE_LOGINS = BrowserContract.Logins.TABLE_LOGINS;
- static final String TABLE_DELETED_LOGINS = BrowserContract.DeletedLogins.TABLE_DELETED_LOGINS;
- static final String TABLE_DISABLED_HOSTS = BrowserContract.LoginsDisabledHosts.TABLE_DISABLED_HOSTS;
- static final String TABLE_ANNOTATIONS = UrlAnnotations.TABLE_NAME;
-
- static final String VIEW_COMBINED = Combined.VIEW_NAME;
- static final String VIEW_BOOKMARKS_WITH_FAVICONS = Bookmarks.VIEW_WITH_FAVICONS;
- static final String VIEW_BOOKMARKS_WITH_ANNOTATIONS = Bookmarks.VIEW_WITH_ANNOTATIONS;
- static final String VIEW_HISTORY_WITH_FAVICONS = History.VIEW_WITH_FAVICONS;
- static final String VIEW_COMBINED_WITH_FAVICONS = Combined.VIEW_WITH_FAVICONS;
-
- static final String TABLE_BOOKMARKS_JOIN_FAVICONS = TABLE_BOOKMARKS + " LEFT OUTER JOIN " +
- TABLE_FAVICONS + " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.FAVICON_ID) + " = " +
- qualifyColumn(TABLE_FAVICONS, Favicons._ID);
-
- static final String TABLE_BOOKMARKS_JOIN_ANNOTATIONS = TABLE_BOOKMARKS + " JOIN " +
- TABLE_ANNOTATIONS + " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " +
- qualifyColumn(TABLE_ANNOTATIONS, UrlAnnotations.URL);
-
- static final String TABLE_HISTORY_JOIN_FAVICONS = TABLE_HISTORY + " LEFT OUTER JOIN " +
- TABLE_FAVICONS + " ON " + qualifyColumn(TABLE_HISTORY, History.FAVICON_ID) + " = " +
- qualifyColumn(TABLE_FAVICONS, Favicons._ID);
-
- static final String TABLE_BOOKMARKS_TMP = TABLE_BOOKMARKS + "_tmp";
- static final String TABLE_HISTORY_TMP = TABLE_HISTORY + "_tmp";
-
- private static final String[] mobileIdColumns = new String[] { Bookmarks._ID };
- private static final String[] mobileIdSelectionArgs = new String[] { Bookmarks.MOBILE_FOLDER_GUID };
-
- private boolean didCreateTabsTable = false;
- private boolean didCreateCurrentReadingListTable = false;
-
- public BrowserDatabaseHelper(Context context, String databasePath) {
- super(context, databasePath, null, DATABASE_VERSION);
- mContext = context;
- }
-
- private void createBookmarksTable(SQLiteDatabase db) {
- debug("Creating " + TABLE_BOOKMARKS + " table");
-
- db.execSQL("CREATE TABLE " + TABLE_BOOKMARKS + "(" +
- Bookmarks._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- Bookmarks.TITLE + " TEXT," +
- Bookmarks.URL + " TEXT," +
- Bookmarks.TYPE + " INTEGER NOT NULL DEFAULT " + Bookmarks.TYPE_BOOKMARK + "," +
- Bookmarks.PARENT + " INTEGER," +
- Bookmarks.POSITION + " INTEGER NOT NULL," +
- Bookmarks.KEYWORD + " TEXT," +
- Bookmarks.DESCRIPTION + " TEXT," +
- Bookmarks.TAGS + " TEXT," +
- Bookmarks.FAVICON_ID + " INTEGER," +
- Bookmarks.DATE_CREATED + " INTEGER," +
- Bookmarks.DATE_MODIFIED + " INTEGER," +
- Bookmarks.GUID + " TEXT NOT NULL," +
- Bookmarks.IS_DELETED + " INTEGER NOT NULL DEFAULT 0, " +
- "FOREIGN KEY (" + Bookmarks.PARENT + ") REFERENCES " +
- TABLE_BOOKMARKS + "(" + Bookmarks._ID + ")" +
- ");");
-
- db.execSQL("CREATE INDEX bookmarks_url_index ON " + TABLE_BOOKMARKS + "("
- + Bookmarks.URL + ")");
- db.execSQL("CREATE INDEX bookmarks_type_deleted_index ON " + TABLE_BOOKMARKS + "("
- + Bookmarks.TYPE + ", " + Bookmarks.IS_DELETED + ")");
- db.execSQL("CREATE UNIQUE INDEX bookmarks_guid_index ON " + TABLE_BOOKMARKS + "("
- + Bookmarks.GUID + ")");
- db.execSQL("CREATE INDEX bookmarks_modified_index ON " + TABLE_BOOKMARKS + "("
- + Bookmarks.DATE_MODIFIED + ")");
- }
-
- private void createHistoryTable(SQLiteDatabase db) {
- debug("Creating " + TABLE_HISTORY + " table");
- db.execSQL("CREATE TABLE " + TABLE_HISTORY + "(" +
- History._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- History.TITLE + " TEXT," +
- History.URL + " TEXT NOT NULL," +
- // Can we drop VISITS count? Can we calculate it in the Combined view as a sum?
- // See Bug 1277329.
- History.VISITS + " INTEGER NOT NULL DEFAULT 0," +
- History.LOCAL_VISITS + " INTEGER NOT NULL DEFAULT 0," +
- History.REMOTE_VISITS + " INTEGER NOT NULL DEFAULT 0," +
- History.FAVICON_ID + " INTEGER," +
- History.DATE_LAST_VISITED + " INTEGER," +
- History.LOCAL_DATE_LAST_VISITED + " INTEGER NOT NULL DEFAULT 0," +
- History.REMOTE_DATE_LAST_VISITED + " INTEGER NOT NULL DEFAULT 0," +
- History.DATE_CREATED + " INTEGER," +
- History.DATE_MODIFIED + " INTEGER," +
- History.GUID + " TEXT NOT NULL," +
- History.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
- ");");
-
- db.execSQL("CREATE INDEX history_url_index ON " + TABLE_HISTORY + '('
- + History.URL + ')');
- db.execSQL("CREATE UNIQUE INDEX history_guid_index ON " + TABLE_HISTORY + '('
- + History.GUID + ')');
- db.execSQL("CREATE INDEX history_modified_index ON " + TABLE_HISTORY + '('
- + History.DATE_MODIFIED + ')');
- db.execSQL("CREATE INDEX history_visited_index ON " + TABLE_HISTORY + '('
- + History.DATE_LAST_VISITED + ')');
- }
-
- private void createVisitsTable(SQLiteDatabase db) {
- debug("Creating " + TABLE_VISITS + " table");
- db.execSQL("CREATE TABLE " + TABLE_VISITS + "(" +
- Visits._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- Visits.HISTORY_GUID + " TEXT NOT NULL," +
- Visits.VISIT_TYPE + " TINYINT NOT NULL DEFAULT 1," +
- Visits.DATE_VISITED + " INTEGER NOT NULL, " +
- Visits.IS_LOCAL + " TINYINT NOT NULL DEFAULT 1, " +
-
- "FOREIGN KEY (" + Visits.HISTORY_GUID + ") REFERENCES " +
- TABLE_HISTORY + "(" + History.GUID + ") ON DELETE CASCADE ON UPDATE CASCADE" +
- ");");
-
- db.execSQL("CREATE UNIQUE INDEX visits_history_guid_and_date_visited_index ON " + TABLE_VISITS + "("
- + Visits.HISTORY_GUID + "," + Visits.DATE_VISITED + ")");
- db.execSQL("CREATE INDEX visits_history_guid_index ON " + TABLE_VISITS + "(" + Visits.HISTORY_GUID + ")");
- }
-
- private void createFaviconsTable(SQLiteDatabase db) {
- debug("Creating " + TABLE_FAVICONS + " table");
- db.execSQL("CREATE TABLE " + TABLE_FAVICONS + " (" +
- Favicons._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- Favicons.URL + " TEXT UNIQUE," +
- Favicons.DATA + " BLOB," +
- Favicons.DATE_CREATED + " INTEGER," +
- Favicons.DATE_MODIFIED + " INTEGER" +
- ");");
-
- db.execSQL("CREATE INDEX favicons_modified_index ON " + TABLE_FAVICONS + "("
- + Favicons.DATE_MODIFIED + ")");
- }
-
- private void createThumbnailsTable(SQLiteDatabase db) {
- debug("Creating " + TABLE_THUMBNAILS + " table");
- db.execSQL("CREATE TABLE " + TABLE_THUMBNAILS + " (" +
- Thumbnails._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- Thumbnails.URL + " TEXT UNIQUE," +
- Thumbnails.DATA + " BLOB" +
- ");");
- }
-
- private void createPageMetadataTable(SQLiteDatabase db) {
- debug("Creating " + TABLE_PAGE_METADATA + " table");
- db.execSQL("CREATE TABLE " + TABLE_PAGE_METADATA + "(" +
- PageMetadata._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- PageMetadata.HISTORY_GUID + " TEXT NOT NULL," +
- PageMetadata.DATE_CREATED + " INTEGER NOT NULL, " +
- PageMetadata.HAS_IMAGE + " TINYINT NOT NULL DEFAULT 0, " +
- PageMetadata.JSON + " TEXT NOT NULL, " +
-
- "FOREIGN KEY (" + Visits.HISTORY_GUID + ") REFERENCES " +
- TABLE_HISTORY + "(" + History.GUID + ") ON DELETE CASCADE ON UPDATE CASCADE" +
- ");");
-
- // Establish a 1-to-1 relationship with History table.
- db.execSQL("CREATE UNIQUE INDEX page_metadata_history_guid ON " + TABLE_PAGE_METADATA + "("
- + PageMetadata.HISTORY_GUID + ")");
- // Improve performance of commonly occurring selections.
- db.execSQL("CREATE INDEX page_metadata_history_guid_and_has_image ON " + TABLE_PAGE_METADATA + "("
- + PageMetadata.HISTORY_GUID + ", " + PageMetadata.HAS_IMAGE + ")");
- }
-
- private void createBookmarksWithFaviconsView(SQLiteDatabase db) {
- debug("Creating " + VIEW_BOOKMARKS_WITH_FAVICONS + " view");
-
- db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_BOOKMARKS_WITH_FAVICONS + " AS " +
- "SELECT " + qualifyColumn(TABLE_BOOKMARKS, "*") +
- ", " + qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + Bookmarks.FAVICON +
- ", " + qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + Bookmarks.FAVICON_URL +
- " FROM " + TABLE_BOOKMARKS_JOIN_FAVICONS);
- }
-
- private void createBookmarksWithAnnotationsView(SQLiteDatabase db) {
- debug("Creating " + VIEW_BOOKMARKS_WITH_ANNOTATIONS + " view");
-
- db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_BOOKMARKS_WITH_ANNOTATIONS + " AS " +
- "SELECT " + qualifyColumn(TABLE_BOOKMARKS, "*") +
- ", " + qualifyColumn(TABLE_ANNOTATIONS, UrlAnnotations.KEY) + " AS " + Bookmarks.ANNOTATION_KEY +
- ", " + qualifyColumn(TABLE_ANNOTATIONS, UrlAnnotations.VALUE) + " AS " + Bookmarks.ANNOTATION_VALUE +
- " FROM " + TABLE_BOOKMARKS_JOIN_ANNOTATIONS);
- }
-
- private void createHistoryWithFaviconsView(SQLiteDatabase db) {
- debug("Creating " + VIEW_HISTORY_WITH_FAVICONS + " view");
-
- db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_HISTORY_WITH_FAVICONS + " AS " +
- "SELECT " + qualifyColumn(TABLE_HISTORY, "*") +
- ", " + qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + History.FAVICON +
- ", " + qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + History.FAVICON_URL +
- " FROM " + TABLE_HISTORY_JOIN_FAVICONS);
- }
-
- private void createClientsTable(SQLiteDatabase db) {
- debug("Creating " + TABLE_CLIENTS + " table");
-
- // Table for client's name-guid mapping.
- db.execSQL("CREATE TABLE " + TABLE_CLIENTS + "(" +
- BrowserContract.Clients.GUID + " TEXT PRIMARY KEY," +
- BrowserContract.Clients.NAME + " TEXT," +
- BrowserContract.Clients.LAST_MODIFIED + " INTEGER," +
- BrowserContract.Clients.DEVICE_TYPE + " TEXT" +
- ");");
- }
-
- private void createTabsTable(SQLiteDatabase db, final String tableName) {
- debug("Creating tabs.db: " + db.getPath());
- debug("Creating " + tableName + " table");
-
- // Table for each tab on any client.
- db.execSQL("CREATE TABLE " + tableName + "(" +
- BrowserContract.Tabs._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- BrowserContract.Tabs.CLIENT_GUID + " TEXT," +
- BrowserContract.Tabs.TITLE + " TEXT," +
- BrowserContract.Tabs.URL + " TEXT," +
- BrowserContract.Tabs.HISTORY + " TEXT," +
- BrowserContract.Tabs.FAVICON + " TEXT," +
- BrowserContract.Tabs.LAST_USED + " INTEGER," +
- BrowserContract.Tabs.POSITION + " INTEGER, " +
- "FOREIGN KEY (" + BrowserContract.Tabs.CLIENT_GUID + ") REFERENCES " +
- TABLE_CLIENTS + "(" + BrowserContract.Clients.GUID + ") ON DELETE CASCADE" +
- ");");
-
- didCreateTabsTable = true;
- }
-
- private void createTabsTableIndices(SQLiteDatabase db, final String tableName) {
- // Indices on CLIENT_GUID and POSITION.
- db.execSQL("CREATE INDEX " + TabsProvider.INDEX_TABS_GUID +
- " ON " + tableName + "(" + BrowserContract.Tabs.CLIENT_GUID + ")");
- db.execSQL("CREATE INDEX " + TabsProvider.INDEX_TABS_POSITION +
- " ON " + tableName + "(" + BrowserContract.Tabs.POSITION + ")");
- }
-
- // Insert a client row for our local Fennec client.
- private void createLocalClient(SQLiteDatabase db) {
- debug("Inserting local Fennec client into " + TABLE_CLIENTS + " table");
-
- ContentValues values = new ContentValues();
- values.put(BrowserContract.Clients.LAST_MODIFIED, System.currentTimeMillis());
- db.insertOrThrow(TABLE_CLIENTS, null, values);
- }
-
- private void createCombinedViewOn19(SQLiteDatabase db) {
- /*
- The v19 combined view removes the redundant subquery from the v16
- combined view and reorders the columns as necessary to prevent this
- from breaking any code that might be referencing columns by index.
-
- The rows in the ensuing view are, in order:
-
- Combined.BOOKMARK_ID
- Combined.HISTORY_ID
- Combined._ID (always 0)
- Combined.URL
- Combined.TITLE
- Combined.VISITS
- Combined.DATE_LAST_VISITED
- Combined.FAVICON_ID
-
- We need to return an _id column because CursorAdapter requires it for its
- default implementation for the getItemId() method. However, since
- we're not using this feature in the parts of the UI using this view,
- we can just use 0 for all rows.
- */
-
- db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED + " AS" +
-
- // Bookmarks without history.
- " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + "," +
- "-1 AS " + Combined.HISTORY_ID + "," +
- "0 AS " + Combined._ID + "," +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
- "-1 AS " + Combined.VISITS + ", " +
- "-1 AS " + Combined.DATE_LAST_VISITED + "," +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.FAVICON_ID) + " AS " + Combined.FAVICON_ID +
- " FROM " + TABLE_BOOKMARKS +
- " WHERE " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
- // Ignore pinned bookmarks.
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " <> " + Bookmarks.FIXED_PINNED_LIST_ID + " AND " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0 AND " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
- " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
- " UNION ALL" +
-
- // History with and without bookmark.
- " SELECT " +
- "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) +
-
- // Give pinned bookmarks a NULL ID so that they're not treated as bookmarks. We can't
- // completely ignore them here because they're joined with history entries we care about.
- " WHEN 0 THEN " +
- "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) +
- " WHEN " + Bookmarks.FIXED_PINNED_LIST_ID + " THEN " +
- "NULL " +
- "ELSE " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) +
- " END " +
- "ELSE " +
- "NULL " +
- "END AS " + Combined.BOOKMARK_ID + "," +
- qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + "," +
- "0 AS " + Combined._ID + "," +
- qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + "," +
-
- // Prioritize bookmark titles over history titles, since the user may have
- // customized the title for a bookmark.
- "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
- qualifyColumn(TABLE_HISTORY, History.TITLE) +
- ") AS " + Combined.TITLE + "," +
- qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + "," +
- qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED + "," +
- qualifyColumn(TABLE_HISTORY, History.FAVICON_ID) + " AS " + Combined.FAVICON_ID +
-
- // We really shouldn't be selecting deleted bookmarks, but oh well.
- " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
- " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
- " WHERE " +
- qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND " +
- "(" +
- // The left outer join didn't match...
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
-
- // ... or it's a bookmark. This is less efficient than filtering prior
- // to the join if you have lots of folders.
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK +
- ")"
- );
-
- debug("Creating " + VIEW_COMBINED_WITH_FAVICONS + " view");
-
- db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED_WITH_FAVICONS + " AS" +
- " SELECT " + qualifyColumn(VIEW_COMBINED, "*") + ", " +
- qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + Combined.FAVICON_URL + ", " +
- qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + Combined.FAVICON +
- " FROM " + VIEW_COMBINED + " LEFT OUTER JOIN " + TABLE_FAVICONS +
- " ON " + Combined.FAVICON_ID + " = " + qualifyColumn(TABLE_FAVICONS, Favicons._ID));
-
- }
-
- private void createCombinedViewOn33(final SQLiteDatabase db) {
- /*
- Builds on top of v19 combined view, and adds the following aggregates:
- - Combined.LOCAL_DATE_LAST_VISITED - last date visited for all local visits
- - Combined.REMOTE_DATE_LAST_VISITED - last date visited for all remote visits
- - Combined.LOCAL_VISITS_COUNT - total number of local visits
- - Combined.REMOTE_VISITS_COUNT - total number of remote visits
-
- Any code written prior to v33 referencing columns by index directly remains intact
- (yet must die a fiery death), as new columns were added to the end of the list.
-
- The rows in the ensuing view are, in order:
- Combined.BOOKMARK_ID
- Combined.HISTORY_ID
- Combined._ID (always 0)
- Combined.URL
- Combined.TITLE
- Combined.VISITS
- Combined.DATE_LAST_VISITED
- Combined.FAVICON_ID
- Combined.LOCAL_DATE_LAST_VISITED
- Combined.REMOTE_DATE_LAST_VISITED
- Combined.LOCAL_VISITS_COUNT
- Combined.REMOTE_VISITS_COUNT
- */
- db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED + " AS" +
-
- // Bookmarks without history.
- " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + "," +
- "-1 AS " + Combined.HISTORY_ID + "," +
- "0 AS " + Combined._ID + "," +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
- "-1 AS " + Combined.VISITS + ", " +
- "-1 AS " + Combined.DATE_LAST_VISITED + "," +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.FAVICON_ID) + " AS " + Combined.FAVICON_ID + "," +
- "0 AS " + Combined.LOCAL_DATE_LAST_VISITED + ", " +
- "0 AS " + Combined.REMOTE_DATE_LAST_VISITED + ", " +
- "0 AS " + Combined.LOCAL_VISITS_COUNT + ", " +
- "0 AS " + Combined.REMOTE_VISITS_COUNT +
- " FROM " + TABLE_BOOKMARKS +
- " WHERE " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
- // Ignore pinned bookmarks.
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " <> " + Bookmarks.FIXED_PINNED_LIST_ID + " AND " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0 AND " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
- " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
- " UNION ALL" +
-
- // History with and without bookmark.
- " SELECT " +
- "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) +
-
- // Give pinned bookmarks a NULL ID so that they're not treated as bookmarks. We can't
- // completely ignore them here because they're joined with history entries we care about.
- " WHEN 0 THEN " +
- "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) +
- " WHEN " + Bookmarks.FIXED_PINNED_LIST_ID + " THEN " +
- "NULL " +
- "ELSE " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) +
- " END " +
- "ELSE " +
- "NULL " +
- "END AS " + Combined.BOOKMARK_ID + "," +
- qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + "," +
- "0 AS " + Combined._ID + "," +
- qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + "," +
-
- // Prioritize bookmark titles over history titles, since the user may have
- // customized the title for a bookmark.
- "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
- qualifyColumn(TABLE_HISTORY, History.TITLE) +
- ") AS " + Combined.TITLE + "," +
- qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + "," +
- qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED + "," +
- qualifyColumn(TABLE_HISTORY, History.FAVICON_ID) + " AS " + Combined.FAVICON_ID + "," +
-
- // Figure out "last visited" days using MAX values for visit timestamps.
- // We use CASE statements here to separate local from remote visits.
- "COALESCE(MAX(CASE " + qualifyColumn(TABLE_VISITS, Visits.IS_LOCAL) + " " +
- "WHEN 1 THEN " + qualifyColumn(TABLE_VISITS, Visits.DATE_VISITED) + " " +
- "ELSE 0 END" +
- "), 0) AS " + Combined.LOCAL_DATE_LAST_VISITED + ", " +
-
- "COALESCE(MAX(CASE " + qualifyColumn(TABLE_VISITS, Visits.IS_LOCAL) + " " +
- "WHEN 0 THEN " + qualifyColumn(TABLE_VISITS, Visits.DATE_VISITED) + " " +
- "ELSE 0 END" +
- "), 0) AS " + Combined.REMOTE_DATE_LAST_VISITED + ", " +
-
- // Sum up visit counts for local and remote visit types. Again, use CASE to separate the two.
- "COALESCE(SUM(" + qualifyColumn(TABLE_VISITS, Visits.IS_LOCAL) + "), 0) AS " + Combined.LOCAL_VISITS_COUNT + ", " +
- "COALESCE(SUM(CASE " + qualifyColumn(TABLE_VISITS, Visits.IS_LOCAL) + " WHEN 0 THEN 1 ELSE 0 END), 0) AS " + Combined.REMOTE_VISITS_COUNT +
-
- // We need to JOIN on Visits in order to compute visit counts
- " FROM " + TABLE_HISTORY + " " +
- "LEFT OUTER JOIN " + TABLE_VISITS +
- " ON " + qualifyColumn(TABLE_HISTORY, History.GUID) + " = " + qualifyColumn(TABLE_VISITS, Visits.HISTORY_GUID) + " " +
-
- // We really shouldn't be selecting deleted bookmarks, but oh well.
- "LEFT OUTER JOIN " + TABLE_BOOKMARKS +
- " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
- " WHERE " +
- qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND " +
- "(" +
- // The left outer join didn't match...
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
-
- // ... or it's a bookmark. This is less efficient than filtering prior
- // to the join if you have lots of folders.
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK +
-
- ") GROUP BY " + qualifyColumn(TABLE_HISTORY, History.GUID)
- );
-
- debug("Creating " + VIEW_COMBINED_WITH_FAVICONS + " view");
-
- db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED_WITH_FAVICONS + " AS" +
- " SELECT " + qualifyColumn(VIEW_COMBINED, "*") + ", " +
- qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + Combined.FAVICON_URL + ", " +
- qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + Combined.FAVICON +
- " FROM " + VIEW_COMBINED + " LEFT OUTER JOIN " + TABLE_FAVICONS +
- " ON " + Combined.FAVICON_ID + " = " + qualifyColumn(TABLE_FAVICONS, Favicons._ID));
- }
-
- private void createCombinedViewOn34(final SQLiteDatabase db) {
- /*
- Builds on top of v33 combined view, and instead of calculating the following aggregates, gets them
- from the history table:
- - Combined.LOCAL_DATE_LAST_VISITED - last date visited for all local visits
- - Combined.REMOTE_DATE_LAST_VISITED - last date visited for all remote visits
- - Combined.LOCAL_VISITS_COUNT - total number of local visits
- - Combined.REMOTE_VISITS_COUNT - total number of remote visits
-
- Any code written prior to v33 referencing columns by index directly remains intact
- (yet must die a fiery death), as new columns were added to the end of the list.
-
- The rows in the ensuing view are, in order:
- Combined.BOOKMARK_ID
- Combined.HISTORY_ID
- Combined._ID (always 0)
- Combined.URL
- Combined.TITLE
- Combined.VISITS
- Combined.DATE_LAST_VISITED
- Combined.FAVICON_ID
- Combined.LOCAL_DATE_LAST_VISITED
- Combined.REMOTE_DATE_LAST_VISITED
- Combined.LOCAL_VISITS_COUNT
- Combined.REMOTE_VISITS_COUNT
- */
- db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED + " AS" +
-
- // Bookmarks without history.
- " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + "," +
- "-1 AS " + Combined.HISTORY_ID + "," +
- "0 AS " + Combined._ID + "," +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
- "-1 AS " + Combined.VISITS + ", " +
- "-1 AS " + Combined.DATE_LAST_VISITED + "," +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.FAVICON_ID) + " AS " + Combined.FAVICON_ID + "," +
- "0 AS " + Combined.LOCAL_DATE_LAST_VISITED + ", " +
- "0 AS " + Combined.REMOTE_DATE_LAST_VISITED + ", " +
- "0 AS " + Combined.LOCAL_VISITS_COUNT + ", " +
- "0 AS " + Combined.REMOTE_VISITS_COUNT +
- " FROM " + TABLE_BOOKMARKS +
- " WHERE " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
- // Ignore pinned bookmarks.
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " <> " + Bookmarks.FIXED_PINNED_LIST_ID + " AND " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0 AND " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
- " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
- " UNION ALL" +
-
- // History with and without bookmark.
- " SELECT " +
- "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) +
-
- // Give pinned bookmarks a NULL ID so that they're not treated as bookmarks. We can't
- // completely ignore them here because they're joined with history entries we care about.
- " WHEN 0 THEN " +
- "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) +
- " WHEN " + Bookmarks.FIXED_PINNED_LIST_ID + " THEN " +
- "NULL " +
- "ELSE " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) +
- " END " +
- "ELSE " +
- "NULL " +
- "END AS " + Combined.BOOKMARK_ID + "," +
- qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + "," +
- "0 AS " + Combined._ID + "," +
- qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + "," +
-
- // Prioritize bookmark titles over history titles, since the user may have
- // customized the title for a bookmark.
- "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
- qualifyColumn(TABLE_HISTORY, History.TITLE) +
- ") AS " + Combined.TITLE + "," +
- qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + "," +
- qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED + "," +
- qualifyColumn(TABLE_HISTORY, History.FAVICON_ID) + " AS " + Combined.FAVICON_ID + "," +
-
- qualifyColumn(TABLE_HISTORY, History.LOCAL_DATE_LAST_VISITED) + " AS " + Combined.LOCAL_DATE_LAST_VISITED + "," +
- qualifyColumn(TABLE_HISTORY, History.REMOTE_DATE_LAST_VISITED) + " AS " + Combined.REMOTE_DATE_LAST_VISITED + "," +
- qualifyColumn(TABLE_HISTORY, History.LOCAL_VISITS) + " AS " + Combined.LOCAL_VISITS_COUNT + "," +
- qualifyColumn(TABLE_HISTORY, History.REMOTE_VISITS) + " AS " + Combined.REMOTE_VISITS_COUNT +
-
- // We need to JOIN on Visits in order to compute visit counts
- " FROM " + TABLE_HISTORY + " " +
-
- // We really shouldn't be selecting deleted bookmarks, but oh well.
- "LEFT OUTER JOIN " + TABLE_BOOKMARKS +
- " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
- " WHERE " +
- qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND " +
- "(" +
- // The left outer join didn't match...
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
-
- // ... or it's a bookmark. This is less efficient than filtering prior
- // to the join if you have lots of folders.
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + ")"
- );
-
- debug("Creating " + VIEW_COMBINED_WITH_FAVICONS + " view");
-
- db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED_WITH_FAVICONS + " AS" +
- " SELECT " + qualifyColumn(VIEW_COMBINED, "*") + ", " +
- qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + Combined.FAVICON_URL + ", " +
- qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + Combined.FAVICON +
- " FROM " + VIEW_COMBINED + " LEFT OUTER JOIN " + TABLE_FAVICONS +
- " ON " + Combined.FAVICON_ID + " = " + qualifyColumn(TABLE_FAVICONS, Favicons._ID));
- }
-
- private void createLoginsTable(SQLiteDatabase db, final String tableName) {
- debug("Creating logins.db: " + db.getPath());
- debug("Creating " + tableName + " table");
-
- // Table for each login.
- db.execSQL("CREATE TABLE " + tableName + "(" +
- BrowserContract.Logins._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- BrowserContract.Logins.HOSTNAME + " TEXT NOT NULL," +
- BrowserContract.Logins.HTTP_REALM + " TEXT," +
- BrowserContract.Logins.FORM_SUBMIT_URL + " TEXT," +
- BrowserContract.Logins.USERNAME_FIELD + " TEXT NOT NULL," +
- BrowserContract.Logins.PASSWORD_FIELD + " TEXT NOT NULL," +
- BrowserContract.Logins.ENCRYPTED_USERNAME + " TEXT NOT NULL," +
- BrowserContract.Logins.ENCRYPTED_PASSWORD + " TEXT NOT NULL," +
- BrowserContract.Logins.GUID + " TEXT UNIQUE NOT NULL," +
- BrowserContract.Logins.ENC_TYPE + " INTEGER NOT NULL, " +
- BrowserContract.Logins.TIME_CREATED + " INTEGER," +
- BrowserContract.Logins.TIME_LAST_USED + " INTEGER," +
- BrowserContract.Logins.TIME_PASSWORD_CHANGED + " INTEGER," +
- BrowserContract.Logins.TIMES_USED + " INTEGER" +
- ");");
- }
-
- private void createLoginsTableIndices(SQLiteDatabase db, final String tableName) {
- // No need to create an index on GUID, it is an unique column.
- db.execSQL("CREATE INDEX " + LoginsProvider.INDEX_LOGINS_HOSTNAME +
- " ON " + tableName + "(" + BrowserContract.Logins.HOSTNAME + ")");
- db.execSQL("CREATE INDEX " + LoginsProvider.INDEX_LOGINS_HOSTNAME_FORM_SUBMIT_URL +
- " ON " + tableName + "(" + BrowserContract.Logins.HOSTNAME + "," + BrowserContract.Logins.FORM_SUBMIT_URL + ")");
- db.execSQL("CREATE INDEX " + LoginsProvider.INDEX_LOGINS_HOSTNAME_HTTP_REALM +
- " ON " + tableName + "(" + BrowserContract.Logins.HOSTNAME + "," + BrowserContract.Logins.HTTP_REALM + ")");
- }
-
- private void createDeletedLoginsTable(SQLiteDatabase db, final String tableName) {
- debug("Creating deleted_logins.db: " + db.getPath());
- debug("Creating " + tableName + " table");
-
- // Table for each deleted login.
- db.execSQL("CREATE TABLE " + tableName + "(" +
- BrowserContract.DeletedLogins._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- BrowserContract.DeletedLogins.GUID + " TEXT UNIQUE NOT NULL," +
- BrowserContract.DeletedLogins.TIME_DELETED + " INTEGER NOT NULL" +
- ");");
- }
-
- private void createDisabledHostsTable(SQLiteDatabase db, final String tableName) {
- debug("Creating disabled_hosts.db: " + db.getPath());
- debug("Creating " + tableName + " table");
-
- // Table for each disabled host.
- db.execSQL("CREATE TABLE " + tableName + "(" +
- BrowserContract.LoginsDisabledHosts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- BrowserContract.LoginsDisabledHosts.HOSTNAME + " TEXT UNIQUE NOT NULL ON CONFLICT REPLACE" +
- ");");
- }
-
- @Override
- public void onCreate(SQLiteDatabase db) {
- debug("Creating browser.db: " + db.getPath());
-
- for (Table table : BrowserProvider.sTables) {
- table.onCreate(db);
- }
-
- createBookmarksTable(db);
- createHistoryTable(db);
- createFaviconsTable(db);
- createThumbnailsTable(db);
- createClientsTable(db);
- createLocalClient(db);
- createTabsTable(db, TABLE_TABS);
- createTabsTableIndices(db, TABLE_TABS);
-
-
- createBookmarksWithFaviconsView(db);
- createHistoryWithFaviconsView(db);
-
- createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID,
- R.string.bookmarks_folder_places, 0);
-
- createOrUpdateAllSpecialFolders(db);
- createSearchHistoryTable(db);
- createUrlAnnotationsTable(db);
- createNumbersTable(db);
-
- createDeletedLoginsTable(db, TABLE_DELETED_LOGINS);
- createDisabledHostsTable(db, TABLE_DISABLED_HOSTS);
- createLoginsTable(db, TABLE_LOGINS);
- createLoginsTableIndices(db, TABLE_LOGINS);
-
- createBookmarksWithAnnotationsView(db);
-
- createVisitsTable(db);
- createCombinedViewOn34(db);
-
- createActivityStreamBlocklistTable(db);
-
- createPageMetadataTable(db);
- }
-
- /**
- * Copies the tabs and clients tables out of the given tabs.db file and into the destinationDB.
- *
- * @param tabsDBFile Path to existing tabs.db.
- * @param destinationDB The destination database.
- */
- public void copyTabsDB(File tabsDBFile, SQLiteDatabase destinationDB) {
- createClientsTable(destinationDB);
- createTabsTable(destinationDB, TABLE_TABS);
- createTabsTableIndices(destinationDB, TABLE_TABS);
-
- SQLiteDatabase oldTabsDB = null;
- try {
- oldTabsDB = SQLiteDatabase.openDatabase(tabsDBFile.getPath(), null, SQLiteDatabase.OPEN_READONLY);
-
- if (!DBUtils.copyTable(oldTabsDB, TABLE_CLIENTS, destinationDB, TABLE_CLIENTS)) {
- Log.e(LOGTAG, "Failed to migrate table clients; ignoring.");
- }
- if (!DBUtils.copyTable(oldTabsDB, TABLE_TABS, destinationDB, TABLE_TABS)) {
- Log.e(LOGTAG, "Failed to migrate table tabs; ignoring.");
- }
- } catch (Exception e) {
- Log.e(LOGTAG, "Exception occurred while trying to copy from " + tabsDBFile.getPath() +
- " to " + destinationDB.getPath() + "; ignoring.", e);
- } finally {
- if (oldTabsDB != null) {
- oldTabsDB.close();
- }
- }
- }
-
- /**
- * We used to have a separate history extensions database which was used by Sync to store arrays
- * of visits for individual History GUIDs. It was only used by Sync.
- * This function migrates contents of that database over to the Visits table.
- *
- * Warning to callers: this method might throw IllegalStateException if we fail to allocate a
- * cursor to read HistoryExtensionsDB data for whatever reason. See Bug 1280409.
- *
- * @param historyExtensionDb Source History Extensions database
- * @param db Destination database
- */
- private void copyHistoryExtensionDataToVisitsTable(final SQLiteDatabase historyExtensionDb, final SQLiteDatabase db) {
- final String historyExtensionTable = "HistoryExtension";
- final String columnGuid = "guid";
- final String columnVisits = "visits";
-
- final Cursor historyExtensionCursor = historyExtensionDb.query(historyExtensionTable,
- new String[] {columnGuid, columnVisits},
- null, null, null, null, null);
- // Ignore null or empty cursor, we can't (or have nothing to) copy at this point.
- if (historyExtensionCursor == null) {
- return;
- }
- try {
- if (!historyExtensionCursor.moveToFirst()) {
- return;
- }
-
- final int guidCol = historyExtensionCursor.getColumnIndexOrThrow(columnGuid);
-
- // Use prepared (aka "compiled") SQL statements because they are much faster when we're inserting
- // lots of data. We avoid GC churn and recompilation of SQL statements on every insert.
- // NB #1: OR IGNORE clause applies to UNIQUE, NOT NULL, CHECK, and PRIMARY KEY constraints.
- // It does not apply to Foreign Key constraints, but in our case, at this point in time, foreign key
- // constraints are disabled anyway.
- // We care about OR IGNORE because we want to ensure that in case of (GUID,DATE)
- // clash (the UNIQUE constraint), we will not fail the transaction, and just skip conflicting row.
- // Clash might occur if visits array we got from Sync has duplicate (guid,date) records.
- // NB #2: IS_LOCAL is always 0, since we consider all visits coming from Sync to be remote.
- final String insertSqlStatement = "INSERT OR IGNORE INTO " + Visits.TABLE_NAME + " (" +
- Visits.DATE_VISITED + "," +
- Visits.VISIT_TYPE + "," +
- Visits.HISTORY_GUID + "," +
- Visits.IS_LOCAL + ") VALUES (?, ?, ?, " + Visits.VISIT_IS_REMOTE + ")";
- final SQLiteStatement compiledInsertStatement = db.compileStatement(insertSqlStatement);
-
- do {
- final String guid = historyExtensionCursor.getString(guidCol);
-
- // Sanity check, let's not risk a bad incoming GUID.
- if (guid == null || guid.isEmpty()) {
- continue;
- }
-
- // First, check if history with given GUID exists in the History table.
- // We might have a lot of entries in the HistoryExtensionDatabase whose GUID doesn't
- // match one in the History table. Let's avoid doing unnecessary work by first checking if
- // GUID exists locally.
- // Note that we don't have foreign key constraints enabled at this point.
- // See Bug 1266232 for details.
- if (!isGUIDPresentInHistoryTable(db, guid)) {
- continue;
- }
-
- final JSONArray visitsInHistoryExtensionDB = RepoUtils.getJSONArrayFromCursor(historyExtensionCursor, columnVisits);
-
- if (visitsInHistoryExtensionDB == null) {
- continue;
- }
-
- final int histExtVisitCount = visitsInHistoryExtensionDB.size();
-
- debug("Inserting " + histExtVisitCount + " visits from history extension db for GUID: " + guid);
- for (int i = 0; i < histExtVisitCount; i++) {
- final JSONObject visit = (JSONObject) visitsInHistoryExtensionDB.get(i);
-
- // Sanity check.
- if (visit == null) {
- continue;
- }
-
- // Let's not rely on underlying data being correct, and guard against casting failures.
- // Since we can't recover from this (other than ignoring this visit), let's not fail user's migration.
- final Long date;
- final Long visitType;
- try {
- date = (Long) visit.get("date");
- visitType = (Long) visit.get("type");
- } catch (ClassCastException e) {
- continue;
- }
- // Sanity check our incoming data.
- if (date == null || visitType == null) {
- continue;
- }
-
- // Bind parameters use a 1-based index.
- compiledInsertStatement.clearBindings();
- compiledInsertStatement.bindLong(1, date);
- compiledInsertStatement.bindLong(2, visitType);
- compiledInsertStatement.bindString(3, guid);
- compiledInsertStatement.executeInsert();
- }
- } while (historyExtensionCursor.moveToNext());
- } finally {
- // We return on a null cursor, so don't have to check it here.
- historyExtensionCursor.close();
- }
- }
-
- private boolean isGUIDPresentInHistoryTable(final SQLiteDatabase db, String guid) {
- final Cursor historyCursor = db.query(
- History.TABLE_NAME,
- new String[] {History.GUID}, History.GUID + " = ?", new String[] {guid},
- null, null, null);
- if (historyCursor == null) {
- return false;
- }
- try {
- // No history record found for given GUID
- if (!historyCursor.moveToFirst()) {
- return false;
- }
- } finally {
- historyCursor.close();
- }
-
- return true;
- }
-
- private void createSearchHistoryTable(SQLiteDatabase db) {
- debug("Creating " + SearchHistory.TABLE_NAME + " table");
-
- db.execSQL("CREATE TABLE " + SearchHistory.TABLE_NAME + "(" +
- SearchHistory._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
- SearchHistory.QUERY + " TEXT UNIQUE NOT NULL, " +
- SearchHistory.DATE_LAST_VISITED + " INTEGER, " +
- SearchHistory.VISITS + " INTEGER ) ");
-
- db.execSQL("CREATE INDEX idx_search_history_last_visited ON " +
- SearchHistory.TABLE_NAME + "(" + SearchHistory.DATE_LAST_VISITED + ")");
- }
-
- private void createActivityStreamBlocklistTable(final SQLiteDatabase db) {
- debug("Creating " + ActivityStreamBlocklist.TABLE_NAME + " table");
-
- db.execSQL("CREATE TABLE " + ActivityStreamBlocklist.TABLE_NAME + "(" +
- ActivityStreamBlocklist._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
- ActivityStreamBlocklist.URL + " TEXT UNIQUE NOT NULL, " +
- ActivityStreamBlocklist.CREATED + " INTEGER NOT NULL)");
- }
-
- private void createReadingListTable(final SQLiteDatabase db, final String tableName) {
- debug("Creating " + TABLE_READING_LIST + " table");
-
- db.execSQL("CREATE TABLE " + tableName + "(" +
- ReadingListItems._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
- ReadingListItems.GUID + " TEXT UNIQUE, " + // Server-assigned.
-
- ReadingListItems.CONTENT_STATUS + " TINYINT NOT NULL DEFAULT " + ReadingListItems.STATUS_UNFETCHED + ", " +
- ReadingListItems.SYNC_STATUS + " TINYINT NOT NULL DEFAULT " + ReadingListItems.SYNC_STATUS_NEW + ", " +
- ReadingListItems.SYNC_CHANGE_FLAGS + " TINYINT NOT NULL DEFAULT " + ReadingListItems.SYNC_CHANGE_NONE + ", " +
-
- ReadingListItems.CLIENT_LAST_MODIFIED + " INTEGER NOT NULL, " + // Client time.
- ReadingListItems.SERVER_LAST_MODIFIED + " INTEGER, " + // Server-assigned.
-
- // Server-assigned.
- ReadingListItems.SERVER_STORED_ON + " INTEGER, " +
- ReadingListItems.ADDED_ON + " INTEGER, " + // Client time. Shouldn't be null, but not enforced. Formerly DATE_CREATED.
- ReadingListItems.MARKED_READ_ON + " INTEGER, " +
-
- // These boolean flags represent the server 'status', 'unread', 'is_article', and 'favorite' fields.
- ReadingListItems.IS_DELETED + " TINYINT NOT NULL DEFAULT 0, " +
- ReadingListItems.IS_ARCHIVED + " TINYINT NOT NULL DEFAULT 0, " +
- ReadingListItems.IS_UNREAD + " TINYINT NOT NULL DEFAULT 1, " +
- ReadingListItems.IS_ARTICLE + " TINYINT NOT NULL DEFAULT 0, " +
- ReadingListItems.IS_FAVORITE + " TINYINT NOT NULL DEFAULT 0, " +
-
- ReadingListItems.URL + " TEXT NOT NULL, " +
- ReadingListItems.TITLE + " TEXT, " +
- ReadingListItems.RESOLVED_URL + " TEXT, " +
- ReadingListItems.RESOLVED_TITLE + " TEXT, " +
-
- ReadingListItems.EXCERPT + " TEXT, " +
-
- ReadingListItems.ADDED_BY + " TEXT, " +
- ReadingListItems.MARKED_READ_BY + " TEXT, " +
-
- ReadingListItems.WORD_COUNT + " INTEGER DEFAULT 0, " +
- ReadingListItems.READ_POSITION + " INTEGER DEFAULT 0 " +
- "); ");
-
- didCreateCurrentReadingListTable = true; // Mostly correct, in the absence of transactions.
- }
-
- private void createReadingListIndices(final SQLiteDatabase db, final String tableName) {
- // No need to create an index on GUID; it's a UNIQUE column.
- db.execSQL("CREATE INDEX reading_list_url ON " + tableName + "("
- + ReadingListItems.URL + ")");
- db.execSQL("CREATE INDEX reading_list_content_status ON " + tableName + "("
- + ReadingListItems.CONTENT_STATUS + ")");
- }
-
- private void createUrlAnnotationsTable(final SQLiteDatabase db) {
- debug("Creating " + UrlAnnotations.TABLE_NAME + " table");
-
- db.execSQL("CREATE TABLE " + UrlAnnotations.TABLE_NAME + "(" +
- UrlAnnotations._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
- UrlAnnotations.URL + " TEXT NOT NULL, " +
- UrlAnnotations.KEY + " TEXT NOT NULL, " +
- UrlAnnotations.VALUE + " TEXT, " +
- UrlAnnotations.DATE_CREATED + " INTEGER NOT NULL, " +
- UrlAnnotations.DATE_MODIFIED + " INTEGER NOT NULL, " +
- UrlAnnotations.SYNC_STATUS + " TINYINT NOT NULL DEFAULT " + UrlAnnotations.SyncStatus.NEW.getDBValue() +
- " );");
-
- db.execSQL("CREATE INDEX idx_url_annotations_url_key ON " +
- UrlAnnotations.TABLE_NAME + "(" + UrlAnnotations.URL + ", " + UrlAnnotations.KEY + ")");
- }
-
- private void createOrUpdateAllSpecialFolders(SQLiteDatabase db) {
- createOrUpdateSpecialFolder(db, Bookmarks.MOBILE_FOLDER_GUID,
- R.string.bookmarks_folder_mobile, 0);
- createOrUpdateSpecialFolder(db, Bookmarks.TOOLBAR_FOLDER_GUID,
- R.string.bookmarks_folder_toolbar, 1);
- createOrUpdateSpecialFolder(db, Bookmarks.MENU_FOLDER_GUID,
- R.string.bookmarks_folder_menu, 2);
- createOrUpdateSpecialFolder(db, Bookmarks.TAGS_FOLDER_GUID,
- R.string.bookmarks_folder_tags, 3);
- createOrUpdateSpecialFolder(db, Bookmarks.UNFILED_FOLDER_GUID,
- R.string.bookmarks_folder_unfiled, 4);
- createOrUpdateSpecialFolder(db, Bookmarks.PINNED_FOLDER_GUID,
- R.string.bookmarks_folder_pinned, 5);
- }
-
- private void createOrUpdateSpecialFolder(SQLiteDatabase db,
- String guid, int titleId, int position) {
- ContentValues values = new ContentValues();
- values.put(Bookmarks.GUID, guid);
- values.put(Bookmarks.TYPE, Bookmarks.TYPE_FOLDER);
- values.put(Bookmarks.POSITION, position);
-
- if (guid.equals(Bookmarks.PLACES_FOLDER_GUID)) {
- values.put(Bookmarks._ID, Bookmarks.FIXED_ROOT_ID);
- } else if (guid.equals(Bookmarks.PINNED_FOLDER_GUID)) {
- values.put(Bookmarks._ID, Bookmarks.FIXED_PINNED_LIST_ID);
- }
-
- // Set the parent to 0, which sync assumes is the root
- values.put(Bookmarks.PARENT, Bookmarks.FIXED_ROOT_ID);
-
- String title = mContext.getResources().getString(titleId);
- values.put(Bookmarks.TITLE, title);
-
- long now = System.currentTimeMillis();
- values.put(Bookmarks.DATE_CREATED, now);
- values.put(Bookmarks.DATE_MODIFIED, now);
-
- int updated = db.update(TABLE_BOOKMARKS, values,
- Bookmarks.GUID + " = ?",
- new String[] { guid });
-
- if (updated == 0) {
- db.insert(TABLE_BOOKMARKS, Bookmarks.GUID, values);
- debug("Inserted special folder: " + guid);
- } else {
- debug("Updated special folder: " + guid);
- }
- }
-
- private void createNumbersTable(SQLiteDatabase db) {
- db.execSQL("CREATE TABLE " + Numbers.TABLE_NAME + " (" + Numbers.POSITION + " INTEGER PRIMARY KEY AUTOINCREMENT)");
-
- if (db.getVersion() >= 3007011) { // SQLite 3.7.11
- // This is only available in SQLite >= 3.7.11, see release notes:
- // "Enhance the INSERT syntax to allow multiple rows to be inserted via the VALUES clause"
- final String numbers = "(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)," +
- "(10),(11),(12),(13),(14),(15),(16),(17),(18),(19)," +
- "(20),(21),(22),(23),(24),(25),(26),(27),(28),(29)," +
- "(30),(31),(32),(33),(34),(35),(36),(37),(38),(39)," +
- "(40),(41),(42),(43),(44),(45),(46),(47),(48),(49)," +
- "(50)";
-
- db.execSQL("INSERT INTO " + Numbers.TABLE_NAME + " (" + Numbers.POSITION + ") VALUES " + numbers);
- } else {
- final SQLiteStatement statement = db.compileStatement("INSERT INTO " + Numbers.TABLE_NAME + " (" + Numbers.POSITION + ") VALUES (?)");
-
- for (int i = 0; i <= Numbers.MAX_VALUE; i++) {
- statement.bindLong(1, i);
- statement.executeInsert();
- }
- }
- }
-
- private boolean isSpecialFolder(ContentValues values) {
- String guid = values.getAsString(Bookmarks.GUID);
- if (guid == null) {
- return false;
- }
-
- return guid.equals(Bookmarks.MOBILE_FOLDER_GUID) ||
- guid.equals(Bookmarks.MENU_FOLDER_GUID) ||
- guid.equals(Bookmarks.TOOLBAR_FOLDER_GUID) ||
- guid.equals(Bookmarks.UNFILED_FOLDER_GUID) ||
- guid.equals(Bookmarks.TAGS_FOLDER_GUID);
- }
-
- private void migrateBookmarkFolder(SQLiteDatabase db, int folderId,
- BookmarkMigrator migrator) {
- Cursor c = null;
-
- debug("Migrating bookmark folder with id = " + folderId);
-
- String selection = Bookmarks.PARENT + " = " + folderId;
- String[] selectionArgs = null;
-
- boolean isRootFolder = (folderId == Bookmarks.FIXED_ROOT_ID);
-
- // If we're loading the root folder, we have to account for
- // any previously created special folder that was created without
- // setting a parent id (e.g. mobile folder) and making sure we're
- // not adding any infinite recursion as root's parent is root itself.
- if (isRootFolder) {
- selection = Bookmarks.GUID + " != ?" + " AND (" +
- selection + " OR " + Bookmarks.PARENT + " = NULL)";
- selectionArgs = new String[] { Bookmarks.PLACES_FOLDER_GUID };
- }
-
- List<Integer> subFolders = new ArrayList<Integer>();
- List<ContentValues> invalidSpecialEntries = new ArrayList<ContentValues>();
-
- try {
- c = db.query(TABLE_BOOKMARKS_TMP,
- null,
- selection,
- selectionArgs,
- null, null, null);
-
- // The key point here is that bookmarks should be added in
- // parent order to avoid any problems with the foreign key
- // in Bookmarks.PARENT.
- while (c.moveToNext()) {
- ContentValues values = new ContentValues();
-
- // We're using a null projection in the query which
- // means we're getting all columns from the table.
- // It's safe to simply transform the row into the
- // values to be inserted on the new table.
- DatabaseUtils.cursorRowToContentValues(c, values);
-
- boolean isSpecialFolder = isSpecialFolder(values);
-
- // The mobile folder used to be created with PARENT = NULL.
- // We want fix that here.
- if (values.getAsLong(Bookmarks.PARENT) == null && isSpecialFolder)
- values.put(Bookmarks.PARENT, Bookmarks.FIXED_ROOT_ID);
-
- if (isRootFolder && !isSpecialFolder) {
- invalidSpecialEntries.add(values);
- continue;
- }
-
- if (migrator != null)
- migrator.updateForNewTable(values);
-
- debug("Migrating bookmark: " + values.getAsString(Bookmarks.TITLE));
- db.insert(TABLE_BOOKMARKS, Bookmarks.URL, values);
-
- Integer type = values.getAsInteger(Bookmarks.TYPE);
- if (type != null && type == Bookmarks.TYPE_FOLDER)
- subFolders.add(values.getAsInteger(Bookmarks._ID));
- }
- } finally {
- if (c != null)
- c.close();
- }
-
- // At this point is safe to assume that the mobile folder is
- // in the new table given that we've always created it on
- // database creation time.
- final int nInvalidSpecialEntries = invalidSpecialEntries.size();
- if (nInvalidSpecialEntries > 0) {
- Integer mobileFolderId = getMobileFolderId(db);
- if (mobileFolderId == null) {
- Log.e(LOGTAG, "Error migrating invalid special folder entries: mobile folder id is null");
- return;
- }
-
- debug("Found " + nInvalidSpecialEntries + " invalid special folder entries");
- for (int i = 0; i < nInvalidSpecialEntries; i++) {
- ContentValues values = invalidSpecialEntries.get(i);
- values.put(Bookmarks.PARENT, mobileFolderId);
-
- db.insert(TABLE_BOOKMARKS, Bookmarks.URL, values);
- }
- }
-
- final int nSubFolders = subFolders.size();
- for (int i = 0; i < nSubFolders; i++) {
- int subFolderId = subFolders.get(i);
- migrateBookmarkFolder(db, subFolderId, migrator);
- }
- }
-
- private void migrateBookmarksTable(SQLiteDatabase db) {
- migrateBookmarksTable(db, null);
- }
-
- private void migrateBookmarksTable(SQLiteDatabase db, BookmarkMigrator migrator) {
- debug("Renaming bookmarks table to " + TABLE_BOOKMARKS_TMP);
- db.execSQL("ALTER TABLE " + TABLE_BOOKMARKS +
- " RENAME TO " + TABLE_BOOKMARKS_TMP);
-
- debug("Dropping views and indexes related to " + TABLE_BOOKMARKS);
-
- db.execSQL("DROP INDEX IF EXISTS bookmarks_url_index");
- db.execSQL("DROP INDEX IF EXISTS bookmarks_type_deleted_index");
- db.execSQL("DROP INDEX IF EXISTS bookmarks_guid_index");
- db.execSQL("DROP INDEX IF EXISTS bookmarks_modified_index");
-
- createBookmarksTable(db);
-
- createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID,
- R.string.bookmarks_folder_places, 0);
-
- migrateBookmarkFolder(db, Bookmarks.FIXED_ROOT_ID, migrator);
-
- // Ensure all special folders exist and have the
- // right folder hierarchy.
- createOrUpdateAllSpecialFolders(db);
-
- debug("Dropping bookmarks temporary table");
- db.execSQL("DROP TABLE IF EXISTS " + TABLE_BOOKMARKS_TMP);
- }
-
- /**
- * Migrate a history table from some old version to the newest one by creating the new table and
- * copying all the data over.
- */
- private void migrateHistoryTable(SQLiteDatabase db) {
- debug("Renaming history table to " + TABLE_HISTORY_TMP);
- db.execSQL("ALTER TABLE " + TABLE_HISTORY +
- " RENAME TO " + TABLE_HISTORY_TMP);
-
- debug("Dropping views and indexes related to " + TABLE_HISTORY);
-
- db.execSQL("DROP INDEX IF EXISTS history_url_index");
- db.execSQL("DROP INDEX IF EXISTS history_guid_index");
- db.execSQL("DROP INDEX IF EXISTS history_modified_index");
- db.execSQL("DROP INDEX IF EXISTS history_visited_index");
-
- createHistoryTable(db);
-
- db.execSQL("INSERT INTO " + TABLE_HISTORY + " SELECT * FROM " + TABLE_HISTORY_TMP);
-
- debug("Dropping history temporary table");
- db.execSQL("DROP TABLE IF EXISTS " + TABLE_HISTORY_TMP);
- }
-
- private void upgradeDatabaseFrom3to4(SQLiteDatabase db) {
- migrateBookmarksTable(db, new BookmarkMigrator3to4());
- }
-
- private void upgradeDatabaseFrom6to7(SQLiteDatabase db) {
- debug("Removing history visits with NULL GUIDs");
- db.execSQL("DELETE FROM " + TABLE_HISTORY + " WHERE " + History.GUID + " IS NULL");
-
- migrateBookmarksTable(db);
- migrateHistoryTable(db);
- }
-
- private void upgradeDatabaseFrom7to8(SQLiteDatabase db) {
- debug("Combining history entries with the same URL");
-
- final String TABLE_DUPES = "duped_urls";
- final String TOTAL = "total";
- final String LATEST = "latest";
- final String WINNER = "winner";
-
- db.execSQL("CREATE TEMP TABLE " + TABLE_DUPES + " AS" +
- " SELECT " + History.URL + ", " +
- "SUM(" + History.VISITS + ") AS " + TOTAL + ", " +
- "MAX(" + History.DATE_MODIFIED + ") AS " + LATEST + ", " +
- "MAX(" + History._ID + ") AS " + WINNER +
- " FROM " + TABLE_HISTORY +
- " GROUP BY " + History.URL +
- " HAVING count(" + History.URL + ") > 1");
-
- db.execSQL("CREATE UNIQUE INDEX " + TABLE_DUPES + "_url_index ON " +
- TABLE_DUPES + " (" + History.URL + ")");
-
- final String fromClause = " FROM " + TABLE_DUPES + " WHERE " +
- qualifyColumn(TABLE_DUPES, History.URL) + " = " +
- qualifyColumn(TABLE_HISTORY, History.URL);
-
- db.execSQL("UPDATE " + TABLE_HISTORY +
- " SET " + History.VISITS + " = (SELECT " + TOTAL + fromClause + "), " +
- History.DATE_MODIFIED + " = (SELECT " + LATEST + fromClause + "), " +
- History.IS_DELETED + " = " +
- "(" + History._ID + " <> (SELECT " + WINNER + fromClause + "))" +
- " WHERE " + History.URL + " IN (SELECT " + History.URL + " FROM " + TABLE_DUPES + ")");
-
- db.execSQL("DROP TABLE " + TABLE_DUPES);
- }
-
- private void upgradeDatabaseFrom10to11(SQLiteDatabase db) {
- db.execSQL("CREATE INDEX bookmarks_type_deleted_index ON " + TABLE_BOOKMARKS + "("
- + Bookmarks.TYPE + ", " + Bookmarks.IS_DELETED + ")");
- }
-
- private void upgradeDatabaseFrom12to13(SQLiteDatabase db) {
- createFaviconsTable(db);
-
- // Add favicon_id column to the history/bookmarks tables. We wrap this in a try-catch
- // because the column *may* already exist at this point (depending on how many upgrade
- // steps have been performed in this operation). In which case these queries will throw,
- // but we don't care.
- try {
- db.execSQL("ALTER TABLE " + TABLE_HISTORY +
- " ADD COLUMN " + History.FAVICON_ID + " INTEGER");
- db.execSQL("ALTER TABLE " + TABLE_BOOKMARKS +
- " ADD COLUMN " + Bookmarks.FAVICON_ID + " INTEGER");
- } catch (SQLException e) {
- // Don't care.
- debug("Exception adding favicon_id column. We're probably fine." + e);
- }
-
- createThumbnailsTable(db);
-
- db.execSQL("DROP VIEW IF EXISTS bookmarks_with_images");
- db.execSQL("DROP VIEW IF EXISTS history_with_images");
- db.execSQL("DROP VIEW IF EXISTS combined_with_images");
-
- createBookmarksWithFaviconsView(db);
- createHistoryWithFaviconsView(db);
-
- db.execSQL("DROP TABLE IF EXISTS images");
- }
-
- private void upgradeDatabaseFrom13to14(SQLiteDatabase db) {
- createOrUpdateSpecialFolder(db, Bookmarks.PINNED_FOLDER_GUID,
- R.string.bookmarks_folder_pinned, 6);
- }
-
- private void upgradeDatabaseFrom14to15(SQLiteDatabase db) {
- Cursor c = null;
- try {
- // Get all the pinned bookmarks
- c = db.query(TABLE_BOOKMARKS,
- new String[] { Bookmarks._ID, Bookmarks.URL },
- Bookmarks.PARENT + " = ?",
- new String[] { Integer.toString(Bookmarks.FIXED_PINNED_LIST_ID) },
- null, null, null);
-
- while (c.moveToNext()) {
- // Check if this URL can be parsed as a URI with a valid scheme.
- String url = c.getString(c.getColumnIndexOrThrow(Bookmarks.URL));
- if (Uri.parse(url).getScheme() != null) {
- continue;
- }
-
- // If it can't, update the URL to be an encoded "user-entered" value.
- ContentValues values = new ContentValues(1);
- String newUrl = Uri.fromParts("user-entered", url, null).toString();
- values.put(Bookmarks.URL, newUrl);
- db.update(TABLE_BOOKMARKS, values, Bookmarks._ID + " = ?",
- new String[] { Integer.toString(c.getInt(c.getColumnIndexOrThrow(Bookmarks._ID))) });
- }
- } finally {
- if (c != null) {
- c.close();
- }
- }
- }
-
- private void upgradeDatabaseFrom15to16(SQLiteDatabase db) {
- // No harm in creating the v19 combined view here: means we don't need two almost-identical
- // functions to define both the v16 and v19 ones. The upgrade path will redundantly drop
- // and recreate the view again. *shrug*
- createV19CombinedView(db);
- }
-
- private void upgradeDatabaseFrom16to17(SQLiteDatabase db) {
- // Purge any 0-byte favicons/thumbnails
- try {
- db.execSQL("DELETE FROM " + TABLE_FAVICONS +
- " WHERE length(" + Favicons.DATA + ") = 0");
- db.execSQL("DELETE FROM " + TABLE_THUMBNAILS +
- " WHERE length(" + Thumbnails.DATA + ") = 0");
- } catch (SQLException e) {
- Log.e(LOGTAG, "Error purging invalid favicons or thumbnails", e);
- }
- }
-
- /*
- * Moves reading list items from 'bookmarks' table to 'reading_list' table.
- */
- private void upgradeDatabaseFrom17to18(SQLiteDatabase db) {
- debug("Moving reading list items from 'bookmarks' table to 'reading_list' table");
-
- final String selection = Bookmarks.PARENT + " = ? AND " + Bookmarks.IS_DELETED + " = ? ";
- final String[] selectionArgs = { String.valueOf(Bookmarks.FIXED_READING_LIST_ID), "0" };
- final String[] projection = { Bookmarks._ID,
- Bookmarks.GUID,
- Bookmarks.URL,
- Bookmarks.DATE_MODIFIED,
- Bookmarks.DATE_CREATED,
- Bookmarks.TITLE };
-
- try {
- db.beginTransaction();
-
- // Create 'reading_list' table.
- createReadingListTable(db, TABLE_READING_LIST);
-
- // Get all the reading list items from bookmarks table.
- final Cursor cursor = db.query(TABLE_BOOKMARKS, projection, selection, selectionArgs, null, null, null);
-
- if (cursor == null) {
- // This should never happen.
- db.setTransactionSuccessful();
- return;
- }
-
- try {
- // Insert reading list items into reading_list table.
- while (cursor.moveToNext()) {
- debug(DatabaseUtils.dumpCurrentRowToString(cursor));
- final ContentValues values = new ContentValues();
-
- // We don't preserve bookmark GUIDs.
- DatabaseUtils.cursorStringToContentValues(cursor, Bookmarks.URL, values, ReadingListItems.URL);
- DatabaseUtils.cursorStringToContentValues(cursor, Bookmarks.TITLE, values, ReadingListItems.TITLE);
- DatabaseUtils.cursorLongToContentValues(cursor, Bookmarks.DATE_CREATED, values, ReadingListItems.ADDED_ON);
- DatabaseUtils.cursorLongToContentValues(cursor, Bookmarks.DATE_MODIFIED, values, ReadingListItems.CLIENT_LAST_MODIFIED);
-
- db.insertOrThrow(TABLE_READING_LIST, null, values);
- }
- } finally {
- cursor.close();
- }
-
- // Delete reading list items from bookmarks table.
- db.delete(TABLE_BOOKMARKS,
- Bookmarks.PARENT + " = ? ",
- new String[] { String.valueOf(Bookmarks.FIXED_READING_LIST_ID) });
-
- // Delete reading list special folder.
- db.delete(TABLE_BOOKMARKS,
- Bookmarks._ID + " = ? ",
- new String[] { String.valueOf(Bookmarks.FIXED_READING_LIST_ID) });
-
- // Create indices.
- createReadingListIndices(db, TABLE_READING_LIST);
-
- // Done.
- db.setTransactionSuccessful();
- } catch (SQLException e) {
- Log.e(LOGTAG, "Error migrating reading list items", e);
- } finally {
- db.endTransaction();
- }
- }
-
- private void upgradeDatabaseFrom18to19(SQLiteDatabase db) {
- // Redefine the "combined" view...
- createV19CombinedView(db);
-
- // Kill any history entries with NULL URL. This ostensibly can't happen...
- db.execSQL("DELETE FROM " + TABLE_HISTORY + " WHERE " + History.URL + " IS NULL");
-
- // Similar for bookmark types. Replaces logic from the combined view, also shouldn't happen.
- db.execSQL("UPDATE " + TABLE_BOOKMARKS + " SET " +
- Bookmarks.TYPE + " = " + Bookmarks.TYPE_BOOKMARK +
- " WHERE " + Bookmarks.TYPE + " IS NULL");
- }
-
- private void upgradeDatabaseFrom19to20(SQLiteDatabase db) {
- createSearchHistoryTable(db);
- }
-
- private void upgradeDatabaseFrom21to22(SQLiteDatabase db) {
- if (didCreateCurrentReadingListTable) {
- debug("No need to add CONTENT_STATUS to reading list; we just created with the current schema.");
- return;
- }
-
- debug("Adding CONTENT_STATUS column to reading list table.");
-
- try {
- db.execSQL("ALTER TABLE " + TABLE_READING_LIST +
- " ADD COLUMN " + ReadingListItems.CONTENT_STATUS +
- " TINYINT DEFAULT " + ReadingListItems.STATUS_UNFETCHED);
-
- db.execSQL("CREATE INDEX reading_list_content_status ON " + TABLE_READING_LIST + "("
- + ReadingListItems.CONTENT_STATUS + ")");
- } catch (SQLiteException e) {
- // We're betting that an error here means that the table already has the column,
- // so we're failing due to the duplicate column name.
- Log.e(LOGTAG, "Error upgrading database from 21 to 22", e);
- }
- }
-
- private void upgradeDatabaseFrom22to23(SQLiteDatabase db) {
- if (didCreateCurrentReadingListTable) {
- // If we just created this table it is already in the expected >= 23 schema. Trying
- // to run this migration will crash because columns that were in the <= 22 schema
- // no longer exist.
- debug("No need to rev reading list schema; we just created with the current schema.");
- return;
- }
-
- debug("Rewriting reading list table.");
- createReadingListTable(db, "tmp_rl");
-
- // Remove indexes. We don't need them now, and we'll be throwing away the table.
- db.execSQL("DROP INDEX IF EXISTS reading_list_url");
- db.execSQL("DROP INDEX IF EXISTS reading_list_guid");
- db.execSQL("DROP INDEX IF EXISTS reading_list_content_status");
-
- // This used to be a part of the no longer existing ReadingListProvider, since we're deleting
- // this table later in the second migration, and since sync for this table never existed,
- // we don't care about the device name here.
- final String thisDevice = "_fake_device_name_that_will_be_discarded_in_the_next_migration_";
- db.execSQL("INSERT INTO tmp_rl (" +
- // Here are the columns we can preserve.
- ReadingListItems._ID + ", " +
- ReadingListItems.URL + ", " +
- ReadingListItems.TITLE + ", " +
- ReadingListItems.RESOLVED_TITLE + ", " + // = TITLE (if CONTENT_STATUS = STATUS_FETCHED_ARTICLE)
- ReadingListItems.RESOLVED_URL + ", " + // = URL (if CONTENT_STATUS = STATUS_FETCHED_ARTICLE)
- ReadingListItems.EXCERPT + ", " +
- ReadingListItems.IS_UNREAD + ", " + // = !READ
- ReadingListItems.IS_DELETED + ", " + // = 0
- ReadingListItems.GUID + ", " + // = NULL
- ReadingListItems.CLIENT_LAST_MODIFIED + ", " + // = DATE_MODIFIED
- ReadingListItems.ADDED_ON + ", " + // = DATE_CREATED
- ReadingListItems.CONTENT_STATUS + ", " +
- ReadingListItems.MARKED_READ_BY + ", " + // if READ + ", = this device
- ReadingListItems.ADDED_BY + // = this device
- ") " +
- "SELECT " +
- "_id, url, title, " +
- "CASE content_status WHEN " + ReadingListItems.STATUS_FETCHED_ARTICLE + " THEN title ELSE NULL END, " + // RESOLVED_TITLE.
- "CASE content_status WHEN " + ReadingListItems.STATUS_FETCHED_ARTICLE + " THEN url ELSE NULL END, " + // RESOLVED_URL.
- "excerpt, " +
- "CASE read WHEN 1 THEN 0 ELSE 1 END, " + // IS_UNREAD.
- "0, " + // IS_DELETED.
- "NULL, modified, created, content_status, " +
- "CASE read WHEN 1 THEN ? ELSE NULL END, " + // MARKED_READ_BY.
- "?" + // ADDED_BY.
- " FROM " + TABLE_READING_LIST +
- " WHERE deleted = 0",
- new String[] {thisDevice, thisDevice});
-
- // Now switch these tables over and recreate the indices.
- db.execSQL("DROP TABLE " + TABLE_READING_LIST);
- db.execSQL("ALTER TABLE tmp_rl RENAME TO " + TABLE_READING_LIST);
-
- createReadingListIndices(db, TABLE_READING_LIST);
- }
-
- private void upgradeDatabaseFrom23to24(SQLiteDatabase db) {
- // Version 24 consolidates the tabs and clients table into browser.db. Before, they lived in tabs.db.
- // It's easier to copy the existing data than to arrange for Sync to re-populate it.
- try {
- final File oldTabsDBFile = new File(GeckoProfile.get(mContext).getDir(), "tabs.db");
- copyTabsDB(oldTabsDBFile, db);
- } catch (Exception e) {
- Log.e(LOGTAG, "Got exception copying tabs and clients data from tabs.db to browser.db; ignoring.", e);
- }
-
- // Delete the database, the shared memory, and the log.
- for (String filename : new String[] { "tabs.db", "tabs.db-shm", "tabs.db-wal" }) {
- final File file = new File(GeckoProfile.get(mContext).getDir(), filename);
- try {
- FileUtils.delete(file);
- } catch (Exception e) {
- Log.e(LOGTAG, "Exception occurred while trying to delete " + file.getPath() + "; ignoring.", e);
- }
- }
- }
-
- private void upgradeDatabaseFrom24to25(SQLiteDatabase db) {
- if (didCreateTabsTable) {
- // This migration adds a foreign key constraint (the table scheme stays identical, except
- // for the new constraint) - hence it is safe to run this migration on a newly created tabs
- // table - but it's unnecessary hence we should avoid doing so.
- debug("No need to rev tabs schema; foreign key constraint exists.");
- return;
- }
-
- debug("Rewriting tabs table.");
- createTabsTable(db, "tmp_tabs");
-
- // Remove indexes. We don't need them now, and we'll be throwing away the table.
- db.execSQL("DROP INDEX IF EXISTS " + TabsProvider.INDEX_TABS_GUID);
- db.execSQL("DROP INDEX IF EXISTS " + TabsProvider.INDEX_TABS_POSITION);
-
- db.execSQL("INSERT INTO tmp_tabs (" +
- // Here are the columns we can preserve.
- BrowserContract.Tabs._ID + ", " +
- BrowserContract.Tabs.CLIENT_GUID + ", " +
- BrowserContract.Tabs.TITLE + ", " +
- BrowserContract.Tabs.URL + ", " +
- BrowserContract.Tabs.HISTORY + ", " +
- BrowserContract.Tabs.FAVICON + ", " +
- BrowserContract.Tabs.LAST_USED + ", " +
- BrowserContract.Tabs.POSITION +
- ") " +
- "SELECT " +
- "_id, client_guid, title, url, history, favicon, last_used, position" +
- " FROM " + TABLE_TABS);
-
- // Now switch these tables over and recreate the indices.
- db.execSQL("DROP TABLE " + TABLE_TABS);
- db.execSQL("ALTER TABLE tmp_tabs RENAME TO " + TABLE_TABS);
- createTabsTableIndices(db, TABLE_TABS);
- didCreateTabsTable = true;
- }
-
- private void upgradeDatabaseFrom25to26(SQLiteDatabase db) {
- debug("Dropping unnecessary indices");
- db.execSQL("DROP INDEX IF EXISTS clients_guid_index");
- db.execSQL("DROP INDEX IF EXISTS thumbnails_url_index");
- db.execSQL("DROP INDEX IF EXISTS favicons_url_index");
- }
-
- private void upgradeDatabaseFrom27to28(final SQLiteDatabase db) {
- debug("Adding url annotations table");
- createUrlAnnotationsTable(db);
- }
-
- private void upgradeDatabaseFrom28to29(SQLiteDatabase db) {
- debug("Adding numbers table");
- createNumbersTable(db);
- }
-
- private void upgradeDatabaseFrom29to30(final SQLiteDatabase db) {
- debug("creating logins table");
- createDeletedLoginsTable(db, TABLE_DELETED_LOGINS);
- createDisabledHostsTable(db, TABLE_DISABLED_HOSTS);
- createLoginsTable(db, TABLE_LOGINS);
- createLoginsTableIndices(db, TABLE_LOGINS);
- }
-
- // Get the cache path for a URL, based on the storage format in place during the 27to28 transition.
- // This is a reimplementation of _toHashedPath from ReaderMode.jsm - given that we're likely
- // to migrate the SavedReaderViewHelper implementation at some point, it seems safest to have a local
- // implementation here - moreover this is probably faster than calling into JS.
- // This is public only to allow for testing.
- @RobocopTarget
- public static String getReaderCacheFileNameForURL(String url) {
- try {
- // On KitKat and above we can use java.nio.charset.StandardCharsets.UTF_8 in place of "UTF8"
- // which avoids having to handle UnsupportedCodingException
- byte[] utf8 = url.getBytes("UTF8");
-
- final MessageDigest digester = MessageDigest.getInstance("MD5");
- byte[] hash = digester.digest(utf8);
-
- final String hashString = new Base32().encodeAsString(hash);
- return hashString.substring(0, hashString.indexOf('=')) + ".json";
- } catch (UnsupportedEncodingException e) {
- // This should never happen
- throw new IllegalStateException("UTF8 encoding not available - can't process readercache filename");
- } catch (NoSuchAlgorithmException e) {
- // This should also never happen
- throw new IllegalStateException("MD5 digester unavailable - can't process readercache filename");
- }
- }
-
- /*
- * Moves reading list items from the 'reading_list' table back into the 'bookmarks' table. This time the
- * reading list items are placed into a "Reading List" folder, which is a subfolder of the mobile-bookmarks table.
- */
- private void upgradeDatabaseFrom30to31(SQLiteDatabase db) {
- // We only need to do the migration if reading-list items already exist. We could do a query of count(*) on
- // TABLE_READING_LIST, however if we are doing the migration, we'll need to query all items in the reading-list,
- // hence we might as well just query all items, and proceed with the migration if cursor.count > 0.
-
- // We try to retain the original ordering below. Our LocalReadingListAccessor actually coalesced
- // SERVER_STORED_ON with ADDED_ON to determine positioning, however reading list syncing was never
- // implemented hence SERVER_STORED will have always been null.
- final Cursor readingListCursor = db.query(TABLE_READING_LIST,
- new String[] {
- ReadingListItems.URL,
- ReadingListItems.TITLE,
- ReadingListItems.ADDED_ON,
- ReadingListItems.CLIENT_LAST_MODIFIED
- },
- ReadingListItems.IS_DELETED + " = 0",
- null,
- null,
- null,
- ReadingListItems.ADDED_ON + " DESC");
-
- // We'll want to walk the cache directory, so that we can (A) bookkeep readercache items
- // that we want and (B) delete unneeded readercache items. (B) shouldn't actually happen, but
- // is possible if there were bugs in our reader-caching code.
- // We need to construct this here since we populate this map while walking the DB cursor,
- // and use the map later when walking the cache.
- final Map<String, String> fileToURLMap = new HashMap<>();
-
-
- try {
- if (!readingListCursor.moveToFirst()) {
- return;
- }
-
- final Integer mobileBookmarksID = getMobileFolderId(db);
-
- if (mobileBookmarksID == null) {
- // This folder is created either on DB creation or during the 3-4 or 6-7 migrations.
- throw new IllegalStateException("mobile bookmarks folder must already exist");
- }
-
- final long now = System.currentTimeMillis();
-
- // We try to retain the same order as the reading-list would show. We should hopefully be reading the
- // items in the order they are displayed on screen (final param of db.query above), by providing
- // a position we should obtain the same ordering in the bookmark folder.
- long position = 0;
-
- final int titleColumnID = readingListCursor.getColumnIndexOrThrow(ReadingListItems.TITLE);
- final int createdColumnID = readingListCursor.getColumnIndexOrThrow(ReadingListItems.ADDED_ON);
-
- // This isn't the most efficient implementation, but the migration is one-off, and this
- // also more maintainable than the SQL equivalent (generating the guids correctly is
- // difficult in SQLite).
- do {
- final ContentValues readingListItemValues = new ContentValues();
-
- final String url = readingListCursor.getString(readingListCursor.getColumnIndexOrThrow(ReadingListItems.URL));
-
- readingListItemValues.put(Bookmarks.PARENT, mobileBookmarksID);
- readingListItemValues.put(Bookmarks.GUID, Utils.generateGuid());
- readingListItemValues.put(Bookmarks.URL, url);
- // Title may be null, however we're expecting a String - we can generate an empty string if needed:
- if (!readingListCursor.isNull(titleColumnID)) {
- readingListItemValues.put(Bookmarks.TITLE, readingListCursor.getString(titleColumnID));
- } else {
- readingListItemValues.put(Bookmarks.TITLE, "");
- }
- readingListItemValues.put(Bookmarks.DATE_CREATED, readingListCursor.getLong(createdColumnID));
- readingListItemValues.put(Bookmarks.DATE_MODIFIED, now);
- readingListItemValues.put(Bookmarks.POSITION, position);
-
- db.insert(TABLE_BOOKMARKS,
- null,
- readingListItemValues);
-
- final String cacheFileName = getReaderCacheFileNameForURL(url);
- fileToURLMap.put(cacheFileName, url);
-
- position++;
- } while (readingListCursor.moveToNext());
-
- } finally {
- readingListCursor.close();
- // We need to do this work here since we might be returning (we return early if the
- // reading-list table is empty).
- db.execSQL("DROP TABLE IF EXISTS " + TABLE_READING_LIST);
- createBookmarksWithAnnotationsView(db);
- }
-
- final File profileDir = GeckoProfile.get(mContext).getDir();
- final File cacheDir = new File(profileDir, "readercache");
-
- // At the time of this migration the SavedReaderViewHelper becomes a 1:1 mirror of reader view
- // url-annotations. This may change in future implementations, however currently we only need to care
- // about standard bookmarks (untouched during this migration) and bookmarks with a reader
- // view annotation (which we're creating here, and which are guaranteed to be saved offline).
- //
- // This is why we have to migrate the cache items (instead of cleaning the cache
- // and rebuilding it). We simply don't support uncached reader view bookmarks, and we would
- // break existing reading list items (they would convert into plain bookmarks without
- // reader view). This helps ensure that offline content isn't lost during the migration.
- if (cacheDir.exists() && cacheDir.isDirectory()) {
- SavedReaderViewHelper savedReaderViewHelper = SavedReaderViewHelper.getSavedReaderViewHelper(mContext);
-
- // Usually we initialise the helper during onOpen(). However onUpgrade() is run before
- // onOpen() hence we need to manually initialise it at this stage.
- savedReaderViewHelper.loadItems();
-
- for (File cacheFile : cacheDir.listFiles()) {
- if (fileToURLMap.containsKey(cacheFile.getName())) {
- final String url = fileToURLMap.get(cacheFile.getName());
- final String path = cacheFile.getAbsolutePath();
- long size = cacheFile.length();
-
- savedReaderViewHelper.put(url, path, size);
- } else {
- // This should never happen, but we don't actually know whether or not orphaned
- // items happened in the wild.
- boolean deleted = cacheFile.delete();
-
- if (!deleted) {
- Log.w(LOGTAG, "Failed to delete orphaned saved reader view file.");
- }
- }
- }
- }
- }
-
- private void upgradeDatabaseFrom31to32(final SQLiteDatabase db) {
- debug("Adding visits table");
- createVisitsTable(db);
-
- debug("Migrating visits from history extension db into visits table");
- String historyExtensionDbName = "history_extension_database";
-
- SQLiteDatabase historyExtensionDb = null;
- final File historyExtensionsDatabase = mContext.getDatabasePath(historyExtensionDbName);
-
- // Primary goal of this migration is to improve Top Sites experience by distinguishing between
- // local and remote visits. If Sync is enabled, we rely on visit data from Sync and treat it as remote.
- // However, if Sync is disabled but we detect evidence that it was enabled at some point (HistoryExtensionsDB is present)
- // then we synthesize visits from the History table, but we mark them all as "remote". This will ensure
- // that once user starts browsing around, their Top Sites will reflect their local browsing history.
- // Otherwise, we risk overwhelming their Top Sites with remote history, just as we did before this migration.
- try {
- // If FxAccount exists (Sync is enabled) then port data over to the Visits table.
- if (FirefoxAccounts.firefoxAccountsExist(mContext)) {
- try {
- historyExtensionDb = SQLiteDatabase.openDatabase(historyExtensionsDatabase.getPath(), null,
- SQLiteDatabase.OPEN_READONLY);
-
- if (historyExtensionDb != null) {
- copyHistoryExtensionDataToVisitsTable(historyExtensionDb, db);
- }
-
- // If we fail to open HistoryExtensionDatabase, then synthesize visits marking them as remote
- } catch (SQLiteException e) {
- Log.w(LOGTAG, "Couldn't open history extension database; synthesizing visits instead", e);
- synthesizeAndInsertVisits(db, false);
-
- // It's possible that we might fail to copy over visit data from the HistoryExtensionsDB,
- // so let's synthesize visits marking them as remote. See Bug 1280409.
- } catch (IllegalStateException e) {
- Log.w(LOGTAG, "Couldn't copy over history extension data; synthesizing visits instead", e);
- synthesizeAndInsertVisits(db, false);
- }
-
- // FxAccount doesn't exist, but there's evidence Sync was enabled at some point.
- // Synthesize visits from History table marking them all as remote.
- } else if (historyExtensionsDatabase.exists()) {
- synthesizeAndInsertVisits(db, false);
-
- // FxAccount doesn't exist and there's no evidence sync was ever enabled.
- // Synthesize visits from History table marking them all as local.
- } else {
- synthesizeAndInsertVisits(db, true);
- }
- } finally {
- if (historyExtensionDb != null) {
- historyExtensionDb.close();
- }
- }
-
- // Delete history extensions database if it's present.
- if (historyExtensionsDatabase.exists()) {
- if (!mContext.deleteDatabase(historyExtensionDbName)) {
- Log.e(LOGTAG, "Couldn't remove history extension database");
- }
- }
- }
-
- private void synthesizeAndInsertVisits(final SQLiteDatabase db, boolean markAsLocal) {
- final Cursor cursor = db.query(
- History.TABLE_NAME,
- new String[] {History.GUID, History.VISITS, History.DATE_LAST_VISITED},
- null, null, null, null, null);
- if (cursor == null) {
- Log.e(LOGTAG, "Null cursor while selecting all history records");
- return;
- }
-
- try {
- if (!cursor.moveToFirst()) {
- Log.e(LOGTAG, "No history records to synthesize visits for.");
- return;
- }
-
- int guidCol = cursor.getColumnIndexOrThrow(History.GUID);
- int visitsCol = cursor.getColumnIndexOrThrow(History.VISITS);
- int dateCol = cursor.getColumnIndexOrThrow(History.DATE_LAST_VISITED);
-
- // Re-use compiled SQL statements for faster inserts.
- // Visit Type is going to be 1, which is the column's default value.
- final String insertSqlStatement = "INSERT OR IGNORE INTO " + Visits.TABLE_NAME + "(" +
- Visits.DATE_VISITED + "," +
- Visits.HISTORY_GUID + "," +
- Visits.IS_LOCAL +
- ") VALUES (?, ?, ?)";
- final SQLiteStatement compiledInsertStatement = db.compileStatement(insertSqlStatement);
-
- // For each history record, insert as many visits as there are recorded in the VISITS column.
- do {
- final int numberOfVisits = cursor.getInt(visitsCol);
- final String guid = cursor.getString(guidCol);
- final long lastVisitedDate = cursor.getLong(dateCol);
-
- // Sanity check.
- if (guid == null) {
- continue;
- }
-
- // In a strange case that lastVisitedDate is a very low number, let's not introduce
- // negative timestamps into our data.
- if (lastVisitedDate - numberOfVisits < 0) {
- continue;
- }
-
- for (int i = 0; i < numberOfVisits; i++) {
- final long offsetVisitedDate = lastVisitedDate - i;
- compiledInsertStatement.clearBindings();
- compiledInsertStatement.bindLong(1, offsetVisitedDate);
- compiledInsertStatement.bindString(2, guid);
- // Very old school, 1 is true and 0 is false :)
- if (markAsLocal) {
- compiledInsertStatement.bindLong(3, Visits.VISIT_IS_LOCAL);
- } else {
- compiledInsertStatement.bindLong(3, Visits.VISIT_IS_REMOTE);
- }
- compiledInsertStatement.executeInsert();
- }
- } while (cursor.moveToNext());
- } catch (Exception e) {
- Log.e(LOGTAG, "Error while synthesizing visits for history record", e);
- } finally {
- cursor.close();
- }
- }
-
- private void updateHistoryTableAddVisitAggregates(final SQLiteDatabase db) {
- db.execSQL("ALTER TABLE " + TABLE_HISTORY +
- " ADD COLUMN " + History.LOCAL_VISITS + " INTEGER NOT NULL DEFAULT 0");
- db.execSQL("ALTER TABLE " + TABLE_HISTORY +
- " ADD COLUMN " + History.REMOTE_VISITS + " INTEGER NOT NULL DEFAULT 0");
- db.execSQL("ALTER TABLE " + TABLE_HISTORY +
- " ADD COLUMN " + History.LOCAL_DATE_LAST_VISITED + " INTEGER NOT NULL DEFAULT 0");
- db.execSQL("ALTER TABLE " + TABLE_HISTORY +
- " ADD COLUMN " + History.REMOTE_DATE_LAST_VISITED + " INTEGER NOT NULL DEFAULT 0");
- }
-
- private void calculateHistoryTableVisitAggregates(final SQLiteDatabase db) {
- // Note that we convert from microseconds (timestamps in the visits table) to milliseconds
- // (timestamps in the history table). Sync works in microseconds, so for visits Fennec stores
- // timestamps in microseconds as well - but the rest of the timestamps are stored in milliseconds.
- db.execSQL("UPDATE " + TABLE_HISTORY + " SET " +
- History.LOCAL_VISITS + " = (" +
- "SELECT COALESCE(SUM(" + qualifyColumn(TABLE_VISITS, Visits.IS_LOCAL) + "), 0)" +
- " FROM " + TABLE_VISITS +
- " WHERE " + qualifyColumn(TABLE_VISITS, Visits.HISTORY_GUID) + " = " + qualifyColumn(TABLE_HISTORY, History.GUID) +
- "), " +
- History.REMOTE_VISITS + " = (" +
- "SELECT COALESCE(SUM(CASE " + Visits.IS_LOCAL + " WHEN 0 THEN 1 ELSE 0 END), 0)" +
- " FROM " + TABLE_VISITS +
- " WHERE " + qualifyColumn(TABLE_VISITS, Visits.HISTORY_GUID) + " = " + qualifyColumn(TABLE_HISTORY, History.GUID) +
- "), " +
- History.LOCAL_DATE_LAST_VISITED + " = (" +
- "SELECT COALESCE(MAX(CASE " + Visits.IS_LOCAL + " WHEN 1 THEN " + Visits.DATE_VISITED + " ELSE 0 END), 0) / 1000" +
- " FROM " + TABLE_VISITS +
- " WHERE " + qualifyColumn(TABLE_VISITS, Visits.HISTORY_GUID) + " = " + qualifyColumn(TABLE_HISTORY, History.GUID) +
- "), " +
- History.REMOTE_DATE_LAST_VISITED + " = (" +
- "SELECT COALESCE(MAX(CASE " + Visits.IS_LOCAL + " WHEN 0 THEN " + Visits.DATE_VISITED + " ELSE 0 END), 0) / 1000" +
- " FROM " + TABLE_VISITS +
- " WHERE " + qualifyColumn(TABLE_VISITS, Visits.HISTORY_GUID) + " = " + qualifyColumn(TABLE_HISTORY, History.GUID) +
- ") " +
- "WHERE EXISTS " +
- "(SELECT " + Visits._ID +
- " FROM " + TABLE_VISITS +
- " WHERE " + qualifyColumn(TABLE_VISITS, Visits.HISTORY_GUID) + " = " + qualifyColumn(TABLE_HISTORY, History.GUID) + ")"
- );
- }
-
- private void upgradeDatabaseFrom32to33(final SQLiteDatabase db) {
- createV33CombinedView(db);
- }
-
- private void upgradeDatabaseFrom33to34(final SQLiteDatabase db) {
- updateHistoryTableAddVisitAggregates(db);
- calculateHistoryTableVisitAggregates(db);
- createV34CombinedView(db);
- }
-
- private void upgradeDatabaseFrom34to35(final SQLiteDatabase db) {
- createActivityStreamBlocklistTable(db);
- }
-
- private void upgradeDatabaseFrom35to36(final SQLiteDatabase db) {
- createPageMetadataTable(db);
- }
-
- private void createV33CombinedView(final SQLiteDatabase db) {
- db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED);
- db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED_WITH_FAVICONS);
-
- createCombinedViewOn33(db);
- }
-
- private void createV34CombinedView(final SQLiteDatabase db) {
- db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED);
- db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED_WITH_FAVICONS);
-
- createCombinedViewOn34(db);
- }
-
- private void createV19CombinedView(SQLiteDatabase db) {
- db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED);
- db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED_WITH_FAVICONS);
-
- createCombinedViewOn19(db);
- }
-
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- debug("Upgrading browser.db: " + db.getPath() + " from " +
- oldVersion + " to " + newVersion);
-
- // We have to do incremental upgrades until we reach the current
- // database schema version.
- for (int v = oldVersion + 1; v <= newVersion; v++) {
- switch (v) {
- case 4:
- upgradeDatabaseFrom3to4(db);
- break;
-
- case 7:
- upgradeDatabaseFrom6to7(db);
- break;
-
- case 8:
- upgradeDatabaseFrom7to8(db);
- break;
-
- case 11:
- upgradeDatabaseFrom10to11(db);
- break;
-
- case 13:
- upgradeDatabaseFrom12to13(db);
- break;
-
- case 14:
- upgradeDatabaseFrom13to14(db);
- break;
-
- case 15:
- upgradeDatabaseFrom14to15(db);
- break;
-
- case 16:
- upgradeDatabaseFrom15to16(db);
- break;
-
- case 17:
- upgradeDatabaseFrom16to17(db);
- break;
-
- case 18:
- upgradeDatabaseFrom17to18(db);
- break;
-
- case 19:
- upgradeDatabaseFrom18to19(db);
- break;
-
- case 20:
- upgradeDatabaseFrom19to20(db);
- break;
-
- case 22:
- upgradeDatabaseFrom21to22(db);
- break;
-
- case 23:
- upgradeDatabaseFrom22to23(db);
- break;
-
- case 24:
- upgradeDatabaseFrom23to24(db);
- break;
-
- case 25:
- upgradeDatabaseFrom24to25(db);
- break;
-
- case 26:
- upgradeDatabaseFrom25to26(db);
- break;
-
- // case 27 occurs in UrlMetadataTable.onUpgrade
-
- case 28:
- upgradeDatabaseFrom27to28(db);
- break;
-
- case 29:
- upgradeDatabaseFrom28to29(db);
- break;
-
- case 30:
- upgradeDatabaseFrom29to30(db);
- break;
-
- case 31:
- upgradeDatabaseFrom30to31(db);
- break;
-
- case 32:
- upgradeDatabaseFrom31to32(db);
- break;
-
- case 33:
- upgradeDatabaseFrom32to33(db);
- break;
-
- case 34:
- upgradeDatabaseFrom33to34(db);
- break;
-
- case 35:
- upgradeDatabaseFrom34to35(db);
- break;
-
- case 36:
- upgradeDatabaseFrom35to36(db);
- break;
- }
- }
-
- for (Table table : BrowserProvider.sTables) {
- table.onUpgrade(db, oldVersion, newVersion);
- }
-
- // Delete the obsolete favicon database after all other upgrades complete.
- // This can probably equivalently be moved into upgradeDatabaseFrom12to13.
- if (oldVersion < 13 && newVersion >= 13) {
- if (mContext.getDatabasePath("favicon_urls.db").exists()) {
- mContext.deleteDatabase("favicon_urls.db");
- }
- }
- }
-
- @Override
- public void onOpen(SQLiteDatabase db) {
- debug("Opening browser.db: " + db.getPath());
-
- // Force explicit readercache loading - we won't access readercache state for bookmarks
- // until we actually know what our bookmarks are. Bookmarks are stored in the DB, hence
- // it is sufficient to ensure that the readercache is loaded before the DB can be accessed.
- // Note, this takes ~4-6ms to load on an N4 (compared to 20-50ms for most DB queries), and
- // is only done once, hence this shouldn't have noticeable impact on performance. Moreover
- // this is run on a background thread and therefore won't block UI code during startup.
- SavedReaderViewHelper.getSavedReaderViewHelper(mContext).loadItems();
-
- Cursor cursor = null;
- try {
- cursor = db.rawQuery("PRAGMA foreign_keys=ON", null);
- } finally {
- if (cursor != null)
- cursor.close();
- }
- cursor = null;
- try {
- cursor = db.rawQuery("PRAGMA synchronous=NORMAL", null);
- } finally {
- if (cursor != null)
- cursor.close();
- }
-
- // From Honeycomb on, it's possible to run several db
- // commands in parallel using multiple connections.
- if (Build.VERSION.SDK_INT >= 11) {
- // Modern Android allows WAL to be enabled through a mode flag.
- if (Build.VERSION.SDK_INT < 16) {
- db.enableWriteAheadLogging();
-
- // This does nothing on 16+.
- db.setLockingEnabled(false);
- }
- } else {
- // Pre-Honeycomb, we can do some lesser optimizations.
- cursor = null;
- try {
- cursor = db.rawQuery("PRAGMA journal_mode=PERSIST", null);
- } finally {
- if (cursor != null)
- cursor.close();
- }
- }
- }
-
- // Calculate these once, at initialization. isLoggable is too expensive to
- // have in-line in each log call.
- private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
- private static final boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
- protected static void trace(String message) {
- if (logVerbose) {
- Log.v(LOGTAG, message);
- }
- }
-
- protected static void debug(String message) {
- if (logDebug) {
- Log.d(LOGTAG, message);
- }
- }
-
- private Integer getMobileFolderId(SQLiteDatabase db) {
- Cursor c = null;
-
- try {
- c = db.query(TABLE_BOOKMARKS,
- mobileIdColumns,
- Bookmarks.GUID + " = ?",
- mobileIdSelectionArgs,
- null, null, null);
-
- if (c == null || !c.moveToFirst())
- return null;
-
- return c.getInt(c.getColumnIndex(Bookmarks._ID));
- } finally {
- if (c != null)
- c.close();
- }
- }
-
- private interface BookmarkMigrator {
- public void updateForNewTable(ContentValues bookmark);
- }
-
- private class BookmarkMigrator3to4 implements BookmarkMigrator {
- @Override
- public void updateForNewTable(ContentValues bookmark) {
- Integer isFolder = bookmark.getAsInteger("folder");
- if (isFolder == null || isFolder != 1) {
- bookmark.put(Bookmarks.TYPE, Bookmarks.TYPE_BOOKMARK);
- } else {
- bookmark.put(Bookmarks.TYPE, Bookmarks.TYPE_FOLDER);
- }
-
- bookmark.remove("folder");
- }
- }
-}
-
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
deleted file mode 100644
index eb75d0be9..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
+++ /dev/null
@@ -1,2340 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import org.mozilla.gecko.AboutPages;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.BrowserContract.ActivityStreamBlocklist;
-import org.mozilla.gecko.db.BrowserContract.Bookmarks;
-import org.mozilla.gecko.db.BrowserContract.Combined;
-import org.mozilla.gecko.db.BrowserContract.FaviconColumns;
-import org.mozilla.gecko.db.BrowserContract.Favicons;
-import org.mozilla.gecko.db.BrowserContract.Highlights;
-import org.mozilla.gecko.db.BrowserContract.History;
-import org.mozilla.gecko.db.BrowserContract.Visits;
-import org.mozilla.gecko.db.BrowserContract.Schema;
-import org.mozilla.gecko.db.BrowserContract.Tabs;
-import org.mozilla.gecko.db.BrowserContract.Thumbnails;
-import org.mozilla.gecko.db.BrowserContract.TopSites;
-import org.mozilla.gecko.db.BrowserContract.UrlAnnotations;
-import org.mozilla.gecko.db.BrowserContract.PageMetadata;
-import org.mozilla.gecko.db.DBUtils.UpdateOperation;
-import org.mozilla.gecko.icons.IconsHelper;
-import org.mozilla.gecko.sync.Utils;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.content.BroadcastReceiver;
-import android.content.ContentProviderOperation;
-import android.content.ContentProviderResult;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.OperationApplicationException;
-import android.content.UriMatcher;
-import android.database.Cursor;
-import android.database.DatabaseUtils;
-import android.database.MatrixCursor;
-import android.database.MergeCursor;
-import android.database.SQLException;
-import android.database.sqlite.SQLiteCursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteQueryBuilder;
-import android.net.Uri;
-import android.support.v4.content.LocalBroadcastManager;
-import android.text.TextUtils;
-import android.util.Log;
-
-public class BrowserProvider extends SharedBrowserDatabaseProvider {
- public static final String ACTION_SHRINK_MEMORY = "org.mozilla.gecko.db.intent.action.SHRINK_MEMORY";
-
- private static final String LOGTAG = "GeckoBrowserProvider";
-
- // How many records to reposition in a single query.
- // This should be less than the SQLite maximum number of query variables
- // (currently 999) divided by the number of variables used per positioning
- // query (currently 3).
- static final int MAX_POSITION_UPDATES_PER_QUERY = 100;
-
- // Minimum number of records to keep when expiring history.
- static final int DEFAULT_EXPIRY_RETAIN_COUNT = 2000;
- static final int AGGRESSIVE_EXPIRY_RETAIN_COUNT = 500;
-
- // Factor used to determine the minimum number of records to keep when expiring the activity stream blocklist
- static final int ACTIVITYSTREAM_BLOCKLIST_EXPIRY_FACTOR = 5;
-
- // Minimum duration to keep when expiring.
- static final long DEFAULT_EXPIRY_PRESERVE_WINDOW = 1000L * 60L * 60L * 24L * 28L; // Four weeks.
- // Minimum number of thumbnails to keep around.
- static final int DEFAULT_EXPIRY_THUMBNAIL_COUNT = 15;
-
- static final String TABLE_BOOKMARKS = Bookmarks.TABLE_NAME;
- static final String TABLE_HISTORY = History.TABLE_NAME;
- static final String TABLE_VISITS = Visits.TABLE_NAME;
- static final String TABLE_FAVICONS = Favicons.TABLE_NAME;
- static final String TABLE_THUMBNAILS = Thumbnails.TABLE_NAME;
- static final String TABLE_TABS = Tabs.TABLE_NAME;
- static final String TABLE_URL_ANNOTATIONS = UrlAnnotations.TABLE_NAME;
- static final String TABLE_ACTIVITY_STREAM_BLOCKLIST = ActivityStreamBlocklist.TABLE_NAME;
- static final String TABLE_PAGE_METADATA = PageMetadata.TABLE_NAME;
-
- static final String VIEW_COMBINED = Combined.VIEW_NAME;
- static final String VIEW_BOOKMARKS_WITH_FAVICONS = Bookmarks.VIEW_WITH_FAVICONS;
- static final String VIEW_BOOKMARKS_WITH_ANNOTATIONS = Bookmarks.VIEW_WITH_ANNOTATIONS;
- static final String VIEW_HISTORY_WITH_FAVICONS = History.VIEW_WITH_FAVICONS;
- static final String VIEW_COMBINED_WITH_FAVICONS = Combined.VIEW_WITH_FAVICONS;
-
- // Bookmark matches
- static final int BOOKMARKS = 100;
- static final int BOOKMARKS_ID = 101;
- static final int BOOKMARKS_FOLDER_ID = 102;
- static final int BOOKMARKS_PARENT = 103;
- static final int BOOKMARKS_POSITIONS = 104;
-
- // History matches
- static final int HISTORY = 200;
- static final int HISTORY_ID = 201;
- static final int HISTORY_OLD = 202;
-
- // Favicon matches
- static final int FAVICONS = 300;
- static final int FAVICON_ID = 301;
-
- // Schema matches
- static final int SCHEMA = 400;
-
- // Combined bookmarks and history matches
- static final int COMBINED = 500;
-
- // Control matches
- static final int CONTROL = 600;
-
- // Search Suggest matches. Obsolete.
- static final int SEARCH_SUGGEST = 700;
-
- // Thumbnail matches
- static final int THUMBNAILS = 800;
- static final int THUMBNAIL_ID = 801;
-
- static final int URL_ANNOTATIONS = 900;
-
- static final int TOPSITES = 1000;
-
- static final int VISITS = 1100;
-
- static final int METADATA = 1200;
-
- static final int HIGHLIGHTS = 1300;
-
- static final int ACTIVITY_STREAM_BLOCKLIST = 1400;
-
- static final int PAGE_METADATA = 1500;
-
- static final String DEFAULT_BOOKMARKS_SORT_ORDER = Bookmarks.TYPE
- + " ASC, " + Bookmarks.POSITION + " ASC, " + Bookmarks._ID
- + " ASC";
-
- static final String DEFAULT_HISTORY_SORT_ORDER = History.DATE_LAST_VISITED + " DESC";
- static final String DEFAULT_VISITS_SORT_ORDER = Visits.DATE_VISITED + " DESC";
-
- static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
-
- static final Map<String, String> BOOKMARKS_PROJECTION_MAP;
- static final Map<String, String> HISTORY_PROJECTION_MAP;
- static final Map<String, String> COMBINED_PROJECTION_MAP;
- static final Map<String, String> SCHEMA_PROJECTION_MAP;
- static final Map<String, String> FAVICONS_PROJECTION_MAP;
- static final Map<String, String> THUMBNAILS_PROJECTION_MAP;
- static final Map<String, String> URL_ANNOTATIONS_PROJECTION_MAP;
- static final Map<String, String> VISIT_PROJECTION_MAP;
- static final Map<String, String> PAGE_METADATA_PROJECTION_MAP;
- static final Table[] sTables;
-
- static {
- sTables = new Table[] {
- // See awful shortcut assumption hack in getURLMetadataTable.
- new URLMetadataTable()
- };
- // We will reuse this.
- HashMap<String, String> map;
-
- // Bookmarks
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks", BOOKMARKS);
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/#", BOOKMARKS_ID);
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/parents", BOOKMARKS_PARENT);
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/positions", BOOKMARKS_POSITIONS);
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/folder/#", BOOKMARKS_FOLDER_ID);
-
- map = new HashMap<String, String>();
- map.put(Bookmarks._ID, Bookmarks._ID);
- map.put(Bookmarks.TITLE, Bookmarks.TITLE);
- map.put(Bookmarks.URL, Bookmarks.URL);
- map.put(Bookmarks.FAVICON, Bookmarks.FAVICON);
- map.put(Bookmarks.FAVICON_ID, Bookmarks.FAVICON_ID);
- map.put(Bookmarks.FAVICON_URL, Bookmarks.FAVICON_URL);
- map.put(Bookmarks.TYPE, Bookmarks.TYPE);
- map.put(Bookmarks.PARENT, Bookmarks.PARENT);
- map.put(Bookmarks.POSITION, Bookmarks.POSITION);
- map.put(Bookmarks.TAGS, Bookmarks.TAGS);
- map.put(Bookmarks.DESCRIPTION, Bookmarks.DESCRIPTION);
- map.put(Bookmarks.KEYWORD, Bookmarks.KEYWORD);
- map.put(Bookmarks.DATE_CREATED, Bookmarks.DATE_CREATED);
- map.put(Bookmarks.DATE_MODIFIED, Bookmarks.DATE_MODIFIED);
- map.put(Bookmarks.GUID, Bookmarks.GUID);
- map.put(Bookmarks.IS_DELETED, Bookmarks.IS_DELETED);
- BOOKMARKS_PROJECTION_MAP = Collections.unmodifiableMap(map);
-
- // History
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history", HISTORY);
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history/#", HISTORY_ID);
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history/old", HISTORY_OLD);
-
- map = new HashMap<String, String>();
- map.put(History._ID, History._ID);
- map.put(History.TITLE, History.TITLE);
- map.put(History.URL, History.URL);
- map.put(History.FAVICON, History.FAVICON);
- map.put(History.FAVICON_ID, History.FAVICON_ID);
- map.put(History.FAVICON_URL, History.FAVICON_URL);
- map.put(History.VISITS, History.VISITS);
- map.put(History.LOCAL_VISITS, History.LOCAL_VISITS);
- map.put(History.REMOTE_VISITS, History.REMOTE_VISITS);
- map.put(History.DATE_LAST_VISITED, History.DATE_LAST_VISITED);
- map.put(History.LOCAL_DATE_LAST_VISITED, History.LOCAL_DATE_LAST_VISITED);
- map.put(History.REMOTE_DATE_LAST_VISITED, History.REMOTE_DATE_LAST_VISITED);
- map.put(History.DATE_CREATED, History.DATE_CREATED);
- map.put(History.DATE_MODIFIED, History.DATE_MODIFIED);
- map.put(History.GUID, History.GUID);
- map.put(History.IS_DELETED, History.IS_DELETED);
- HISTORY_PROJECTION_MAP = Collections.unmodifiableMap(map);
-
- // Visits
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, "visits", VISITS);
-
- map = new HashMap<String, String>();
- map.put(Visits._ID, Visits._ID);
- map.put(Visits.HISTORY_GUID, Visits.HISTORY_GUID);
- map.put(Visits.VISIT_TYPE, Visits.VISIT_TYPE);
- map.put(Visits.DATE_VISITED, Visits.DATE_VISITED);
- map.put(Visits.IS_LOCAL, Visits.IS_LOCAL);
- VISIT_PROJECTION_MAP = Collections.unmodifiableMap(map);
-
- // Favicons
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, "favicons", FAVICONS);
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, "favicons/#", FAVICON_ID);
-
- map = new HashMap<String, String>();
- map.put(Favicons._ID, Favicons._ID);
- map.put(Favicons.URL, Favicons.URL);
- map.put(Favicons.DATA, Favicons.DATA);
- map.put(Favicons.DATE_CREATED, Favicons.DATE_CREATED);
- map.put(Favicons.DATE_MODIFIED, Favicons.DATE_MODIFIED);
- FAVICONS_PROJECTION_MAP = Collections.unmodifiableMap(map);
-
- // Thumbnails
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, "thumbnails", THUMBNAILS);
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, "thumbnails/#", THUMBNAIL_ID);
-
- map = new HashMap<String, String>();
- map.put(Thumbnails._ID, Thumbnails._ID);
- map.put(Thumbnails.URL, Thumbnails.URL);
- map.put(Thumbnails.DATA, Thumbnails.DATA);
- THUMBNAILS_PROJECTION_MAP = Collections.unmodifiableMap(map);
-
- // Url annotations
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, TABLE_URL_ANNOTATIONS, URL_ANNOTATIONS);
-
- map = new HashMap<>();
- map.put(UrlAnnotations._ID, UrlAnnotations._ID);
- map.put(UrlAnnotations.URL, UrlAnnotations.URL);
- map.put(UrlAnnotations.KEY, UrlAnnotations.KEY);
- map.put(UrlAnnotations.VALUE, UrlAnnotations.VALUE);
- map.put(UrlAnnotations.DATE_CREATED, UrlAnnotations.DATE_CREATED);
- map.put(UrlAnnotations.DATE_MODIFIED, UrlAnnotations.DATE_MODIFIED);
- map.put(UrlAnnotations.SYNC_STATUS, UrlAnnotations.SYNC_STATUS);
- URL_ANNOTATIONS_PROJECTION_MAP = Collections.unmodifiableMap(map);
-
- // Combined bookmarks and history
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, "combined", COMBINED);
-
- map = new HashMap<String, String>();
- map.put(Combined._ID, Combined._ID);
- map.put(Combined.BOOKMARK_ID, Combined.BOOKMARK_ID);
- map.put(Combined.HISTORY_ID, Combined.HISTORY_ID);
- map.put(Combined.URL, Combined.URL);
- map.put(Combined.TITLE, Combined.TITLE);
- map.put(Combined.VISITS, Combined.VISITS);
- map.put(Combined.DATE_LAST_VISITED, Combined.DATE_LAST_VISITED);
- map.put(Combined.FAVICON, Combined.FAVICON);
- map.put(Combined.FAVICON_ID, Combined.FAVICON_ID);
- map.put(Combined.FAVICON_URL, Combined.FAVICON_URL);
- map.put(Combined.LOCAL_DATE_LAST_VISITED, Combined.LOCAL_DATE_LAST_VISITED);
- map.put(Combined.REMOTE_DATE_LAST_VISITED, Combined.REMOTE_DATE_LAST_VISITED);
- map.put(Combined.LOCAL_VISITS_COUNT, Combined.LOCAL_VISITS_COUNT);
- map.put(Combined.REMOTE_VISITS_COUNT, Combined.REMOTE_VISITS_COUNT);
- COMBINED_PROJECTION_MAP = Collections.unmodifiableMap(map);
-
- map = new HashMap<>();
- map.put(PageMetadata._ID, PageMetadata._ID);
- map.put(PageMetadata.HISTORY_GUID, PageMetadata.HISTORY_GUID);
- map.put(PageMetadata.DATE_CREATED, PageMetadata.DATE_CREATED);
- map.put(PageMetadata.HAS_IMAGE, PageMetadata.HAS_IMAGE);
- map.put(PageMetadata.JSON, PageMetadata.JSON);
- PAGE_METADATA_PROJECTION_MAP = Collections.unmodifiableMap(map);
-
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, "page_metadata", PAGE_METADATA);
-
- // Schema
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, "schema", SCHEMA);
-
- map = new HashMap<String, String>();
- map.put(Schema.VERSION, Schema.VERSION);
- SCHEMA_PROJECTION_MAP = Collections.unmodifiableMap(map);
-
-
- // Control
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, "control", CONTROL);
-
- for (Table table : sTables) {
- for (Table.ContentProviderInfo type : table.getContentProviderInfo()) {
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, type.name, type.id);
- }
- }
-
- // Combined pinned sites, top visited sites, and suggested sites
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, "topsites", TOPSITES);
-
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, "highlights", HIGHLIGHTS);
-
- URI_MATCHER.addURI(BrowserContract.AUTHORITY, ActivityStreamBlocklist.TABLE_NAME, ACTIVITY_STREAM_BLOCKLIST);
- }
-
- private static class ShrinkMemoryReceiver extends BroadcastReceiver {
- private final WeakReference<BrowserProvider> mBrowserProviderWeakReference;
-
- public ShrinkMemoryReceiver(final BrowserProvider browserProvider) {
- mBrowserProviderWeakReference = new WeakReference<>(browserProvider);
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- final BrowserProvider browserProvider = mBrowserProviderWeakReference.get();
- if (browserProvider == null) {
- return;
- }
- final PerProfileDatabases<BrowserDatabaseHelper> databases = browserProvider.getDatabases();
- if (databases == null) {
- return;
- }
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- databases.shrinkMemory();
- }
- });
- }
- }
-
- private final ShrinkMemoryReceiver mShrinkMemoryReceiver = new ShrinkMemoryReceiver(this);
-
- @Override
- public boolean onCreate() {
- if (!super.onCreate()) {
- return false;
- }
-
- LocalBroadcastManager.getInstance(getContext()).registerReceiver(mShrinkMemoryReceiver,
- new IntentFilter(ACTION_SHRINK_MEMORY));
-
- return true;
- }
-
- @Override
- public void shutdown() {
- LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(mShrinkMemoryReceiver);
-
- super.shutdown();
- }
-
- // Convenience accessor.
- // Assumes structure of sTables!
- private URLMetadataTable getURLMetadataTable() {
- return (URLMetadataTable) sTables[0];
- }
-
- private static boolean hasFaviconsInProjection(String[] projection) {
- if (projection == null) return true;
- for (int i = 0; i < projection.length; ++i) {
- if (projection[i].equals(FaviconColumns.FAVICON) ||
- projection[i].equals(FaviconColumns.FAVICON_URL))
- return true;
- }
-
- return false;
- }
-
- // Calculate these once, at initialization. isLoggable is too expensive to
- // have in-line in each log call.
- private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
- private static final boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
- protected static void trace(String message) {
- if (logVerbose) {
- Log.v(LOGTAG, message);
- }
- }
-
- protected static void debug(String message) {
- if (logDebug) {
- Log.d(LOGTAG, message);
- }
- }
-
- /**
- * Remove enough activity stream blocklist items to bring the database count below <code>retain</code>.
- *
- * Items will be removed according to their creation date, oldest being removed first.
- */
- private void expireActivityStreamBlocklist(final SQLiteDatabase db, final int retain) {
- Log.d(LOGTAG, "Expiring highlights blocklist.");
- final long rows = DatabaseUtils.queryNumEntries(db, TABLE_ACTIVITY_STREAM_BLOCKLIST);
-
- if (retain >= rows) {
- debug("Not expiring highlights blocklist: only have " + rows + " rows.");
- return;
- }
-
- final long toRemove = rows - retain;
-
- final String statement = "DELETE FROM " + TABLE_ACTIVITY_STREAM_BLOCKLIST + " WHERE " + ActivityStreamBlocklist._ID + " IN " +
- " ( SELECT " + ActivityStreamBlocklist._ID + " FROM " + TABLE_ACTIVITY_STREAM_BLOCKLIST + " " +
- "ORDER BY " + ActivityStreamBlocklist.CREATED + " ASC LIMIT " + toRemove + ")";
-
- beginWrite(db);
- db.execSQL(statement);
- }
-
- /**
- * Remove enough history items to bring the database count below <code>retain</code>,
- * removing no items with a modified time after <code>keepAfter</code>.
- *
- * Provide <code>keepAfter</code> less than or equal to zero to skip that check.
- *
- * Items will be removed according to last visited date.
- */
- private void expireHistory(final SQLiteDatabase db, final int retain, final long keepAfter) {
- Log.d(LOGTAG, "Expiring history.");
- final long rows = DatabaseUtils.queryNumEntries(db, TABLE_HISTORY);
-
- if (retain >= rows) {
- debug("Not expiring history: only have " + rows + " rows.");
- return;
- }
-
- final long toRemove = rows - retain;
- debug("Expiring at most " + toRemove + " rows earlier than " + keepAfter + ".");
-
- final String sql;
- if (keepAfter > 0) {
- sql = "DELETE FROM " + TABLE_HISTORY + " " +
- "WHERE MAX(" + History.DATE_LAST_VISITED + ", " + History.DATE_MODIFIED + ") < " + keepAfter + " " +
- " AND " + History._ID + " IN ( SELECT " +
- History._ID + " FROM " + TABLE_HISTORY + " " +
- "ORDER BY " + History.DATE_LAST_VISITED + " ASC LIMIT " + toRemove +
- ")";
- } else {
- sql = "DELETE FROM " + TABLE_HISTORY + " WHERE " + History._ID + " " +
- "IN ( SELECT " + History._ID + " FROM " + TABLE_HISTORY + " " +
- "ORDER BY " + History.DATE_LAST_VISITED + " ASC LIMIT " + toRemove + ")";
- }
- trace("Deleting using query: " + sql);
-
- beginWrite(db);
- db.execSQL(sql);
- }
-
- /**
- * Remove any thumbnails that for sites that aren't likely to be ever shown.
- * Items will be removed according to a frecency calculation and only if they are not pinned
- *
- * Call this method within a transaction.
- */
- private void expireThumbnails(final SQLiteDatabase db) {
- Log.d(LOGTAG, "Expiring thumbnails.");
- final String sortOrder = BrowserContract.getCombinedFrecencySortOrder(true, false);
- final String sql = "DELETE FROM " + TABLE_THUMBNAILS +
- " WHERE " + Thumbnails.URL + " NOT IN ( " +
- " SELECT " + Combined.URL +
- " FROM " + Combined.VIEW_NAME +
- " ORDER BY " + sortOrder +
- " LIMIT " + DEFAULT_EXPIRY_THUMBNAIL_COUNT +
- ") AND " + Thumbnails.URL + " NOT IN ( " +
- " SELECT " + Bookmarks.URL +
- " FROM " + TABLE_BOOKMARKS +
- " WHERE " + Bookmarks.PARENT + " = " + Bookmarks.FIXED_PINNED_LIST_ID +
- ") AND " + Thumbnails.URL + " NOT IN ( " +
- " SELECT " + Tabs.URL +
- " FROM " + TABLE_TABS +
- ")";
- trace("Clear thumbs using query: " + sql);
- db.execSQL(sql);
- }
-
- private boolean shouldIncrementVisits(Uri uri) {
- String incrementVisits = uri.getQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS);
- return Boolean.parseBoolean(incrementVisits);
- }
-
- private boolean shouldIncrementRemoteAggregates(Uri uri) {
- final String incrementRemoteAggregates = uri.getQueryParameter(BrowserContract.PARAM_INCREMENT_REMOTE_AGGREGATES);
- return Boolean.parseBoolean(incrementRemoteAggregates);
- }
-
- @Override
- public String getType(Uri uri) {
- final int match = URI_MATCHER.match(uri);
-
- trace("Getting URI type: " + uri);
-
- switch (match) {
- case BOOKMARKS:
- trace("URI is BOOKMARKS: " + uri);
- return Bookmarks.CONTENT_TYPE;
- case BOOKMARKS_ID:
- trace("URI is BOOKMARKS_ID: " + uri);
- return Bookmarks.CONTENT_ITEM_TYPE;
- case HISTORY:
- trace("URI is HISTORY: " + uri);
- return History.CONTENT_TYPE;
- case HISTORY_ID:
- trace("URI is HISTORY_ID: " + uri);
- return History.CONTENT_ITEM_TYPE;
- default:
- String type = getContentItemType(match);
- if (type != null) {
- trace("URI is " + type);
- return type;
- }
-
- debug("URI has unrecognized type: " + uri);
- return null;
- }
- }
-
- @SuppressWarnings("fallthrough")
- @Override
- public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
- trace("Calling delete in transaction on URI: " + uri);
- final SQLiteDatabase db = getWritableDatabase(uri);
-
- final int match = URI_MATCHER.match(uri);
- int deleted = 0;
-
- switch (match) {
- case BOOKMARKS_ID:
- trace("Delete on BOOKMARKS_ID: " + uri);
-
- selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?");
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { Long.toString(ContentUris.parseId(uri)) });
- // fall through
- case BOOKMARKS: {
- trace("Deleting bookmarks: " + uri);
- deleted = deleteBookmarks(uri, selection, selectionArgs);
- deleteUnusedImages(uri);
- break;
- }
-
- case HISTORY_ID:
- trace("Delete on HISTORY_ID: " + uri);
-
- selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?");
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { Long.toString(ContentUris.parseId(uri)) });
- // fall through
- case HISTORY: {
- trace("Deleting history: " + uri);
- beginWrite(db);
- /**
- * Deletes from Sync are actual DELETE statements, which will cascade delete relevant visits.
- * Fennec's deletes mark records as deleted and wipe out all information (except for GUID).
- * Eventually, Fennec will purge history records that were marked as deleted for longer than some
- * period of time (e.g. 20 days).
- * See {@link SharedBrowserDatabaseProvider#cleanUpSomeDeletedRecords(Uri, String)}.
- */
- final ArrayList<String> historyGUIDs = getHistoryGUIDsFromSelection(db, uri, selection, selectionArgs);
-
- if (!isCallerSync(uri)) {
- deleteVisitsForHistory(db, historyGUIDs);
- }
- deletePageMetadataForHistory(db, historyGUIDs);
- deleted = deleteHistory(db, uri, selection, selectionArgs);
- deleteUnusedImages(uri);
- break;
- }
-
- case VISITS:
- trace("Deleting visits: " + uri);
- beginWrite(db);
- deleted = deleteVisits(uri, selection, selectionArgs);
- break;
-
- case HISTORY_OLD: {
- String priority = uri.getQueryParameter(BrowserContract.PARAM_EXPIRE_PRIORITY);
- long keepAfter = System.currentTimeMillis() - DEFAULT_EXPIRY_PRESERVE_WINDOW;
- int retainCount = DEFAULT_EXPIRY_RETAIN_COUNT;
-
- if (BrowserContract.ExpirePriority.AGGRESSIVE.toString().equals(priority)) {
- keepAfter = 0;
- retainCount = AGGRESSIVE_EXPIRY_RETAIN_COUNT;
- }
- expireHistory(db, retainCount, keepAfter);
- expireActivityStreamBlocklist(db, retainCount / ACTIVITYSTREAM_BLOCKLIST_EXPIRY_FACTOR);
- expireThumbnails(db);
- deleteUnusedImages(uri);
- break;
- }
-
- case FAVICON_ID:
- debug("Delete on FAVICON_ID: " + uri);
-
- selection = DBUtils.concatenateWhere(selection, TABLE_FAVICONS + "._id = ?");
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { Long.toString(ContentUris.parseId(uri)) });
- // fall through
- case FAVICONS: {
- trace("Deleting favicons: " + uri);
- beginWrite(db);
- deleted = deleteFavicons(uri, selection, selectionArgs);
- break;
- }
-
- case THUMBNAIL_ID:
- debug("Delete on THUMBNAIL_ID: " + uri);
-
- selection = DBUtils.concatenateWhere(selection, TABLE_THUMBNAILS + "._id = ?");
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { Long.toString(ContentUris.parseId(uri)) });
- // fall through
- case THUMBNAILS: {
- trace("Deleting thumbnails: " + uri);
- beginWrite(db);
- deleted = deleteThumbnails(uri, selection, selectionArgs);
- break;
- }
-
- case URL_ANNOTATIONS:
- trace("Delete on URL_ANNOTATIONS: " + uri);
- deleteUrlAnnotation(uri, selection, selectionArgs);
- break;
-
- case PAGE_METADATA:
- trace("Delete on PAGE_METADATA: " + uri);
- deleted = deletePageMetadata(uri, selection, selectionArgs);
- break;
-
- default: {
- Table table = findTableFor(match);
- if (table == null) {
- throw new UnsupportedOperationException("Unknown delete URI " + uri);
- }
- trace("Deleting TABLE: " + uri);
- beginWrite(db);
- deleted = table.delete(db, uri, match, selection, selectionArgs);
- }
- }
-
- debug("Deleted " + deleted + " rows for URI: " + uri);
-
- return deleted;
- }
-
- @Override
- public Uri insertInTransaction(Uri uri, ContentValues values) {
- trace("Calling insert in transaction on URI: " + uri);
-
- int match = URI_MATCHER.match(uri);
- long id = -1;
-
- switch (match) {
- case BOOKMARKS: {
- trace("Insert on BOOKMARKS: " + uri);
- id = insertBookmark(uri, values);
- break;
- }
-
- case HISTORY: {
- trace("Insert on HISTORY: " + uri);
- id = insertHistory(uri, values);
- break;
- }
-
- case VISITS: {
- trace("Insert on VISITS: " + uri);
- id = insertVisit(uri, values);
- break;
- }
-
- case FAVICONS: {
- trace("Insert on FAVICONS: " + uri);
- id = insertFavicon(uri, values);
- break;
- }
-
- case THUMBNAILS: {
- trace("Insert on THUMBNAILS: " + uri);
- id = insertThumbnail(uri, values);
- break;
- }
-
- case URL_ANNOTATIONS: {
- trace("Insert on URL_ANNOTATIONS: " + uri);
- id = insertUrlAnnotation(uri, values);
- break;
- }
-
- case ACTIVITY_STREAM_BLOCKLIST: {
- trace("Insert on ACTIVITY_STREAM_BLOCKLIST: " + uri);
- id = insertActivityStreamBlocklistSite(uri, values);
- break;
- }
-
- case PAGE_METADATA: {
- trace("Insert on PAGE_METADATA: " + uri);
- id = insertPageMetadata(uri, values);
- break;
- }
-
- default: {
- Table table = findTableFor(match);
- if (table == null) {
- throw new UnsupportedOperationException("Unknown insert URI " + uri);
- }
-
- trace("Insert on TABLE: " + uri);
- final SQLiteDatabase db = getWritableDatabase(uri);
- beginWrite(db);
- id = table.insert(db, uri, match, values);
- }
- }
-
- debug("Inserted ID in database: " + id);
-
- if (id >= 0)
- return ContentUris.withAppendedId(uri, id);
-
- return null;
- }
-
- @SuppressWarnings("fallthrough")
- @Override
- public int updateInTransaction(Uri uri, ContentValues values, String selection,
- String[] selectionArgs) {
- trace("Calling update in transaction on URI: " + uri);
-
- int match = URI_MATCHER.match(uri);
- int updated = 0;
-
- final SQLiteDatabase db = getWritableDatabase(uri);
- switch (match) {
- // We provide a dedicated (hacky) API for callers to bulk-update the positions of
- // folder children by passing an array of GUID strings as `selectionArgs`.
- // Each child will have its position column set to its index in the provided array.
- //
- // This avoids callers having to issue a large number of UPDATE queries through
- // the usual channels. See Bug 728783.
- //
- // Note that this is decidedly not a general-purpose API; use at your own risk.
- // `values` and `selection` are ignored.
- case BOOKMARKS_POSITIONS: {
- debug("Update on BOOKMARKS_POSITIONS: " + uri);
-
- // This already starts and finishes its own transaction.
- updated = updateBookmarkPositions(uri, selectionArgs);
- break;
- }
-
- case BOOKMARKS_PARENT: {
- debug("Update on BOOKMARKS_PARENT: " + uri);
- beginWrite(db);
- updated = updateBookmarkParents(db, values, selection, selectionArgs);
- break;
- }
-
- case BOOKMARKS_ID:
- debug("Update on BOOKMARKS_ID: " + uri);
-
- selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?");
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { Long.toString(ContentUris.parseId(uri)) });
- // fall through
- case BOOKMARKS: {
- debug("Updating bookmark: " + uri);
- if (shouldUpdateOrInsert(uri)) {
- updated = updateOrInsertBookmark(uri, values, selection, selectionArgs);
- } else {
- updated = updateBookmarks(uri, values, selection, selectionArgs);
- }
- break;
- }
-
- case HISTORY_ID:
- debug("Update on HISTORY_ID: " + uri);
-
- selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?");
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { Long.toString(ContentUris.parseId(uri)) });
- // fall through
- case HISTORY: {
- debug("Updating history: " + uri);
- if (shouldUpdateOrInsert(uri)) {
- updated = updateOrInsertHistory(uri, values, selection, selectionArgs);
- } else {
- updated = updateHistory(uri, values, selection, selectionArgs);
- }
- if (shouldIncrementVisits(uri)) {
- insertVisitForHistory(uri, values, selection, selectionArgs);
- }
- break;
- }
-
- case FAVICONS: {
- debug("Update on FAVICONS: " + uri);
-
- String url = values.getAsString(Favicons.URL);
- String faviconSelection = null;
- String[] faviconSelectionArgs = null;
-
- if (!TextUtils.isEmpty(url)) {
- faviconSelection = Favicons.URL + " = ?";
- faviconSelectionArgs = new String[] { url };
- }
-
- if (shouldUpdateOrInsert(uri)) {
- updated = updateOrInsertFavicon(uri, values, faviconSelection, faviconSelectionArgs);
- } else {
- updated = updateExistingFavicon(uri, values, faviconSelection, faviconSelectionArgs);
- }
- break;
- }
-
- case THUMBNAILS: {
- debug("Update on THUMBNAILS: " + uri);
-
- String url = values.getAsString(Thumbnails.URL);
-
- // if no URL is provided, update all of the entries
- if (TextUtils.isEmpty(values.getAsString(Thumbnails.URL))) {
- updated = updateExistingThumbnail(uri, values, null, null);
- } else if (shouldUpdateOrInsert(uri)) {
- updated = updateOrInsertThumbnail(uri, values, Thumbnails.URL + " = ?",
- new String[] { url });
- } else {
- updated = updateExistingThumbnail(uri, values, Thumbnails.URL + " = ?",
- new String[] { url });
- }
- break;
- }
-
- case URL_ANNOTATIONS:
- updateUrlAnnotation(uri, values, selection, selectionArgs);
- break;
-
- default: {
- Table table = findTableFor(match);
- if (table == null) {
- throw new UnsupportedOperationException("Unknown update URI " + uri);
- }
- trace("Update TABLE: " + uri);
-
- beginWrite(db);
- updated = table.update(db, uri, match, values, selection, selectionArgs);
- if (shouldUpdateOrInsert(uri) && updated == 0) {
- trace("No update, inserting for URL: " + uri);
- table.insert(db, uri, match, values);
- updated = 1;
- }
- }
- }
-
- debug("Updated " + updated + " rows for URI: " + uri);
- return updated;
- }
-
- /**
- * Get topsites by themselves, without the inclusion of pinned sites. Suggested sites
- * will be appended (if necessary) to the end of the list in order to provide up to PARAM_LIMIT items.
- */
- private Cursor getPlainTopSites(final Uri uri) {
- final SQLiteDatabase db = getReadableDatabase(uri);
-
- final String limitParam = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
- final int limit;
- if (limitParam != null) {
- limit = Integer.parseInt(limitParam);
- } else {
- limit = 12;
- }
-
- // Filter out: unvisited pages (history_id == -1) pinned (and other special) sites, deleted sites,
- // and about: pages.
- final String ignoreForTopSitesWhereClause =
- "(" + Combined.HISTORY_ID + " IS NOT -1)" +
- " AND " +
- Combined.URL + " NOT IN (SELECT " +
- Bookmarks.URL + " FROM " + TABLE_BOOKMARKS + " WHERE " +
- DBUtils.qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " < " + Bookmarks.FIXED_ROOT_ID + " AND " +
- DBUtils.qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " == 0)" +
- " AND " +
- "(" + Combined.URL + " NOT LIKE ?)";
-
- final String[] ignoreForTopSitesArgs = new String[] {
- AboutPages.URL_FILTER
- };
-
- final Cursor c = db.rawQuery("SELECT " +
- Bookmarks._ID + ", " +
- Combined.BOOKMARK_ID + ", " +
- Combined.HISTORY_ID + ", " +
- Bookmarks.URL + ", " +
- Bookmarks.TITLE + ", " +
- Combined.HISTORY_ID + ", " +
- TopSites.TYPE_TOP + " AS " + TopSites.TYPE +
- " FROM " + Combined.VIEW_NAME +
- " WHERE " + ignoreForTopSitesWhereClause +
- " ORDER BY " + BrowserContract.getCombinedFrecencySortOrder(true, false) +
- " LIMIT " + limit,
- ignoreForTopSitesArgs);
-
- c.setNotificationUri(getContext().getContentResolver(),
- BrowserContract.AUTHORITY_URI);
-
- if (c.getCount() == limit) {
- return c;
- }
-
- // If we don't have enough data: get suggested sites too
- final SuggestedSites suggestedSites = BrowserDB.from(GeckoProfile.get(
- getContext(), uri.getQueryParameter(BrowserContract.PARAM_PROFILE))).getSuggestedSites();
-
- final Cursor suggestedSitesCursor = suggestedSites.get(limit - c.getCount());
-
- return new MergeCursor(new Cursor[]{
- c,
- suggestedSitesCursor
- });
- }
-
- private Cursor getTopSites(final Uri uri) {
- // In order to correctly merge the top and pinned sites we:
- //
- // 1. Generate a list of free ids for topsites - this is the positions that are NOT used by pinned sites.
- // We do this using a subquery with a self-join in order to generate rowids, that allow us to join with
- // the list of topsites.
- // 2. Generate the list of topsites in order of frecency.
- // 3. Join these, so that each topsite is given its resulting position
- // 4. UNION all with the pinned sites, and order by position
- //
- // Suggested sites are placed after the topsites, but might still be interspersed with the suggested sites,
- // hence we append these to the topsite list, and treat these identically to topsites from this point on.
- //
- // We require rowids to join the two lists, however subqueries aren't given rowids - hence we use two different
- // tricks to generate these:
- // 1. The list of free ids is small, hence we can do a self-join to generate rowids.
- // 2. The topsites list is larger, hence we use a temporary table, which automatically provides rowids.
-
- final SQLiteDatabase db = getWritableDatabase(uri);
-
- final String TABLE_TOPSITES = "topsites";
-
- final String limitParam = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
- final String gridLimitParam = uri.getQueryParameter(BrowserContract.PARAM_SUGGESTEDSITES_LIMIT);
-
- final int totalLimit;
- final int suggestedGridLimit;
-
- if (limitParam == null) {
- totalLimit = 50;
- } else {
- totalLimit = Integer.parseInt(limitParam, 10);
- }
-
- if (gridLimitParam == null) {
- suggestedGridLimit = getContext().getResources().getInteger(R.integer.number_of_top_sites);
- } else {
- suggestedGridLimit = Integer.parseInt(gridLimitParam, 10);
- }
-
- final String pinnedSitesFromClause = "FROM " + TABLE_BOOKMARKS + " WHERE " +
- Bookmarks.PARENT + " == " + Bookmarks.FIXED_PINNED_LIST_ID +
- " AND " + Bookmarks.IS_DELETED + " IS NOT 1";
-
- // Ideally we'd use a recursive CTE to generate our sequence, e.g. something like this worked at one point:
- // " WITH RECURSIVE" +
- // " cnt(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM cnt WHERE x < 6)" +
- // However that requires SQLite >= 3.8.3 (available on Android >= 5.0), so in the meantime
- // we use a temporary numbers table.
- // Note: SQLite rowids are 1-indexed, whereas we're expecting 0-indexed values for the position. Our numbers
- // table starts at position = 0, which ensures the correct results here.
- final String freeIDSubquery =
- " SELECT count(free_ids.position) + 1 AS rowid, numbers.position AS " + Bookmarks.POSITION +
- " FROM (SELECT position FROM numbers WHERE position NOT IN (SELECT " + Bookmarks.POSITION + " " + pinnedSitesFromClause + ")) AS numbers" +
- " LEFT OUTER JOIN " +
- " (SELECT position FROM numbers WHERE position NOT IN (SELECT " + Bookmarks.POSITION + " " + pinnedSitesFromClause + ")) AS free_ids" +
- " ON numbers.position > free_ids.position" +
- " GROUP BY numbers.position" +
- " ORDER BY numbers.position ASC" +
- " LIMIT " + suggestedGridLimit;
-
- // Filter out: unvisited pages (history_id == -1) pinned (and other special) sites, deleted sites,
- // and about: pages.
- final String ignoreForTopSitesWhereClause =
- "(" + Combined.HISTORY_ID + " IS NOT -1)" +
- " AND " +
- Combined.URL + " NOT IN (SELECT " +
- Bookmarks.URL + " FROM bookmarks WHERE " +
- DBUtils.qualifyColumn("bookmarks", Bookmarks.PARENT) + " < " + Bookmarks.FIXED_ROOT_ID + " AND " +
- DBUtils.qualifyColumn("bookmarks", Bookmarks.IS_DELETED) + " == 0)" +
- " AND " +
- "(" + Combined.URL + " NOT LIKE ?)";
-
- final String[] ignoreForTopSitesArgs = new String[] {
- AboutPages.URL_FILTER
- };
-
- // Stuff the suggested sites into SQL: this allows us to filter pinned and topsites out of the suggested
- // sites list as part of the final query (as opposed to walking cursors in java)
- final SuggestedSites suggestedSites = BrowserDB.from(GeckoProfile.get(
- getContext(), uri.getQueryParameter(BrowserContract.PARAM_PROFILE))).getSuggestedSites();
-
- StringBuilder suggestedSitesBuilder = new StringBuilder();
- // We could access the underlying data here, however SuggestedSites also performs filtering on the suggested
- // sites list, which means we'd need to process the lists within SuggestedSites in any case. If we're doing
- // that processing, there is little real between us using a MatrixCursor, or a Map (or List) instead of the
- // MatrixCursor.
- final Cursor suggestedSitesCursor = suggestedSites.get(suggestedGridLimit);
-
- String[] suggestedSiteArgs = new String[0];
-
- boolean hasProcessedAnySuggestedSites = false;
-
- final int idColumnIndex = suggestedSitesCursor.getColumnIndexOrThrow(Bookmarks._ID);
- final int urlColumnIndex = suggestedSitesCursor.getColumnIndexOrThrow(Bookmarks.URL);
- final int titleColumnIndex = suggestedSitesCursor.getColumnIndexOrThrow(Bookmarks.TITLE);
-
- while (suggestedSitesCursor.moveToNext()) {
- // We'll be using this as a subquery, hence we need to avoid the preceding UNION ALL
- if (hasProcessedAnySuggestedSites) {
- suggestedSitesBuilder.append(" UNION ALL");
- } else {
- hasProcessedAnySuggestedSites = true;
- }
- suggestedSitesBuilder.append(" SELECT" +
- " ? AS " + Bookmarks._ID + "," +
- " ? AS " + Bookmarks.URL + "," +
- " ? AS " + Bookmarks.TITLE);
-
- suggestedSiteArgs = DBUtils.appendSelectionArgs(suggestedSiteArgs,
- new String[] {
- suggestedSitesCursor.getString(idColumnIndex),
- suggestedSitesCursor.getString(urlColumnIndex),
- suggestedSitesCursor.getString(titleColumnIndex)
- });
- }
- suggestedSitesCursor.close();
-
- boolean hasPreparedBlankTiles = false;
-
- // We can somewhat reduce the number of blanks we produce by eliminating suggested sites.
- // We do the actual limit calculation in SQL (since we need to take into account the number
- // of pinned sites too), but this might avoid producing 5 or so additional blank tiles
- // that would then need to be filtered out.
- final int maxBlanksNeeded = suggestedGridLimit - suggestedSitesCursor.getCount();
-
- final StringBuilder blanksBuilder = new StringBuilder();
- for (int i = 0; i < maxBlanksNeeded; i++) {
- if (hasPreparedBlankTiles) {
- blanksBuilder.append(" UNION ALL");
- } else {
- hasPreparedBlankTiles = true;
- }
-
- blanksBuilder.append(" SELECT" +
- " -1 AS " + Bookmarks._ID + "," +
- " '' AS " + Bookmarks.URL + "," +
- " '' AS " + Bookmarks.TITLE);
- }
-
-
-
- // To restrict suggested sites to the grid, we simply subtract the number of topsites (which have already had
- // the pinned sites filtered out), and the number of pinned sites.
- // SQLite completely ignores negative limits, hence we need to manually limit to 0 in this case.
- final String suggestedLimitClause = " LIMIT MAX(0, (" + suggestedGridLimit + " - (SELECT COUNT(*) FROM " + TABLE_TOPSITES + ") - (SELECT COUNT(*) " + pinnedSitesFromClause + "))) ";
-
- // Pinned site positions are zero indexed, but we need to get the maximum 1-indexed position.
- // Hence to correctly calculate the largest pinned position (which should be 0 if there are
- // no sites, or 1-6 if we have at least one pinned site), we coalesce the DB position (0-5)
- // with -1 to represent no-sites, which allows us to directly add 1 to obtain the expected value
- // regardless of whether a position was actually retrieved.
- final String blanksLimitClause = " LIMIT MAX(0, " +
- "COALESCE((SELECT " + Bookmarks.POSITION + " " + pinnedSitesFromClause + "), -1) + 1" +
- " - (SELECT COUNT(*) " + pinnedSitesFromClause + ")" +
- " - (SELECT COUNT(*) FROM " + TABLE_TOPSITES + ")" +
- ")";
-
- db.beginTransaction();
- try {
- db.execSQL("DROP TABLE IF EXISTS " + TABLE_TOPSITES);
-
- db.execSQL("CREATE TEMP TABLE " + TABLE_TOPSITES + " AS" +
- " SELECT " +
- Bookmarks._ID + ", " +
- Combined.BOOKMARK_ID + ", " +
- Combined.HISTORY_ID + ", " +
- Bookmarks.URL + ", " +
- Bookmarks.TITLE + ", " +
- Combined.HISTORY_ID + ", " +
- TopSites.TYPE_TOP + " AS " + TopSites.TYPE +
- " FROM " + Combined.VIEW_NAME +
- " WHERE " + ignoreForTopSitesWhereClause +
- " ORDER BY " + BrowserContract.getCombinedFrecencySortOrder(true, false) +
- " LIMIT " + totalLimit,
-
- ignoreForTopSitesArgs);
-
- if (hasProcessedAnySuggestedSites) {
- db.execSQL("INSERT INTO " + TABLE_TOPSITES +
- // We need to LIMIT _after_ selecting the relevant suggested sites, which requires us to
- // use an additional internal subquery, since we cannot LIMIT a subquery that is part of UNION ALL.
- // Hence the weird SELECT * FROM (SELECT ...relevant suggested sites... LIMIT ?)
- " SELECT * FROM (SELECT " +
- Bookmarks._ID + ", " +
- Bookmarks._ID + " AS " + Combined.BOOKMARK_ID + ", " +
- " -1 AS " + Combined.HISTORY_ID + ", " +
- Bookmarks.URL + ", " +
- Bookmarks.TITLE + ", " +
- "NULL AS " + Combined.HISTORY_ID + ", " +
- TopSites.TYPE_SUGGESTED + " as " + TopSites.TYPE +
- " FROM ( " + suggestedSitesBuilder.toString() + " )" +
- " WHERE " +
- Bookmarks.URL + " NOT IN (SELECT url FROM " + TABLE_TOPSITES + ")" +
- " AND " +
- Bookmarks.URL + " NOT IN (SELECT url " + pinnedSitesFromClause + ")" +
- suggestedLimitClause + " )",
-
- suggestedSiteArgs);
- }
-
- if (hasPreparedBlankTiles) {
- db.execSQL("INSERT INTO " + TABLE_TOPSITES +
- // We need to LIMIT _after_ selecting the relevant suggested sites, which requires us to
- // use an additional internal subquery, since we cannot LIMIT a subquery that is part of UNION ALL.
- // Hence the weird SELECT * FROM (SELECT ...relevant suggested sites... LIMIT ?)
- " SELECT * FROM (SELECT " +
- Bookmarks._ID + ", " +
- Bookmarks._ID + " AS " + Combined.BOOKMARK_ID + ", " +
- " -1 AS " + Combined.HISTORY_ID + ", " +
- Bookmarks.URL + ", " +
- Bookmarks.TITLE + ", " +
- "NULL AS " + Combined.HISTORY_ID + ", " +
- TopSites.TYPE_BLANK + " as " + TopSites.TYPE +
- " FROM ( " + blanksBuilder.toString() + " )" +
- blanksLimitClause + " )");
- }
-
- // If we retrieve more topsites than we have free positions for in the freeIdSubquery,
- // we will have topsites that don't receive a position when joining TABLE_TOPSITES
- // with freeIdSubquery. Hence we need to coalesce the position with a generated position.
- // We know that the difference in positions will be at most suggestedGridLimit, hence we
- // can add that to the rowid to generate a safe position.
- // I.e. if we have 6 pinned sites then positions 0..5 are filled, the JOIN results in
- // the first N rows having positions 6..(N+6), so row N+1 should receive a position that is at
- // least N+1+6, which is equal to rowid + 6.
- final SQLiteCursor c = (SQLiteCursor) db.rawQuery(
- "SELECT " +
- Bookmarks._ID + ", " +
- TopSites.BOOKMARK_ID + ", " +
- TopSites.HISTORY_ID + ", " +
- Bookmarks.URL + ", " +
- Bookmarks.TITLE + ", " +
- "COALESCE(" + Bookmarks.POSITION + ", " +
- DBUtils.qualifyColumn(TABLE_TOPSITES, "rowid") + " + " + suggestedGridLimit +
- ")" + " AS " + Bookmarks.POSITION + ", " +
- Combined.HISTORY_ID + ", " +
- TopSites.TYPE +
- " FROM " + TABLE_TOPSITES +
- " LEFT OUTER JOIN " + // TABLE_IDS +
- "(" + freeIDSubquery + ") AS id_results" +
- " ON " + DBUtils.qualifyColumn(TABLE_TOPSITES, "rowid") +
- " = " + DBUtils.qualifyColumn("id_results", "rowid") +
-
- " UNION ALL " +
-
- "SELECT " +
- Bookmarks._ID + ", " +
- Bookmarks._ID + " AS " + TopSites.BOOKMARK_ID + ", " +
- " -1 AS " + TopSites.HISTORY_ID + ", " +
- Bookmarks.URL + ", " +
- Bookmarks.TITLE + ", " +
- Bookmarks.POSITION + ", " +
- "NULL AS " + Combined.HISTORY_ID + ", " +
- TopSites.TYPE_PINNED + " as " + TopSites.TYPE +
- " " + pinnedSitesFromClause +
-
- " ORDER BY " + Bookmarks.POSITION,
-
- null);
-
- c.setNotificationUri(getContext().getContentResolver(),
- BrowserContract.AUTHORITY_URI);
-
- // Force the cursor to be compiled and the cursor-window filled now:
- // (A) without compiling the cursor now we won't have access to the TEMP table which
- // is removed as soon as we close our connection.
- // (B) this might also mitigate the situation causing this crash where we're accessing
- // a cursor and crashing in fillWindow.
- c.moveToFirst();
-
- db.setTransactionSuccessful();
- return c;
- } finally {
- db.endTransaction();
- }
- }
-
- /**
- * Obtain a set of links for highlights (from bookmarks and history).
- *
- * Based on the query for Activity^ Stream (desktop):
- * https://github.com/mozilla/activity-stream/blob/9eb9f451b553bb62ae9b8d6b41a8ef94a2e020ea/addon/PlacesProvider.js#L578
- */
- public Cursor getHighlights(final SQLiteDatabase db, String limit) {
- final int totalLimit = limit == null ? 20 : Integer.parseInt(limit);
-
- final long threeDaysAgo = System.currentTimeMillis() - (1000 * 60 * 60 * 24 * 3);
- final long bookmarkLimit = 1;
-
- // Select recent bookmarks that have not been visited much
- final String bookmarksQuery = "SELECT * FROM (SELECT " +
- "-1 AS " + Combined.HISTORY_ID + ", " +
- DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
- DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.URL) + ", " +
- DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.TITLE) + ", " +
- DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.DATE_CREATED) + " AS " + Highlights.DATE + " " +
- "FROM " + Bookmarks.TABLE_NAME + " " +
- "LEFT JOIN " + History.TABLE_NAME + " ON " +
- DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.URL) + " = " +
- DBUtils.qualifyColumn(History.TABLE_NAME, History.URL) + " " +
- "WHERE " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.DATE_CREATED) + " > " + threeDaysAgo + " " +
- "AND (" + DBUtils.qualifyColumn(History.TABLE_NAME, History.VISITS) + " <= 3 " +
- "OR " + DBUtils.qualifyColumn(History.TABLE_NAME, History.VISITS) + " IS NULL) " +
- "AND " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.IS_DELETED) + " = 0 " +
- "AND " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " " +
- "AND " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.URL) + " NOT IN (SELECT " + ActivityStreamBlocklist.URL + " FROM " + ActivityStreamBlocklist.TABLE_NAME + " )" +
- "ORDER BY " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.DATE_CREATED) + " DESC " +
- "LIMIT " + bookmarkLimit + ")";
-
- final long last30Minutes = System.currentTimeMillis() - (1000 * 60 * 30);
- final long historyLimit = totalLimit - bookmarkLimit;
-
- // Select recent history that has not been visited much.
- final String historyQuery = "SELECT * FROM (SELECT " +
- History._ID + " AS " + Combined.HISTORY_ID + ", " +
- "-1 AS " + Combined.BOOKMARK_ID + ", " +
- History.URL + ", " +
- History.TITLE + ", " +
- History.DATE_LAST_VISITED + " AS " + Highlights.DATE + " " +
- "FROM " + History.TABLE_NAME + " " +
- "WHERE " + History.DATE_LAST_VISITED + " < " + last30Minutes + " " +
- "AND " + History.VISITS + " <= 3 " +
- "AND " + History.TITLE + " NOT NULL AND " + History.TITLE + " != '' " +
- "AND " + History.IS_DELETED + " = 0 " +
- "AND " + History.URL + " NOT IN (SELECT " + ActivityStreamBlocklist.URL + " FROM " + ActivityStreamBlocklist.TABLE_NAME + " )" +
- // TODO: Implement domain black list (bug 1298786)
- // TODO: Group by host (bug 1298785)
- "ORDER BY " + History.DATE_LAST_VISITED + " DESC " +
- "LIMIT " + historyLimit + ")";
-
- final String query = "SELECT DISTINCT * " +
- "FROM (" + bookmarksQuery + " " +
- "UNION ALL " + historyQuery + ") " +
- "GROUP BY " + Combined.URL + ";";
-
- final Cursor cursor = db.rawQuery(query, null);
-
- cursor.setNotificationUri(getContext().getContentResolver(),
- BrowserContract.AUTHORITY_URI);
-
- return cursor;
- }
-
- @Override
- public Cursor query(Uri uri, String[] projection, String selection,
- String[] selectionArgs, String sortOrder) {
- final int match = URI_MATCHER.match(uri);
-
- // Handle only queries requiring a writable DB connection here: most queries need only a readable
- // connection, hence we can get a readable DB once, and then handle most queries within a switch.
- // TopSites requires a writable connection (because of the temporary tables it uses), hence
- // we handle that separately, i.e. before retrieving a readable connection.
- if (match == TOPSITES) {
- if (uri.getBooleanQueryParameter(BrowserContract.PARAM_TOPSITES_DISABLE_PINNED, false)) {
- return getPlainTopSites(uri);
- } else {
- return getTopSites(uri);
- }
- }
-
- SQLiteDatabase db = getReadableDatabase(uri);
-
- SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
- String groupBy = null;
-
- switch (match) {
- case BOOKMARKS_FOLDER_ID:
- case BOOKMARKS_ID:
- case BOOKMARKS: {
- debug("Query is on bookmarks: " + uri);
-
- if (match == BOOKMARKS_ID) {
- selection = DBUtils.concatenateWhere(selection, Bookmarks._ID + " = ?");
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { Long.toString(ContentUris.parseId(uri)) });
- } else if (match == BOOKMARKS_FOLDER_ID) {
- selection = DBUtils.concatenateWhere(selection, Bookmarks.PARENT + " = ?");
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { Long.toString(ContentUris.parseId(uri)) });
- }
-
- if (!shouldShowDeleted(uri))
- selection = DBUtils.concatenateWhere(Bookmarks.IS_DELETED + " = 0", selection);
-
- if (TextUtils.isEmpty(sortOrder)) {
- sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER;
- } else {
- debug("Using sort order " + sortOrder + ".");
- }
-
- qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
-
- if (hasFaviconsInProjection(projection)) {
- qb.setTables(VIEW_BOOKMARKS_WITH_FAVICONS);
- } else if (selection != null && selection.contains(Bookmarks.ANNOTATION_KEY)) {
- qb.setTables(VIEW_BOOKMARKS_WITH_ANNOTATIONS);
-
- groupBy = uri.getQueryParameter(BrowserContract.PARAM_GROUP_BY);
- } else {
- qb.setTables(TABLE_BOOKMARKS);
- }
-
- break;
- }
-
- case HISTORY_ID:
- selection = DBUtils.concatenateWhere(selection, History._ID + " = ?");
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { Long.toString(ContentUris.parseId(uri)) });
- // fall through
- case HISTORY: {
- debug("Query is on history: " + uri);
-
- if (!shouldShowDeleted(uri))
- selection = DBUtils.concatenateWhere(History.IS_DELETED + " = 0", selection);
-
- if (TextUtils.isEmpty(sortOrder))
- sortOrder = DEFAULT_HISTORY_SORT_ORDER;
-
- qb.setProjectionMap(HISTORY_PROJECTION_MAP);
-
- if (hasFaviconsInProjection(projection))
- qb.setTables(VIEW_HISTORY_WITH_FAVICONS);
- else
- qb.setTables(TABLE_HISTORY);
-
- break;
- }
-
- case VISITS:
- debug("Query is on visits: " + uri);
- qb.setProjectionMap(VISIT_PROJECTION_MAP);
- qb.setTables(TABLE_VISITS);
-
- if (TextUtils.isEmpty(sortOrder)) {
- sortOrder = DEFAULT_VISITS_SORT_ORDER;
- }
- break;
-
- case FAVICON_ID:
- selection = DBUtils.concatenateWhere(selection, Favicons._ID + " = ?");
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { Long.toString(ContentUris.parseId(uri)) });
- // fall through
- case FAVICONS: {
- debug("Query is on favicons: " + uri);
-
- qb.setProjectionMap(FAVICONS_PROJECTION_MAP);
- qb.setTables(TABLE_FAVICONS);
-
- break;
- }
-
- case THUMBNAIL_ID:
- selection = DBUtils.concatenateWhere(selection, Thumbnails._ID + " = ?");
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { Long.toString(ContentUris.parseId(uri)) });
- // fall through
- case THUMBNAILS: {
- debug("Query is on thumbnails: " + uri);
-
- qb.setProjectionMap(THUMBNAILS_PROJECTION_MAP);
- qb.setTables(TABLE_THUMBNAILS);
-
- break;
- }
-
- case URL_ANNOTATIONS:
- debug("Query is on url annotations: " + uri);
-
- qb.setProjectionMap(URL_ANNOTATIONS_PROJECTION_MAP);
- qb.setTables(TABLE_URL_ANNOTATIONS);
- break;
-
- case SCHEMA: {
- debug("Query is on schema.");
- MatrixCursor schemaCursor = new MatrixCursor(new String[] { Schema.VERSION });
- schemaCursor.newRow().add(BrowserDatabaseHelper.DATABASE_VERSION);
-
- return schemaCursor;
- }
-
- case COMBINED: {
- debug("Query is on combined: " + uri);
-
- if (TextUtils.isEmpty(sortOrder))
- sortOrder = DEFAULT_HISTORY_SORT_ORDER;
-
- // This will avoid duplicate entries in the awesomebar
- // results when a history entry has multiple bookmarks.
- groupBy = Combined.URL;
-
- qb.setProjectionMap(COMBINED_PROJECTION_MAP);
-
- if (hasFaviconsInProjection(projection))
- qb.setTables(VIEW_COMBINED_WITH_FAVICONS);
- else
- qb.setTables(Combined.VIEW_NAME);
-
- break;
- }
-
- case HIGHLIGHTS: {
- debug("Highlights query: " + uri);
-
- return getHighlights(db, limit);
- }
-
- case PAGE_METADATA: {
- debug("PageMetadata query: " + uri);
-
- qb.setProjectionMap(PAGE_METADATA_PROJECTION_MAP);
- qb.setTables(TABLE_PAGE_METADATA);
- break;
- }
-
- default: {
- Table table = findTableFor(match);
- if (table == null) {
- throw new UnsupportedOperationException("Unknown query URI " + uri);
- }
- trace("Update TABLE: " + uri);
- return table.query(db, uri, match, projection, selection, selectionArgs, sortOrder, groupBy, limit);
- }
- }
-
- trace("Running built query.");
- Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy,
- null, sortOrder, limit);
- cursor.setNotificationUri(getContext().getContentResolver(),
- BrowserContract.AUTHORITY_URI);
-
- return cursor;
- }
-
- /**
- * Update the positions of bookmarks in batches.
- *
- * Begins and ends its own transactions.
- *
- * @see #updateBookmarkPositionsInTransaction(SQLiteDatabase, String[], int, int)
- */
- private int updateBookmarkPositions(Uri uri, String[] guids) {
- if (guids == null) {
- return 0;
- }
-
- int guidsCount = guids.length;
- if (guidsCount == 0) {
- return 0;
- }
-
- int offset = 0;
- int updated = 0;
-
- final SQLiteDatabase db = getWritableDatabase(uri);
- db.beginTransaction();
-
- while (offset < guidsCount) {
- try {
- updated += updateBookmarkPositionsInTransaction(db, guids, offset,
- MAX_POSITION_UPDATES_PER_QUERY);
- } catch (SQLException e) {
- Log.e(LOGTAG, "Got SQLite exception updating bookmark positions at offset " + offset, e);
-
- // Need to restart the transaction.
- // The only way a caller knows that anything failed is that the
- // returned update count will be smaller than the requested
- // number of records.
- db.setTransactionSuccessful();
- db.endTransaction();
-
- db.beginTransaction();
- }
-
- offset += MAX_POSITION_UPDATES_PER_QUERY;
- }
-
- db.setTransactionSuccessful();
- db.endTransaction();
-
- return updated;
- }
-
- /**
- * Construct and execute an update expression that will modify the positions
- * of records in-place.
- */
- private static int updateBookmarkPositionsInTransaction(final SQLiteDatabase db, final String[] guids,
- final int offset, final int max) {
- int guidsCount = guids.length;
- int processCount = Math.min(max, guidsCount - offset);
-
- // Each must appear twice: once in a CASE, and once in the IN clause.
- String[] args = new String[processCount * 2];
- System.arraycopy(guids, offset, args, 0, processCount);
- System.arraycopy(guids, offset, args, processCount, processCount);
-
- StringBuilder b = new StringBuilder("UPDATE " + TABLE_BOOKMARKS +
- " SET " + Bookmarks.POSITION +
- " = CASE guid");
-
- // Build the CASE statement body for GUID/index pairs from offset up to
- // the computed limit.
- final int end = offset + processCount;
- int i = offset;
- for (; i < end; ++i) {
- if (guids[i] == null) {
- // We don't want to issue the query if not every GUID is specified.
- debug("updateBookmarkPositions called with null GUID at index " + i);
- return 0;
- }
- b.append(" WHEN ? THEN " + i);
- }
-
- b.append(" END WHERE " + DBUtils.computeSQLInClause(processCount, Bookmarks.GUID));
- db.execSQL(b.toString(), args);
-
- // We can't easily get a modified count without calling something like changes().
- return processCount;
- }
-
- /**
- * Construct an update expression that will modify the parents of any records
- * that match.
- */
- private int updateBookmarkParents(SQLiteDatabase db, ContentValues values, String selection, String[] selectionArgs) {
- trace("Updating bookmark parents of " + selection + " (" + selectionArgs[0] + ")");
- String where = Bookmarks._ID + " IN (" +
- " SELECT DISTINCT " + Bookmarks.PARENT +
- " FROM " + TABLE_BOOKMARKS +
- " WHERE " + selection + " )";
- return db.update(TABLE_BOOKMARKS, values, where, selectionArgs);
- }
-
- private long insertBookmark(Uri uri, ContentValues values) {
- // Generate values if not specified. Don't overwrite
- // if specified by caller.
- long now = System.currentTimeMillis();
- if (!values.containsKey(Bookmarks.DATE_CREATED)) {
- values.put(Bookmarks.DATE_CREATED, now);
- }
-
- if (!values.containsKey(Bookmarks.DATE_MODIFIED)) {
- values.put(Bookmarks.DATE_MODIFIED, now);
- }
-
- if (!values.containsKey(Bookmarks.GUID)) {
- values.put(Bookmarks.GUID, Utils.generateGuid());
- }
-
- if (!values.containsKey(Bookmarks.POSITION)) {
- debug("Inserting bookmark with no position for URI");
- values.put(Bookmarks.POSITION,
- Long.toString(BrowserContract.Bookmarks.DEFAULT_POSITION));
- }
-
- if (!values.containsKey(Bookmarks.TITLE)) {
- // Desktop Places barfs on insertion of a bookmark with no title,
- // so we don't store them that way.
- values.put(Bookmarks.TITLE, "");
- }
-
- String url = values.getAsString(Bookmarks.URL);
-
- debug("Inserting bookmark in database with URL: " + url);
- final SQLiteDatabase db = getWritableDatabase(uri);
- beginWrite(db);
- return db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.TITLE, values);
- }
-
-
- private int updateOrInsertBookmark(Uri uri, ContentValues values, String selection,
- String[] selectionArgs) {
- int updated = updateBookmarks(uri, values, selection, selectionArgs);
- if (updated > 0) {
- return updated;
- }
-
- // Transaction already begun by updateBookmarks.
- if (0 <= insertBookmark(uri, values)) {
- // We 'updated' one row.
- return 1;
- }
-
- // If something went wrong, then we updated zero rows.
- return 0;
- }
-
- private int updateBookmarks(Uri uri, ContentValues values, String selection,
- String[] selectionArgs) {
- trace("Updating bookmarks on URI: " + uri);
-
- final String[] bookmarksProjection = new String[] {
- Bookmarks._ID, // 0
- };
-
- if (!values.containsKey(Bookmarks.DATE_MODIFIED)) {
- values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
- }
-
- trace("Querying bookmarks to update on URI: " + uri);
- final SQLiteDatabase db = getWritableDatabase(uri);
-
- // Compute matching IDs.
- final Cursor cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection,
- selection, selectionArgs, null, null, null);
-
- // Now that we're done reading, open a transaction.
- final String inClause;
- try {
- inClause = DBUtils.computeSQLInClauseFromLongs(cursor, Bookmarks._ID);
- } finally {
- cursor.close();
- }
-
- beginWrite(db);
- return db.update(TABLE_BOOKMARKS, values, inClause, null);
- }
-
- private long insertHistory(Uri uri, ContentValues values) {
- final long now = System.currentTimeMillis();
- values.put(History.DATE_CREATED, now);
- values.put(History.DATE_MODIFIED, now);
-
- // Generate GUID for new history entry. Don't override specified GUIDs.
- if (!values.containsKey(History.GUID)) {
- values.put(History.GUID, Utils.generateGuid());
- }
-
- String url = values.getAsString(History.URL);
-
- debug("Inserting history in database with URL: " + url);
- final SQLiteDatabase db = getWritableDatabase(uri);
- beginWrite(db);
- return db.insertOrThrow(TABLE_HISTORY, History.VISITS, values);
- }
-
- private int updateOrInsertHistory(Uri uri, ContentValues values, String selection,
- String[] selectionArgs) {
- final int updated = updateHistory(uri, values, selection, selectionArgs);
- if (updated > 0) {
- return updated;
- }
-
- // Insert a new entry if necessary, setting visit and date aggregate values.
- if (!values.containsKey(History.VISITS)) {
- values.put(History.VISITS, 1);
- values.put(History.LOCAL_VISITS, 1);
- } else {
- values.put(History.LOCAL_VISITS, values.getAsInteger(History.VISITS));
- }
- if (values.containsKey(History.DATE_LAST_VISITED)) {
- values.put(History.LOCAL_DATE_LAST_VISITED, values.getAsLong(History.DATE_LAST_VISITED));
- }
- if (!values.containsKey(History.TITLE)) {
- values.put(History.TITLE, values.getAsString(History.URL));
- }
-
- if (0 <= insertHistory(uri, values)) {
- return 1;
- }
-
- return 0;
- }
-
- private int updateHistory(Uri uri, ContentValues values, String selection,
- String[] selectionArgs) {
- trace("Updating history on URI: " + uri);
-
- final SQLiteDatabase db = getWritableDatabase(uri);
-
- if (!values.containsKey(History.DATE_MODIFIED)) {
- values.put(History.DATE_MODIFIED, System.currentTimeMillis());
- }
-
- // Use the simple code path for easy updates.
- if (!shouldIncrementVisits(uri) && !shouldIncrementRemoteAggregates(uri)) {
- trace("Updating history meta data only");
- return db.update(TABLE_HISTORY, values, selection, selectionArgs);
- }
-
- trace("Updating history meta data and incrementing visits");
-
- if (values.containsKey(History.DATE_LAST_VISITED)) {
- values.put(History.LOCAL_DATE_LAST_VISITED, values.getAsLong(History.DATE_LAST_VISITED));
- }
-
- // Create a separate set of values that will be updated as an expression.
- final ContentValues visits = new ContentValues();
- if (shouldIncrementVisits(uri)) {
- // Update data and increment visits by 1.
- final long incVisits = 1;
-
- visits.put(History.VISITS, History.VISITS + " + " + incVisits);
- visits.put(History.LOCAL_VISITS, History.LOCAL_VISITS + " + " + incVisits);
- }
-
- if (shouldIncrementRemoteAggregates(uri)) {
- // Let's fail loudly instead of trying to assume what users of this API meant to do.
- if (!values.containsKey(History.REMOTE_VISITS)) {
- throw new IllegalArgumentException(
- "Tried incrementing History.REMOTE_VISITS by unknown value");
- }
- visits.put(
- History.REMOTE_VISITS,
- History.REMOTE_VISITS + " + " + values.getAsInteger(History.REMOTE_VISITS)
- );
- // Need to remove passed in value, so that we increment REMOTE_VISITS, and not just set it.
- values.remove(History.REMOTE_VISITS);
- }
-
- final ContentValues[] valuesAndVisits = { values, visits };
- final UpdateOperation[] ops = { UpdateOperation.ASSIGN, UpdateOperation.EXPRESSION };
-
- return DBUtils.updateArrays(db, TABLE_HISTORY, valuesAndVisits, ops, selection, selectionArgs);
- }
-
- private long insertVisitForHistory(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
- trace("Inserting visit for history on URI: " + uri);
-
- final SQLiteDatabase db = getReadableDatabase(uri);
-
- final Cursor cursor = db.query(
- History.TABLE_NAME, new String[] {History.GUID}, selection, selectionArgs,
- null, null, null);
- if (cursor == null) {
- Log.e(LOGTAG, "Null cursor while trying to insert visit for history URI: " + uri);
- return 0;
- }
- final ContentValues[] visitValues;
- try {
- visitValues = new ContentValues[cursor.getCount()];
-
- if (!cursor.moveToFirst()) {
- Log.e(LOGTAG, "No history records found while inserting visit(s) for history URI: " + uri);
- return 0;
- }
-
- // Sync works in microseconds, so we store visit timestamps in microseconds as well.
- // History timestamps are in milliseconds.
- // This is the conversion point for locally generated visits.
- final long visitDate;
- if (values.containsKey(History.DATE_LAST_VISITED)) {
- visitDate = values.getAsLong(History.DATE_LAST_VISITED) * 1000;
- } else {
- visitDate = System.currentTimeMillis() * 1000;
- }
-
- final int guidColumn = cursor.getColumnIndexOrThrow(History.GUID);
- while (!cursor.isAfterLast()) {
- final ContentValues visit = new ContentValues();
- visit.put(Visits.HISTORY_GUID, cursor.getString(guidColumn));
- visit.put(Visits.DATE_VISITED, visitDate);
- visitValues[cursor.getPosition()] = visit;
- cursor.moveToNext();
- }
- } finally {
- cursor.close();
- }
-
- if (visitValues.length == 1) {
- return insertVisit(Visits.CONTENT_URI, visitValues[0]);
- } else {
- return bulkInsert(Visits.CONTENT_URI, visitValues);
- }
- }
-
- private long insertVisit(Uri uri, ContentValues values) {
- final SQLiteDatabase db = getWritableDatabase(uri);
-
- debug("Inserting history in database with URL: " + uri);
- beginWrite(db);
-
- // We ignore insert conflicts here to simplify inserting visits records coming in from Sync.
- // Visits table has a unique index on (history_guid,date), so a conflict might arise when we're
- // trying to insert history record visits coming in from sync which are already present locally
- // as a result of previous sync operations.
- // An alternative to doing this is to filter out already present records when we're doing history inserts
- // from Sync, which is a costly operation to do en masse.
- return db.insertWithOnConflict(
- TABLE_VISITS, null, values, SQLiteDatabase.CONFLICT_IGNORE);
- }
-
- private void updateFaviconIdsForUrl(SQLiteDatabase db, String pageUrl, Long faviconId) {
- ContentValues updateValues = new ContentValues(1);
- updateValues.put(FaviconColumns.FAVICON_ID, faviconId);
- db.update(TABLE_HISTORY,
- updateValues,
- History.URL + " = ?",
- new String[] { pageUrl });
- db.update(TABLE_BOOKMARKS,
- updateValues,
- Bookmarks.URL + " = ?",
- new String[] { pageUrl });
- }
-
- private long insertFavicon(Uri uri, ContentValues values) {
- return insertFavicon(getWritableDatabase(uri), values);
- }
-
- private long insertFavicon(SQLiteDatabase db, ContentValues values) {
- String faviconUrl = values.getAsString(Favicons.URL);
- String pageUrl = null;
-
- trace("Inserting favicon for URL: " + faviconUrl);
-
- DBUtils.stripEmptyByteArray(values, Favicons.DATA);
-
- // Extract the page URL from the ContentValues
- if (values.containsKey(Favicons.PAGE_URL)) {
- pageUrl = values.getAsString(Favicons.PAGE_URL);
- values.remove(Favicons.PAGE_URL);
- }
-
- // If no URL is provided, insert using the default one.
- if (TextUtils.isEmpty(faviconUrl) && !TextUtils.isEmpty(pageUrl)) {
- values.put(Favicons.URL, IconsHelper.guessDefaultFaviconURL(pageUrl));
- }
-
- final long now = System.currentTimeMillis();
- values.put(Favicons.DATE_CREATED, now);
- values.put(Favicons.DATE_MODIFIED, now);
-
- beginWrite(db);
- final long faviconId = db.insertOrThrow(TABLE_FAVICONS, null, values);
-
- if (pageUrl != null) {
- updateFaviconIdsForUrl(db, pageUrl, faviconId);
- }
- return faviconId;
- }
-
- private int updateOrInsertFavicon(Uri uri, ContentValues values, String selection,
- String[] selectionArgs) {
- return updateFavicon(uri, values, selection, selectionArgs,
- true /* insert if needed */);
- }
-
- private int updateExistingFavicon(Uri uri, ContentValues values, String selection,
- String[] selectionArgs) {
- return updateFavicon(uri, values, selection, selectionArgs,
- false /* only update, no insert */);
- }
-
- private int updateFavicon(Uri uri, ContentValues values, String selection,
- String[] selectionArgs, boolean insertIfNeeded) {
- String faviconUrl = values.getAsString(Favicons.URL);
- String pageUrl = null;
- int updated = 0;
- Long faviconId = null;
- long now = System.currentTimeMillis();
-
- trace("Updating favicon for URL: " + faviconUrl);
-
- DBUtils.stripEmptyByteArray(values, Favicons.DATA);
-
- // Extract the page URL from the ContentValues
- if (values.containsKey(Favicons.PAGE_URL)) {
- pageUrl = values.getAsString(Favicons.PAGE_URL);
- values.remove(Favicons.PAGE_URL);
- }
-
- values.put(Favicons.DATE_MODIFIED, now);
-
- final SQLiteDatabase db = getWritableDatabase(uri);
-
- // If there's no favicon URL given and we're inserting if needed, skip
- // the update and only do an insert (otherwise all rows would be
- // updated).
- if (!(insertIfNeeded && (faviconUrl == null))) {
- updated = db.update(TABLE_FAVICONS, values, selection, selectionArgs);
- }
-
- if (updated > 0) {
- if ((faviconUrl != null) && (pageUrl != null)) {
- final Cursor cursor = db.query(TABLE_FAVICONS,
- new String[] { Favicons._ID },
- Favicons.URL + " = ?",
- new String[] { faviconUrl },
- null, null, null);
- try {
- if (cursor.moveToFirst()) {
- faviconId = cursor.getLong(cursor.getColumnIndexOrThrow(Favicons._ID));
- }
- } finally {
- cursor.close();
- }
- }
- if (pageUrl != null) {
- beginWrite(db);
- }
- } else if (insertIfNeeded) {
- values.put(Favicons.DATE_CREATED, now);
-
- trace("No update, inserting favicon for URL: " + faviconUrl);
- beginWrite(db);
- faviconId = db.insert(TABLE_FAVICONS, null, values);
- updated = 1;
- }
-
- if (pageUrl != null) {
- updateFaviconIdsForUrl(db, pageUrl, faviconId);
- }
-
- return updated;
- }
-
- private long insertThumbnail(Uri uri, ContentValues values) {
- final String url = values.getAsString(Thumbnails.URL);
-
- trace("Inserting thumbnail for URL: " + url);
-
- DBUtils.stripEmptyByteArray(values, Thumbnails.DATA);
-
- final SQLiteDatabase db = getWritableDatabase(uri);
- beginWrite(db);
- return db.insertOrThrow(TABLE_THUMBNAILS, null, values);
- }
-
- private long insertActivityStreamBlocklistSite(final Uri uri, final ContentValues values) {
- final String url = values.getAsString(ActivityStreamBlocklist.URL);
- trace("Inserting url into highlights blocklist, URL: " + url);
-
- final SQLiteDatabase db = getWritableDatabase(uri);
- values.put(ActivityStreamBlocklist.CREATED, System.currentTimeMillis());
-
- beginWrite(db);
- return db.insertOrThrow(TABLE_ACTIVITY_STREAM_BLOCKLIST, null, values);
- }
-
- private long insertPageMetadata(final Uri uri, final ContentValues values) {
- final SQLiteDatabase db = getWritableDatabase(uri);
-
- if (!values.containsKey(PageMetadata.DATE_CREATED)) {
- values.put(PageMetadata.DATE_CREATED, System.currentTimeMillis());
- }
-
- beginWrite(db);
-
- // Perform INSERT OR REPLACE, there might be page metadata present and we want to replace it.
- // Depends on a conflict arising from unique foreign key (history_guid) constraint violation.
- return db.insertWithOnConflict(
- TABLE_PAGE_METADATA, null, values, SQLiteDatabase.CONFLICT_REPLACE);
- }
-
- private long insertUrlAnnotation(final Uri uri, final ContentValues values) {
- final String url = values.getAsString(UrlAnnotations.URL);
- trace("Inserting url annotations for URL: " + url);
-
- final SQLiteDatabase db = getWritableDatabase(uri);
- beginWrite(db);
- return db.insertOrThrow(TABLE_URL_ANNOTATIONS, null, values);
- }
-
- private void deleteUrlAnnotation(final Uri uri, final String selection, final String[] selectionArgs) {
- trace("Deleting url annotation for URI: " + uri);
-
- final SQLiteDatabase db = getWritableDatabase(uri);
- db.delete(TABLE_URL_ANNOTATIONS, selection, selectionArgs);
- }
-
- private int deletePageMetadata(final Uri uri, final String selection, final String[] selectionArgs) {
- trace("Deleting page metadata for URI: " + uri);
-
- final SQLiteDatabase db = getWritableDatabase(uri);
- return db.delete(TABLE_PAGE_METADATA, selection, selectionArgs);
- }
-
- private void updateUrlAnnotation(final Uri uri, final ContentValues values, final String selection, final String[] selectionArgs) {
- trace("Updating url annotation for URI: " + uri);
-
- final SQLiteDatabase db = getWritableDatabase(uri);
- db.update(TABLE_URL_ANNOTATIONS, values, selection, selectionArgs);
- }
-
- private int updateOrInsertThumbnail(Uri uri, ContentValues values, String selection,
- String[] selectionArgs) {
- return updateThumbnail(uri, values, selection, selectionArgs,
- true /* insert if needed */);
- }
-
- private int updateExistingThumbnail(Uri uri, ContentValues values, String selection,
- String[] selectionArgs) {
- return updateThumbnail(uri, values, selection, selectionArgs,
- false /* only update, no insert */);
- }
-
- private int updateThumbnail(Uri uri, ContentValues values, String selection,
- String[] selectionArgs, boolean insertIfNeeded) {
- final String url = values.getAsString(Thumbnails.URL);
- DBUtils.stripEmptyByteArray(values, Thumbnails.DATA);
-
- trace("Updating thumbnail for URL: " + url);
-
- final SQLiteDatabase db = getWritableDatabase(uri);
- beginWrite(db);
- int updated = db.update(TABLE_THUMBNAILS, values, selection, selectionArgs);
-
- if (updated == 0 && insertIfNeeded) {
- trace("No update, inserting thumbnail for URL: " + url);
- db.insert(TABLE_THUMBNAILS, null, values);
- updated = 1;
- }
-
- return updated;
- }
-
- /**
- * This method does not create a new transaction. Its first operation is
- * guaranteed to be a write, which in the case of a new enclosing
- * transaction will guarantee that a read does not need to be upgraded to
- * a write.
- */
- private int deleteHistory(SQLiteDatabase db, Uri uri, String selection, String[] selectionArgs) {
- debug("Deleting history entry for URI: " + uri);
-
- if (isCallerSync(uri)) {
- return db.delete(TABLE_HISTORY, selection, selectionArgs);
- }
-
- debug("Marking history entry as deleted for URI: " + uri);
-
- ContentValues values = new ContentValues();
- values.put(History.IS_DELETED, 1);
-
- // Wipe sensitive data.
- values.putNull(History.TITLE);
- values.put(History.URL, ""); // Column is NOT NULL.
- values.put(History.DATE_CREATED, 0);
- values.put(History.DATE_LAST_VISITED, 0);
- values.put(History.VISITS, 0);
- values.put(History.DATE_MODIFIED, System.currentTimeMillis());
-
- // Doing this UPDATE (or the DELETE above) first ensures that the
- // first operation within a new enclosing transaction is a write.
- // The cleanup call below will do a SELECT first, and thus would
- // require the transaction to be upgraded from a reader to a writer.
- // In some cases that upgrade can fail (SQLITE_BUSY), so we avoid
- // it if we can.
- final int updated = db.update(TABLE_HISTORY, values, selection, selectionArgs);
- try {
- cleanUpSomeDeletedRecords(uri, TABLE_HISTORY);
- } catch (Exception e) {
- // We don't care.
- Log.e(LOGTAG, "Unable to clean up deleted history records: ", e);
- }
- return updated;
- }
-
- private ArrayList<String> getHistoryGUIDsFromSelection(SQLiteDatabase db, Uri uri, String selection, String[] selectionArgs) {
- final ArrayList<String> historyGUIDs = new ArrayList<>();
-
- final Cursor cursor = db.query(
- History.TABLE_NAME, new String[] {History.GUID}, selection, selectionArgs,
- null, null, null);
- if (cursor == null) {
- Log.e(LOGTAG, "Null cursor while trying to delete visits for history URI: " + uri);
- return historyGUIDs;
- }
-
- try {
- if (!cursor.moveToFirst()) {
- trace("No history items for which to remove visits matched for URI: " + uri);
- return historyGUIDs;
- }
- final int historyColumn = cursor.getColumnIndexOrThrow(History.GUID);
- while (!cursor.isAfterLast()) {
- historyGUIDs.add(cursor.getString(historyColumn));
- cursor.moveToNext();
- }
- } finally {
- cursor.close();
- }
-
- return historyGUIDs;
- }
-
- private int deletePageMetadataForHistory(SQLiteDatabase db, ArrayList<String> historyGUIDs) {
- return bulkDeleteByHistoryGUID(db, historyGUIDs, PageMetadata.TABLE_NAME, PageMetadata.HISTORY_GUID);
- }
-
- private int deleteVisitsForHistory(SQLiteDatabase db, ArrayList<String> historyGUIDs) {
- return bulkDeleteByHistoryGUID(db, historyGUIDs, Visits.TABLE_NAME, Visits.HISTORY_GUID);
- }
-
- private int bulkDeleteByHistoryGUID(SQLiteDatabase db, ArrayList<String> historyGUIDs, String table, String historyGUIDColumn) {
- // Due to SQLite's maximum variable limitation, we need to chunk our delete statements.
- // For example, if there were 1200 GUIDs, this will perform 2 delete statements.
- int deleted = 0;
- for (int chunk = 0; chunk <= historyGUIDs.size() / DBUtils.SQLITE_MAX_VARIABLE_NUMBER; chunk++) {
- final int chunkStart = chunk * DBUtils.SQLITE_MAX_VARIABLE_NUMBER;
- int chunkEnd = (chunk + 1) * DBUtils.SQLITE_MAX_VARIABLE_NUMBER;
- if (chunkEnd > historyGUIDs.size()) {
- chunkEnd = historyGUIDs.size();
- }
- final List<String> chunkGUIDs = historyGUIDs.subList(chunkStart, chunkEnd);
- deleted += db.delete(
- table,
- DBUtils.computeSQLInClause(chunkGUIDs.size(), historyGUIDColumn),
- chunkGUIDs.toArray(new String[chunkGUIDs.size()])
- );
- }
-
- return deleted;
- }
-
- private int deleteVisits(Uri uri, String selection, String[] selectionArgs) {
- debug("Deleting visits for URI: " + uri);
-
- final SQLiteDatabase db = getWritableDatabase(uri);
-
- beginWrite(db);
- return db.delete(TABLE_VISITS, selection, selectionArgs);
- }
-
- private int deleteBookmarks(Uri uri, String selection, String[] selectionArgs) {
- debug("Deleting bookmarks for URI: " + uri);
-
- final SQLiteDatabase db = getWritableDatabase(uri);
-
- if (isCallerSync(uri)) {
- beginWrite(db);
- return db.delete(TABLE_BOOKMARKS, selection, selectionArgs);
- }
-
- debug("Marking bookmarks as deleted for URI: " + uri);
-
- ContentValues values = new ContentValues();
- values.put(Bookmarks.IS_DELETED, 1);
- values.put(Bookmarks.POSITION, 0);
- values.putNull(Bookmarks.PARENT);
- values.putNull(Bookmarks.URL);
- values.putNull(Bookmarks.TITLE);
- values.putNull(Bookmarks.DESCRIPTION);
- values.putNull(Bookmarks.KEYWORD);
- values.putNull(Bookmarks.TAGS);
- values.putNull(Bookmarks.FAVICON_ID);
-
- // Doing this UPDATE (or the DELETE above) first ensures that the
- // first operation within this transaction is a write.
- // The cleanup call below will do a SELECT first, and thus would
- // require the transaction to be upgraded from a reader to a writer.
- final int updated = updateBookmarks(uri, values, selection, selectionArgs);
- try {
- cleanUpSomeDeletedRecords(uri, TABLE_BOOKMARKS);
- } catch (Exception e) {
- // We don't care.
- Log.e(LOGTAG, "Unable to clean up deleted bookmark records: ", e);
- }
- return updated;
- }
-
- private int deleteFavicons(Uri uri, String selection, String[] selectionArgs) {
- debug("Deleting favicons for URI: " + uri);
-
- final SQLiteDatabase db = getWritableDatabase(uri);
-
- return db.delete(TABLE_FAVICONS, selection, selectionArgs);
- }
-
- private int deleteThumbnails(Uri uri, String selection, String[] selectionArgs) {
- debug("Deleting thumbnails for URI: " + uri);
-
- final SQLiteDatabase db = getWritableDatabase(uri);
-
- return db.delete(TABLE_THUMBNAILS, selection, selectionArgs);
- }
-
- private int deleteUnusedImages(Uri uri) {
- debug("Deleting all unused favicons and thumbnails for URI: " + uri);
-
- String faviconSelection = Favicons._ID + " NOT IN "
- + "(SELECT " + History.FAVICON_ID
- + " FROM " + TABLE_HISTORY
- + " WHERE " + History.IS_DELETED + " = 0"
- + " AND " + History.FAVICON_ID + " IS NOT NULL"
- + " UNION ALL SELECT " + Bookmarks.FAVICON_ID
- + " FROM " + TABLE_BOOKMARKS
- + " WHERE " + Bookmarks.IS_DELETED + " = 0"
- + " AND " + Bookmarks.FAVICON_ID + " IS NOT NULL)";
-
- String thumbnailSelection = Thumbnails.URL + " NOT IN "
- + "(SELECT " + History.URL
- + " FROM " + TABLE_HISTORY
- + " WHERE " + History.IS_DELETED + " = 0"
- + " AND " + History.URL + " IS NOT NULL"
- + " UNION ALL SELECT " + Bookmarks.URL
- + " FROM " + TABLE_BOOKMARKS
- + " WHERE " + Bookmarks.IS_DELETED + " = 0"
- + " AND " + Bookmarks.URL + " IS NOT NULL)";
-
- return deleteFavicons(uri, faviconSelection, null) +
- deleteThumbnails(uri, thumbnailSelection, null) +
- getURLMetadataTable().deleteUnused(getWritableDatabase(uri));
- }
-
- @Override
- public ContentProviderResult[] applyBatch (ArrayList<ContentProviderOperation> operations)
- throws OperationApplicationException {
- final int numOperations = operations.size();
- final ContentProviderResult[] results = new ContentProviderResult[numOperations];
-
- if (numOperations < 1) {
- debug("applyBatch: no operations; returning immediately.");
- // The original Android implementation returns a zero-length
- // array in this case. We do the same.
- return results;
- }
-
- boolean failures = false;
-
- // We only have 1 database for all Uris that we can get.
- SQLiteDatabase db = getWritableDatabase(operations.get(0).getUri());
-
- // Note that the apply() call may cause us to generate
- // additional transactions for the individual operations.
- // But Android's wrapper for SQLite supports nested transactions,
- // so this will do the right thing.
- //
- // Note further that in some circumstances this can result in
- // exceptions: if this transaction is first involved in reading,
- // and then (naturally) tries to perform writes, SQLITE_BUSY can
- // be raised. See Bug 947939 and friends.
- beginBatch(db);
-
- for (int i = 0; i < numOperations; i++) {
- try {
- final ContentProviderOperation operation = operations.get(i);
- results[i] = operation.apply(this, results, i);
- } catch (SQLException e) {
- Log.w(LOGTAG, "SQLite Exception during applyBatch.", e);
- // The Android API makes it implementation-defined whether
- // the failure of a single operation makes all others abort
- // or not. For our use cases, best-effort operation makes
- // more sense. Rolling back and forcing the caller to retry
- // after it figures out what went wrong isn't very convenient
- // anyway.
- // Signal failed operation back, so the caller knows what
- // went through and what didn't.
- results[i] = new ContentProviderResult(0);
- failures = true;
- // http://www.sqlite.org/lang_conflict.html
- // Note that we need a new transaction, subsequent operations
- // on this one will fail (we're in ABORT by default, which
- // isn't IGNORE). We still need to set it as successful to let
- // everything before the failed op go through.
- // We can't set conflict resolution on API level < 8, and even
- // above 8 it requires splitting the call per operation
- // (insert/update/delete).
- db.setTransactionSuccessful();
- db.endTransaction();
- db.beginTransaction();
- } catch (OperationApplicationException e) {
- // Repeat of above.
- results[i] = new ContentProviderResult(0);
- failures = true;
- db.setTransactionSuccessful();
- db.endTransaction();
- db.beginTransaction();
- }
- }
-
- trace("Flushing DB applyBatch...");
- markBatchSuccessful(db);
- endBatch(db);
-
- if (failures) {
- throw new OperationApplicationException();
- }
-
- return results;
- }
-
- private static Table findTableFor(int id) {
- for (Table table : sTables) {
- for (Table.ContentProviderInfo type : table.getContentProviderInfo()) {
- if (type.id == id) {
- return table;
- }
- }
- }
- return null;
- }
-
- private static void addTablesToMatcher(Table[] tables, final UriMatcher matcher) {
- }
-
- private static String getContentItemType(final int match) {
- for (Table table : sTables) {
- for (Table.ContentProviderInfo type : table.getContentProviderInfo()) {
- if (type.id == match) {
- return "vnd.android.cursor.item/" + type.name;
- }
- }
- }
-
- return null;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/DBUtils.java b/mobile/android/base/java/org/mozilla/gecko/db/DBUtils.java
deleted file mode 100644
index cfa2f870f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/DBUtils.java
+++ /dev/null
@@ -1,450 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import android.annotation.TargetApi;
-import android.database.DatabaseUtils;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteStatement;
-import android.os.Build;
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoProfile;
-
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.net.Uri;
-import android.text.TextUtils;
-import android.util.Log;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.Telemetry;
-
-import java.util.Map;
-
-public class DBUtils {
- private static final String LOGTAG = "GeckoDBUtils";
-
- public static final int SQLITE_MAX_VARIABLE_NUMBER = 999;
-
- public static final String qualifyColumn(String table, String column) {
- return table + "." + column;
- }
-
- // This is available in Android >= 11. Implemented locally to be
- // compatible with older versions.
- public static String concatenateWhere(String a, String b) {
- if (TextUtils.isEmpty(a)) {
- return b;
- }
-
- if (TextUtils.isEmpty(b)) {
- return a;
- }
-
- return "(" + a + ") AND (" + b + ")";
- }
-
- // This is available in Android >= 11. Implemented locally to be
- // compatible with older versions.
- public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) {
- if (originalValues == null || originalValues.length == 0) {
- return newValues;
- }
-
- if (newValues == null || newValues.length == 0) {
- return originalValues;
- }
-
- String[] result = new String[originalValues.length + newValues.length];
- System.arraycopy(originalValues, 0, result, 0, originalValues.length);
- System.arraycopy(newValues, 0, result, originalValues.length, newValues.length);
-
- return result;
- }
-
- /**
- * Concatenate multiple lists of selection arguments. <code>values</code> may be <code>null</code>.
- */
- public static String[] concatenateSelectionArgs(String[]... values) {
- // Since we're most likely to be concatenating a few arrays of many values, it is most
- // efficient to iterate over the arrays once to obtain their lengths, allowing us to create one target array
- // (as opposed to copying arrays on every iteration, which would result in many more copies).
- int totalLength = 0;
- for (String[] v : values) {
- if (v != null) {
- totalLength += v.length;
- }
- }
-
- String[] result = new String[totalLength];
-
- int position = 0;
- for (String[] v: values) {
- if (v != null) {
- int currentLength = v.length;
- System.arraycopy(v, 0, result, position, currentLength);
- position += currentLength;
- }
- }
-
- return result;
- }
-
- public static void replaceKey(ContentValues aValues, String aOriginalKey,
- String aNewKey, String aDefault) {
- String value = aDefault;
- if (aOriginalKey != null && aValues.containsKey(aOriginalKey)) {
- value = aValues.get(aOriginalKey).toString();
- aValues.remove(aOriginalKey);
- }
-
- if (!aValues.containsKey(aNewKey)) {
- aValues.put(aNewKey, value);
- }
- }
-
- private static String HISTOGRAM_DATABASE_LOCKED = "DATABASE_LOCKED_EXCEPTION";
- private static String HISTOGRAM_DATABASE_UNLOCKED = "DATABASE_SUCCESSFUL_UNLOCK";
- public static void ensureDatabaseIsNotLocked(SQLiteOpenHelper dbHelper, String databasePath) {
- final int maxAttempts = 5;
- int attempt = 0;
- SQLiteDatabase db = null;
- for (; attempt < maxAttempts; attempt++) {
- try {
- // Try a simple test and exit the loop.
- db = dbHelper.getWritableDatabase();
- break;
- } catch (Exception e) {
- // We assume that this is a android.database.sqlite.SQLiteDatabaseLockedException.
- // That class is only available on API 11+.
- Telemetry.addToHistogram(HISTOGRAM_DATABASE_LOCKED, attempt);
-
- // Things could get very bad if we don't find a way to unlock the DB.
- Log.d(LOGTAG, "Database is locked, trying to kill any zombie processes: " + databasePath);
- GeckoAppShell.killAnyZombies();
- try {
- Thread.sleep(attempt * 100);
- } catch (InterruptedException ie) {
- }
- }
- }
-
- if (db == null) {
- Log.w(LOGTAG, "Failed to unlock database.");
- GeckoAppShell.listOfOpenFiles();
- return;
- }
-
- // If we needed to retry, but we succeeded, report that in telemetry.
- // Failures are indicated by a lower frequency of UNLOCKED than LOCKED.
- if (attempt > 1) {
- Telemetry.addToHistogram(HISTOGRAM_DATABASE_UNLOCKED, attempt - 1);
- }
- }
-
- /**
- * Copies a table <b>between</b> database files.
- *
- * This method assumes that the source table and destination table already exist in the
- * source and destination databases, respectively.
- *
- * The table is copied row-by-row in a single transaction.
- *
- * @param source The source database that the table will be copied from.
- * @param sourceTableName The name of the source table.
- * @param destination The destination database that the table will be copied to.
- * @param destinationTableName The name of the destination table.
- * @return true if all rows were copied; false otherwise.
- */
- public static boolean copyTable(SQLiteDatabase source, String sourceTableName,
- SQLiteDatabase destination, String destinationTableName) {
- Cursor cursor = null;
- try {
- destination.beginTransaction();
-
- cursor = source.query(sourceTableName, null, null, null, null, null, null);
- Log.d(LOGTAG, "Trying to copy " + cursor.getCount() + " rows from " + sourceTableName + " to " + destinationTableName);
-
- final ContentValues contentValues = new ContentValues();
- while (cursor.moveToNext()) {
- contentValues.clear();
- DatabaseUtils.cursorRowToContentValues(cursor, contentValues);
- destination.insert(destinationTableName, null, contentValues);
- }
-
- destination.setTransactionSuccessful();
- Log.d(LOGTAG, "Successfully copied " + cursor.getCount() + " rows from " + sourceTableName + " to " + destinationTableName);
- return true;
- } catch (Exception e) {
- Log.w(LOGTAG, "Got exception copying rows from " + sourceTableName + " to " + destinationTableName + "; ignoring.", e);
- return false;
- } finally {
- destination.endTransaction();
- if (cursor != null) {
- cursor.close();
- }
- }
- }
-
- /**
- * Verifies that 0-byte arrays aren't added as favicon or thumbnail data.
- * @param values ContentValues of query
- * @param columnName Name of data column to verify
- */
- public static void stripEmptyByteArray(ContentValues values, String columnName) {
- if (values.containsKey(columnName)) {
- byte[] data = values.getAsByteArray(columnName);
- if (data == null || data.length == 0) {
- Log.w(LOGTAG, "Tried to insert an empty or non-byte-array image. Ignoring.");
- values.putNull(columnName);
- }
- }
- }
-
- /**
- * Builds a selection string that searches for a list of arguments in a particular column.
- * For example URL in (?,?,?). Callers should pass the actual arguments into their query
- * as selection args.
- * @para columnName The column to search in
- * @para size The number of arguments to search for
- */
- public static String computeSQLInClause(int items, String field) {
- final StringBuilder builder = new StringBuilder(field);
- builder.append(" IN (");
- int i = 0;
- for (; i < items - 1; ++i) {
- builder.append("?, ");
- }
- if (i < items) {
- builder.append("?");
- }
- builder.append(")");
- return builder.toString();
- }
-
- /**
- * Turn a single-column cursor of longs into a single SQL "IN" clause.
- * We can do this without using selection arguments because Long isn't
- * vulnerable to injection.
- */
- public static String computeSQLInClauseFromLongs(final Cursor cursor, String field) {
- final StringBuilder builder = new StringBuilder(field);
- builder.append(" IN (");
- final int commaLimit = cursor.getCount() - 1;
- int i = 0;
- while (cursor.moveToNext()) {
- builder.append(cursor.getLong(0));
- if (i++ < commaLimit) {
- builder.append(", ");
- }
- }
- builder.append(")");
- return builder.toString();
- }
-
- public static Uri appendProfile(final String profile, final Uri uri) {
- return uri.buildUpon().appendQueryParameter(BrowserContract.PARAM_PROFILE, profile).build();
- }
-
- public static Uri appendProfileWithDefault(final String profile, final Uri uri) {
- if (profile == null) {
- return appendProfile(GeckoProfile.DEFAULT_PROFILE, uri);
- }
- return appendProfile(profile, uri);
- }
-
- /**
- * Use the following when no conflict action is specified.
- */
- private static final int CONFLICT_NONE = 0;
- private static final String[] CONFLICT_VALUES = new String[] {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "};
-
- /**
- * Convenience method for updating rows in the database.
- *
- * @param table the table to update in
- * @param values a map from column names to new column values. null is a
- * valid value that will be translated to NULL.
- * @param whereClause the optional WHERE clause to apply when updating.
- * Passing null will update all rows.
- * @param whereArgs You may include ?s in the where clause, which
- * will be replaced by the values from whereArgs. The values
- * will be bound as Strings.
- * @return the number of rows affected
- */
- @RobocopTarget
- public static int updateArrays(SQLiteDatabase db, String table, ContentValues[] values, UpdateOperation[] ops, String whereClause, String[] whereArgs) {
- return updateArraysWithOnConflict(db, table, values, ops, whereClause, whereArgs, CONFLICT_NONE, true);
- }
-
- public static void updateArraysBlindly(SQLiteDatabase db, String table, ContentValues[] values, UpdateOperation[] ops, String whereClause, String[] whereArgs) {
- updateArraysWithOnConflict(db, table, values, ops, whereClause, whereArgs, CONFLICT_NONE, false);
- }
-
- @RobocopTarget
- public enum UpdateOperation {
- /**
- * ASSIGN is the usual update: replaces the value in the named column with the provided value.
- *
- * foo = ?
- */
- ASSIGN,
-
- /**
- * BITWISE_OR applies the provided value to the existing value with a bitwise OR. This is useful for adding to flags.
- *
- * foo |= ?
- */
- BITWISE_OR,
-
- /**
- * EXPRESSION is an end-run around the API: it allows callers to specify a fragment of SQL to splice into the
- * SET part of the query.
- *
- * foo = $value
- *
- * Be very careful not to use user input in this.
- */
- EXPRESSION,
- }
-
- /**
- * This is an evil reimplementation of SQLiteDatabase's methods to allow for
- * smarter updating.
- *
- * Each ContentValues has an associated enum that describes how to unify input values with the existing column values.
- */
- private static int updateArraysWithOnConflict(SQLiteDatabase db, String table,
- ContentValues[] values,
- UpdateOperation[] ops,
- String whereClause,
- String[] whereArgs,
- int conflictAlgorithm,
- boolean returnChangedRows) {
- if (values == null || values.length == 0) {
- throw new IllegalArgumentException("Empty values");
- }
-
- if (ops == null || ops.length != values.length) {
- throw new IllegalArgumentException("ops and values don't match");
- }
-
- StringBuilder sql = new StringBuilder(120);
- sql.append("UPDATE ");
- sql.append(CONFLICT_VALUES[conflictAlgorithm]);
- sql.append(table);
- sql.append(" SET ");
-
- // move all bind args to one array
- int setValuesSize = 0;
- for (int i = 0; i < values.length; i++) {
- // EXPRESSION types don't contribute any placeholders.
- if (ops[i] != UpdateOperation.EXPRESSION) {
- setValuesSize += values[i].size();
- }
- }
-
- int bindArgsSize = (whereArgs == null) ? setValuesSize : (setValuesSize + whereArgs.length);
- Object[] bindArgs = new Object[bindArgsSize];
-
- int arg = 0;
- for (int i = 0; i < values.length; i++) {
- final ContentValues v = values[i];
- final UpdateOperation op = ops[i];
-
- // Alas, code duplication.
- switch (op) {
- case ASSIGN:
- for (Map.Entry<String, Object> entry : v.valueSet()) {
- final String colName = entry.getKey();
- sql.append((arg > 0) ? "," : "");
- sql.append(colName);
- bindArgs[arg++] = entry.getValue();
- sql.append("= ?");
- }
- break;
- case BITWISE_OR:
- for (Map.Entry<String, Object> entry : v.valueSet()) {
- final String colName = entry.getKey();
- sql.append((arg > 0) ? "," : "");
- sql.append(colName);
- bindArgs[arg++] = entry.getValue();
- sql.append("= ? | ");
- sql.append(colName);
- }
- break;
- case EXPRESSION:
- // Treat each value as a literal SQL string.
- for (Map.Entry<String, Object> entry : v.valueSet()) {
- final String colName = entry.getKey();
- sql.append((arg > 0) ? "," : "");
- sql.append(colName);
- sql.append(" = ");
- sql.append(entry.getValue());
- }
- break;
- }
- }
-
- if (whereArgs != null) {
- for (arg = setValuesSize; arg < bindArgsSize; arg++) {
- bindArgs[arg] = whereArgs[arg - setValuesSize];
- }
- }
- if (!TextUtils.isEmpty(whereClause)) {
- sql.append(" WHERE ");
- sql.append(whereClause);
- }
-
- // What a huge pain in the ass, all because SQLiteDatabase doesn't expose .executeSql,
- // and we can't get a DB handle. Nor can we easily construct a statement with arguments
- // already bound.
- final SQLiteStatement statement = db.compileStatement(sql.toString());
- try {
- bindAllArgs(statement, bindArgs);
- if (!returnChangedRows) {
- statement.execute();
- return 0;
- }
- // This is a separate method so we can annotate it with @TargetApi.
- return executeStatementReturningChangedRows(statement);
- } finally {
- statement.close();
- }
- }
-
- @TargetApi(Build.VERSION_CODES.HONEYCOMB)
- private static int executeStatementReturningChangedRows(SQLiteStatement statement) {
- return statement.executeUpdateDelete();
- }
-
- // All because {@link SQLiteProgram#bind(integer, Object)} is private.
- private static void bindAllArgs(SQLiteStatement statement, Object[] bindArgs) {
- if (bindArgs == null) {
- return;
- }
- for (int i = bindArgs.length; i != 0; i--) {
- Object v = bindArgs[i - 1];
- if (v == null) {
- statement.bindNull(i);
- } else if (v instanceof String) {
- statement.bindString(i, (String) v);
- } else if (v instanceof Double) {
- statement.bindDouble(i, (Double) v);
- } else if (v instanceof Float) {
- statement.bindDouble(i, (Float) v);
- } else if (v instanceof Long) {
- statement.bindLong(i, (Long) v);
- } else if (v instanceof Integer) {
- statement.bindLong(i, (Integer) v);
- } else if (v instanceof Byte) {
- statement.bindLong(i, (Byte) v);
- } else if (v instanceof byte[]) {
- statement.bindBlob(i, (byte[]) v);
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/FormHistoryProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/FormHistoryProvider.java
deleted file mode 100644
index ff2f5238e..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/FormHistoryProvider.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import java.lang.IllegalArgumentException;
-import java.util.HashMap;
-
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.db.BrowserContract.FormHistory;
-import org.mozilla.gecko.db.BrowserContract.DeletedFormHistory;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.sqlite.SQLiteBridge;
-import org.mozilla.gecko.sync.Utils;
-
-import android.content.ContentValues;
-import android.content.UriMatcher;
-import android.database.Cursor;
-import android.net.Uri;
-import android.text.TextUtils;
-
-public class FormHistoryProvider extends SQLiteBridgeContentProvider {
- static final String TABLE_FORM_HISTORY = "moz_formhistory";
- static final String TABLE_DELETED_FORM_HISTORY = "moz_deleted_formhistory";
-
- private static final int FORM_HISTORY = 100;
- private static final int DELETED_FORM_HISTORY = 101;
-
- private static final UriMatcher URI_MATCHER;
-
-
- // This should be kept in sync with the db version in toolkit/components/satchel/nsFormHistory.js
- private static final int DB_VERSION = 4;
- private static final String DB_FILENAME = "formhistory.sqlite";
- private static final String TELEMETRY_TAG = "SQLITEBRIDGE_PROVIDER_FORMS";
-
- private static final String WHERE_GUID_IS_NULL = BrowserContract.DeletedFormHistory.GUID + " IS NULL";
- private static final String WHERE_GUID_IS_VALUE = BrowserContract.DeletedFormHistory.GUID + " = ?";
-
- private static final String LOG_TAG = "FormHistoryProvider";
-
- static {
- URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
- URI_MATCHER.addURI(BrowserContract.FORM_HISTORY_AUTHORITY, "formhistory", FORM_HISTORY);
- URI_MATCHER.addURI(BrowserContract.FORM_HISTORY_AUTHORITY, "deleted-formhistory", DELETED_FORM_HISTORY);
- }
-
- public FormHistoryProvider() {
- super(LOG_TAG);
- }
-
-
- @Override
- public String getType(Uri uri) {
- final int match = URI_MATCHER.match(uri);
-
- switch (match) {
- case FORM_HISTORY:
- return FormHistory.CONTENT_TYPE;
-
- case DELETED_FORM_HISTORY:
- return DeletedFormHistory.CONTENT_TYPE;
-
- default:
- throw new UnsupportedOperationException("Unknown type " + uri);
- }
- }
-
- @Override
- public String getTable(Uri uri) {
- String table = null;
- final int match = URI_MATCHER.match(uri);
- switch (match) {
- case DELETED_FORM_HISTORY:
- table = TABLE_DELETED_FORM_HISTORY;
- break;
-
- case FORM_HISTORY:
- table = TABLE_FORM_HISTORY;
- break;
-
- default:
- throw new UnsupportedOperationException("Unknown table " + uri);
- }
- return table;
- }
-
- @Override
- public String getSortOrder(Uri uri, String aRequested) {
- if (!TextUtils.isEmpty(aRequested)) {
- return aRequested;
- }
-
- return null;
- }
-
- @Override
- public void setupDefaults(Uri uri, ContentValues values) {
- int match = URI_MATCHER.match(uri);
- long now = System.currentTimeMillis();
-
- switch (match) {
- case DELETED_FORM_HISTORY:
- values.put(DeletedFormHistory.TIME_DELETED, now);
-
- // Deleted entries must contain a guid
- if (!values.containsKey(FormHistory.GUID)) {
- throw new IllegalArgumentException("Must provide a GUID for a deleted form history");
- }
- break;
-
- case FORM_HISTORY:
- // Generate GUID for new entry. Don't override specified GUIDs.
- if (!values.containsKey(FormHistory.GUID)) {
- String guid = Utils.generateGuid();
- values.put(FormHistory.GUID, guid);
- }
- break;
-
- default:
- throw new UnsupportedOperationException("Unknown insert URI " + uri);
- }
- }
-
- @Override
- public void initGecko() {
- GeckoAppShell.notifyObservers("FormHistory:Init", null);
- }
-
- @Override
- public void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db) {
- if (!values.containsKey(FormHistory.GUID)) {
- return;
- }
-
- String guid = values.getAsString(FormHistory.GUID);
- if (guid == null) {
- db.delete(TABLE_DELETED_FORM_HISTORY, WHERE_GUID_IS_NULL, null);
- return;
- }
- String[] args = new String[] { guid };
- db.delete(TABLE_DELETED_FORM_HISTORY, WHERE_GUID_IS_VALUE, args);
- }
-
- @Override
- public void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db) { }
-
- @Override
- public void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db) { }
-
- @Override
- protected String getDBName() {
- return DB_FILENAME;
- }
-
- @Override
- protected String getTelemetryPrefix() {
- return TELEMETRY_TAG;
- }
-
- @Override
- protected int getDBVersion() {
- return DB_VERSION;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/HomeProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/HomeProvider.java
deleted file mode 100644
index 1a241f9da..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/HomeProvider.java
+++ /dev/null
@@ -1,194 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import java.io.IOException;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.BrowserContract.HomeItems;
-import org.mozilla.gecko.db.DBUtils;
-import org.mozilla.gecko.sqlite.SQLiteBridge;
-import org.mozilla.gecko.util.RawResource;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.UriMatcher;
-import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.net.Uri;
-import android.util.Log;
-
-public class HomeProvider extends SQLiteBridgeContentProvider {
- private static final String LOGTAG = "GeckoHomeProvider";
-
- // This should be kept in sync with the db version in mobile/android/modules/HomeProvider.jsm
- private static final int DB_VERSION = 3;
- private static final String DB_FILENAME = "home.sqlite";
- private static final String TELEMETRY_TAG = "SQLITEBRIDGE_PROVIDER_HOME";
-
- private static final String TABLE_ITEMS = "items";
-
- // Endpoint to return static fake data.
- static final int ITEMS_FAKE = 100;
- static final int ITEMS = 101;
- static final int ITEMS_ID = 102;
-
- static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
-
- static {
- URI_MATCHER.addURI(BrowserContract.HOME_AUTHORITY, "items/fake", ITEMS_FAKE);
- URI_MATCHER.addURI(BrowserContract.HOME_AUTHORITY, "items", ITEMS);
- URI_MATCHER.addURI(BrowserContract.HOME_AUTHORITY, "items/#", ITEMS_ID);
- }
-
- public HomeProvider() {
- super(LOGTAG);
- }
-
- @Override
- public String getType(Uri uri) {
- final int match = URI_MATCHER.match(uri);
-
- switch (match) {
- case ITEMS_FAKE: {
- return HomeItems.CONTENT_TYPE;
- }
- case ITEMS: {
- return HomeItems.CONTENT_TYPE;
- }
- default: {
- throw new UnsupportedOperationException("Unknown type " + uri);
- }
- }
- }
-
- @Override
- public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
- final int match = URI_MATCHER.match(uri);
-
- // If we're querying the fake items, don't try to get the database.
- if (match == ITEMS_FAKE) {
- return queryFakeItems(uri, projection, selection, selectionArgs, sortOrder);
- }
-
- final String datasetId = uri.getQueryParameter(BrowserContract.PARAM_DATASET_ID);
- if (datasetId == null) {
- throw new IllegalArgumentException("All queries should contain a dataset ID parameter");
- }
-
- selection = DBUtils.concatenateWhere(selection, HomeItems.DATASET_ID + " = ?");
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { datasetId });
-
- // Otherwise, let the SQLiteContentProvider implementation take care of this query for us!
- Cursor c = super.query(uri, projection, selection, selectionArgs, sortOrder);
-
- // SQLiteBridgeContentProvider may return a null Cursor if the database hasn't been created yet.
- // However, we need a non-null cursor in order to listen for notifications.
- if (c == null) {
- c = new MatrixCursor(projection != null ? projection : HomeItems.DEFAULT_PROJECTION);
- }
-
- final ContentResolver cr = getContext().getContentResolver();
- c.setNotificationUri(cr, getDatasetNotificationUri(datasetId));
-
- return c;
- }
-
- /**
- * Returns a cursor populated with static fake data.
- */
- private Cursor queryFakeItems(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
- JSONArray items = null;
- try {
- final String jsonString = RawResource.getAsString(getContext(), R.raw.fake_home_items);
- items = new JSONArray(jsonString);
- } catch (IOException e) {
- Log.e(LOGTAG, "Error getting fake home items", e);
- return null;
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error parsing fake_home_items.json", e);
- return null;
- }
-
- final MatrixCursor c = new MatrixCursor(HomeItems.DEFAULT_PROJECTION);
- for (int i = 0; i < items.length(); i++) {
- try {
- final JSONObject item = items.getJSONObject(i);
- c.addRow(new Object[] {
- item.getInt("id"),
- item.getString("dataset_id"),
- item.getString("url"),
- item.getString("title"),
- item.getString("description"),
- item.getString("image_url"),
- item.getString("filter")
- });
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error creating cursor row for fake home item", e);
- }
- }
- return c;
- }
-
- /**
- * SQLiteBridgeContentProvider implementation
- */
-
- @Override
- protected String getDBName() {
- return DB_FILENAME;
- }
-
- @Override
- protected String getTelemetryPrefix() {
- return TELEMETRY_TAG;
- }
-
- @Override
- protected int getDBVersion() {
- return DB_VERSION;
- }
-
- @Override
- public String getTable(Uri uri) {
- final int match = URI_MATCHER.match(uri);
- switch (match) {
- case ITEMS: {
- return TABLE_ITEMS;
- }
- default: {
- throw new UnsupportedOperationException("Unknown table " + uri);
- }
- }
- }
-
- @Override
- public String getSortOrder(Uri uri, String aRequested) {
- return null;
- }
-
- @Override
- public void setupDefaults(Uri uri, ContentValues values) { }
-
- @Override
- public void initGecko() { }
-
- @Override
- public void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db) { }
-
- @Override
- public void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db) { }
-
- @Override
- public void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db) { }
-
- public static Uri getDatasetNotificationUri(String datasetId) {
- return Uri.withAppendedPath(HomeItems.CONTENT_URI, datasetId);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java b/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
deleted file mode 100644
index 8c219282f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
+++ /dev/null
@@ -1,1938 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import java.io.ByteArrayOutputStream;
-import java.io.InputStream;
-import java.lang.IllegalAccessException;
-import java.lang.NoSuchFieldException;
-import java.lang.reflect.Array;
-import java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.BrowserContract.ActivityStreamBlocklist;
-import org.mozilla.gecko.db.BrowserContract.Bookmarks;
-import org.mozilla.gecko.db.BrowserContract.Combined;
-import org.mozilla.gecko.db.BrowserContract.ExpirePriority;
-import org.mozilla.gecko.db.BrowserContract.Favicons;
-import org.mozilla.gecko.db.BrowserContract.History;
-import org.mozilla.gecko.db.BrowserContract.SyncColumns;
-import org.mozilla.gecko.db.BrowserContract.Thumbnails;
-import org.mozilla.gecko.db.BrowserContract.TopSites;
-import org.mozilla.gecko.db.BrowserContract.Highlights;
-import org.mozilla.gecko.db.BrowserContract.PageMetadata;
-import org.mozilla.gecko.distribution.Distribution;
-import org.mozilla.gecko.icons.decoders.FaviconDecoder;
-import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
-import org.mozilla.gecko.gfx.BitmapUtils;
-import org.mozilla.gecko.restrictions.Restrictions;
-import org.mozilla.gecko.sync.Utils;
-import org.mozilla.gecko.util.GeckoJarReader;
-import org.mozilla.gecko.util.StringUtils;
-
-import android.content.ContentProviderClient;
-import android.content.ContentProviderOperation;
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.database.MergeCursor;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.drawable.BitmapDrawable;
-import android.net.Uri;
-import android.os.RemoteException;
-import android.os.SystemClock;
-import android.support.annotation.CheckResult;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v4.content.CursorLoader;
-import android.text.TextUtils;
-import android.util.Log;
-import org.mozilla.gecko.util.IOUtils;
-
-import static org.mozilla.gecko.util.IOUtils.ConsumedInputStream;
-
-public class LocalBrowserDB extends BrowserDB {
- // The default size of the buffer to use for downloading Favicons in the event no size is given
- // by the server.
- public static final int DEFAULT_FAVICON_BUFFER_SIZE_BYTES = 25000;
-
- private static final String LOGTAG = "GeckoLocalBrowserDB";
-
- // Calculate this once, at initialization. isLoggable is too expensive to
- // have in-line in each log call.
- private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
- protected static void debug(String message) {
- if (logDebug) {
- Log.d(LOGTAG, message);
- }
- }
-
- // Sentinel value used to indicate a failure to locate an ID for a default favicon.
- private static final int FAVICON_ID_NOT_FOUND = Integer.MIN_VALUE;
-
- // Constant used to indicate that no folder was found for particular GUID.
- private static final long FOLDER_NOT_FOUND = -1L;
-
- private final String mProfile;
-
- // Map of folder GUIDs to IDs. Used for caching.
- private final HashMap<String, Long> mFolderIdMap;
-
- // Use wrapped Boolean so that we can have a null state
- private volatile Boolean mDesktopBookmarksExist;
-
- private volatile SuggestedSites mSuggestedSites;
-
- // Constants used when importing history data from legacy browser.
- public static String HISTORY_VISITS_DATE = "date";
- public static String HISTORY_VISITS_COUNT = "visits";
- public static String HISTORY_VISITS_URL = "url";
-
- private static final String TELEMETRY_HISTOGRAM_ACITIVITY_STREAM_TOPSITES = "FENNEC_ACTIVITY_STREAM_TOPSITES_LOADER_TIME_MS";
-
- private final Uri mBookmarksUriWithProfile;
- private final Uri mParentsUriWithProfile;
- private final Uri mHistoryUriWithProfile;
- private final Uri mHistoryExpireUriWithProfile;
- private final Uri mCombinedUriWithProfile;
- private final Uri mUpdateHistoryUriWithProfile;
- private final Uri mFaviconsUriWithProfile;
- private final Uri mThumbnailsUriWithProfile;
- private final Uri mTopSitesUriWithProfile;
- private final Uri mHighlightsUriWithProfile;
- private final Uri mSearchHistoryUri;
- private final Uri mActivityStreamBlockedUriWithProfile;
- private final Uri mPageMetadataWithProfile;
-
- private LocalSearches searches;
- private LocalTabsAccessor tabsAccessor;
- private LocalURLMetadata urlMetadata;
- private LocalUrlAnnotations urlAnnotations;
-
- private static final String[] DEFAULT_BOOKMARK_COLUMNS =
- new String[] { Bookmarks._ID,
- Bookmarks.GUID,
- Bookmarks.URL,
- Bookmarks.TITLE,
- Bookmarks.TYPE,
- Bookmarks.PARENT };
-
- public LocalBrowserDB(String profile) {
- mProfile = profile;
- mFolderIdMap = new HashMap<String, Long>();
-
- mBookmarksUriWithProfile = DBUtils.appendProfile(profile, Bookmarks.CONTENT_URI);
- mParentsUriWithProfile = DBUtils.appendProfile(profile, Bookmarks.PARENTS_CONTENT_URI);
- mHistoryUriWithProfile = DBUtils.appendProfile(profile, History.CONTENT_URI);
- mHistoryExpireUriWithProfile = DBUtils.appendProfile(profile, History.CONTENT_OLD_URI);
- mCombinedUriWithProfile = DBUtils.appendProfile(profile, Combined.CONTENT_URI);
- mFaviconsUriWithProfile = DBUtils.appendProfile(profile, Favicons.CONTENT_URI);
- mTopSitesUriWithProfile = DBUtils.appendProfile(profile, TopSites.CONTENT_URI);
- mHighlightsUriWithProfile = DBUtils.appendProfile(profile, Highlights.CONTENT_URI);
- mThumbnailsUriWithProfile = DBUtils.appendProfile(profile, Thumbnails.CONTENT_URI);
- mActivityStreamBlockedUriWithProfile = DBUtils.appendProfile(profile, ActivityStreamBlocklist.CONTENT_URI);
-
- mPageMetadataWithProfile = DBUtils.appendProfile(profile, PageMetadata.CONTENT_URI);
-
- mSearchHistoryUri = BrowserContract.SearchHistory.CONTENT_URI;
-
- mUpdateHistoryUriWithProfile =
- mHistoryUriWithProfile.buildUpon()
- .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true")
- .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true")
- .build();
-
- searches = new LocalSearches(mProfile);
- tabsAccessor = new LocalTabsAccessor(mProfile);
- urlMetadata = new LocalURLMetadata(mProfile);
- urlAnnotations = new LocalUrlAnnotations(mProfile);
- }
-
- @Override
- public Searches getSearches() {
- return searches;
- }
-
- @Override
- public TabsAccessor getTabsAccessor() {
- return tabsAccessor;
- }
-
- @Override
- public URLMetadata getURLMetadata() {
- return urlMetadata;
- }
-
- @RobocopTarget
- @Override
- public UrlAnnotations getUrlAnnotations() {
- return urlAnnotations;
- }
-
- /**
- * Not thread safe. A helper to allocate new IDs for arbitrary strings.
- */
- private static class NameCounter {
- private final HashMap<String, Integer> names = new HashMap<String, Integer>();
- private int counter;
- private final int increment;
-
- public NameCounter(int start, int increment) {
- this.counter = start;
- this.increment = increment;
- }
-
- public int get(final String name) {
- Integer mapping = names.get(name);
- if (mapping == null) {
- int ours = counter;
- counter += increment;
- names.put(name, ours);
- return ours;
- }
-
- return mapping;
- }
-
- public boolean has(final String name) {
- return names.containsKey(name);
- }
- }
-
- /**
- * Add default bookmarks to the database.
- * Takes an offset; returns a new offset.
- */
- @Override
- public int addDefaultBookmarks(Context context, ContentResolver cr, final int offset) {
- final long folderID = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID);
- if (folderID == FOLDER_NOT_FOUND) {
- Log.e(LOGTAG, "No mobile folder: cannot add default bookmarks.");
- return offset;
- }
-
- // Use reflection to walk the set of bookmark defaults.
- // This is horrible.
- final Class<?> stringsClass = R.string.class;
- final Field[] fields = stringsClass.getFields();
- final Pattern p = Pattern.compile("^bookmarkdefaults_title_");
-
- int pos = offset;
- final long now = System.currentTimeMillis();
-
- final ArrayList<ContentValues> bookmarkValues = new ArrayList<ContentValues>();
- final ArrayList<ContentValues> faviconValues = new ArrayList<ContentValues>();
-
- // Count down from -offset into negative values to get new favicon IDs.
- final NameCounter faviconIDs = new NameCounter((-1 - offset), -1);
-
- for (int i = 0; i < fields.length; i++) {
- final String name = fields[i].getName();
- final Matcher m = p.matcher(name);
- if (!m.find()) {
- continue;
- }
-
- try {
- if (Restrictions.isRestrictedProfile(context)) {
- // matching on variable name from strings.xml.in
- final String addons = "bookmarkdefaults_title_addons";
- final String regularSumo = "bookmarkdefaults_title_support";
- if (name.equals(addons) || name.equals(regularSumo)) {
- continue;
- }
- }
- if (!Restrictions.isRestrictedProfile(context)) {
- // if we're not in kidfox, skip the kidfox specific bookmark(s)
- if (name.startsWith("bookmarkdefaults_title_restricted")) {
- continue;
- }
- }
- final int titleID = fields[i].getInt(null);
- final String title = context.getString(titleID);
-
- final Field urlField = stringsClass.getField(name.replace("_title_", "_url_"));
- final int urlID = urlField.getInt(null);
- final String url = context.getString(urlID);
-
- final ContentValues bookmarkValue = createBookmark(now, title, url, pos++, folderID);
- bookmarkValues.add(bookmarkValue);
-
- ConsumedInputStream faviconStream = getDefaultFaviconFromDrawable(context, name);
- if (faviconStream == null) {
- faviconStream = getDefaultFaviconFromPath(context, name);
- }
-
- if (faviconStream == null) {
- continue;
- }
-
- // In the event that truncating the buffer fails, give up and move on.
- byte[] icon;
- try {
- icon = faviconStream.getTruncatedData();
- } catch (OutOfMemoryError e) {
- continue;
- }
-
- final ContentValues iconValue = createFavicon(url, icon);
-
- // Assign a reserved negative _id to each new favicon.
- // For now, each name is expected to be unique, and duplicate
- // icons will be duplicated in the DB. See Bug 1040806 Comment 8.
- if (iconValue != null) {
- final int faviconID = faviconIDs.get(name);
- iconValue.put("_id", faviconID);
- bookmarkValue.put(Bookmarks.FAVICON_ID, faviconID);
- faviconValues.add(iconValue);
- }
- } catch (IllegalAccessException | IllegalArgumentException | NoSuchFieldException e) {
- Log.wtf(LOGTAG, "Reflection failure.", e);
- }
- }
-
- if (!faviconValues.isEmpty()) {
- try {
- cr.bulkInsert(mFaviconsUriWithProfile, faviconValues.toArray(new ContentValues[faviconValues.size()]));
- } catch (Exception e) {
- Log.e(LOGTAG, "Error bulk-inserting default favicons.", e);
- }
- }
-
- if (!bookmarkValues.isEmpty()) {
- try {
- final int inserted = cr.bulkInsert(mBookmarksUriWithProfile, bookmarkValues.toArray(new ContentValues[bookmarkValues.size()]));
- return offset + inserted;
- } catch (Exception e) {
- Log.e(LOGTAG, "Error bulk-inserting default bookmarks.", e);
- }
- }
-
- return offset;
- }
-
- /**
- * Add bookmarks from the provided distribution.
- * Takes an offset; returns a new offset.
- */
- @Override
- public int addDistributionBookmarks(ContentResolver cr, Distribution distribution, int offset) {
- if (!distribution.exists()) {
- Log.d(LOGTAG, "No distribution from which to add bookmarks.");
- return offset;
- }
-
- final JSONArray bookmarks = distribution.getBookmarks();
- if (bookmarks == null) {
- Log.d(LOGTAG, "No distribution bookmarks.");
- return offset;
- }
-
- final long folderID = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID);
- if (folderID == FOLDER_NOT_FOUND) {
- Log.e(LOGTAG, "No mobile folder: cannot add distribution bookmarks.");
- return offset;
- }
-
- final Locale locale = Locale.getDefault();
- final long now = System.currentTimeMillis();
- int mobilePos = offset;
- int pinnedPos = 0; // Assume nobody has pinned anything yet.
-
- final ArrayList<ContentValues> bookmarkValues = new ArrayList<ContentValues>();
- final ArrayList<ContentValues> faviconValues = new ArrayList<ContentValues>();
-
- // Count down from -offset into negative values to get new favicon IDs.
- final NameCounter faviconIDs = new NameCounter((-1 - offset), -1);
-
- for (int i = 0; i < bookmarks.length(); i++) {
- try {
- final JSONObject bookmark = bookmarks.getJSONObject(i);
-
- final String title = getLocalizedProperty(bookmark, "title", locale);
- final String url = getLocalizedProperty(bookmark, "url", locale);
- final long parent;
- final int pos;
- if (bookmark.has("pinned")) {
- parent = Bookmarks.FIXED_PINNED_LIST_ID;
- pos = pinnedPos++;
- } else {
- parent = folderID;
- pos = mobilePos++;
- }
-
- final ContentValues bookmarkValue = createBookmark(now, title, url, pos, parent);
- bookmarkValues.add(bookmarkValue);
-
- // Return early if there is no icon for this bookmark.
- if (!bookmark.has("icon")) {
- continue;
- }
-
- try {
- final String iconData = bookmark.getString("icon");
-
- byte[] icon = BitmapUtils.getBytesFromDataURI(iconData);
- if (icon == null) {
- continue;
- }
-
- final ContentValues iconValue = createFavicon(url, icon);
- if (iconValue == null) {
- continue;
- }
-
- /*
- * Find out if this icon is a duplicate. If it is, don't try
- * to insert it again, but reuse the shared ID.
- * Otherwise, assign a new reserved negative _id.
- * Duplicates won't be detected in default bookmarks, or
- * those already in the database.
- */
- final boolean seen = faviconIDs.has(iconData);
- final int faviconID = faviconIDs.get(iconData);
-
- iconValue.put("_id", faviconID);
- bookmarkValue.put(Bookmarks.FAVICON_ID, faviconID);
-
- if (!seen) {
- faviconValues.add(iconValue);
- }
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error creating distribution bookmark icon.", e);
- }
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error creating distribution bookmark.", e);
- }
- }
-
- if (!faviconValues.isEmpty()) {
- try {
- cr.bulkInsert(mFaviconsUriWithProfile, faviconValues.toArray(new ContentValues[faviconValues.size()]));
- } catch (Exception e) {
- Log.e(LOGTAG, "Error bulk-inserting distribution favicons.", e);
- }
- }
-
- if (!bookmarkValues.isEmpty()) {
- try {
- final int inserted = cr.bulkInsert(mBookmarksUriWithProfile, bookmarkValues.toArray(new ContentValues[bookmarkValues.size()]));
- return offset + inserted;
- } catch (Exception e) {
- Log.e(LOGTAG, "Error bulk-inserting distribution bookmarks.", e);
- }
- }
-
- return offset;
- }
-
- private static ContentValues createBookmark(final long timestamp, final String title, final String url, final int pos, final long parent) {
- final ContentValues v = new ContentValues();
-
- v.put(Bookmarks.DATE_CREATED, timestamp);
- v.put(Bookmarks.DATE_MODIFIED, timestamp);
- v.put(Bookmarks.GUID, Utils.generateGuid());
-
- v.put(Bookmarks.PARENT, parent);
- v.put(Bookmarks.POSITION, pos);
- v.put(Bookmarks.TITLE, title);
- v.put(Bookmarks.URL, url);
- return v;
- }
-
- private static ContentValues createFavicon(final String url, final byte[] icon) {
- ContentValues iconValues = new ContentValues();
- iconValues.put(Favicons.PAGE_URL, url);
- iconValues.put(Favicons.DATA, icon);
-
- return iconValues;
- }
-
- private static String getLocalizedProperty(final JSONObject bookmark, final String property, final Locale locale) throws JSONException {
- // Try the full locale.
- final String fullLocale = property + "." + locale.toString();
- if (bookmark.has(fullLocale)) {
- return bookmark.getString(fullLocale);
- }
-
- // Try without a variant.
- if (!TextUtils.isEmpty(locale.getVariant())) {
- String noVariant = fullLocale.substring(0, fullLocale.lastIndexOf("_"));
- if (bookmark.has(noVariant)) {
- return bookmark.getString(noVariant);
- }
- }
-
- // Try just the language.
- String lang = property + "." + locale.getLanguage();
- if (bookmark.has(lang)) {
- return bookmark.getString(lang);
- }
-
- // Default to the non-localized property name.
- return bookmark.getString(property);
- }
-
- private static int getFaviconId(String name) {
- try {
- Class<?> drawablesClass = R.raw.class;
-
- // Look for a favicon with the id R.raw.bookmarkdefaults_favicon_*.
- Field faviconField = drawablesClass.getField(name.replace("_title_", "_favicon_"));
- faviconField.setAccessible(true);
-
- return faviconField.getInt(null);
- } catch (IllegalAccessException | NoSuchFieldException e) {
- // We'll end up here for any default bookmark that doesn't have a favicon in
- // resources/raw/ (i.e., about:firefox). When this happens, the Favicons service will
- // fall back to the default branding icon for about pages. Non-about pages should always
- // specify an icon; otherwise, the placeholder globe favicon will be used.
- Log.d(LOGTAG, "No raw favicon resource found for " + name);
- }
-
- Log.e(LOGTAG, "Failed to find favicon resource ID for " + name);
- return FAVICON_ID_NOT_FOUND;
- }
-
- @Override
- public boolean insertPageMetadata(ContentProviderClient contentProviderClient, String pageUrl, boolean hasImage, String metadataJSON) {
- final String historyGUID = lookupHistoryGUIDByPageUri(contentProviderClient, pageUrl);
-
- if (historyGUID == null) {
- return false;
- }
-
- // We have the GUID, insert the metadata.
- final ContentValues cv = new ContentValues();
- cv.put(PageMetadata.HISTORY_GUID, historyGUID);
- cv.put(PageMetadata.HAS_IMAGE, hasImage);
- cv.put(PageMetadata.JSON, metadataJSON);
-
- try {
- contentProviderClient.insert(mPageMetadataWithProfile, cv);
- } catch (RemoteException e) {
- throw new IllegalStateException("Unexpected RemoteException", e);
- }
-
- return true;
- }
-
- @Override
- public int deletePageMetadata(ContentProviderClient contentProviderClient, String pageUrl) {
- final String historyGUID = lookupHistoryGUIDByPageUri(contentProviderClient, pageUrl);
-
- if (historyGUID == null) {
- return 0;
- }
-
- try {
- return contentProviderClient.delete(mPageMetadataWithProfile, PageMetadata.HISTORY_GUID + " = ?", new String[]{historyGUID});
- } catch (RemoteException e) {
- throw new IllegalStateException("Unexpected RemoteException", e);
- }
- }
-
- @Nullable
- private String lookupHistoryGUIDByPageUri(ContentProviderClient contentProviderClient, String uri) {
- // Unfortunately we might have duplicate history records for the same URL.
- final Cursor cursor;
- try {
- cursor = contentProviderClient.query(
- mHistoryUriWithProfile
- .buildUpon()
- .appendQueryParameter(BrowserContract.PARAM_LIMIT, "1")
- .build(),
- new String[]{
- History.GUID,
- },
- History.URL + "= ?",
- new String[]{uri}, History.DATE_LAST_VISITED + " DESC"
- );
- } catch (RemoteException e) {
- // Won't happen, we control the implementation.
- throw new IllegalStateException("Unexpected RemoteException", e);
- }
-
- if (cursor == null) {
- return null;
- }
-
- try {
- if (!cursor.moveToFirst()) {
- return null;
- }
-
- final int historyGUIDCol = cursor.getColumnIndexOrThrow(History.GUID);
- return cursor.getString(historyGUIDCol);
- } finally {
- cursor.close();
- }
- }
-
- /**
- * Load a favicon from the omnijar.
- * @return A ConsumedInputStream containing the bytes loaded from omnijar. This must be a format
- * compatible with the favicon decoder (most probably a PNG or ICO file).
- */
- private static ConsumedInputStream getDefaultFaviconFromPath(Context context, String name) {
- final int faviconId = getFaviconId(name);
- if (faviconId == FAVICON_ID_NOT_FOUND) {
- return null;
- }
-
- final String bitmapPath = GeckoJarReader.getJarURL(context, context.getString(faviconId));
- final InputStream iStream = GeckoJarReader.getStream(context, bitmapPath);
-
- return IOUtils.readFully(iStream, DEFAULT_FAVICON_BUFFER_SIZE_BYTES);
- }
-
- private static ConsumedInputStream getDefaultFaviconFromDrawable(Context context, String name) {
- int faviconId = getFaviconId(name);
- if (faviconId == FAVICON_ID_NOT_FOUND) {
- return null;
- }
-
- InputStream iStream = context.getResources().openRawResource(faviconId);
- return IOUtils.readFully(iStream, DEFAULT_FAVICON_BUFFER_SIZE_BYTES);
- }
-
- // Invalidate cached data
- @Override
- public void invalidate() {
- mDesktopBookmarksExist = null;
- }
-
- private Uri bookmarksUriWithLimit(int limit) {
- return mBookmarksUriWithProfile.buildUpon()
- .appendQueryParameter(BrowserContract.PARAM_LIMIT,
- String.valueOf(limit))
- .build();
- }
-
- private Uri combinedUriWithLimit(int limit) {
- return mCombinedUriWithProfile.buildUpon()
- .appendQueryParameter(BrowserContract.PARAM_LIMIT,
- String.valueOf(limit))
- .build();
- }
-
- private static Uri withDeleted(final Uri uri) {
- return uri.buildUpon()
- .appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1")
- .build();
- }
-
- private Cursor filterAllSites(ContentResolver cr, String[] projection, CharSequence constraint,
- int limit, CharSequence urlFilter, String selection, String[] selectionArgs) {
- // The combined history/bookmarks selection queries for sites with a URL or title containing
- // the constraint string(s), treating space-separated words as separate constraints
- if (!TextUtils.isEmpty(constraint)) {
- final String[] constraintWords = constraint.toString().split(" ");
-
- // Only create a filter query with a maximum of 10 constraint words.
- final int constraintCount = Math.min(constraintWords.length, 10);
- for (int i = 0; i < constraintCount; i++) {
- selection = DBUtils.concatenateWhere(selection, "(" + Combined.URL + " LIKE ? OR " +
- Combined.TITLE + " LIKE ?)");
- String constraintWord = "%" + constraintWords[i] + "%";
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { constraintWord, constraintWord });
- }
- }
-
- if (urlFilter != null) {
- selection = DBUtils.concatenateWhere(selection, "(" + Combined.URL + " NOT LIKE ?)");
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, new String[] { urlFilter.toString() });
- }
-
- // Order by combined remote+local frecency score.
- // Local visits are preferred, so they will by far outweigh remote visits.
- // Bookmarked history items get extra frecency points.
- final String sortOrder = BrowserContract.getCombinedFrecencySortOrder(true, false);
-
- return cr.query(combinedUriWithLimit(limit),
- projection,
- selection,
- selectionArgs,
- sortOrder);
- }
-
- @Override
- public int getCount(ContentResolver cr, String database) {
- int count = 0;
- String[] columns = null;
- String constraint = null;
- Uri uri = null;
-
- if ("history".equals(database)) {
- uri = mHistoryUriWithProfile;
- columns = new String[] { History._ID };
- constraint = Combined.VISITS + " > 0";
- } else if ("bookmarks".equals(database)) {
- uri = mBookmarksUriWithProfile;
- columns = new String[] { Bookmarks._ID };
- // ignore folders, tags, keywords, separators, etc.
- constraint = Bookmarks.TYPE + " = " + Bookmarks.TYPE_BOOKMARK;
- } else if ("thumbnails".equals(database)) {
- uri = mThumbnailsUriWithProfile;
- columns = new String[] { Thumbnails._ID };
- } else if ("favicons".equals(database)) {
- uri = mFaviconsUriWithProfile;
- columns = new String[] { Favicons._ID };
- }
-
- if (uri != null) {
- final Cursor cursor = cr.query(uri, columns, constraint, null, null);
-
- try {
- count = cursor.getCount();
- } finally {
- cursor.close();
- }
- }
-
- debug("Got count " + count + " for " + database);
- return count;
- }
-
- @Override
- @RobocopTarget
- public Cursor filter(ContentResolver cr, CharSequence constraint, int limit,
- EnumSet<FilterFlags> flags) {
- String selection = "";
- String[] selectionArgs = null;
-
- if (flags.contains(FilterFlags.EXCLUDE_PINNED_SITES)) {
- selection = Combined.URL + " NOT IN (SELECT " +
- Bookmarks.URL + " FROM bookmarks WHERE " +
- DBUtils.qualifyColumn("bookmarks", Bookmarks.PARENT) + " = ? AND " +
- DBUtils.qualifyColumn("bookmarks", Bookmarks.IS_DELETED) + " == 0)";
- selectionArgs = new String[] { String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) };
- }
-
- return filterAllSites(cr,
- new String[] { Combined._ID,
- Combined.URL,
- Combined.TITLE,
- Combined.BOOKMARK_ID,
- Combined.HISTORY_ID },
- constraint,
- limit,
- null,
- selection, selectionArgs);
- }
-
- @Override
- public void updateVisitedHistory(ContentResolver cr, String uri) {
- ContentValues values = new ContentValues();
-
- values.put(History.URL, uri);
- values.put(History.DATE_LAST_VISITED, System.currentTimeMillis());
- values.put(History.IS_DELETED, 0);
-
- // This will insert a new history entry if one for this URL
- // doesn't already exist
- cr.update(mUpdateHistoryUriWithProfile,
- values,
- History.URL + " = ?",
- new String[] { uri });
- }
-
- @Override
- public void updateHistoryTitle(ContentResolver cr, String uri, String title) {
- ContentValues values = new ContentValues();
- values.put(History.TITLE, title);
-
- cr.update(mHistoryUriWithProfile,
- values,
- History.URL + " = ?",
- new String[] { uri });
- }
-
- @Override
- @RobocopTarget
- public Cursor getAllVisitedHistory(ContentResolver cr) {
- return cr.query(mHistoryUriWithProfile,
- new String[] { History.URL },
- History.VISITS + " > 0",
- null,
- null);
- }
-
- @Override
- public Cursor getRecentHistory(ContentResolver cr, int limit) {
- return cr.query(combinedUriWithLimit(limit),
- new String[] { Combined._ID,
- Combined.BOOKMARK_ID,
- Combined.HISTORY_ID,
- Combined.URL,
- Combined.TITLE,
- Combined.DATE_LAST_VISITED,
- Combined.VISITS },
- History.DATE_LAST_VISITED + " > 0",
- null,
- History.DATE_LAST_VISITED + " DESC");
- }
-
- @Override
- public Cursor getRecentHistoryBetweenTime(ContentResolver cr, int limit, long start, long end) {
- return cr.query(combinedUriWithLimit(limit),
- new String[] { Combined._ID,
- Combined.BOOKMARK_ID,
- Combined.HISTORY_ID,
- Combined.URL,
- Combined.TITLE,
- Combined.DATE_LAST_VISITED,
- Combined.VISITS },
- History.DATE_LAST_VISITED + " >= " + start + " AND " + History.DATE_LAST_VISITED + " < " + end,
- null,
- History.DATE_LAST_VISITED + " DESC");
- }
-
- public Cursor getHistoryForURL(ContentResolver cr, String uri) {
- return cr.query(mHistoryUriWithProfile,
- new String[] {
- History.VISITS,
- History.DATE_LAST_VISITED
- },
- History.URL + "= ?",
- new String[] { uri },
- History.DATE_LAST_VISITED + " DESC"
- );
- }
-
- @Override
- public long getPrePathLastVisitedTimeMilliseconds(ContentResolver cr, String prePath) {
- if (prePath == null) {
- return 0;
- }
- // If we don't end with a trailing slash, then both https://foo.com and https://foo.company.biz will match.
- if (!prePath.endsWith("/")) {
- prePath = prePath + "/";
- }
- final Cursor cursor = cr.query(BrowserContract.History.CONTENT_URI,
- new String[] { "MAX(" + BrowserContract.HistoryColumns.DATE_LAST_VISITED + ") AS date" },
- BrowserContract.URLColumns.URL + " BETWEEN ? AND ?", new String[] { prePath, prePath + "\u007f" }, null);
- try {
- cursor.moveToFirst();
- if (cursor.isAfterLast()) {
- return 0;
- }
- return cursor.getLong(0);
- } finally {
- cursor.close();
- }
- }
-
- @Override
- public void expireHistory(ContentResolver cr, ExpirePriority priority) {
- Uri url = mHistoryExpireUriWithProfile;
- url = url.buildUpon().appendQueryParameter(BrowserContract.PARAM_EXPIRE_PRIORITY, priority.toString()).build();
- cr.delete(url, null, null);
- }
-
- @Override
- @RobocopTarget
- public void removeHistoryEntry(ContentResolver cr, String url) {
- cr.delete(mHistoryUriWithProfile,
- History.URL + " = ?",
- new String[] { url });
- }
-
- @Override
- public void clearHistory(ContentResolver cr, boolean clearSearchHistory) {
- if (clearSearchHistory) {
- cr.delete(mSearchHistoryUri, null, null);
- } else {
- cr.delete(mHistoryUriWithProfile, null, null);
- }
- }
-
- private void assertDefaultBookmarkColumnOrdering() {
- // We need to insert MatrixCursor values in a specific order - in order to protect against changes
- // in DEFAULT_BOOKMARK_COLUMNS we can just assert that we're using the correct ordering.
- // Alternatively we could use RowBuilder.add(columnName, value) but that needs api >= 19,
- // or we could iterate over DEFAULT_BOOKMARK_COLUMNS, but that gets messy once we need
- // to add more than one artificial folder.
- if (!((DEFAULT_BOOKMARK_COLUMNS[0].equals(Bookmarks._ID)) &&
- (DEFAULT_BOOKMARK_COLUMNS[1].equals(Bookmarks.GUID)) &&
- (DEFAULT_BOOKMARK_COLUMNS[2].equals(Bookmarks.URL)) &&
- (DEFAULT_BOOKMARK_COLUMNS[3].equals(Bookmarks.TITLE)) &&
- (DEFAULT_BOOKMARK_COLUMNS[4].equals(Bookmarks.TYPE)) &&
- (DEFAULT_BOOKMARK_COLUMNS[5].equals(Bookmarks.PARENT)) &&
- (DEFAULT_BOOKMARK_COLUMNS.length == 6))) {
- // If DEFAULT_BOOKMARK_COLUMNS changes we need to update all the MatrixCursor rows
- // to contain appropriate data.
- throw new IllegalStateException("Fake folder MatrixCursor creation code must be updated to match DEFAULT_BOOKMARK_COLUMNS");
- }
- }
-
- /**
- * Retrieve the list of reader-view bookmarks, i.e. the equivalent of the former reading-list.
- * This is the result of a join of bookmarks with reader-view annotations (as stored in
- * UrlAnnotations).
- */
- private Cursor getReadingListBookmarks(ContentResolver cr) {
- // group by URL to avoid having duplicate bookmarks listed. It's possible to have multiple
- // bookmarks pointing to the same URL (this would most commonly happen by manually
- // copying bookmarks on desktop, followed by syncing with mobile), and we don't want
- // to show the same URL multiple times in the reading list folder.
- final Uri bookmarksGroupedByUri = mBookmarksUriWithProfile.buildUpon()
- .appendQueryParameter(BrowserContract.PARAM_GROUP_BY, Bookmarks.URL)
- .build();
-
- return cr.query(bookmarksGroupedByUri,
- DEFAULT_BOOKMARK_COLUMNS,
- Bookmarks.ANNOTATION_KEY + " == ? AND " +
- Bookmarks.ANNOTATION_VALUE + " == ? AND " +
- "(" + Bookmarks.TYPE + " = ? AND " + Bookmarks.URL + " IS NOT NULL)",
- new String[] {
- BrowserContract.UrlAnnotations.Key.READER_VIEW.getDbValue(),
- BrowserContract.UrlAnnotations.READER_VIEW_SAVED_VALUE,
- String.valueOf(Bookmarks.TYPE_BOOKMARK) },
- null);
- }
-
- @Override
- @RobocopTarget
- public Cursor getBookmarksInFolder(ContentResolver cr, long folderId) {
- final boolean addDesktopFolder;
- final boolean addScreenshotsFolder;
- final boolean addReadingListFolder;
-
- // We always want to show mobile bookmarks in the root view.
- if (folderId == Bookmarks.FIXED_ROOT_ID) {
- folderId = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID);
-
- // We'll add a fake "Desktop Bookmarks" folder to the root view if desktop
- // bookmarks exist, so that the user can still access non-mobile bookmarks.
- addDesktopFolder = desktopBookmarksExist(cr);
- addScreenshotsFolder = AppConstants.SCREENSHOTS_IN_BOOKMARKS_ENABLED;
-
- final int readingListItemCount = getBookmarkCountForFolder(cr, Bookmarks.FAKE_READINGLIST_SMARTFOLDER_ID);
- addReadingListFolder = (readingListItemCount > 0);
- } else {
- addDesktopFolder = false;
- addScreenshotsFolder = false;
- addReadingListFolder = false;
- }
-
- final Cursor c;
-
- // (You can't switch on a long in Java, hence the if statements)
- if (folderId == Bookmarks.FAKE_DESKTOP_FOLDER_ID) {
- // Since the "Desktop Bookmarks" folder doesn't actually exist, we
- // just fake it by querying specifically certain known desktop folders.
- c = cr.query(mBookmarksUriWithProfile,
- DEFAULT_BOOKMARK_COLUMNS,
- Bookmarks.GUID + " = ? OR " +
- Bookmarks.GUID + " = ? OR " +
- Bookmarks.GUID + " = ?",
- new String[] { Bookmarks.TOOLBAR_FOLDER_GUID,
- Bookmarks.MENU_FOLDER_GUID,
- Bookmarks.UNFILED_FOLDER_GUID },
- null);
- } else if (folderId == Bookmarks.FIXED_SCREENSHOT_FOLDER_ID) {
- c = getUrlAnnotations().getScreenshots(cr);
- } else if (folderId == Bookmarks.FAKE_READINGLIST_SMARTFOLDER_ID) {
- c = getReadingListBookmarks(cr);
- } else {
- // Right now, we only support showing folder and bookmark type of
- // entries. We should add support for other types though (bug 737024)
- c = cr.query(mBookmarksUriWithProfile,
- DEFAULT_BOOKMARK_COLUMNS,
- Bookmarks.PARENT + " = ? AND " +
- "(" + Bookmarks.TYPE + " = ? OR " +
- "(" + Bookmarks.TYPE + " = ? AND " + Bookmarks.URL + " IS NOT NULL))",
- new String[] { String.valueOf(folderId),
- String.valueOf(Bookmarks.TYPE_FOLDER),
- String.valueOf(Bookmarks.TYPE_BOOKMARK) },
- null);
- }
-
- final List<Cursor> cursorsToMerge = getSpecialFoldersCursorList(addDesktopFolder, addScreenshotsFolder, addReadingListFolder);
- if (cursorsToMerge.size() >= 1) {
- cursorsToMerge.add(c);
- final Cursor[] arr = (Cursor[]) Array.newInstance(Cursor.class, cursorsToMerge.size());
- return new MergeCursor(cursorsToMerge.toArray(arr));
- } else {
- return c;
- }
- }
-
- @Override
- public int getBookmarkCountForFolder(ContentResolver cr, long folderID) {
- if (folderID == Bookmarks.FAKE_READINGLIST_SMARTFOLDER_ID) {
- return getUrlAnnotations().getAnnotationCount(cr, BrowserContract.UrlAnnotations.Key.READER_VIEW);
- } else {
- throw new IllegalArgumentException("Retrieving bookmark count for folder with ID=" + folderID + " not supported yet");
- }
- }
-
- @CheckResult
- private ArrayList<Cursor> getSpecialFoldersCursorList(final boolean addDesktopFolder,
- final boolean addScreenshotsFolder, final boolean addReadingListFolder) {
- if (addDesktopFolder || addScreenshotsFolder || addReadingListFolder) {
- // Avoid calling this twice.
- assertDefaultBookmarkColumnOrdering();
- }
-
- // Capacity is number of cursors added below plus one for non-special data.
- final ArrayList<Cursor> out = new ArrayList<>(4);
- if (addDesktopFolder) {
- out.add(getSpecialFolderCursor(Bookmarks.FAKE_DESKTOP_FOLDER_ID, Bookmarks.FAKE_DESKTOP_FOLDER_GUID));
- }
-
- if (addScreenshotsFolder) {
- out.add(getSpecialFolderCursor(Bookmarks.FIXED_SCREENSHOT_FOLDER_ID, Bookmarks.SCREENSHOT_FOLDER_GUID));
- }
-
- if (addReadingListFolder) {
- out.add(getSpecialFolderCursor(Bookmarks.FAKE_READINGLIST_SMARTFOLDER_ID, Bookmarks.FAKE_READINGLIST_SMARTFOLDER_GUID));
- }
-
- return out;
- }
-
- @CheckResult
- private MatrixCursor getSpecialFolderCursor(final int folderId, final String folderGuid) {
- final MatrixCursor out = new MatrixCursor(DEFAULT_BOOKMARK_COLUMNS);
- out.addRow(new Object[] {
- folderId,
- folderGuid,
- "",
- "", // Title localisation is done later, in the UI layer (BookmarksListAdapter)
- Bookmarks.TYPE_FOLDER,
- Bookmarks.FIXED_ROOT_ID
- });
- return out;
- }
-
- // Returns true if any desktop bookmarks exist, which will be true if the user
- // has set up sync at one point, or done a profile migration from XUL fennec.
- private boolean desktopBookmarksExist(ContentResolver cr) {
- if (mDesktopBookmarksExist != null) {
- return mDesktopBookmarksExist;
- }
-
- // Check to see if there are any bookmarks in one of our three
- // fixed "Desktop Bookmarks" folders.
- final Cursor c = cr.query(bookmarksUriWithLimit(1),
- new String[] { Bookmarks._ID },
- Bookmarks.PARENT + " = ? OR " +
- Bookmarks.PARENT + " = ? OR " +
- Bookmarks.PARENT + " = ?",
- new String[] { String.valueOf(getFolderIdFromGuid(cr, Bookmarks.TOOLBAR_FOLDER_GUID)),
- String.valueOf(getFolderIdFromGuid(cr, Bookmarks.MENU_FOLDER_GUID)),
- String.valueOf(getFolderIdFromGuid(cr, Bookmarks.UNFILED_FOLDER_GUID)) },
- null);
-
- try {
- // Don't read back out of the cache to avoid races with invalidation.
- final boolean e = c.getCount() > 0;
- mDesktopBookmarksExist = e;
- return e;
- } finally {
- c.close();
- }
- }
-
- @Override
- @RobocopTarget
- public boolean isBookmark(ContentResolver cr, String uri) {
- final Cursor c = cr.query(bookmarksUriWithLimit(1),
- new String[] { Bookmarks._ID },
- Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " != ?",
- new String[] { uri, String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) },
- Bookmarks.URL);
-
- if (c == null) {
- Log.e(LOGTAG, "Null cursor in isBookmark");
- return false;
- }
-
- try {
- return c.getCount() > 0;
- } finally {
- c.close();
- }
- }
-
- @Override
- public String getUrlForKeyword(ContentResolver cr, String keyword) {
- final Cursor c = cr.query(mBookmarksUriWithProfile,
- new String[] { Bookmarks.URL },
- Bookmarks.KEYWORD + " = ?",
- new String[] { keyword },
- null);
- try {
- if (!c.moveToFirst()) {
- return null;
- }
-
- return c.getString(c.getColumnIndexOrThrow(Bookmarks.URL));
- } finally {
- c.close();
- }
- }
-
- private synchronized long getFolderIdFromGuid(final ContentResolver cr, final String guid) {
- if (mFolderIdMap.containsKey(guid)) {
- return mFolderIdMap.get(guid);
- }
-
- final Cursor c = cr.query(mBookmarksUriWithProfile,
- new String[] { Bookmarks._ID },
- Bookmarks.GUID + " = ?",
- new String[] { guid },
- null);
- try {
- final int col = c.getColumnIndexOrThrow(Bookmarks._ID);
- if (!c.moveToFirst() || c.isNull(col)) {
- return FOLDER_NOT_FOUND;
- }
-
- final long id = c.getLong(col);
- mFolderIdMap.put(guid, id);
- return id;
- } finally {
- c.close();
- }
- }
-
- /**
- * Find parents of records that match the provided criteria, and bump their
- * modified timestamp.
- */
- protected void bumpParents(ContentResolver cr, String param, String value) {
- ContentValues values = new ContentValues();
- values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
-
- String where = param + " = ?";
- String[] args = new String[] { value };
- int updated = cr.update(mParentsUriWithProfile, values, where, args);
- debug("Updated " + updated + " rows to new modified time.");
- }
-
- private void addBookmarkItem(ContentResolver cr, String title, String uri, long folderId) {
- final long now = System.currentTimeMillis();
- ContentValues values = new ContentValues();
- if (title != null) {
- values.put(Bookmarks.TITLE, title);
- }
-
- values.put(Bookmarks.URL, uri);
- values.put(Bookmarks.PARENT, folderId);
- values.put(Bookmarks.DATE_MODIFIED, now);
-
- // Get the page's favicon ID from the history table
- final Cursor c = cr.query(mHistoryUriWithProfile,
- new String[] { History.FAVICON_ID },
- History.URL + " = ?",
- new String[] { uri },
- null);
- try {
- if (c.moveToFirst()) {
- int columnIndex = c.getColumnIndexOrThrow(History.FAVICON_ID);
- if (!c.isNull(columnIndex)) {
- values.put(Bookmarks.FAVICON_ID, c.getLong(columnIndex));
- }
- }
- } finally {
- c.close();
- }
-
- // Restore deleted record if possible
- values.put(Bookmarks.IS_DELETED, 0);
-
- final Uri bookmarksWithInsert = mBookmarksUriWithProfile.buildUpon()
- .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true")
- .build();
- cr.update(bookmarksWithInsert,
- values,
- Bookmarks.URL + " = ? AND " +
- Bookmarks.PARENT + " = " + folderId,
- new String[] { uri });
-
- // Bump parent modified time using its ID.
- debug("Bumping parent modified time for addition to: " + folderId);
- final String where = Bookmarks._ID + " = ?";
- final String[] args = new String[] { String.valueOf(folderId) };
-
- ContentValues bumped = new ContentValues();
- bumped.put(Bookmarks.DATE_MODIFIED, now);
-
- final int updated = cr.update(mBookmarksUriWithProfile, bumped, where, args);
- debug("Updated " + updated + " rows to new modified time.");
- }
-
- @Override
- @RobocopTarget
- public boolean addBookmark(ContentResolver cr, String title, String uri) {
- long folderId = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID);
- if (isBookmarkForUrlInFolder(cr, uri, folderId)) {
- // Bookmark added already.
- return false;
- }
-
- // Add a new bookmark.
- addBookmarkItem(cr, title, uri, folderId);
- return true;
- }
-
- private boolean isBookmarkForUrlInFolder(ContentResolver cr, String uri, long folderId) {
- final Cursor c = cr.query(bookmarksUriWithLimit(1),
- new String[] { Bookmarks._ID },
- Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " = ? AND " + Bookmarks.IS_DELETED + " == 0",
- new String[] { uri, String.valueOf(folderId) },
- Bookmarks.URL);
-
- if (c == null) {
- return false;
- }
-
- try {
- return c.getCount() > 0;
- } finally {
- c.close();
- }
- }
-
- @Override
- @RobocopTarget
- public void removeBookmarksWithURL(ContentResolver cr, String uri) {
- Uri contentUri = mBookmarksUriWithProfile;
-
- // Do this now so that the items still exist!
- bumpParents(cr, Bookmarks.URL, uri);
-
- final String[] urlArgs = new String[] { uri, String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) };
- final String urlEquals = Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " != ? ";
-
- cr.delete(contentUri, urlEquals, urlArgs);
- }
-
- @Override
- public void registerBookmarkObserver(ContentResolver cr, ContentObserver observer) {
- cr.registerContentObserver(mBookmarksUriWithProfile, false, observer);
- }
-
- @Override
- @RobocopTarget
- public void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword) {
- ContentValues values = new ContentValues();
- values.put(Bookmarks.TITLE, title);
- values.put(Bookmarks.URL, uri);
- values.put(Bookmarks.KEYWORD, keyword);
- values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
-
- cr.update(mBookmarksUriWithProfile,
- values,
- Bookmarks._ID + " = ?",
- new String[] { String.valueOf(id) });
- }
-
- @Override
- public boolean hasBookmarkWithGuid(ContentResolver cr, String guid) {
- Cursor c = cr.query(bookmarksUriWithLimit(1),
- new String[] { Bookmarks.GUID },
- Bookmarks.GUID + " = ?",
- new String[] { guid },
- null);
-
- try {
- return c != null && c.getCount() > 0;
- } finally {
- if (c != null) {
- c.close();
- }
- }
- }
-
- /**
- * Get the favicon from the database, if any, associated with the given favicon URL. (That is,
- * the URL of the actual favicon image, not the URL of the page with which the favicon is associated.)
- * @param cr The ContentResolver to use.
- * @param faviconURL The URL of the favicon to fetch from the database.
- * @return The decoded Bitmap from the database, if any. null if none is stored.
- */
- @Override
- public LoadFaviconResult getFaviconForUrl(Context context, ContentResolver cr, String faviconURL) {
- final Cursor c = cr.query(mFaviconsUriWithProfile,
- new String[] { Favicons.DATA },
- Favicons.URL + " = ? AND " + Favicons.DATA + " IS NOT NULL",
- new String[] { faviconURL },
- null);
-
- boolean shouldDelete = false;
- byte[] b = null;
- try {
- if (!c.moveToFirst()) {
- return null;
- }
-
- final int faviconIndex = c.getColumnIndexOrThrow(Favicons.DATA);
- try {
- b = c.getBlob(faviconIndex);
- } catch (IllegalStateException e) {
- // This happens when the blob is more than 1MB: Bug 1106347.
- // Delete that row.
- shouldDelete = true;
- }
- } finally {
- c.close();
- }
-
- if (shouldDelete) {
- try {
- Log.d(LOGTAG, "Deleting invalid favicon.");
- cr.delete(mFaviconsUriWithProfile,
- Favicons.URL + " = ?",
- new String[] { faviconURL });
- } catch (Exception e) {
- // Do nothing.
- }
- }
-
- if (b == null) {
- return null;
- }
-
- return FaviconDecoder.decodeFavicon(context, b);
- }
-
- /**
- * Try to find a usable favicon URL in the history or bookmarks table.
- */
- @Override
- public String getFaviconURLFromPageURL(ContentResolver cr, String uri) {
- // Check first in the history table.
- Cursor c = cr.query(mHistoryUriWithProfile,
- new String[] { History.FAVICON_URL },
- Combined.URL + " = ?",
- new String[] { uri },
- null);
-
- try {
- if (c.moveToFirst()) {
- // Interrupted page loads can leave History items without a valid favicon_id.
- final int columnIndex = c.getColumnIndexOrThrow(History.FAVICON_URL);
- if (!c.isNull(columnIndex)) {
- final String faviconURL = c.getString(columnIndex);
- if (faviconURL != null) {
- return faviconURL;
- }
- }
- }
- } finally {
- c.close();
- }
-
- // If that fails, check in the bookmarks table.
- c = cr.query(mBookmarksUriWithProfile,
- new String[] { Bookmarks.FAVICON_URL },
- Bookmarks.URL + " = ?",
- new String[] { uri },
- null);
-
- try {
- if (c.moveToFirst()) {
- return c.getString(c.getColumnIndexOrThrow(Bookmarks.FAVICON_URL));
- }
-
- return null;
- } finally {
- c.close();
- }
- }
-
- @Override
- public boolean hideSuggestedSite(String url) {
- if (mSuggestedSites == null) {
- return false;
- }
-
- return mSuggestedSites.hideSite(url);
- }
-
- @Override
- public void updateThumbnailForUrl(ContentResolver cr, String uri,
- BitmapDrawable thumbnail) {
- // If a null thumbnail was passed in, delete the stored thumbnail for this url.
- if (thumbnail == null) {
- cr.delete(mThumbnailsUriWithProfile, Thumbnails.URL + " == ?", new String[] { uri });
- return;
- }
-
- Bitmap bitmap = thumbnail.getBitmap();
-
- byte[] data = null;
- ByteArrayOutputStream stream = new ByteArrayOutputStream();
- if (bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream)) {
- data = stream.toByteArray();
- } else {
- Log.w(LOGTAG, "Favicon compression failed.");
- }
-
- ContentValues values = new ContentValues();
- values.put(Thumbnails.URL, uri);
- values.put(Thumbnails.DATA, data);
-
- Uri thumbnailsUri = mThumbnailsUriWithProfile.buildUpon().
- appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build();
- cr.update(thumbnailsUri,
- values,
- Thumbnails.URL + " = ?",
- new String[] { uri });
- }
-
- @Override
- @RobocopTarget
- public byte[] getThumbnailForUrl(ContentResolver cr, String uri) {
- final Cursor c = cr.query(mThumbnailsUriWithProfile,
- new String[]{ Thumbnails.DATA },
- Thumbnails.URL + " = ? AND " + Thumbnails.DATA + " IS NOT NULL",
- new String[]{ uri },
- null);
- try {
- if (!c.moveToFirst()) {
- return null;
- }
-
- int thumbnailIndex = c.getColumnIndexOrThrow(Thumbnails.DATA);
-
- return c.getBlob(thumbnailIndex);
- } finally {
- c.close();
- }
-
- }
-
- /**
- * Query for non-null thumbnails matching the provided <code>urls</code>.
- * The returned cursor will have no more than, but possibly fewer than,
- * the requested number of thumbnails.
- *
- * Returns null if the provided list of URLs is empty or null.
- */
- @Override
- public Cursor getThumbnailsForUrls(ContentResolver cr, List<String> urls) {
- final int urlCount = urls.size();
- if (urlCount == 0) {
- return null;
- }
-
- // Don't match against null thumbnails.
- final String selection = Thumbnails.DATA + " IS NOT NULL AND " +
- DBUtils.computeSQLInClause(urlCount, Thumbnails.URL);
- final String[] selectionArgs = urls.toArray(new String[urlCount]);
-
- return cr.query(mThumbnailsUriWithProfile,
- new String[] { Thumbnails.URL, Thumbnails.DATA },
- selection,
- selectionArgs,
- null);
- }
-
- @Override
- @RobocopTarget
- public void removeThumbnails(ContentResolver cr) {
- cr.delete(mThumbnailsUriWithProfile, null, null);
- }
-
- /**
- * Utility method used by AndroidImport for updating existing history record using batch operations.
- *
- * @param cr <code>ContentResolver</code> used for querying information about existing history records.
- * @param operations Collection of operations for queueing record updates.
- * @param url URL used for querying history records to update.
- * @param title Optional new title.
- * @param date New last visited date. Will be used if newer than current last visited date.
- * @param visits Will increment existing visit counts by this number.
- */
- @Override
- public void updateHistoryInBatch(@NonNull ContentResolver cr,
- @NonNull Collection<ContentProviderOperation> operations,
- @NonNull String url, @Nullable String title,
- long date, int visits) {
- final String[] projection = {
- History._ID,
- History.VISITS,
- History.LOCAL_VISITS,
- History.DATE_LAST_VISITED,
- History.LOCAL_DATE_LAST_VISITED
- };
-
- // We need to get the old visit and date aggregates.
- final Cursor cursor = cr.query(withDeleted(mHistoryUriWithProfile),
- projection,
- History.URL + " = ?",
- new String[] { url },
- null);
- if (cursor == null) {
- Log.w(LOGTAG, "Null cursor while querying for old visit and date aggregates");
- return;
- }
-
- try {
- final ContentValues values = new ContentValues();
-
- // Restore deleted record if possible
- values.put(History.IS_DELETED, 0);
-
- if (cursor.moveToFirst()) {
- final int visitsCol = cursor.getColumnIndexOrThrow(History.VISITS);
- final int localVisitsCol = cursor.getColumnIndexOrThrow(History.LOCAL_VISITS);
- final int dateCol = cursor.getColumnIndexOrThrow(History.DATE_LAST_VISITED);
- final int localDateCol = cursor.getColumnIndexOrThrow(History.LOCAL_DATE_LAST_VISITED);
-
- final int oldVisits = cursor.getInt(visitsCol);
- final int oldLocalVisits = cursor.getInt(localVisitsCol);
- final long oldDate = cursor.getLong(dateCol);
- final long oldLocalDate = cursor.getLong(localDateCol);
-
- // NB: This will increment visit counts even if subsequent "insert visits" operations
- // insert no new visits (see insertVisitsFromImportHistoryInBatch).
- // So, we're doing a wrong thing here if user imports history more than once.
- // See Bug 1277330.
- values.put(History.VISITS, oldVisits + visits);
- values.put(History.LOCAL_VISITS, oldLocalVisits + visits);
- // Only update last visited if newer.
- if (date > oldDate) {
- values.put(History.DATE_LAST_VISITED, date);
- }
- if (date > oldLocalDate) {
- values.put(History.LOCAL_DATE_LAST_VISITED, date);
- }
- } else {
- values.put(History.VISITS, visits);
- values.put(History.LOCAL_VISITS, visits);
- values.put(History.DATE_LAST_VISITED, date);
- values.put(History.LOCAL_DATE_LAST_VISITED, date);
- }
- if (title != null) {
- values.put(History.TITLE, title);
- }
- values.put(History.URL, url);
-
- final Uri historyUri = withDeleted(mHistoryUriWithProfile).buildUpon().
- appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build();
-
- // Update or insert
- final ContentProviderOperation.Builder builder =
- ContentProviderOperation.newUpdate(historyUri);
- builder.withSelection(History.URL + " = ?", new String[] { url });
- builder.withValues(values);
-
- // Queue the operation
- operations.add(builder.build());
- } finally {
- cursor.close();
- }
- }
-
- /**
- * Utility method used by AndroidImport to insert visit data for history records that were just imported.
- * Uses batch operations.
- *
- * @param cr <code>ContentResolver</code> used to query history table and bulkInsert visit records
- * @param operations Collection of operations for queueing inserts
- * @param visitsToSynthesize List of ContentValues describing visit information for each history record:
- * (History URL, LAST DATE VISITED, VISIT COUNT)
- */
- public void insertVisitsFromImportHistoryInBatch(ContentResolver cr,
- Collection<ContentProviderOperation> operations,
- ArrayList<ContentValues> visitsToSynthesize) {
- // If for any reason we fail to obtain history GUID for a tuple we're processing,
- // let's just ignore it. It's possible that the "best-effort" history import
- // did not fully succeed, so we could be missing some of the records.
- int historyGUIDCol = -1;
- for (ContentValues visitsInformation : visitsToSynthesize) {
- final Cursor cursor = cr.query(mHistoryUriWithProfile,
- new String[] {History.GUID},
- History.URL + " = ?",
- new String[] {visitsInformation.getAsString(HISTORY_VISITS_URL)},
- null);
- if (cursor == null) {
- continue;
- }
-
- final String historyGUID;
-
- try {
- if (!cursor.moveToFirst()) {
- continue;
- }
- if (historyGUIDCol == -1) {
- historyGUIDCol = cursor.getColumnIndexOrThrow(History.GUID);
- }
-
- historyGUID = cursor.getString(historyGUIDCol);
- } finally {
- // We "continue" on a null cursor above, so it's safe to act upon it without checking.
- cursor.close();
- }
- if (historyGUID == null) {
- continue;
- }
-
- // This fakes the individual visit records, using last visited date as the starting point.
- for (int i = 0; i < visitsInformation.getAsInteger(HISTORY_VISITS_COUNT); i++) {
- // We rely on database defaults for IS_LOCAL and VISIT_TYPE.
- final ContentValues visitToInsert = new ContentValues();
- visitToInsert.put(BrowserContract.Visits.HISTORY_GUID, historyGUID);
-
- // Visit timestamps are stored in microseconds, while Android Browser visit timestmaps
- // are in milliseconds. This is the conversion point for imports.
- visitToInsert.put(BrowserContract.Visits.DATE_VISITED,
- (visitsInformation.getAsLong(HISTORY_VISITS_DATE) - i) * 1000);
-
- final ContentProviderOperation.Builder builder =
- ContentProviderOperation.newInsert(BrowserContract.Visits.CONTENT_URI);
- builder.withValues(visitToInsert);
-
- // Queue the insert operation
- operations.add(builder.build());
- }
- }
- }
-
- @Override
- public void updateBookmarkInBatch(ContentResolver cr,
- Collection<ContentProviderOperation> operations,
- String url, String title, String guid,
- long parent, long added,
- long modified, long position,
- String keyword, int type) {
- ContentValues values = new ContentValues();
- if (title == null && url != null) {
- title = url;
- }
- if (title != null) {
- values.put(Bookmarks.TITLE, title);
- }
- if (url != null) {
- values.put(Bookmarks.URL, url);
- }
- if (guid != null) {
- values.put(SyncColumns.GUID, guid);
- }
- if (keyword != null) {
- values.put(Bookmarks.KEYWORD, keyword);
- }
- if (added > 0) {
- values.put(SyncColumns.DATE_CREATED, added);
- }
- if (modified > 0) {
- values.put(SyncColumns.DATE_MODIFIED, modified);
- }
- values.put(Bookmarks.POSITION, position);
- // Restore deleted record if possible
- values.put(Bookmarks.IS_DELETED, 0);
-
- // This assumes no "real" folder has a negative ID. Only
- // things like the reading list folder do.
- if (parent < 0) {
- parent = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID);
- }
- values.put(Bookmarks.PARENT, parent);
- values.put(Bookmarks.TYPE, type);
-
- Uri bookmarkUri = withDeleted(mBookmarksUriWithProfile).buildUpon().
- appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build();
- // Update or insert
- ContentProviderOperation.Builder builder =
- ContentProviderOperation.newUpdate(bookmarkUri);
- if (url != null) {
- // Bookmarks are defined by their URL and Folder.
- builder.withSelection(Bookmarks.URL + " = ? AND "
- + Bookmarks.PARENT + " = ?",
- new String[] { url,
- Long.toString(parent)
- });
- } else if (title != null) {
- // Or their title and parent folder. (Folders!)
- builder.withSelection(Bookmarks.TITLE + " = ? AND "
- + Bookmarks.PARENT + " = ?",
- new String[]{ title,
- Long.toString(parent)
- });
- } else if (type == Bookmarks.TYPE_SEPARATOR) {
- // Or their their position (separators)
- builder.withSelection(Bookmarks.POSITION + " = ? AND "
- + Bookmarks.PARENT + " = ?",
- new String[] { Long.toString(position),
- Long.toString(parent)
- });
- } else {
- Log.e(LOGTAG, "Bookmark entry without url or title and not a separator, not added.");
- }
- builder.withValues(values);
-
- // Queue the operation
- operations.add(builder.build());
- }
-
- @Override
- public void pinSite(ContentResolver cr, String url, String title, int position) {
- ContentValues values = new ContentValues();
- final long now = System.currentTimeMillis();
- values.put(Bookmarks.TITLE, title);
- values.put(Bookmarks.URL, url);
- values.put(Bookmarks.PARENT, Bookmarks.FIXED_PINNED_LIST_ID);
- values.put(Bookmarks.DATE_MODIFIED, now);
- values.put(Bookmarks.POSITION, position);
- values.put(Bookmarks.IS_DELETED, 0);
-
- // We do an update-and-replace here without deleting any existing pins for the given URL.
- // That means if the user pins a URL, then edits another thumbnail to use the same URL,
- // we'll end up with two pins for that site. This is the intended behavior, which
- // incidentally saves us a delete query.
- Uri uri = mBookmarksUriWithProfile.buildUpon()
- .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build();
- cr.update(uri,
- values,
- Bookmarks.POSITION + " = ? AND " +
- Bookmarks.PARENT + " = ?",
- new String[] { Integer.toString(position),
- String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) });
- }
-
- @Override
- public void unpinSite(ContentResolver cr, int position) {
- cr.delete(mBookmarksUriWithProfile,
- Bookmarks.PARENT + " == ? AND " + Bookmarks.POSITION + " = ?",
- new String[] {
- String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID),
- Integer.toString(position)
- });
- }
-
- @Override
- @RobocopTarget
- public Cursor getBookmarkForUrl(ContentResolver cr, String url) {
- Cursor c = cr.query(bookmarksUriWithLimit(1),
- new String[] { Bookmarks._ID,
- Bookmarks.URL,
- Bookmarks.TITLE,
- Bookmarks.KEYWORD },
- Bookmarks.URL + " = ?",
- new String[] { url },
- null);
-
- if (c != null && c.getCount() == 0) {
- c.close();
- c = null;
- }
-
- return c;
- }
-
- @Override
- public Cursor getBookmarksForPartialUrl(ContentResolver cr, String partialUrl) {
- Cursor c = cr.query(mBookmarksUriWithProfile,
- new String[] { Bookmarks.GUID, Bookmarks._ID, Bookmarks.URL },
- Bookmarks.URL + " LIKE '%" + partialUrl + "%'", // TODO: Escaping!
- null,
- null);
-
- if (c != null && c.getCount() == 0) {
- c.close();
- c = null;
- }
-
- return c;
- }
-
- @Override
- public void setSuggestedSites(SuggestedSites suggestedSites) {
- mSuggestedSites = suggestedSites;
- }
-
- @Override
- public SuggestedSites getSuggestedSites() {
- return mSuggestedSites;
- }
-
- @Override
- public boolean hasSuggestedImageUrl(String url) {
- if (mSuggestedSites == null) {
- return false;
- }
- return mSuggestedSites.contains(url);
- }
-
- @Override
- public String getSuggestedImageUrlForUrl(String url) {
- if (mSuggestedSites == null) {
- return null;
- }
- return mSuggestedSites.getImageUrlForUrl(url);
- }
-
- @Override
- public int getSuggestedBackgroundColorForUrl(String url) {
- if (mSuggestedSites == null) {
- return 0;
- }
- final String bgColor = mSuggestedSites.getBackgroundColorForUrl(url);
- if (bgColor != null) {
- return Color.parseColor(bgColor);
- }
-
- return 0;
- }
-
- private static void appendUrlsFromCursor(List<String> urls, Cursor c) {
- if (!c.moveToFirst()) {
- return;
- }
-
- do {
- String url = c.getString(c.getColumnIndex(History.URL));
-
- // Do a simpler check before decoding to avoid parsing
- // all URLs unnecessarily.
- if (StringUtils.isUserEnteredUrl(url)) {
- url = StringUtils.decodeUserEnteredUrl(url);
- }
-
- urls.add(url);
- } while (c.moveToNext());
- }
-
-
- /**
- * Internal CursorLoader that extends the framework CursorLoader in order to measure
- * performance for telemetry purposes.
- */
- private static final class TelemetrisedCursorLoader extends CursorLoader {
- final String mHistogramName;
-
- public TelemetrisedCursorLoader(Context context, Uri uri, String[] projection, String selection,
- String[] selectionArgs, String sortOrder,
- final String histogramName) {
- super(context, uri, projection, selection, selectionArgs, sortOrder);
- mHistogramName = histogramName;
- }
-
- @Override
- public Cursor loadInBackground() {
- final long start = SystemClock.uptimeMillis();
-
- final Cursor cursor = super.loadInBackground();
-
- final long end = SystemClock.uptimeMillis();
- final long took = end - start;
-
- Telemetry.addToHistogram(mHistogramName, (int) Math.min(took, Integer.MAX_VALUE));
- return cursor;
- }
- }
-
- public CursorLoader getActivityStreamTopSites(Context context, int limit) {
- final Uri uri = mTopSitesUriWithProfile.buildUpon()
- .appendQueryParameter(BrowserContract.PARAM_LIMIT,
- String.valueOf(limit))
- .appendQueryParameter(BrowserContract.PARAM_TOPSITES_DISABLE_PINNED, Boolean.TRUE.toString())
- .build();
-
- return new TelemetrisedCursorLoader(context,
- uri,
- new String[]{ Combined._ID,
- Combined.URL,
- Combined.TITLE,
- Combined.BOOKMARK_ID,
- Combined.HISTORY_ID },
- null,
- null,
- null,
- TELEMETRY_HISTOGRAM_ACITIVITY_STREAM_TOPSITES);
- }
-
- @Override
- public Cursor getTopSites(ContentResolver cr, int suggestedRangeLimit, int limit) {
- final Uri uri = mTopSitesUriWithProfile.buildUpon()
- .appendQueryParameter(BrowserContract.PARAM_LIMIT,
- String.valueOf(limit))
- .appendQueryParameter(BrowserContract.PARAM_SUGGESTEDSITES_LIMIT,
- String.valueOf(suggestedRangeLimit))
- .build();
-
- Cursor topSitesCursor = cr.query(uri,
- new String[] { Combined._ID,
- Combined.URL,
- Combined.TITLE,
- Combined.BOOKMARK_ID,
- Combined.HISTORY_ID },
- null,
- null,
- null);
-
- // It's possible that we will retrieve fewer sites than are required to fill the top-sites panel - in this case
- // we need to add "blank" tiles. It's much easier to add these here (as opposed to SQL), since we don't care
- // about their ordering (they go after all the other sites), but we do care about their number (and calculating
- // that inside out topsites SQL query would be difficult given the other processing we're already doing there).
- final int blanksRequired = suggestedRangeLimit - topSitesCursor.getCount();
-
- if (blanksRequired <= 0) {
- return topSitesCursor;
- }
-
- MatrixCursor blanksCursor = new MatrixCursor(new String[] {
- TopSites._ID,
- TopSites.BOOKMARK_ID,
- TopSites.HISTORY_ID,
- TopSites.URL,
- TopSites.TITLE,
- TopSites.TYPE});
-
- final MatrixCursor.RowBuilder rb = blanksCursor.newRow();
- rb.add(-1);
- rb.add(-1);
- rb.add(-1);
- rb.add("");
- rb.add("");
- rb.add(TopSites.TYPE_BLANK);
-
- return new MergeCursor(new Cursor[] {topSitesCursor, blanksCursor});
- }
-
- @Override
- public CursorLoader getHighlights(Context context, int limit) {
- final Uri uri = mHighlightsUriWithProfile.buildUpon()
- .appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit))
- .build();
-
- return new CursorLoader(context, uri, null, null, null, null);
- }
-
- @Override
- public void blockActivityStreamSite(ContentResolver cr, String url) {
- final ContentValues values = new ContentValues();
- values.put(ActivityStreamBlocklist.URL, url);
- cr.insert(mActivityStreamBlockedUriWithProfile, values);
- }
-
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/LocalSearches.java b/mobile/android/base/java/org/mozilla/gecko/db/LocalSearches.java
deleted file mode 100644
index a9a55e51d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalSearches.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.net.Uri;
-
-/**
- * Helper class for dealing with the search provider inside Fennec.
- */
-public class LocalSearches implements Searches {
- private final Uri uriWithProfile;
-
- public LocalSearches(String mProfile) {
- uriWithProfile = DBUtils.appendProfileWithDefault(mProfile, BrowserContract.SearchHistory.CONTENT_URI);
- }
-
- @Override
- public void insert(ContentResolver cr, String query) {
- final ContentValues values = new ContentValues();
- values.put(BrowserContract.SearchHistory.QUERY, query);
- cr.insert(uriWithProfile, values);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/LocalTabsAccessor.java b/mobile/android/base/java/org/mozilla/gecko/db/LocalTabsAccessor.java
deleted file mode 100644
index c7bd9475c..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalTabsAccessor.java
+++ /dev/null
@@ -1,320 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-import java.util.regex.Pattern;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.UIAsyncTask;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.net.Uri;
-import android.text.TextUtils;
-import android.util.Log;
-
-public class LocalTabsAccessor implements TabsAccessor {
- private static final String LOGTAG = "GeckoTabsAccessor";
- private static final long THREE_WEEKS_IN_MILLISECONDS = TimeUnit.MILLISECONDS.convert(21L, TimeUnit.DAYS);
-
- public static final String[] TABS_PROJECTION_COLUMNS = new String[] {
- BrowserContract.Tabs.TITLE,
- BrowserContract.Tabs.URL,
- BrowserContract.Clients.GUID,
- BrowserContract.Clients.NAME,
- BrowserContract.Tabs.LAST_USED,
- BrowserContract.Clients.LAST_MODIFIED,
- BrowserContract.Clients.DEVICE_TYPE,
- };
-
- public static final String[] CLIENTS_PROJECTION_COLUMNS = new String[] {
- BrowserContract.Clients.GUID,
- BrowserContract.Clients.NAME,
- BrowserContract.Clients.LAST_MODIFIED,
- BrowserContract.Clients.DEVICE_TYPE
- };
-
- private static final String REMOTE_CLIENTS_SELECTION = BrowserContract.Clients.GUID + " IS NOT NULL";
- private static final String LOCAL_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NULL";
- private static final String REMOTE_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NOT NULL";
- private static final String REMOTE_TABS_SELECTION_CLIENT_RECENCY = REMOTE_TABS_SELECTION +
- " AND " + BrowserContract.Clients.LAST_MODIFIED + " > ?";
-
- private static final String REMOTE_TABS_SORT_ORDER =
- // Most recently synced clients first.
- BrowserContract.Clients.LAST_MODIFIED + " DESC, " +
- // If two clients somehow had the same last modified time, this will
- // group them (arbitrarily).
- BrowserContract.Clients.GUID + " DESC, " +
- // Within a single client, most recently used tabs first.
- BrowserContract.Tabs.LAST_USED + " DESC";
-
- private static final String LOCAL_CLIENT_SELECTION = BrowserContract.Clients.GUID + " IS NULL";
-
- private static final Pattern FILTERED_URL_PATTERN = Pattern.compile("^(about|chrome|wyciwyg|file):");
-
- private final Uri clientsRecencyUriWithProfile;
- private final Uri tabsUriWithProfile;
- private final Uri clientsUriWithProfile;
-
- public LocalTabsAccessor(String profileName) {
- tabsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Tabs.CONTENT_URI);
- clientsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Clients.CONTENT_URI);
- clientsRecencyUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Clients.CONTENT_RECENCY_URI);
- }
-
- /**
- * Extracts a List of just RemoteClients from a cursor.
- * The supplied cursor should be grouped by guid and sorted by most recently used.
- */
- @Override
- public List<RemoteClient> getClientsWithoutTabsByRecencyFromCursor(Cursor cursor) {
- final ArrayList<RemoteClient> clients = new ArrayList<>(cursor.getCount());
-
- final int originalPosition = cursor.getPosition();
- try {
- if (!cursor.moveToFirst()) {
- return clients;
- }
-
- final int clientGuidIndex = cursor.getColumnIndex(BrowserContract.Clients.GUID);
- final int clientNameIndex = cursor.getColumnIndex(BrowserContract.Clients.NAME);
- final int clientLastModifiedIndex = cursor.getColumnIndex(BrowserContract.Clients.LAST_MODIFIED);
- final int clientDeviceTypeIndex = cursor.getColumnIndex(BrowserContract.Clients.DEVICE_TYPE);
-
- while (!cursor.isAfterLast()) {
- final String clientGuid = cursor.getString(clientGuidIndex);
- final String clientName = cursor.getString(clientNameIndex);
- final String deviceType = cursor.getString(clientDeviceTypeIndex);
- final long lastModified = cursor.getLong(clientLastModifiedIndex);
-
- clients.add(new RemoteClient(clientGuid, clientName, lastModified, deviceType));
-
- cursor.moveToNext();
- }
- } finally {
- cursor.moveToPosition(originalPosition);
- }
- return clients;
- }
-
- /**
- * Extract client and tab records from a cursor.
- * <p>
- * The position of the cursor is moved to before the first record before
- * reading. The cursor is advanced until there are no more records to be
- * read. The position of the cursor is restored before returning.
- *
- * @param cursor
- * to extract records from. The records should already be grouped
- * by client GUID.
- * @return list of clients, each containing list of tabs.
- */
- @Override
- public List<RemoteClient> getClientsFromCursor(final Cursor cursor) {
- final ArrayList<RemoteClient> clients = new ArrayList<RemoteClient>();
-
- final int originalPosition = cursor.getPosition();
- try {
- if (!cursor.moveToFirst()) {
- return clients;
- }
-
- final int tabTitleIndex = cursor.getColumnIndex(BrowserContract.Tabs.TITLE);
- final int tabUrlIndex = cursor.getColumnIndex(BrowserContract.Tabs.URL);
- final int tabLastUsedIndex = cursor.getColumnIndex(BrowserContract.Tabs.LAST_USED);
- final int clientGuidIndex = cursor.getColumnIndex(BrowserContract.Clients.GUID);
- final int clientNameIndex = cursor.getColumnIndex(BrowserContract.Clients.NAME);
- final int clientLastModifiedIndex = cursor.getColumnIndex(BrowserContract.Clients.LAST_MODIFIED);
- final int clientDeviceTypeIndex = cursor.getColumnIndex(BrowserContract.Clients.DEVICE_TYPE);
-
- // A walking partition, chunking by client GUID. We assume the
- // cursor records are already grouped by client GUID; see the query
- // sort order.
- RemoteClient lastClient = null;
- while (!cursor.isAfterLast()) {
- final String clientGuid = cursor.getString(clientGuidIndex);
- if (lastClient == null || !TextUtils.equals(lastClient.guid, clientGuid)) {
- final String clientName = cursor.getString(clientNameIndex);
- final long lastModified = cursor.getLong(clientLastModifiedIndex);
- final String deviceType = cursor.getString(clientDeviceTypeIndex);
- lastClient = new RemoteClient(clientGuid, clientName, lastModified, deviceType);
- clients.add(lastClient);
- }
-
- final String tabTitle = cursor.getString(tabTitleIndex);
- final String tabUrl = cursor.getString(tabUrlIndex);
- final long tabLastUsed = cursor.getLong(tabLastUsedIndex);
- lastClient.tabs.add(new RemoteTab(tabTitle, tabUrl, tabLastUsed));
-
- cursor.moveToNext();
- }
- } finally {
- cursor.moveToPosition(originalPosition);
- }
-
- return clients;
- }
-
- @Override
- public Cursor getRemoteClientsByRecencyCursor(Context context) {
- final Uri uri = clientsRecencyUriWithProfile;
- return context.getContentResolver().query(uri, CLIENTS_PROJECTION_COLUMNS,
- REMOTE_CLIENTS_SELECTION, null, null);
- }
-
- @Override
- public Cursor getRemoteTabsCursor(Context context) {
- return getRemoteTabsCursor(context, -1);
- }
-
- @Override
- public Cursor getRemoteTabsCursor(Context context, int limit) {
- Uri uri = tabsUriWithProfile;
-
- if (limit > 0) {
- uri = uri.buildUpon()
- .appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit))
- .build();
- }
-
- final String threeWeeksAgoTimestampMillis = Long.valueOf(
- System.currentTimeMillis() - THREE_WEEKS_IN_MILLISECONDS).toString();
- return context.getContentResolver().query(uri,
- TABS_PROJECTION_COLUMNS,
- REMOTE_TABS_SELECTION_CLIENT_RECENCY,
- new String[] {threeWeeksAgoTimestampMillis},
- REMOTE_TABS_SORT_ORDER);
- }
-
- // This method returns all tabs from all remote clients,
- // ordered by most recent client first, most recent tab first
- @Override
- public void getTabs(final Context context, final OnQueryTabsCompleteListener listener) {
- getTabs(context, 0, listener);
- }
-
- // This method returns limited number of tabs from all remote clients,
- // ordered by most recent client first, most recent tab first
- @Override
- public void getTabs(final Context context, final int limit, final OnQueryTabsCompleteListener listener) {
- // If there is no listener, no point in doing work.
- if (listener == null)
- return;
-
- (new UIAsyncTask.WithoutParams<List<RemoteClient>>(ThreadUtils.getBackgroundHandler()) {
- @Override
- protected List<RemoteClient> doInBackground() {
- final Cursor cursor = getRemoteTabsCursor(context, limit);
- if (cursor == null)
- return null;
-
- try {
- return Collections.unmodifiableList(getClientsFromCursor(cursor));
- } finally {
- cursor.close();
- }
- }
-
- @Override
- protected void onPostExecute(List<RemoteClient> clients) {
- listener.onQueryTabsComplete(clients);
- }
- }).execute();
- }
-
- // Updates the modified time of the local client with the current time.
- private void updateLocalClient(final ContentResolver cr) {
- ContentValues values = new ContentValues();
- values.put(BrowserContract.Clients.LAST_MODIFIED, System.currentTimeMillis());
-
- cr.update(clientsUriWithProfile, values, LOCAL_CLIENT_SELECTION, null);
- }
-
- // Deletes all local tabs.
- private void deleteLocalTabs(final ContentResolver cr) {
- cr.delete(tabsUriWithProfile, LOCAL_TABS_SELECTION, null);
- }
-
- /**
- * Tabs are positioned in the DB in the same order that they appear in the tabs param.
- * - URL should never empty or null. Skip this tab if there's no URL.
- * - TITLE should always a string, either a page title or empty.
- * - LAST_USED should always be numeric.
- * - FAVICON should be a URL or null.
- * - HISTORY should be serialized JSON array of URLs.
- * - POSITION should always be numeric.
- * - CLIENT_GUID should always be null to represent the local client.
- */
- private void insertLocalTabs(final ContentResolver cr, final Iterable<Tab> tabs) {
- // Reuse this for serializing individual history URLs as JSON.
- JSONArray history = new JSONArray();
- ArrayList<ContentValues> valuesToInsert = new ArrayList<ContentValues>();
-
- int position = 0;
- for (Tab tab : tabs) {
- // Skip this tab if it has a null URL or is in private browsing mode, or is a filtered URL.
- String url = tab.getURL();
- if (url == null || tab.isPrivate() || isFilteredURL(url))
- continue;
-
- ContentValues values = new ContentValues();
- values.put(BrowserContract.Tabs.URL, url);
- values.put(BrowserContract.Tabs.TITLE, tab.getTitle());
- values.put(BrowserContract.Tabs.LAST_USED, tab.getLastUsed());
-
- String favicon = tab.getFaviconURL();
- if (favicon != null)
- values.put(BrowserContract.Tabs.FAVICON, favicon);
- else
- values.putNull(BrowserContract.Tabs.FAVICON);
-
- // We don't have access to session history in Java, so for now, we'll
- // just use a JSONArray that holds most recent history item.
- try {
- history.put(0, tab.getURL());
- values.put(BrowserContract.Tabs.HISTORY, history.toString());
- } catch (JSONException e) {
- Log.w(LOGTAG, "JSONException adding URL to tab history array.", e);
- }
-
- values.put(BrowserContract.Tabs.POSITION, position++);
-
- // A null client guid corresponds to the local client.
- values.putNull(BrowserContract.Tabs.CLIENT_GUID);
-
- valuesToInsert.add(values);
- }
-
- ContentValues[] valuesToInsertArray = valuesToInsert.toArray(new ContentValues[valuesToInsert.size()]);
- cr.bulkInsert(tabsUriWithProfile, valuesToInsertArray);
- }
-
- // Deletes all local tabs and replaces them with a new list of tabs.
- @Override
- public synchronized void persistLocalTabs(final ContentResolver cr, final Iterable<Tab> tabs) {
- deleteLocalTabs(cr);
- insertLocalTabs(cr, tabs);
- updateLocalClient(cr);
- }
-
- /**
- * Matches the supplied URL string against the set of URLs to filter.
- *
- * @return true if the supplied URL should be skipped; false otherwise.
- */
- private boolean isFilteredURL(String url) {
- return FILTERED_URL_PATTERN.matcher(url).lookingAt();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/LocalURLMetadata.java b/mobile/android/base/java/org/mozilla/gecko/db/LocalURLMetadata.java
deleted file mode 100644
index 7f2c4a736..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalURLMetadata.java
+++ /dev/null
@@ -1,240 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
- */
-package org.mozilla.gecko.db;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.net.Uri;
-import android.util.Log;
-import android.util.LruCache;
-
-// Holds metadata info about URLs. Supports some helper functions for getting back a HashMap of key value data.
-public class LocalURLMetadata implements URLMetadata {
- private static final String LOGTAG = "GeckoURLMetadata";
- private final Uri uriWithProfile;
-
- public LocalURLMetadata(String mProfile) {
- uriWithProfile = DBUtils.appendProfileWithDefault(mProfile, URLMetadataTable.CONTENT_URI);
- }
-
- // A list of columns in the table. It's used to simplify some loops for reading/writing data.
- private static final Set<String> COLUMNS;
- static {
- final HashSet<String> tempModel = new HashSet<>(4);
- tempModel.add(URLMetadataTable.URL_COLUMN);
- tempModel.add(URLMetadataTable.TILE_IMAGE_URL_COLUMN);
- tempModel.add(URLMetadataTable.TILE_COLOR_COLUMN);
- tempModel.add(URLMetadataTable.TOUCH_ICON_COLUMN);
- COLUMNS = Collections.unmodifiableSet(tempModel);
- }
-
- // Store a cache of recent results. This number is chosen to match the max number of tiles on about:home
- private static final int CACHE_SIZE = 9;
- // Note: Members of this cache are unmodifiable.
- private final LruCache<String, Map<String, Object>> cache = new LruCache<String, Map<String, Object>>(CACHE_SIZE);
-
- /**
- * Converts a JSON object into a unmodifiable Map of known metadata properties.
- * Will throw away any properties that aren't stored in the database.
- *
- * Incoming data can include a list like: {touchIconList:{56:"http://x.com/56.png", 76:"http://x.com/76.png"}}.
- * This will then be filtered to find the most appropriate touchIcon, i.e. the closest icon size that is larger
- * than (or equal to) the preferred homescreen launcher icon size, which is then stored in the "touchIcon" property.
- */
- @Override
- public Map<String, Object> fromJSON(JSONObject obj) {
- Map<String, Object> data = new HashMap<String, Object>();
-
- for (String key : COLUMNS) {
- if (obj.has(key)) {
- data.put(key, obj.optString(key));
- }
- }
-
-
- try {
- JSONObject icons;
- if (obj.has("touchIconList") &&
- (icons = obj.getJSONObject("touchIconList")).length() > 0) {
- int preferredSize = GeckoAppShell.getPreferredIconSize();
-
- Iterator<String> keys = icons.keys();
-
- ArrayList<Integer> sizes = new ArrayList<Integer>(icons.length());
- while (keys.hasNext()) {
- sizes.add(new Integer(keys.next()));
- }
-
- final int bestSize = LoadFaviconResult.selectBestSizeFromList(sizes, preferredSize);
- final String iconURL = icons.getString(Integer.toString(bestSize));
-
- data.put(URLMetadataTable.TOUCH_ICON_COLUMN, iconURL);
- }
- } catch (JSONException e) {
- Log.w(LOGTAG, "Exception processing touchIconList for LocalURLMetadata; ignoring.", e);
- }
-
- return Collections.unmodifiableMap(data);
- }
-
- /**
- * Converts a Cursor into a unmodifiable Map of known metadata properties.
- * Will throw away any properties that aren't stored in the database.
- * Will also not iterate through multiple rows in the cursor.
- */
- private Map<String, Object> fromCursor(Cursor c) {
- Map<String, Object> data = new HashMap<String, Object>();
-
- String[] columns = c.getColumnNames();
- for (String column : columns) {
- if (COLUMNS.contains(column)) {
- try {
- data.put(column, c.getString(c.getColumnIndexOrThrow(column)));
- } catch (Exception ex) {
- Log.i(LOGTAG, "Error getting data for " + column, ex);
- }
- }
- }
-
- return Collections.unmodifiableMap(data);
- }
-
- /**
- * Returns an unmodifiable Map of url->Metadata (i.e. A second HashMap) for a list of urls.
- * Must not be called from UI or Gecko threads.
- */
- @Override
- public Map<String, Map<String, Object>> getForURLs(final ContentResolver cr,
- final Collection<String> urls,
- final List<String> requestedColumns) {
- ThreadUtils.assertNotOnUiThread();
- ThreadUtils.assertNotOnGeckoThread();
-
- final Map<String, Map<String, Object>> data = new HashMap<String, Map<String, Object>>();
-
- // Nothing to query for
- if (urls.isEmpty() || requestedColumns.isEmpty()) {
- Log.e(LOGTAG, "Queried metadata for nothing");
- return data;
- }
-
- // Search the cache for any of these urls
- List<String> urlsToQuery = new ArrayList<String>();
- for (String url : urls) {
- final Map<String, Object> hit = cache.get(url);
- if (hit != null) {
- // Cache hit: we've found the URL in the cache, however we may not have cached the desired columns
- // for that URL. Hence we need to check whether our cache hit contains those columns, and directly
- // retrieve the desired data if not. (E.g. the top sites panel retrieves the tile, and tilecolor. If
- // we later try to retrieve the touchIcon for a top-site the cache hit will only point to
- // tile+tilecolor, and not the required touchIcon. In this case we don't want to use the cache.)
- boolean useCache = true;
- for (String c: requestedColumns) {
- if (!hit.containsKey(c)) {
- useCache = false;
- }
- }
- if (useCache) {
- data.put(url, hit);
- } else {
- urlsToQuery.add(url);
- }
- } else {
- urlsToQuery.add(url);
- }
- }
-
- // If everything was in the cache, we're done!
- if (urlsToQuery.size() == 0) {
- return Collections.unmodifiableMap(data);
- }
-
- final String selection = DBUtils.computeSQLInClause(urlsToQuery.size(), URLMetadataTable.URL_COLUMN);
- List<String> columns = requestedColumns;
- // We need the url to build our final HashMap, so we force it to be included in the query.
- if (!columns.contains(URLMetadataTable.URL_COLUMN)) {
- // The requestedColumns may be immutable (e.g. if the caller used Collections.singletonList), hence
- // we have to create a copy.
- columns = new ArrayList<String>(columns);
- columns.add(URLMetadataTable.URL_COLUMN);
- }
-
- final Cursor cursor = cr.query(uriWithProfile,
- columns.toArray(new String[columns.size()]), // columns,
- selection, // selection
- urlsToQuery.toArray(new String[urlsToQuery.size()]), // selectionargs
- null);
- try {
- if (!cursor.moveToFirst()) {
- return Collections.unmodifiableMap(data);
- }
-
- do {
- final Map<String, Object> metadata = fromCursor(cursor);
- final String url = cursor.getString(cursor.getColumnIndexOrThrow(URLMetadataTable.URL_COLUMN));
-
- data.put(url, metadata);
- cache.put(url, metadata);
- } while (cursor.moveToNext());
-
- } finally {
- cursor.close();
- }
-
- return Collections.unmodifiableMap(data);
- }
-
- /**
- * Saves a HashMap of metadata into the database. Will iterate through columns
- * in the Database and only save rows with matching keys in the HashMap.
- * Must not be called from UI or Gecko threads.
- */
- @Override
- public void save(final ContentResolver cr, final Map<String, Object> data) {
- ThreadUtils.assertNotOnUiThread();
- ThreadUtils.assertNotOnGeckoThread();
-
- try {
- ContentValues values = new ContentValues();
-
- for (String key : COLUMNS) {
- if (data.containsKey(key)) {
- values.put(key, (String) data.get(key));
- }
- }
-
- if (values.size() == 0) {
- return;
- }
-
- Uri uri = uriWithProfile.buildUpon()
- .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true")
- .build();
- cr.update(uri, values, URLMetadataTable.URL_COLUMN + "=?", new String[] {
- (String) data.get(URLMetadataTable.URL_COLUMN)
- });
- } catch (Exception ex) {
- Log.e(LOGTAG, "error saving", ex);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/LocalUrlAnnotations.java b/mobile/android/base/java/org/mozilla/gecko/db/LocalUrlAnnotations.java
deleted file mode 100644
index 9df41a169..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalUrlAnnotations.java
+++ /dev/null
@@ -1,253 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.net.Uri;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.util.Log;
-
-import org.json.JSONException;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.db.BrowserContract.UrlAnnotations.Key;
-import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
-
-public class LocalUrlAnnotations implements UrlAnnotations {
- private static final String LOGTAG = "LocalUrlAnnotations";
-
- private Uri urlAnnotationsTableWithProfile;
-
- public LocalUrlAnnotations(final String profile) {
- urlAnnotationsTableWithProfile = DBUtils.appendProfile(profile, BrowserContract.UrlAnnotations.CONTENT_URI);
- }
-
- /**
- * Get all feed subscriptions.
- */
- @Override
- public Cursor getFeedSubscriptions(ContentResolver cr) {
- return queryByKey(cr,
- Key.FEED_SUBSCRIPTION,
- new String[] { BrowserContract.UrlAnnotations.URL, BrowserContract.UrlAnnotations.VALUE },
- null);
- }
-
- /**
- * Insert mapping from website URL to URL of the feed.
- */
- @Override
- public void insertFeedUrl(ContentResolver cr, String originUrl, String feedUrl) {
- insertAnnotation(cr, originUrl, Key.FEED, feedUrl);
- }
-
- @Override
- public boolean hasAcceptedOrDeclinedHomeScreenShortcut(ContentResolver cr, String url) {
- return hasResultsForSelection(cr,
- BrowserContract.UrlAnnotations.URL + " = ?",
- new String[]{url});
- }
-
- @Override
- public void insertHomeScreenShortcut(ContentResolver cr, String url, boolean hasCreatedShortCut) {
- insertAnnotation(cr, url, Key.HOME_SCREEN_SHORTCUT, String.valueOf(hasCreatedShortCut));
- }
-
- /**
- * Returns true if there's a mapping from the given website URL to a feed URL. False otherwise.
- */
- @Override
- public boolean hasFeedUrlForWebsite(ContentResolver cr, String websiteUrl) {
- return hasResultsForSelection(cr,
- BrowserContract.UrlAnnotations.URL + " = ? AND " + BrowserContract.UrlAnnotations.KEY + " = ?",
- new String[]{websiteUrl, Key.FEED.getDbValue()});
- }
-
- /**
- * Returns true if there's a website URL with this feed URL. False otherwise.
- */
- @Override
- public boolean hasWebsiteForFeedUrl(ContentResolver cr, String feedUrl) {
- return hasResultsForSelection(cr,
- BrowserContract.UrlAnnotations.VALUE + " = ? AND " + BrowserContract.UrlAnnotations.KEY + " = ?",
- new String[]{feedUrl, Key.FEED.getDbValue()});
- }
-
- /**
- * Delete the feed URL mapping for this website URL.
- */
- @Override
- public void deleteFeedUrl(ContentResolver cr, String websiteUrl) {
- deleteAnnotation(cr, websiteUrl, Key.FEED);
- }
-
- /**
- * Get website URLs that are mapped to the given feed URL.
- */
- @Override
- public Cursor getWebsitesWithFeedUrl(ContentResolver cr) {
- return cr.query(urlAnnotationsTableWithProfile,
- new String[] { BrowserContract.UrlAnnotations.URL },
- BrowserContract.UrlAnnotations.KEY + " = ?",
- new String[] { Key.FEED.getDbValue() },
- null);
- }
-
- /**
- * Returns true if there's a subscription for this feed URL. False otherwise.
- */
- @Override
- public boolean hasFeedSubscription(ContentResolver cr, String feedUrl) {
- return hasResultsForSelection(cr,
- BrowserContract.UrlAnnotations.URL + " = ? AND " + BrowserContract.UrlAnnotations.KEY + " = ?",
- new String[]{feedUrl, Key.FEED_SUBSCRIPTION.getDbValue()});
- }
-
- /**
- * Insert the given feed subscription (Mapping from feed URL to the subscription object).
- */
- @Override
- public void insertFeedSubscription(ContentResolver cr, FeedSubscription subscription) {
- try {
- insertAnnotation(cr, subscription.getFeedUrl(), Key.FEED_SUBSCRIPTION, subscription.toJSON().toString());
- } catch (JSONException e) {
- Log.w(LOGTAG, "Could not serialize subscription");
- }
- }
-
- /**
- * Update the feed subscription with new values.
- */
- @Override
- public void updateFeedSubscription(ContentResolver cr, FeedSubscription subscription) {
- try {
- updateAnnotation(cr, subscription.getFeedUrl(), Key.FEED_SUBSCRIPTION, subscription.toJSON().toString());
- } catch (JSONException e) {
- Log.w(LOGTAG, "Could not serialize subscription");
- }
- }
-
- /**
- * Delete the subscription for the feed URL.
- */
- @Override
- public void deleteFeedSubscription(ContentResolver cr, FeedSubscription subscription) {
- deleteAnnotation(cr, subscription.getFeedUrl(), Key.FEED_SUBSCRIPTION);
- }
-
- private int deleteAnnotation(final ContentResolver cr, final String url, final Key key) {
- return cr.delete(urlAnnotationsTableWithProfile,
- BrowserContract.UrlAnnotations.KEY + " = ? AND " + BrowserContract.UrlAnnotations.URL + " = ?",
- new String[] { key.getDbValue(), url });
- }
-
- private int updateAnnotation(final ContentResolver cr, final String url, final Key key, final String value) {
- ContentValues values = new ContentValues();
- values.put(BrowserContract.UrlAnnotations.VALUE, value);
- values.put(BrowserContract.UrlAnnotations.DATE_MODIFIED, System.currentTimeMillis());
-
- return cr.update(urlAnnotationsTableWithProfile,
- values,
- BrowserContract.UrlAnnotations.KEY + " = ? AND " + BrowserContract.UrlAnnotations.URL + " = ?",
- new String[]{key.getDbValue(), url});
- }
-
- private void insertAnnotation(final ContentResolver cr, final String url, final Key key, final String value) {
- insertAnnotation(cr, url, key.getDbValue(), value);
- }
-
- @RobocopTarget
- @Override
- public void insertAnnotation(final ContentResolver cr, final String url, final String key, final String value) {
- final long creationTime = System.currentTimeMillis();
- final ContentValues values = new ContentValues(5);
- values.put(BrowserContract.UrlAnnotations.URL, url);
- values.put(BrowserContract.UrlAnnotations.KEY, key);
- values.put(BrowserContract.UrlAnnotations.VALUE, value);
- values.put(BrowserContract.UrlAnnotations.DATE_CREATED, creationTime);
- values.put(BrowserContract.UrlAnnotations.DATE_MODIFIED, creationTime);
- cr.insert(urlAnnotationsTableWithProfile, values);
- }
-
- /**
- * @return true if the table contains rows for the given selection.
- */
- private boolean hasResultsForSelection(ContentResolver cr, String selection, String[] selectionArgs) {
- Cursor cursor = cr.query(urlAnnotationsTableWithProfile,
- new String[] { BrowserContract.UrlAnnotations._ID },
- selection,
- selectionArgs,
- null);
- if (cursor == null) {
- return false;
- }
-
- try {
- return cursor.getCount() > 0;
- } finally {
- cursor.close();
- }
- }
-
- private Cursor queryByKey(final ContentResolver cr, @NonNull final Key key, @Nullable final String[] projections,
- @Nullable final String sortOrder) {
- return cr.query(urlAnnotationsTableWithProfile,
- projections,
- BrowserContract.UrlAnnotations.KEY + " = ?", new String[] { key.getDbValue() },
- sortOrder);
- }
-
- @Override
- public Cursor getScreenshots(ContentResolver cr) {
- return queryByKey(cr,
- Key.SCREENSHOT,
- new String[] {
- BrowserContract.UrlAnnotations._ID,
- BrowserContract.UrlAnnotations.URL,
- BrowserContract.UrlAnnotations.KEY,
- BrowserContract.UrlAnnotations.VALUE,
- BrowserContract.UrlAnnotations.DATE_CREATED,
- },
- BrowserContract.UrlAnnotations.DATE_CREATED + " DESC");
- }
-
- public void insertScreenshot(final ContentResolver cr, final String pageUrl, final String screenshotPath) {
- insertAnnotation(cr, pageUrl, Key.SCREENSHOT.getDbValue(), screenshotPath);
- }
-
- @Override
- public void insertReaderViewUrl(final ContentResolver cr, final String pageUrl) {
- insertAnnotation(cr, pageUrl, Key.READER_VIEW.getDbValue(), BrowserContract.UrlAnnotations.READER_VIEW_SAVED_VALUE);
- }
-
- @Override
- public void deleteReaderViewUrl(ContentResolver cr, String pageURL) {
- deleteAnnotation(cr, pageURL, Key.READER_VIEW);
- }
-
- public int getAnnotationCount(ContentResolver cr, Key key) {
- final String countColumnname = "count";
- final Cursor c = queryByKey(cr,
- key,
- new String[] {
- "COUNT(*) AS " + countColumnname
- },
- null);
-
- try {
- if (c != null && c.moveToFirst()) {
- return c.getInt(c.getColumnIndexOrThrow(countColumnname));
- } else {
- return 0;
- }
- } finally {
- if (c != null) {
- c.close();
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/LoginsProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/LoginsProvider.java
deleted file mode 100644
index d2d504851..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/LoginsProvider.java
+++ /dev/null
@@ -1,520 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.UriMatcher;
-import android.database.Cursor;
-import android.database.DatabaseUtils;
-import android.database.MatrixCursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteQueryBuilder;
-import android.net.Uri;
-import android.support.annotation.NonNull;
-import android.text.TextUtils;
-import android.util.Base64;
-
-import org.mozilla.gecko.db.BrowserContract.DeletedLogins;
-import org.mozilla.gecko.db.BrowserContract.Logins;
-import org.mozilla.gecko.db.BrowserContract.LoginsDisabledHosts;
-import org.mozilla.gecko.sync.Utils;
-
-import java.io.UnsupportedEncodingException;
-import java.security.GeneralSecurityException;
-import java.util.HashMap;
-
-import javax.crypto.Cipher;
-import javax.crypto.NullCipher;
-
-import static org.mozilla.gecko.db.BrowserContract.DeletedLogins.TABLE_DELETED_LOGINS;
-import static org.mozilla.gecko.db.BrowserContract.Logins.TABLE_LOGINS;
-import static org.mozilla.gecko.db.BrowserContract.LoginsDisabledHosts.TABLE_DISABLED_HOSTS;
-
-public class LoginsProvider extends SharedBrowserDatabaseProvider {
-
- private static final int LOGINS = 100;
- private static final int LOGINS_ID = 101;
- private static final int DELETED_LOGINS = 102;
- private static final int DELETED_LOGINS_ID = 103;
- private static final int DISABLED_HOSTS = 104;
- private static final int DISABLED_HOSTS_HOSTNAME = 105;
- private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
-
- private static final HashMap<String, String> LOGIN_PROJECTION_MAP;
- private static final HashMap<String, String> DELETED_LOGIN_PROJECTION_MAP;
- private static final HashMap<String, String> DISABLED_HOSTS_PROJECTION_MAP;
-
- private static final String DEFAULT_LOGINS_SORT_ORDER = Logins.HOSTNAME + " ASC";
- private static final String DEFAULT_DELETED_LOGINS_SORT_ORDER = DeletedLogins.TIME_DELETED + " ASC";
- private static final String DEFAULT_DISABLED_HOSTS_SORT_ORDER = LoginsDisabledHosts.HOSTNAME + " ASC";
- private static final String WHERE_GUID_IS_NULL = DeletedLogins.GUID + " IS NULL";
- private static final String WHERE_GUID_IS_VALUE = DeletedLogins.GUID + " = ?";
-
- protected static final String INDEX_LOGINS_HOSTNAME = "login_hostname_index";
- protected static final String INDEX_LOGINS_HOSTNAME_FORM_SUBMIT_URL = "login_hostname_formSubmitURL_index";
- protected static final String INDEX_LOGINS_HOSTNAME_HTTP_REALM = "login_hostname_httpRealm_index";
-
- static {
- URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "logins", LOGINS);
- URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "logins/#", LOGINS_ID);
- URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "deleted-logins", DELETED_LOGINS);
- URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "deleted-logins/#", DELETED_LOGINS_ID);
- URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "logins-disabled-hosts", DISABLED_HOSTS);
- URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "logins-disabled-hosts/hostname/*", DISABLED_HOSTS_HOSTNAME);
-
- LOGIN_PROJECTION_MAP = new HashMap<>();
- LOGIN_PROJECTION_MAP.put(Logins._ID, Logins._ID);
- LOGIN_PROJECTION_MAP.put(Logins.HOSTNAME, Logins.HOSTNAME);
- LOGIN_PROJECTION_MAP.put(Logins.HTTP_REALM, Logins.HTTP_REALM);
- LOGIN_PROJECTION_MAP.put(Logins.FORM_SUBMIT_URL, Logins.FORM_SUBMIT_URL);
- LOGIN_PROJECTION_MAP.put(Logins.USERNAME_FIELD, Logins.USERNAME_FIELD);
- LOGIN_PROJECTION_MAP.put(Logins.PASSWORD_FIELD, Logins.PASSWORD_FIELD);
- LOGIN_PROJECTION_MAP.put(Logins.ENCRYPTED_USERNAME, Logins.ENCRYPTED_USERNAME);
- LOGIN_PROJECTION_MAP.put(Logins.ENCRYPTED_PASSWORD, Logins.ENCRYPTED_PASSWORD);
- LOGIN_PROJECTION_MAP.put(Logins.GUID, Logins.GUID);
- LOGIN_PROJECTION_MAP.put(Logins.ENC_TYPE, Logins.ENC_TYPE);
- LOGIN_PROJECTION_MAP.put(Logins.TIME_CREATED, Logins.TIME_CREATED);
- LOGIN_PROJECTION_MAP.put(Logins.TIME_LAST_USED, Logins.TIME_LAST_USED);
- LOGIN_PROJECTION_MAP.put(Logins.TIME_PASSWORD_CHANGED, Logins.TIME_PASSWORD_CHANGED);
- LOGIN_PROJECTION_MAP.put(Logins.TIMES_USED, Logins.TIMES_USED);
-
- DELETED_LOGIN_PROJECTION_MAP = new HashMap<>();
- DELETED_LOGIN_PROJECTION_MAP.put(DeletedLogins._ID, DeletedLogins._ID);
- DELETED_LOGIN_PROJECTION_MAP.put(DeletedLogins.GUID, DeletedLogins.GUID);
- DELETED_LOGIN_PROJECTION_MAP.put(DeletedLogins.TIME_DELETED, DeletedLogins.TIME_DELETED);
-
- DISABLED_HOSTS_PROJECTION_MAP = new HashMap<>();
- DISABLED_HOSTS_PROJECTION_MAP.put(LoginsDisabledHosts._ID, LoginsDisabledHosts._ID);
- DISABLED_HOSTS_PROJECTION_MAP.put(LoginsDisabledHosts.HOSTNAME, LoginsDisabledHosts.HOSTNAME);
- }
-
- private static String projectColumn(String table, String column) {
- return table + "." + column;
- }
-
- private static String selectColumn(String table, String column) {
- return projectColumn(table, column) + " = ?";
- }
-
- @Override
- protected Uri insertInTransaction(Uri uri, ContentValues values) {
- trace("Calling insert in transaction on URI: " + uri);
-
- final int match = URI_MATCHER.match(uri);
- final SQLiteDatabase db = getWritableDatabase(uri);
- final long id;
- String guid;
-
- setupDefaultValues(values, uri);
- switch (match) {
- case LOGINS:
- removeDeletedLoginsByGUIDInTransaction(values, db);
- // Encrypt sensitive data.
- encryptContentValueFields(values);
- guid = values.getAsString(Logins.GUID);
- debug("Inserting login in database with GUID: " + guid);
- id = db.insertOrThrow(TABLE_LOGINS, Logins.GUID, values);
- break;
-
- case DELETED_LOGINS:
- guid = values.getAsString(DeletedLogins.GUID);
- debug("Inserting deleted-login in database with GUID: " + guid);
- id = db.insertOrThrow(TABLE_DELETED_LOGINS, DeletedLogins.GUID, values);
- break;
-
- case DISABLED_HOSTS:
- String hostname = values.getAsString(LoginsDisabledHosts.HOSTNAME);
- debug("Inserting disabled-host in database with hostname: " + hostname);
- id = db.insertOrThrow(TABLE_DISABLED_HOSTS, LoginsDisabledHosts.HOSTNAME, values);
- break;
-
- default:
- throw new UnsupportedOperationException("Unknown insert URI " + uri);
- }
-
- debug("Inserted ID in database: " + id);
-
- if (id >= 0) {
- return ContentUris.withAppendedId(uri, id);
- }
-
- return null;
- }
-
- @Override
- @SuppressWarnings("fallthrough")
- protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
- trace("Calling delete in transaction on URI: " + uri);
-
- final int match = URI_MATCHER.match(uri);
- final String table;
- final SQLiteDatabase db = getWritableDatabase(uri);
-
- beginWrite(db);
- switch (match) {
- case LOGINS_ID:
- trace("Delete on LOGINS_ID: " + uri);
- selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_LOGINS, Logins._ID));
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[]{Long.toString(ContentUris.parseId(uri))});
- // Store the deleted client in deleted-logins table.
- final String guid = getLoginGUIDByID(selection, selectionArgs, db);
- if (guid == null) {
- // No matching logins found for the id.
- return 0;
- }
- boolean isInsertSuccessful = storeDeletedLoginForGUIDInTransaction(guid, db);
- if (!isInsertSuccessful) {
- // Failed to insert into deleted-logins, return early.
- return 0;
- }
- // fall through
- case LOGINS:
- trace("Delete on LOGINS: " + uri);
- table = TABLE_LOGINS;
- break;
-
- case DELETED_LOGINS_ID:
- trace("Delete on DELETED_LOGINS_ID: " + uri);
- selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_DELETED_LOGINS, DeletedLogins._ID));
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[]{Long.toString(ContentUris.parseId(uri))});
- // fall through
- case DELETED_LOGINS:
- trace("Delete on DELETED_LOGINS_ID: " + uri);
- table = TABLE_DELETED_LOGINS;
- break;
-
- case DISABLED_HOSTS_HOSTNAME:
- trace("Delete on DISABLED_HOSTS_HOSTNAME: " + uri);
- selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_DISABLED_HOSTS, LoginsDisabledHosts.HOSTNAME));
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[]{uri.getLastPathSegment()});
- // fall through
- case DISABLED_HOSTS:
- trace("Delete on DISABLED_HOSTS: " + uri);
- table = TABLE_DISABLED_HOSTS;
- break;
-
- default:
- throw new UnsupportedOperationException("Unknown delete URI " + uri);
- }
-
- debug("Deleting " + table + " for URI: " + uri);
- return db.delete(table, selection, selectionArgs);
- }
-
- @Override
- @SuppressWarnings("fallthrough")
- protected int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
- trace("Calling update in transaction on URI: " + uri);
-
- final int match = URI_MATCHER.match(uri);
- final SQLiteDatabase db = getWritableDatabase(uri);
- final String table;
-
- beginWrite(db);
- switch (match) {
- case LOGINS_ID:
- trace("Update on LOGINS_ID: " + uri);
- selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_LOGINS, Logins._ID));
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[]{Long.toString(ContentUris.parseId(uri))});
-
- case LOGINS:
- trace("Update on LOGINS: " + uri);
- table = TABLE_LOGINS;
- // Encrypt sensitive data.
- encryptContentValueFields(values);
- break;
-
- default:
- throw new UnsupportedOperationException("Unknown update URI " + uri);
- }
-
- trace("Updating " + table + " on URI: " + uri);
- return db.update(table, values, selection, selectionArgs);
-
- }
-
- @Override
- @SuppressWarnings("fallthrough")
- public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
- trace("Calling query on URI: " + uri);
-
- final SQLiteDatabase db = getReadableDatabase(uri);
- final int match = URI_MATCHER.match(uri);
- final String groupBy = null;
- final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- final String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
-
- switch (match) {
- case LOGINS_ID:
- trace("Query is on LOGINS_ID: " + uri);
- selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_LOGINS, Logins._ID));
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { Long.toString(ContentUris.parseId(uri)) });
-
- // fall through
- case LOGINS:
- trace("Query is on LOGINS: " + uri);
- if (TextUtils.isEmpty(sortOrder)) {
- sortOrder = DEFAULT_LOGINS_SORT_ORDER;
- } else {
- debug("Using sort order " + sortOrder + ".");
- }
-
- qb.setProjectionMap(LOGIN_PROJECTION_MAP);
- qb.setTables(TABLE_LOGINS);
- break;
-
- case DELETED_LOGINS_ID:
- trace("Query is on DELETED_LOGINS_ID: " + uri);
- selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_DELETED_LOGINS, DeletedLogins._ID));
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { Long.toString(ContentUris.parseId(uri)) });
-
- // fall through
- case DELETED_LOGINS:
- trace("Query is on DELETED_LOGINS: " + uri);
- if (TextUtils.isEmpty(sortOrder)) {
- sortOrder = DEFAULT_DELETED_LOGINS_SORT_ORDER;
- } else {
- debug("Using sort order " + sortOrder + ".");
- }
-
- qb.setProjectionMap(DELETED_LOGIN_PROJECTION_MAP);
- qb.setTables(TABLE_DELETED_LOGINS);
- break;
-
- case DISABLED_HOSTS_HOSTNAME:
- trace("Query is on DISABLED_HOSTS_HOSTNAME: " + uri);
- selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_DISABLED_HOSTS, LoginsDisabledHosts.HOSTNAME));
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { uri.getLastPathSegment() });
-
- // fall through
- case DISABLED_HOSTS:
- trace("Query is on DISABLED_HOSTS: " + uri);
- if (TextUtils.isEmpty(sortOrder)) {
- sortOrder = DEFAULT_DISABLED_HOSTS_SORT_ORDER;
- } else {
- debug("Using sort order " + sortOrder + ".");
- }
-
- qb.setProjectionMap(DISABLED_HOSTS_PROJECTION_MAP);
- qb.setTables(TABLE_DISABLED_HOSTS);
- break;
-
- default:
- throw new UnsupportedOperationException("Unknown query URI " + uri);
- }
-
- trace("Running built query.");
- Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, null, sortOrder, limit);
- // If decryptManyCursorRows does not return the original cursor, it closes it, so there's
- // no need to close here.
- cursor = decryptManyCursorRows(cursor);
- cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.LOGINS_AUTHORITY_URI);
- return cursor;
- }
-
- @Override
- public String getType(@NonNull Uri uri) {
- final int match = URI_MATCHER.match(uri);
-
- switch (match) {
- case LOGINS:
- return Logins.CONTENT_TYPE;
-
- case LOGINS_ID:
- return Logins.CONTENT_ITEM_TYPE;
-
- case DELETED_LOGINS:
- return DeletedLogins.CONTENT_TYPE;
-
- case DELETED_LOGINS_ID:
- return DeletedLogins.CONTENT_ITEM_TYPE;
-
- case DISABLED_HOSTS:
- return LoginsDisabledHosts.CONTENT_TYPE;
-
- case DISABLED_HOSTS_HOSTNAME:
- return LoginsDisabledHosts.CONTENT_ITEM_TYPE;
-
- default:
- throw new UnsupportedOperationException("Unknown type " + uri);
- }
- }
-
- /**
- * Caller is responsible for invoking this method inside a transaction.
- */
- private String getLoginGUIDByID(final String selection, final String[] selectionArgs, final SQLiteDatabase db) {
- final Cursor cursor = db.query(Logins.TABLE_LOGINS, new String[]{Logins.GUID}, selection, selectionArgs, null, null, DEFAULT_LOGINS_SORT_ORDER);
- try {
- if (!cursor.moveToFirst()) {
- return null;
- }
- return cursor.getString(cursor.getColumnIndexOrThrow(Logins.GUID));
- } finally {
- cursor.close();
- }
- }
-
- /**
- * Caller is responsible for invoking this method inside a transaction.
- */
- private boolean storeDeletedLoginForGUIDInTransaction(final String guid, final SQLiteDatabase db) {
- if (guid == null) {
- return false;
- }
- final ContentValues values = new ContentValues();
- values.put(DeletedLogins.GUID, guid);
- values.put(DeletedLogins.TIME_DELETED, System.currentTimeMillis());
- return db.insert(TABLE_DELETED_LOGINS, DeletedLogins.GUID, values) > 0;
- }
-
- /**
- * Caller is responsible for invoking this method inside a transaction.
- */
- private void removeDeletedLoginsByGUIDInTransaction(ContentValues values, SQLiteDatabase db) {
- if (values.containsKey(Logins.GUID)) {
- final String guid = values.getAsString(Logins.GUID);
- if (guid == null) {
- db.delete(TABLE_DELETED_LOGINS, WHERE_GUID_IS_NULL, null);
- } else {
- String[] args = new String[]{guid};
- db.delete(TABLE_DELETED_LOGINS, WHERE_GUID_IS_VALUE, args);
- }
- }
- }
-
- private void setupDefaultValues(ContentValues values, Uri uri) throws IllegalArgumentException {
- final int match = URI_MATCHER.match(uri);
- final long now = System.currentTimeMillis();
- switch (match) {
- case DELETED_LOGINS:
- values.put(DeletedLogins.TIME_DELETED, now);
- // deleted-logins must contain a guid
- if (!values.containsKey(DeletedLogins.GUID)) {
- throw new IllegalArgumentException("Must provide GUID for deleted-login");
- }
- break;
-
- case LOGINS:
- values.put(Logins.TIME_CREATED, now);
- // Generate GUID for new login. Don't override specified GUIDs.
- if (!values.containsKey(Logins.GUID)) {
- final String guid = Utils.generateGuid();
- values.put(Logins.GUID, guid);
- }
- // The database happily accepts strings for long values; this just lets us re-use
- // the existing helper method.
- String nowString = Long.toString(now);
- DBUtils.replaceKey(values, null, Logins.HTTP_REALM, null);
- DBUtils.replaceKey(values, null, Logins.FORM_SUBMIT_URL, null);
- DBUtils.replaceKey(values, null, Logins.ENC_TYPE, "0");
- DBUtils.replaceKey(values, null, Logins.TIME_LAST_USED, nowString);
- DBUtils.replaceKey(values, null, Logins.TIME_PASSWORD_CHANGED, nowString);
- DBUtils.replaceKey(values, null, Logins.TIMES_USED, "0");
- break;
-
- case DISABLED_HOSTS:
- if (!values.containsKey(LoginsDisabledHosts.HOSTNAME)) {
- throw new IllegalArgumentException("Must provide hostname for disabled-host");
- }
- break;
-
- default:
- throw new UnsupportedOperationException("Unknown URI in setupDefaultValues " + uri);
- }
- }
-
- private void encryptContentValueFields(final ContentValues values) {
- if (values.containsKey(Logins.ENCRYPTED_PASSWORD)) {
- final String res = encrypt(values.getAsString(Logins.ENCRYPTED_PASSWORD));
- values.put(Logins.ENCRYPTED_PASSWORD, res);
- }
-
- if (values.containsKey(Logins.ENCRYPTED_USERNAME)) {
- final String res = encrypt(values.getAsString(Logins.ENCRYPTED_USERNAME));
- values.put(Logins.ENCRYPTED_USERNAME, res);
- }
- }
-
- /**
- * Replace each password and username encrypted ciphertext with its equivalent decrypted
- * plaintext in the given cursor.
- * <p/>
- * The encryption algorithm used to protect logins is unspecified; and further, a consumer of
- * consumers should never have access to encrypted ciphertext.
- *
- * @param cursor containing at least one of password and username encrypted ciphertexts.
- * @return a new {@link Cursor} with password and username decrypted plaintexts.
- */
- private Cursor decryptManyCursorRows(final Cursor cursor) {
- final int passwordIndex = cursor.getColumnIndex(Logins.ENCRYPTED_PASSWORD);
- final int usernameIndex = cursor.getColumnIndex(Logins.ENCRYPTED_USERNAME);
-
- if (passwordIndex == -1 && usernameIndex == -1) {
- return cursor;
- }
-
- // Special case, decrypt the encrypted username or password before returning the cursor.
- final MatrixCursor newCursor = new MatrixCursor(cursor.getColumnNames(), cursor.getColumnCount());
- try {
- for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
- final ContentValues values = new ContentValues();
- DatabaseUtils.cursorRowToContentValues(cursor, values);
-
- if (passwordIndex > -1) {
- String decrypted = decrypt(values.getAsString(Logins.ENCRYPTED_PASSWORD));
- values.put(Logins.ENCRYPTED_PASSWORD, decrypted);
- }
-
- if (usernameIndex > -1) {
- String decrypted = decrypt(values.getAsString(Logins.ENCRYPTED_USERNAME));
- values.put(Logins.ENCRYPTED_USERNAME, decrypted);
- }
-
- final MatrixCursor.RowBuilder rowBuilder = newCursor.newRow();
- for (String key : cursor.getColumnNames()) {
- rowBuilder.add(values.get(key));
- }
- }
- } finally {
- // Close the old cursor before returning the new one.
- cursor.close();
- }
-
- return newCursor;
- }
-
- private String encrypt(@NonNull String initialValue) {
- try {
- final Cipher cipher = getCipher(Cipher.ENCRYPT_MODE);
- return Base64.encodeToString(cipher.doFinal(initialValue.getBytes("UTF-8")), Base64.URL_SAFE);
- } catch (Exception e) {
- debug("encryption failed : " + e);
- throw new IllegalStateException("Logins encryption failed", e);
- }
- }
-
- private String decrypt(@NonNull String initialValue) {
- try {
- final Cipher cipher = getCipher(Cipher.DECRYPT_MODE);
- return new String(cipher.doFinal(Base64.decode(initialValue.getBytes("UTF-8"), Base64.URL_SAFE)));
- } catch (Exception e) {
- debug("Decryption failed : " + e);
- throw new IllegalStateException("Logins decryption failed", e);
- }
- }
-
- private Cipher getCipher(int mode) throws UnsupportedEncodingException, GeneralSecurityException {
- return new NullCipher();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/PasswordsProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/PasswordsProvider.java
deleted file mode 100644
index 2f5e11ed4..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/PasswordsProvider.java
+++ /dev/null
@@ -1,348 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import java.util.HashMap;
-
-import org.mozilla.gecko.CrashHandler;
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoMessageReceiver;
-import org.mozilla.gecko.NSSBridge;
-import org.mozilla.gecko.db.BrowserContract.DeletedPasswords;
-import org.mozilla.gecko.db.BrowserContract.GeckoDisabledHosts;
-import org.mozilla.gecko.db.BrowserContract.Passwords;
-import org.mozilla.gecko.mozglue.GeckoLoader;
-import org.mozilla.gecko.sqlite.MatrixBlobCursor;
-import org.mozilla.gecko.sqlite.SQLiteBridge;
-import org.mozilla.gecko.sync.Utils;
-
-import android.content.ContentValues;
-import android.content.Intent;
-import android.content.UriMatcher;
-import android.database.Cursor;
-import android.net.Uri;
-import android.text.TextUtils;
-import android.util.Log;
-
-public class PasswordsProvider extends SQLiteBridgeContentProvider {
- static final String TABLE_PASSWORDS = "moz_logins";
- static final String TABLE_DELETED_PASSWORDS = "moz_deleted_logins";
- static final String TABLE_DISABLED_HOSTS = "moz_disabledHosts";
-
- private static final String TELEMETRY_TAG = "SQLITEBRIDGE_PROVIDER_PASSWORDS";
-
- private static final int PASSWORDS = 100;
- private static final int DELETED_PASSWORDS = 101;
- private static final int DISABLED_HOSTS = 102;
-
- static final String DEFAULT_PASSWORDS_SORT_ORDER = Passwords.HOSTNAME + " ASC";
- static final String DEFAULT_DELETED_PASSWORDS_SORT_ORDER = DeletedPasswords.TIME_DELETED + " ASC";
-
- private static final UriMatcher URI_MATCHER;
-
- private static final HashMap<String, String> PASSWORDS_PROJECTION_MAP;
- private static final HashMap<String, String> DELETED_PASSWORDS_PROJECTION_MAP;
- private static final HashMap<String, String> DISABLED_HOSTS_PROJECTION_MAP;
-
- // this should be kept in sync with the version in toolkit/components/passwordmgr/storage-mozStorage.js
- private static final int DB_VERSION = 6;
- private static final String DB_FILENAME = "signons.sqlite";
- private static final String WHERE_GUID_IS_NULL = BrowserContract.DeletedPasswords.GUID + " IS NULL";
- private static final String WHERE_GUID_IS_VALUE = BrowserContract.DeletedPasswords.GUID + " = ?";
-
- private static final String LOG_TAG = "GeckoPasswordsProvider";
-
- private CrashHandler mCrashHandler;
-
- static {
- URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
-
- // content://org.mozilla.gecko.providers.browser/passwords/#
- URI_MATCHER.addURI(BrowserContract.PASSWORDS_AUTHORITY, "passwords", PASSWORDS);
-
- PASSWORDS_PROJECTION_MAP = new HashMap<String, String>();
- PASSWORDS_PROJECTION_MAP.put(Passwords.ID, Passwords.ID);
- PASSWORDS_PROJECTION_MAP.put(Passwords.HOSTNAME, Passwords.HOSTNAME);
- PASSWORDS_PROJECTION_MAP.put(Passwords.HTTP_REALM, Passwords.HTTP_REALM);
- PASSWORDS_PROJECTION_MAP.put(Passwords.FORM_SUBMIT_URL, Passwords.FORM_SUBMIT_URL);
- PASSWORDS_PROJECTION_MAP.put(Passwords.USERNAME_FIELD, Passwords.USERNAME_FIELD);
- PASSWORDS_PROJECTION_MAP.put(Passwords.PASSWORD_FIELD, Passwords.PASSWORD_FIELD);
- PASSWORDS_PROJECTION_MAP.put(Passwords.ENCRYPTED_USERNAME, Passwords.ENCRYPTED_USERNAME);
- PASSWORDS_PROJECTION_MAP.put(Passwords.ENCRYPTED_PASSWORD, Passwords.ENCRYPTED_PASSWORD);
- PASSWORDS_PROJECTION_MAP.put(Passwords.GUID, Passwords.GUID);
- PASSWORDS_PROJECTION_MAP.put(Passwords.ENC_TYPE, Passwords.ENC_TYPE);
- PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_CREATED, Passwords.TIME_CREATED);
- PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_LAST_USED, Passwords.TIME_LAST_USED);
- PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_PASSWORD_CHANGED, Passwords.TIME_PASSWORD_CHANGED);
- PASSWORDS_PROJECTION_MAP.put(Passwords.TIMES_USED, Passwords.TIMES_USED);
-
- URI_MATCHER.addURI(BrowserContract.PASSWORDS_AUTHORITY, "deleted-passwords", DELETED_PASSWORDS);
-
- DELETED_PASSWORDS_PROJECTION_MAP = new HashMap<String, String>();
- DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.ID, DeletedPasswords.ID);
- DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.GUID, DeletedPasswords.GUID);
- DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.TIME_DELETED, DeletedPasswords.TIME_DELETED);
-
- URI_MATCHER.addURI(BrowserContract.PASSWORDS_AUTHORITY, "disabled-hosts", DISABLED_HOSTS);
-
- DISABLED_HOSTS_PROJECTION_MAP = new HashMap<String, String>();
- DISABLED_HOSTS_PROJECTION_MAP.put(GeckoDisabledHosts.HOSTNAME, GeckoDisabledHosts.HOSTNAME);
- }
-
- public PasswordsProvider() {
- super(LOG_TAG);
- }
-
- @Override
- public boolean onCreate() {
- mCrashHandler = CrashHandler.createDefaultCrashHandler(getContext());
-
- // We don't use .loadMozGlue because we're in a different process,
- // and we just want to reuse code rather than use the loader lock etc.
- GeckoLoader.doLoadLibrary(getContext(), "mozglue");
- return super.onCreate();
- }
-
- @Override
- public void shutdown() {
- super.shutdown();
-
- if (mCrashHandler != null) {
- mCrashHandler.unregister();
- mCrashHandler = null;
- }
- }
-
- @Override
- protected String getDBName() {
- return DB_FILENAME;
- }
-
- @Override
- protected String getTelemetryPrefix() {
- return TELEMETRY_TAG;
- }
-
- @Override
- protected int getDBVersion() {
- return DB_VERSION;
- }
-
- @Override
- public String getType(Uri uri) {
- final int match = URI_MATCHER.match(uri);
-
- switch (match) {
- case PASSWORDS:
- return Passwords.CONTENT_TYPE;
-
- case DELETED_PASSWORDS:
- return DeletedPasswords.CONTENT_TYPE;
-
- case DISABLED_HOSTS:
- return GeckoDisabledHosts.CONTENT_TYPE;
-
- default:
- throw new UnsupportedOperationException("Unknown type " + uri);
- }
- }
-
- @Override
- public String getTable(Uri uri) {
- final int match = URI_MATCHER.match(uri);
- switch (match) {
- case DELETED_PASSWORDS:
- return TABLE_DELETED_PASSWORDS;
-
- case PASSWORDS:
- return TABLE_PASSWORDS;
-
- case DISABLED_HOSTS:
- return TABLE_DISABLED_HOSTS;
-
- default:
- throw new UnsupportedOperationException("Unknown table " + uri);
- }
- }
-
- @Override
- public String getSortOrder(Uri uri, String aRequested) {
- if (!TextUtils.isEmpty(aRequested)) {
- return aRequested;
- }
-
- final int match = URI_MATCHER.match(uri);
- switch (match) {
- case DELETED_PASSWORDS:
- return DEFAULT_DELETED_PASSWORDS_SORT_ORDER;
-
- case PASSWORDS:
- return DEFAULT_PASSWORDS_SORT_ORDER;
-
- case DISABLED_HOSTS:
- return null;
-
- default:
- throw new UnsupportedOperationException("Unknown URI " + uri);
- }
- }
-
- @Override
- public void setupDefaults(Uri uri, ContentValues values)
- throws IllegalArgumentException {
- int match = URI_MATCHER.match(uri);
- long now = System.currentTimeMillis();
- switch (match) {
- case DELETED_PASSWORDS:
- values.put(DeletedPasswords.TIME_DELETED, now);
-
- // Deleted passwords must contain a guid
- if (!values.containsKey(Passwords.GUID)) {
- throw new IllegalArgumentException("Must provide a GUID for a deleted password");
- }
- break;
-
- case PASSWORDS:
- values.put(Passwords.TIME_CREATED, now);
-
- // Generate GUID for new password. Don't override specified GUIDs.
- if (!values.containsKey(Passwords.GUID)) {
- String guid = Utils.generateGuid();
- values.put(Passwords.GUID, guid);
- }
- String nowString = Long.toString(now);
- DBUtils.replaceKey(values, null, Passwords.HOSTNAME, "");
- DBUtils.replaceKey(values, null, Passwords.HTTP_REALM, "");
- DBUtils.replaceKey(values, null, Passwords.FORM_SUBMIT_URL, "");
- DBUtils.replaceKey(values, null, Passwords.USERNAME_FIELD, "");
- DBUtils.replaceKey(values, null, Passwords.PASSWORD_FIELD, "");
- DBUtils.replaceKey(values, null, Passwords.ENCRYPTED_USERNAME, "");
- DBUtils.replaceKey(values, null, Passwords.ENCRYPTED_PASSWORD, "");
- DBUtils.replaceKey(values, null, Passwords.ENC_TYPE, "0");
- DBUtils.replaceKey(values, null, Passwords.TIME_LAST_USED, nowString);
- DBUtils.replaceKey(values, null, Passwords.TIME_PASSWORD_CHANGED, nowString);
- DBUtils.replaceKey(values, null, Passwords.TIMES_USED, "0");
- break;
-
- case DISABLED_HOSTS:
- if (!values.containsKey(GeckoDisabledHosts.HOSTNAME)) {
- throw new IllegalArgumentException("Must provide a hostname for a disabled host");
- }
- break;
-
- default:
- throw new UnsupportedOperationException("Unknown URI " + uri);
- }
- }
-
- @Override
- public void initGecko() {
- // We're not in the main process. The receiver of this Intent can
- // communicate with Gecko in the main process.
- Intent initIntent = new Intent(getContext(), GeckoMessageReceiver.class);
- initIntent.setAction(GeckoApp.ACTION_INIT_PW);
- mContext.sendBroadcast(initIntent);
- }
-
- private String doCrypto(String initialValue, Uri uri, Boolean encrypt) {
- String profilePath = null;
- if (uri != null) {
- profilePath = uri.getQueryParameter(BrowserContract.PARAM_PROFILE_PATH);
- }
-
- String result = "";
- try {
- if (encrypt) {
- if (profilePath != null) {
- result = NSSBridge.encrypt(mContext, profilePath, initialValue);
- } else {
- result = NSSBridge.encrypt(mContext, initialValue);
- }
- } else {
- if (profilePath != null) {
- result = NSSBridge.decrypt(mContext, profilePath, initialValue);
- } else {
- result = NSSBridge.decrypt(mContext, initialValue);
- }
- }
- } catch (Exception ex) {
- Log.e(LOG_TAG, "Error in NSSBridge");
- throw new RuntimeException(ex);
- }
- return result;
- }
-
- @Override
- public void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db) {
- if (values.containsKey(Passwords.GUID)) {
- String guid = values.getAsString(Passwords.GUID);
- if (guid == null) {
- db.delete(TABLE_DELETED_PASSWORDS, WHERE_GUID_IS_NULL, null);
- return;
- }
- String[] args = new String[] { guid };
- db.delete(TABLE_DELETED_PASSWORDS, WHERE_GUID_IS_VALUE, args);
- }
-
- if (values.containsKey(Passwords.ENCRYPTED_PASSWORD)) {
- String res = doCrypto(values.getAsString(Passwords.ENCRYPTED_PASSWORD), uri, true);
- values.put(Passwords.ENCRYPTED_PASSWORD, res);
- values.put(Passwords.ENC_TYPE, Passwords.ENCTYPE_SDR);
- }
-
- if (values.containsKey(Passwords.ENCRYPTED_USERNAME)) {
- String res = doCrypto(values.getAsString(Passwords.ENCRYPTED_USERNAME), uri, true);
- values.put(Passwords.ENCRYPTED_USERNAME, res);
- values.put(Passwords.ENC_TYPE, Passwords.ENCTYPE_SDR);
- }
- }
-
- @Override
- public void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db) {
- if (values.containsKey(Passwords.ENCRYPTED_PASSWORD)) {
- String res = doCrypto(values.getAsString(Passwords.ENCRYPTED_PASSWORD), uri, true);
- values.put(Passwords.ENCRYPTED_PASSWORD, res);
- values.put(Passwords.ENC_TYPE, Passwords.ENCTYPE_SDR);
- }
-
- if (values.containsKey(Passwords.ENCRYPTED_USERNAME)) {
- String res = doCrypto(values.getAsString(Passwords.ENCRYPTED_USERNAME), uri, true);
- values.put(Passwords.ENCRYPTED_USERNAME, res);
- values.put(Passwords.ENC_TYPE, Passwords.ENCTYPE_SDR);
- }
- }
-
- @Override
- public void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db) {
- int passwordIndex = -1;
- int usernameIndex = -1;
- String profilePath = null;
-
- try {
- passwordIndex = cursor.getColumnIndexOrThrow(Passwords.ENCRYPTED_PASSWORD);
- } catch (Exception ex) { }
- try {
- usernameIndex = cursor.getColumnIndexOrThrow(Passwords.ENCRYPTED_USERNAME);
- } catch (Exception ex) { }
-
- if (passwordIndex > -1 || usernameIndex > -1) {
- MatrixBlobCursor m = (MatrixBlobCursor)cursor;
- if (cursor.moveToFirst()) {
- do {
- if (passwordIndex > -1) {
- String decrypted = doCrypto(cursor.getString(passwordIndex), uri, false);;
- m.set(passwordIndex, decrypted);
- }
-
- if (usernameIndex > -1) {
- String decrypted = doCrypto(cursor.getString(usernameIndex), uri, false);
- m.set(usernameIndex, decrypted);
- }
- } while (cursor.moveToNext());
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/PerProfileDatabaseProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/PerProfileDatabaseProvider.java
deleted file mode 100644
index 7075c6e8a..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/PerProfileDatabaseProvider.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
-
-import android.content.Context;
-import android.database.sqlite.SQLiteOpenHelper;
-
-/**
- * Abstract class containing methods needed to make a SQLite-based content
- * provider with a database helper of type T, where one database helper is
- * held per profile.
- */
-public abstract class PerProfileDatabaseProvider<T extends SQLiteOpenHelper> extends AbstractPerProfileDatabaseProvider {
- private PerProfileDatabases<T> databases;
-
- @Override
- protected PerProfileDatabases<T> getDatabases() {
- return databases;
- }
-
- protected abstract String getDatabaseName();
-
- /**
- * Creates and returns an instance of the appropriate DB helper.
- *
- * @param context to use to create the database helper
- * @param databasePath path to the DB file
- * @return instance of the database helper
- */
- protected abstract T createDatabaseHelper(Context context, String databasePath);
-
- @Override
- public boolean onCreate() {
- synchronized (this) {
- databases = new PerProfileDatabases<T>(
- getContext(), getDatabaseName(), new DatabaseHelperFactory<T>() {
- @Override
- public T makeDatabaseHelper(Context context, String databasePath) {
- final T helper = createDatabaseHelper(context, databasePath);
- if (Versions.feature16Plus) {
- helper.setWriteAheadLoggingEnabled(true);
- }
- return helper;
- }
- });
- }
-
- return true;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/PerProfileDatabases.java b/mobile/android/base/java/org/mozilla/gecko/db/PerProfileDatabases.java
deleted file mode 100644
index 288d9cae7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/PerProfileDatabases.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import java.io.File;
-import java.util.HashMap;
-
-import org.mozilla.gecko.GeckoProfile;
-
-import android.content.Context;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.text.TextUtils;
-
-/**
- * Manages a set of per-profile database storage helpers.
- */
-public class PerProfileDatabases<T extends SQLiteOpenHelper> {
-
- private final HashMap<String, T> mStorages = new HashMap<String, T>();
-
- private final Context mContext;
- private final String mDatabaseName;
- private final DatabaseHelperFactory<T> mHelperFactory;
-
- // Only used during tests.
- public void shutdown() {
- synchronized (this) {
- for (T t : mStorages.values()) {
- try {
- t.close();
- } catch (Throwable e) {
- // Never mind.
- }
- }
- }
- }
-
- public interface DatabaseHelperFactory<T> {
- public T makeDatabaseHelper(Context context, String databasePath);
- }
-
- public PerProfileDatabases(final Context context, final String databaseName, final DatabaseHelperFactory<T> helperFactory) {
- mContext = context;
- mDatabaseName = databaseName;
- mHelperFactory = helperFactory;
- }
-
- public String getDatabasePathForProfile(String profile) {
- final File profileDir = GeckoProfile.get(mContext, profile).getDir();
- if (profileDir == null) {
- return null;
- }
-
- return new File(profileDir, mDatabaseName).getAbsolutePath();
- }
-
- public T getDatabaseHelperForProfile(String profile) {
- return getDatabaseHelperForProfile(profile, false);
- }
-
- public T getDatabaseHelperForProfile(String profile, boolean isTest) {
- // Always fall back to default profile if none has been provided.
- if (profile == null) {
- profile = GeckoProfile.get(mContext).getName();
- }
-
- synchronized (this) {
- if (mStorages.containsKey(profile)) {
- return mStorages.get(profile);
- }
-
- final String databasePath = isTest ? mDatabaseName : getDatabasePathForProfile(profile);
- if (databasePath == null) {
- throw new IllegalStateException("Database path is null for profile: " + profile);
- }
-
- final T helper = mHelperFactory.makeDatabaseHelper(mContext, databasePath);
- DBUtils.ensureDatabaseIsNotLocked(helper, databasePath);
-
- mStorages.put(profile, helper);
- return helper;
- }
- }
-
- public synchronized void shrinkMemory() {
- for (T t : mStorages.values()) {
- final SQLiteDatabase db = t.getWritableDatabase();
- db.execSQL("PRAGMA shrink_memory");
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/RemoteClient.java b/mobile/android/base/java/org/mozilla/gecko/db/RemoteClient.java
deleted file mode 100644
index 07f057c11..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/RemoteClient.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import java.util.ArrayList;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-
-/**
- * A thin representation of a remote client.
- * <p>
- * We use the hash of the client's GUID as the ID elsewhere.
- */
-public class RemoteClient implements Parcelable {
- public final String guid;
- public final String name;
- public final long lastModified;
- public final String deviceType;
- public final ArrayList<RemoteTab> tabs;
-
- public RemoteClient(String guid, String name, long lastModified, String deviceType) {
- this.guid = guid;
- this.name = name;
- this.lastModified = lastModified;
- this.deviceType = deviceType;
- this.tabs = new ArrayList<>();
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel parcel, int flags) {
- parcel.writeString(guid);
- parcel.writeString(name);
- parcel.writeLong(lastModified);
- parcel.writeString(deviceType);
- parcel.writeTypedList(tabs);
- }
-
- public static final Creator<RemoteClient> CREATOR = new Creator<RemoteClient>() {
- @Override
- public RemoteClient createFromParcel(final Parcel source) {
- final String guid = source.readString();
- final String name = source.readString();
- final long lastModified = source.readLong();
- final String deviceType = source.readString();
-
- final RemoteClient client = new RemoteClient(guid, name, lastModified, deviceType);
- source.readTypedList(client.tabs, RemoteTab.CREATOR);
-
- return client;
- }
-
- @Override
- public RemoteClient[] newArray(final int size) {
- return new RemoteClient[size];
- }
- };
-
- public boolean isDesktop() {
- return "desktop".equals(deviceType);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/RemoteTab.java b/mobile/android/base/java/org/mozilla/gecko/db/RemoteTab.java
deleted file mode 100644
index f7660c1f7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/RemoteTab.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-
-/**
- * A thin representation of a remote tab.
- * <p>
- * These are generated functions.
- */
-public class RemoteTab implements Parcelable {
- public final String title;
- public final String url;
- public final long lastUsed;
-
- public RemoteTab(String title, String url, long lastUsed) {
- this.title = title;
- this.url = url;
- this.lastUsed = lastUsed;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel parcel, int flags) {
- parcel.writeString(title);
- parcel.writeString(url);
- parcel.writeLong(lastUsed);
- }
-
- public static final Creator<RemoteTab> CREATOR = new Creator<RemoteTab>() {
- @Override
- public RemoteTab createFromParcel(final Parcel source) {
- final String title = source.readString();
- final String url = source.readString();
- final long lastUsed = source.readLong();
- return new RemoteTab(title, url, lastUsed);
- }
-
- @Override
- public RemoteTab[] newArray(final int size) {
- return new RemoteTab[size];
- }
- };
-
- @Override
- public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = prime * result + ((title == null) ? 0 : title.hashCode());
- result = prime * result + ((url == null) ? 0 : url.hashCode());
- return result;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj == null) {
- return false;
- }
- if (getClass() != obj.getClass()) {
- return false;
- }
- RemoteTab other = (RemoteTab) obj;
- if (title == null) {
- if (other.title != null) {
- return false;
- }
- } else if (!title.equals(other.title)) {
- return false;
- }
- if (url == null) {
- if (other.url != null) {
- return false;
- }
- } else if (!url.equals(other.url)) {
- return false;
- }
- return true;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/SQLiteBridgeContentProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/SQLiteBridgeContentProvider.java
deleted file mode 100644
index d48604f03..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/SQLiteBridgeContentProvider.java
+++ /dev/null
@@ -1,471 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import java.io.File;
-import java.util.HashMap;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.GeckoThread;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.mozglue.GeckoLoader;
-import org.mozilla.gecko.sqlite.SQLiteBridge;
-import org.mozilla.gecko.sqlite.SQLiteBridgeException;
-
-import android.content.ContentProvider;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.net.Uri;
-import android.text.TextUtils;
-import android.util.Log;
-
-/*
- * Provides a basic ContentProvider that sets up and sends queries through
- * SQLiteBridge. Content providers should extend this by setting the appropriate
- * table and version numbers in onCreate, and implementing the abstract methods:
- *
- * public abstract String getTable(Uri uri);
- * public abstract String getSortOrder(Uri uri, String aRequested);
- * public abstract void setupDefaults(Uri uri, ContentValues values);
- * public abstract void initGecko();
- */
-
-public abstract class SQLiteBridgeContentProvider extends ContentProvider {
- private static final String ERROR_MESSAGE_DATABASE_IS_LOCKED = "Can't step statement: (5) database is locked";
-
- private HashMap<String, SQLiteBridge> mDatabasePerProfile;
- protected Context mContext;
- private final String mLogTag;
-
- protected SQLiteBridgeContentProvider(String logTag) {
- mLogTag = logTag;
- }
-
- /**
- * Subclasses must override this to allow error reporting code to compose
- * the correct histogram name.
- *
- * Ensure that you define the new histograms if you define a new class!
- */
- protected abstract String getTelemetryPrefix();
-
- /**
- * Errors are recorded in telemetry using an enumerated histogram.
- *
- * <https://developer.mozilla.org/en-US/docs/Mozilla/Performance/
- * Adding_a_new_Telemetry_probe#Choosing_a_Histogram_Type>
- *
- * These are the allowable enumeration values. Keep these in sync with the
- * histogram definition!
- *
- */
- private static enum TelemetryErrorOp {
- BULKINSERT (0),
- DELETE (1),
- INSERT (2),
- QUERY (3),
- UPDATE (4);
-
- private final int bucket;
-
- TelemetryErrorOp(final int bucket) {
- this.bucket = bucket;
- }
-
- public int getBucket() {
- return bucket;
- }
- }
-
- @Override
- public void shutdown() {
- if (mDatabasePerProfile == null) {
- return;
- }
-
- synchronized (this) {
- for (SQLiteBridge bridge : mDatabasePerProfile.values()) {
- if (bridge != null) {
- try {
- bridge.close();
- } catch (Exception ex) { }
- }
- }
- mDatabasePerProfile = null;
- }
- super.shutdown();
- }
-
- @Override
- public void finalize() {
- shutdown();
- }
-
- /**
- * Return true of the query is from Firefox Sync.
- * @param uri query URI
- */
- public static boolean isCallerSync(Uri uri) {
- String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
- return !TextUtils.isEmpty(isSync);
- }
-
- private SQLiteBridge getDB(Context context, final String databasePath) {
- SQLiteBridge bridge = null;
-
- boolean dbNeedsSetup = true;
- try {
- String resourcePath = context.getPackageResourcePath();
- GeckoLoader.loadSQLiteLibs(context, resourcePath);
- GeckoLoader.loadNSSLibs(context, resourcePath);
- bridge = SQLiteBridge.openDatabase(databasePath, null, 0);
- int version = bridge.getVersion();
- dbNeedsSetup = version != getDBVersion();
- } catch (SQLiteBridgeException ex) {
- // close the database
- if (bridge != null) {
- bridge.close();
- }
-
- // this will throw if the database can't be found
- // we should attempt to set it up if Gecko is running
- dbNeedsSetup = true;
- Log.e(mLogTag, "Error getting version ", ex);
-
- // if Gecko is not running, we should bail out. Otherwise we try to
- // let Gecko build the database for us
- if (!GeckoThread.isRunning()) {
- Log.e(mLogTag, "Can not set up database. Gecko is not running");
- return null;
- }
- }
-
- // If the database is not set up yet, or is the wrong schema version, we send an initialize
- // call to Gecko. Gecko will handle building the database file correctly, as well as any
- // migrations that are necessary
- if (dbNeedsSetup) {
- bridge = null;
- initGecko();
- }
- return bridge;
- }
-
- /**
- * Returns the absolute path of a database file depending on the specified profile and dbName.
- * @param profile
- * the profile whose dbPath must be returned
- * @param dbName
- * the name of the db file whose absolute path must be returned
- * @return the absolute path of the db file or <code>null</code> if it was not possible to retrieve a valid path
- *
- */
- private String getDatabasePathForProfile(String profile, String dbName) {
- // Depends on the vagaries of GeckoProfile.get, so null check for safety.
- File profileDir = GeckoProfile.get(mContext, profile).getDir();
- if (profileDir == null) {
- return null;
- }
-
- String databasePath = new File(profileDir, dbName).getAbsolutePath();
- return databasePath;
- }
-
- /**
- * Returns a SQLiteBridge object according to the specified profile id and to the name of db related to the
- * current provider instance.
- * @param profile
- * the id of the profile to be used to retrieve the related SQLiteBridge
- * @return the <code>SQLiteBridge</code> related to the specified profile id or <code>null</code> if it was
- * not possible to retrieve a valid SQLiteBridge
- */
- private SQLiteBridge getDatabaseForProfile(String profile) {
- if (profile == null) {
- profile = GeckoProfile.get(mContext).getName();
- Log.d(mLogTag, "No profile provided, using '" + profile + "'");
- }
-
- final String dbName = getDBName();
- String mapKey = profile + "/" + dbName;
-
- SQLiteBridge db = null;
- synchronized (this) {
- db = mDatabasePerProfile.get(mapKey);
- if (db != null) {
- return db;
- }
- final String dbPath = getDatabasePathForProfile(profile, dbName);
- if (dbPath == null) {
- Log.e(mLogTag, "Failed to get a valid db path for profile '" + profile + "'' dbName '" + dbName + "'");
- return null;
- }
- db = getDB(mContext, dbPath);
- if (db != null) {
- mDatabasePerProfile.put(mapKey, db);
- }
- }
- return db;
- }
-
- /**
- * Returns a SQLiteBridge object according to the specified profile path and to the name of db related to the
- * current provider instance.
- * @param profilePath
- * the profilePath to be used to retrieve the related SQLiteBridge
- * @return the <code>SQLiteBridge</code> related to the specified profile path or <code>null</code> if it was
- * not possible to retrieve a valid <code>SQLiteBridge</code>
- */
- private SQLiteBridge getDatabaseForProfilePath(String profilePath) {
- File profileDir = new File(profilePath, getDBName());
- final String dbPath = profileDir.getPath();
- return getDatabaseForDBPath(dbPath);
- }
-
- /**
- * Returns a SQLiteBridge object according to the specified file path.
- * @param dbPath
- * the path of the file to be used to retrieve the related SQLiteBridge
- * @return the <code>SQLiteBridge</code> related to the specified file path or <code>null</code> if it was
- * not possible to retrieve a valid <code>SQLiteBridge</code>
- *
- */
- private SQLiteBridge getDatabaseForDBPath(String dbPath) {
- SQLiteBridge db = null;
- synchronized (this) {
- db = mDatabasePerProfile.get(dbPath);
- if (db != null) {
- return db;
- }
- db = getDB(mContext, dbPath);
- if (db != null) {
- mDatabasePerProfile.put(dbPath, db);
- }
- }
- return db;
- }
-
- /**
- * Returns a SQLiteBridge object to be used to perform operations on the given <code>Uri</code>.
- * @param uri
- * the <code>Uri</code> to be used to retrieve the related SQLiteBridge
- * @return a <code>SQLiteBridge</code> object to be used on the given uri or <code>null</code> if it was
- * not possible to retrieve a valid <code>SQLiteBridge</code>
- *
- */
- private SQLiteBridge getDatabase(Uri uri) {
- String profile = null;
- String profilePath = null;
-
- profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
- profilePath = uri.getQueryParameter(BrowserContract.PARAM_PROFILE_PATH);
-
- // Testing will specify the absolute profile path
- if (profilePath != null) {
- return getDatabaseForProfilePath(profilePath);
- }
- return getDatabaseForProfile(profile);
- }
-
- @Override
- public boolean onCreate() {
- mContext = getContext();
- synchronized (this) {
- mDatabasePerProfile = new HashMap<String, SQLiteBridge>();
- }
- return true;
- }
-
- @Override
- public String getType(Uri uri) {
- return null;
- }
-
- @Override
- public int delete(Uri uri, String selection, String[] selectionArgs) {
- int deleted = 0;
- final SQLiteBridge db = getDatabase(uri);
- if (db == null) {
- return deleted;
- }
-
- try {
- deleted = db.delete(getTable(uri), selection, selectionArgs);
- } catch (SQLiteBridgeException ex) {
- reportError(ex, TelemetryErrorOp.DELETE);
- throw ex;
- }
-
- return deleted;
- }
-
- @Override
- public Uri insert(Uri uri, ContentValues values) {
- long id = -1;
- final SQLiteBridge db = getDatabase(uri);
-
- // If we can not get a SQLiteBridge instance, its likely that the database
- // has not been set up and Gecko is not running. We return null and expect
- // callers to try again later
- if (db == null) {
- return null;
- }
-
- setupDefaults(uri, values);
-
- boolean useTransaction = !db.inTransaction();
- try {
- if (useTransaction) {
- db.beginTransaction();
- }
-
- // onPreInsert does a check for the item in the deleted table in some cases
- // so we put it inside this transaction
- onPreInsert(values, uri, db);
- id = db.insert(getTable(uri), null, values);
-
- if (useTransaction) {
- db.setTransactionSuccessful();
- }
- } catch (SQLiteBridgeException ex) {
- reportError(ex, TelemetryErrorOp.INSERT);
- throw ex;
- } finally {
- if (useTransaction) {
- db.endTransaction();
- }
- }
-
- return ContentUris.withAppendedId(uri, id);
- }
-
- @Override
- public int bulkInsert(Uri uri, ContentValues[] allValues) {
- final SQLiteBridge db = getDatabase(uri);
- // If we can not get a SQLiteBridge instance, its likely that the database
- // has not been set up and Gecko is not running. We return 0 and expect
- // callers to try again later
- if (db == null) {
- return 0;
- }
-
- int rowsAdded = 0;
-
- String table = getTable(uri);
-
- try {
- db.beginTransaction();
- for (ContentValues initialValues : allValues) {
- ContentValues values = new ContentValues(initialValues);
- setupDefaults(uri, values);
- onPreInsert(values, uri, db);
- db.insert(table, null, values);
- rowsAdded++;
- }
- db.setTransactionSuccessful();
- } catch (SQLiteBridgeException ex) {
- reportError(ex, TelemetryErrorOp.BULKINSERT);
- throw ex;
- } finally {
- db.endTransaction();
- }
-
- if (rowsAdded > 0) {
- final boolean shouldSyncToNetwork = !isCallerSync(uri);
- mContext.getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
- }
-
- return rowsAdded;
- }
-
- @Override
- public int update(Uri uri, ContentValues values, String selection,
- String[] selectionArgs) {
- int updated = 0;
- final SQLiteBridge db = getDatabase(uri);
-
- // If we can not get a SQLiteBridge instance, its likely that the database
- // has not been set up and Gecko is not running. We return null and expect
- // callers to try again later
- if (db == null) {
- return updated;
- }
-
- onPreUpdate(values, uri, db);
-
- try {
- updated = db.update(getTable(uri), values, selection, selectionArgs);
- } catch (SQLiteBridgeException ex) {
- reportError(ex, TelemetryErrorOp.UPDATE);
- throw ex;
- }
-
- return updated;
- }
-
- @Override
- public Cursor query(Uri uri, String[] projection, String selection,
- String[] selectionArgs, String sortOrder) {
- Cursor cursor = null;
- final SQLiteBridge db = getDatabase(uri);
-
- // If we can not get a SQLiteBridge instance, its likely that the database
- // has not been set up and Gecko is not running. We return null and expect
- // callers to try again later
- if (db == null) {
- return cursor;
- }
-
- sortOrder = getSortOrder(uri, sortOrder);
-
- try {
- cursor = db.query(getTable(uri), projection, selection, selectionArgs, null, null, sortOrder, null);
- onPostQuery(cursor, uri, db);
- } catch (SQLiteBridgeException ex) {
- reportError(ex, TelemetryErrorOp.QUERY);
- throw ex;
- }
-
- return cursor;
- }
-
- private String getHistogram(SQLiteBridgeException e) {
- // If you add values here, make sure to update
- // toolkit/components/telemetry/Histograms.json.
- if (ERROR_MESSAGE_DATABASE_IS_LOCKED.equals(e.getMessage())) {
- return getTelemetryPrefix() + "_LOCKED";
- }
- return null;
- }
-
- protected void reportError(SQLiteBridgeException e, TelemetryErrorOp op) {
- Log.e(mLogTag, "Error in database " + op.name(), e);
- final String histogram = getHistogram(e);
- if (histogram == null) {
- return;
- }
-
- Telemetry.addToHistogram(histogram, op.getBucket());
- }
-
- protected abstract String getDBName();
-
- protected abstract int getDBVersion();
-
- protected abstract String getTable(Uri uri);
-
- protected abstract String getSortOrder(Uri uri, String aRequested);
-
- protected abstract void setupDefaults(Uri uri, ContentValues values);
-
- protected abstract void initGecko();
-
- protected abstract void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db);
-
- protected abstract void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db);
-
- protected abstract void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db);
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/SearchHistoryProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/SearchHistoryProvider.java
deleted file mode 100644
index 05d31fefd..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/SearchHistoryProvider.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.BrowserContract.SearchHistory;
-
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.database.SQLException;
-import android.database.sqlite.SQLiteDatabase;
-import android.net.Uri;
-import android.text.TextUtils;
-import android.util.Log;
-
-public class SearchHistoryProvider extends SharedBrowserDatabaseProvider {
- private static final String LOG_TAG = "GeckoSearchProvider";
- private static final boolean DEBUG_ENABLED = false;
-
- /**
- * Collapse whitespace.
- */
- private String stripWhitespace(String query) {
- if (TextUtils.isEmpty(query)) {
- return "";
- }
-
- // Collapse whitespace
- return query.trim().replaceAll("\\s+", " ");
- }
-
-
- @Override
- public Uri insertInTransaction(Uri uri, ContentValues cv) {
- final String query = stripWhitespace(cv.getAsString(SearchHistory.QUERY));
-
- // We don't support inserting empty search queries.
- if (TextUtils.isEmpty(query)) {
- return null;
- }
-
- final SQLiteDatabase db = getWritableDatabase(uri);
- long id = -1;
-
- /*
- * Attempt to insert the query. The catch block handles the case when
- * the query already exists in the DB.
- */
- try {
- cv.put(SearchHistory.QUERY, query);
- cv.put(SearchHistory.VISITS, 1);
- cv.put(SearchHistory.DATE_LAST_VISITED, System.currentTimeMillis());
-
- id = db.insertOrThrow(SearchHistory.TABLE_NAME, null, cv);
-
- if (id > 0) {
- return ContentUris.withAppendedId(uri, id);
- }
- } catch (SQLException e) {
- // This happens when the column already exists for this term.
- if (DEBUG_ENABLED) {
- Log.w(LOG_TAG, String.format("Query `%s` already in db", query));
- }
- }
-
- /*
- * Increment the VISITS counter and update the DATE_LAST_VISITED.
- */
- final String sql = "UPDATE " + SearchHistory.TABLE_NAME + " SET " +
- SearchHistory.VISITS + " = " + SearchHistory.VISITS + " + 1, " +
- SearchHistory.DATE_LAST_VISITED + " = " + System.currentTimeMillis() +
- " WHERE " + SearchHistory.QUERY + " = ?";
-
- final Cursor c = db.rawQuery(sql, new String[] { query });
-
- try {
- if (c.getCount() > 1) {
- // There is a UNIQUE constraint on the QUERY column,
- // so there should only be one match.
- return null;
- }
- if (c.moveToFirst()) {
- return ContentUris.withAppendedId(uri, c.getInt(c.getColumnIndex(SearchHistory._ID)));
- }
- } finally {
- c.close();
- }
-
- return null;
- }
-
- @Override
- public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
- return getWritableDatabase(uri).delete(SearchHistory.TABLE_NAME,
- selection, selectionArgs);
- }
-
- /**
- * Since we are managing counts and the full-text db, an update
- * could mangle the internal state. So we disable it.
- */
- @Override
- public int updateInTransaction(Uri uri, ContentValues values, String selection,
- String[] selectionArgs) {
- throw new UnsupportedOperationException("This content provider does not support updating items");
- }
-
- @Override
- public Cursor query(Uri uri, String[] projection, String selection,
- String[] selectionArgs, String sortOrder) {
- final String groupBy = null;
- final String having = null;
- final String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
- final Cursor cursor = getReadableDatabase(uri).query(SearchHistory.TABLE_NAME, projection,
- selection, selectionArgs, groupBy, having, sortOrder, limit);
- cursor.setNotificationUri(getContext().getContentResolver(), uri);
- return cursor;
- }
-
- @Override
- public String getType(Uri uri) {
- return SearchHistory.CONTENT_TYPE;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/Searches.java b/mobile/android/base/java/org/mozilla/gecko/db/Searches.java
deleted file mode 100644
index e050a4f93..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/Searches.java
+++ /dev/null
@@ -1,12 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import android.content.ContentResolver;
-
-public interface Searches {
- public void insert(ContentResolver cr, String query);
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/SharedBrowserDatabaseProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/SharedBrowserDatabaseProvider.java
deleted file mode 100644
index 8be18c089..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/SharedBrowserDatabaseProvider.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.db.BrowserContract.CommonColumns;
-import org.mozilla.gecko.db.BrowserContract.SyncColumns;
-import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.net.Uri;
-import android.util.Log;
-
-/**
- * A ContentProvider subclass that provides per-profile browser.db access
- * that can be safely shared between multiple providers.
- *
- * If multiple ContentProvider classes wish to share a database, it's
- * vitally important that they use the same SQLiteOpenHelpers for access.
- *
- * Failure to do so can cause accidental concurrent writes, with the result
- * being unexpected SQLITE_BUSY errors.
- *
- * This class provides a static {@link PerProfileDatabases} instance, lazily
- * initialized within {@link SharedBrowserDatabaseProvider#onCreate()}.
- */
-public abstract class SharedBrowserDatabaseProvider extends AbstractPerProfileDatabaseProvider {
- private static final String LOGTAG = SharedBrowserDatabaseProvider.class.getSimpleName();
-
- private static PerProfileDatabases<BrowserDatabaseHelper> databases;
-
- @Override
- protected PerProfileDatabases<BrowserDatabaseHelper> getDatabases() {
- return databases;
- }
-
- @Override
- public void shutdown() {
- synchronized (SharedBrowserDatabaseProvider.class) {
- databases.shutdown();
- databases = null;
- }
- }
-
- @Override
- public boolean onCreate() {
- // If necessary, do the shared DB work.
- synchronized (SharedBrowserDatabaseProvider.class) {
- if (databases != null) {
- return true;
- }
-
- final DatabaseHelperFactory<BrowserDatabaseHelper> helperFactory = new DatabaseHelperFactory<BrowserDatabaseHelper>() {
- @Override
- public BrowserDatabaseHelper makeDatabaseHelper(Context context, String databasePath) {
- final BrowserDatabaseHelper helper = new BrowserDatabaseHelper(context, databasePath);
- if (Versions.feature16Plus) {
- helper.setWriteAheadLoggingEnabled(true);
- }
- return helper;
- }
- };
-
- databases = new PerProfileDatabases<BrowserDatabaseHelper>(getContext(), BrowserDatabaseHelper.DATABASE_NAME, helperFactory);
- }
-
- return true;
- }
-
- /**
- * Clean up some deleted records from the specified table.
- *
- * If called in an existing transaction, it is the caller's responsibility
- * to ensure that the transaction is already upgraded to a writer, because
- * this method issues a read followed by a write, and thus is potentially
- * vulnerable to an unhandled SQLITE_BUSY failure during the upgrade.
- *
- * If not called in an existing transaction, no new explicit transaction
- * will be begun.
- */
- protected void cleanUpSomeDeletedRecords(Uri fromUri, String tableName) {
- Log.d(LOGTAG, "Cleaning up deleted records from " + tableName);
-
- // We clean up records marked as deleted that are older than a
- // predefined max age. It's important not be too greedy here and
- // remove only a few old deleted records at a time.
-
- // we cleanup records marked as deleted that are older than a
- // predefined max age. It's important not be too greedy here and
- // remove only a few old deleted records at a time.
-
- // Maximum age of deleted records to be cleaned up (20 days in ms)
- final long MAX_AGE_OF_DELETED_RECORDS = 86400000 * 20;
-
- // Number of records marked as deleted to be removed
- final long DELETED_RECORDS_PURGE_LIMIT = 5;
-
- // Android SQLite doesn't have LIMIT on DELETE. Instead, query for the
- // IDs of matching rows, then delete them in one go.
- final long now = System.currentTimeMillis();
- final String selection = getDeletedItemSelection(now - MAX_AGE_OF_DELETED_RECORDS);
-
- final String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE);
- final SQLiteDatabase db = getWritableDatabaseForProfile(profile, isTest(fromUri));
- final String limit = Long.toString(DELETED_RECORDS_PURGE_LIMIT, 10);
- final Cursor cursor = db.query(tableName, new String[] { CommonColumns._ID }, selection, null, null, null, null, limit);
- final String inClause;
- try {
- inClause = DBUtils.computeSQLInClauseFromLongs(cursor, CommonColumns._ID);
- } finally {
- cursor.close();
- }
-
- db.delete(tableName, inClause, null);
- }
-
- // Override this, or override cleanUpSomeDeletedRecords.
- protected String getDeletedItemSelection(long earlierThan) {
- if (earlierThan == -1L) {
- return SyncColumns.IS_DELETED + " = 1";
- }
- return SyncColumns.IS_DELETED + " = 1 AND " + SyncColumns.DATE_MODIFIED + " <= " + earlierThan;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/SuggestedSites.java b/mobile/android/base/java/org/mozilla/gecko/db/SuggestedSites.java
deleted file mode 100644
index 89b12904b..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/SuggestedSites.java
+++ /dev/null
@@ -1,629 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import android.content.Context;
-import android.content.ContentResolver;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
-import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.database.MatrixCursor.RowBuilder;
-import android.net.Uri;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Scanner;
-import java.util.Set;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.Locales;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.distribution.Distribution;
-import org.mozilla.gecko.restrictions.Restrictions;
-import org.mozilla.gecko.util.RawResource;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-
-/**
- * {@code SuggestedSites} provides API to get a list of locale-specific
- * suggested sites to be used in Fennec's top sites panel. It provides
- * only a single method to fetch the list as a {@code Cursor}. This cursor
- * will then be wrapped by {@code TopSitesCursorWrapper} to blend top,
- * pinned, and suggested sites in the UI. The returned {@code Cursor}
- * uses its own schema defined in {@code BrowserContract.SuggestedSites}
- * for clarity.
- *
- * Under the hood, {@code SuggestedSites} keeps reference to the
- * parsed list of sites to avoid reparsing the JSON file on every
- * {@code get()} call.
- *
- * The default list of suggested sites is stored in a raw Android
- * resource ({@code R.raw.suggestedsites}) which is dynamically
- * generated at build time for each target locale.
- *
- * Changes to the list of suggested sites are saved in SharedPreferences.
- */
-@RobocopTarget
-public class SuggestedSites {
- private static final String LOGTAG = "GeckoSuggestedSites";
-
- // SharedPreference key for suggested sites that should be hidden.
- public static final String PREF_SUGGESTED_SITES_HIDDEN = GeckoPreferences.NON_PREF_PREFIX + "suggestedSites.hidden";
- public static final String PREF_SUGGESTED_SITES_HIDDEN_OLD = "suggestedSites.hidden";
-
- // Locale used to generate the current suggested sites.
- public static final String PREF_SUGGESTED_SITES_LOCALE = GeckoPreferences.NON_PREF_PREFIX + "suggestedSites.locale";
- public static final String PREF_SUGGESTED_SITES_LOCALE_OLD = "suggestedSites.locale";
-
- // File in profile dir with the list of suggested sites.
- private static final String FILENAME = "suggestedsites.json";
-
- private static final String[] COLUMNS = new String[] {
- BrowserContract.SuggestedSites._ID,
- BrowserContract.SuggestedSites.URL,
- BrowserContract.SuggestedSites.TITLE,
- BrowserContract.Combined.HISTORY_ID
- };
-
- private static final String JSON_KEY_URL = "url";
- private static final String JSON_KEY_TITLE = "title";
- private static final String JSON_KEY_IMAGE_URL = "imageurl";
- private static final String JSON_KEY_BG_COLOR = "bgcolor";
- private static final String JSON_KEY_RESTRICTED = "restricted";
-
- private static class Site {
- public final String url;
- public final String title;
- public final String imageUrl;
- public final String bgColor;
- public final boolean restricted;
-
- public Site(JSONObject json) throws JSONException {
- this.restricted = !json.isNull(JSON_KEY_RESTRICTED);
- this.url = json.getString(JSON_KEY_URL);
- this.title = json.getString(JSON_KEY_TITLE);
- this.imageUrl = json.getString(JSON_KEY_IMAGE_URL);
- this.bgColor = json.getString(JSON_KEY_BG_COLOR);
-
- validate();
- }
-
- public Site(String url, String title, String imageUrl, String bgColor) {
- this.url = url;
- this.title = title;
- this.imageUrl = imageUrl;
- this.bgColor = bgColor;
- this.restricted = false;
-
- validate();
- }
-
- private void validate() {
- // Site instances must have non-empty values for all properties except IDs.
- if (TextUtils.isEmpty(url) ||
- TextUtils.isEmpty(title) ||
- TextUtils.isEmpty(imageUrl) ||
- TextUtils.isEmpty(bgColor)) {
- throw new IllegalStateException("Suggested sites must have a URL, title, " +
- "image URL, and background color.");
- }
- }
-
- @Override
- public String toString() {
- return "{ url = " + url + "\n" +
- "restricted = " + restricted + "\n" +
- "title = " + title + "\n" +
- "imageUrl = " + imageUrl + "\n" +
- "bgColor = " + bgColor + " }";
- }
-
- public JSONObject toJSON() throws JSONException {
- final JSONObject json = new JSONObject();
-
- if (restricted) {
- json.put(JSON_KEY_RESTRICTED, true);
- }
-
- json.put(JSON_KEY_URL, url);
- json.put(JSON_KEY_TITLE, title);
- json.put(JSON_KEY_IMAGE_URL, imageUrl);
- json.put(JSON_KEY_BG_COLOR, bgColor);
-
- return json;
- }
- }
-
- final Context context;
- final Distribution distribution;
- private File cachedFile;
- private Map<String, Site> cachedSites;
- private Set<String> cachedBlacklist;
-
- public SuggestedSites(Context appContext) {
- this(appContext, null);
- }
-
- public SuggestedSites(Context appContext, Distribution distribution) {
- this(appContext, distribution, null);
- }
-
- public SuggestedSites(Context appContext, Distribution distribution, File file) {
- this.context = appContext;
- this.distribution = distribution;
- this.cachedFile = file;
- }
-
- synchronized File getFile() {
- if (cachedFile == null) {
- cachedFile = GeckoProfile.get(context).getFile(FILENAME);
- }
- return cachedFile;
- }
-
- private static boolean isNewLocale(Context context, Locale requestedLocale) {
- final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context);
-
- String locale = prefs.getString(PREF_SUGGESTED_SITES_LOCALE_OLD, null);
- if (locale != null) {
- // Migrate the old pref and remove it
- final Editor editor = prefs.edit();
- editor.remove(PREF_SUGGESTED_SITES_LOCALE_OLD);
- editor.putString(PREF_SUGGESTED_SITES_LOCALE, locale);
- editor.apply();
- } else {
- locale = prefs.getString(PREF_SUGGESTED_SITES_LOCALE, null);
- }
- if (locale == null) {
- // Initialize config with the current locale
- updateSuggestedSitesLocale(context);
- return true;
- }
-
- return !TextUtils.equals(requestedLocale.toString(), locale);
- }
-
- /**
- * Return the current locale and its fallback (en_US) in order.
- */
- private static List<Locale> getAcceptableLocales() {
- final List<Locale> locales = new ArrayList<Locale>();
-
- final Locale defaultLocale = Locale.getDefault();
- locales.add(defaultLocale);
-
- if (!defaultLocale.equals(Locale.US)) {
- locales.add(Locale.US);
- }
-
- return locales;
- }
-
- private static Map<String, Site> loadSites(File f) throws IOException {
- Scanner scanner = null;
-
- try {
- scanner = new Scanner(f, "UTF-8");
- return loadSites(scanner.useDelimiter("\\A").next());
- } finally {
- if (scanner != null) {
- scanner.close();
- }
- }
- }
-
- private static Map<String, Site> loadSites(String jsonString) {
- if (TextUtils.isEmpty(jsonString)) {
- return null;
- }
-
- Map<String, Site> sites = null;
-
- try {
- final JSONArray jsonSites = new JSONArray(jsonString);
- sites = new LinkedHashMap<String, Site>(jsonSites.length());
-
- final int count = jsonSites.length();
- for (int i = 0; i < count; i++) {
- final Site site = new Site(jsonSites.getJSONObject(i));
- sites.put(site.url, site);
- }
- } catch (Exception e) {
- Log.e(LOGTAG, "Failed to refresh suggested sites", e);
- return null;
- }
-
- return sites;
- }
-
- /**
- * Saves suggested sites file to disk. Access to this method should
- * be synchronized on 'file'.
- */
- static void saveSites(File f, Map<String, Site> sites) {
- ThreadUtils.assertNotOnUiThread();
-
- if (sites == null || sites.isEmpty()) {
- return;
- }
-
- OutputStreamWriter osw = null;
-
- try {
- final JSONArray jsonSites = new JSONArray();
- for (Site site : sites.values()) {
- jsonSites.put(site.toJSON());
- }
-
- osw = new OutputStreamWriter(new FileOutputStream(f), "UTF-8");
-
- final String jsonString = jsonSites.toString();
- osw.write(jsonString, 0, jsonString.length());
- } catch (Exception e) {
- Log.e(LOGTAG, "Failed to save suggested sites", e);
- } finally {
- if (osw != null) {
- try {
- osw.close();
- } catch (IOException e) {
- // Ignore.
- }
- }
- }
- }
-
- private void maybeWaitForDistribution() {
- if (distribution == null) {
- return;
- }
-
- distribution.addOnDistributionReadyCallback(new Distribution.ReadyCallback() {
- @Override
- public void distributionNotFound() {
- // If distribution doesn't exist, simply continue to load
- // suggested sites directly from resources. See refresh().
- }
-
- @Override
- public void distributionFound(Distribution distribution) {
- Log.d(LOGTAG, "Running post-distribution task: suggested sites.");
- // Merge suggested sites from distribution with the
- // default ones. Distribution takes precedence.
- Map<String, Site> sites = loadFromDistribution(distribution);
- if (sites == null) {
- sites = new LinkedHashMap<String, Site>();
- }
- sites.putAll(loadFromResource());
-
- // Update cached list of sites.
- setCachedSites(sites);
-
- // Save the result to disk.
- final File file = getFile();
- synchronized (file) {
- saveSites(file, sites);
- }
-
- // Then notify any active loaders about the changes.
- final ContentResolver cr = context.getContentResolver();
- cr.notifyChange(BrowserContract.SuggestedSites.CONTENT_URI, null);
- }
-
- @Override
- public void distributionArrivedLate(Distribution distribution) {
- distributionFound(distribution);
- }
- });
- }
-
- /**
- * Loads suggested sites from a distribution file either matching the
- * current locale or with the fallback locale (en-US).
- *
- * It's assumed that the given distribution instance is ready to be
- * used and exists.
- */
- static Map<String, Site> loadFromDistribution(Distribution dist) {
- for (Locale locale : getAcceptableLocales()) {
- try {
- final String languageTag = Locales.getLanguageTag(locale);
- final String path = String.format("suggestedsites/locales/%s/%s",
- languageTag, FILENAME);
-
- final File f = dist.getDistributionFile(path);
- if (f == null) {
- Log.d(LOGTAG, "No suggested sites for locale: " + languageTag);
- continue;
- }
-
- return loadSites(f);
- } catch (Exception e) {
- Log.e(LOGTAG, "Failed to open suggested sites for locale " +
- locale + " in distribution.", e);
- }
- }
-
- return null;
- }
-
- private Map<String, Site> loadFromProfile() {
- try {
- final File file = getFile();
- synchronized (file) {
- return loadSites(file);
- }
- } catch (FileNotFoundException e) {
- maybeWaitForDistribution();
- } catch (IOException e) {
- // Fall through, return null.
- }
-
- return null;
- }
-
- Map<String, Site> loadFromResource() {
- try {
- return loadSites(RawResource.getAsString(context, R.raw.suggestedsites));
- } catch (IOException e) {
- return null;
- }
- }
-
- private synchronized void setCachedSites(Map<String, Site> sites) {
- cachedSites = Collections.unmodifiableMap(sites);
- updateSuggestedSitesLocale(context);
- }
-
- /**
- * Refreshes the cached list of sites either from the default raw
- * source or standard file location. This will be called on every
- * cache miss during a {@code get()} call.
- */
- private void refresh() {
- Log.d(LOGTAG, "Refreshing suggested sites from file");
-
- Map<String, Site> sites = loadFromProfile();
- if (sites == null) {
- sites = loadFromResource();
- }
-
- // Update cached list of sites.
- if (sites != null) {
- setCachedSites(sites);
- }
- }
-
- private static void updateSuggestedSitesLocale(Context context) {
- final Editor editor = GeckoSharedPrefs.forProfile(context).edit();
- editor.putString(PREF_SUGGESTED_SITES_LOCALE, Locale.getDefault().toString());
- editor.apply();
- }
-
- private synchronized Site getSiteForUrl(String url) {
- if (cachedSites == null) {
- return null;
- }
-
- return cachedSites.get(url);
- }
-
- /**
- * Returns a {@code Cursor} with the list of suggested websites.
- *
- * @param limit maximum number of suggested sites.
- */
- public Cursor get(int limit) {
- return get(limit, Locale.getDefault());
- }
-
- /**
- * Returns a {@code Cursor} with the list of suggested websites.
- *
- * @param limit maximum number of suggested sites.
- * @param locale the target locale.
- */
- public Cursor get(int limit, Locale locale) {
- return get(limit, locale, null);
- }
-
- /**
- * Returns a {@code Cursor} with the list of suggested websites.
- *
- * @param limit maximum number of suggested sites.
- * @param excludeUrls list of URLs to be excluded from the list.
- */
- public Cursor get(int limit, List<String> excludeUrls) {
- return get(limit, Locale.getDefault(), excludeUrls);
- }
-
- /**
- * Returns a {@code Cursor} with the list of suggested websites.
- *
- * @param limit maximum number of suggested sites.
- * @param locale the target locale.
- * @param excludeUrls list of URLs to be excluded from the list.
- */
- public synchronized Cursor get(int limit, Locale locale, List<String> excludeUrls) {
- final MatrixCursor cursor = new MatrixCursor(COLUMNS);
- final boolean isNewLocale = isNewLocale(context, locale);
-
- // Force the suggested sites file in profile dir to be re-generated
- // if the locale has changed.
- if (isNewLocale) {
- getFile().delete();
- }
-
- if (cachedSites == null || isNewLocale) {
- Log.d(LOGTAG, "No cached sites, refreshing.");
- refresh();
- }
-
- // Return empty cursor if there was an error when
- // loading the suggested sites or the list is empty.
- if (cachedSites == null || cachedSites.isEmpty()) {
- return cursor;
- }
-
- excludeUrls = includeBlacklist(excludeUrls);
-
- final int sitesCount = cachedSites.size();
- Log.d(LOGTAG, "Number of suggested sites: " + sitesCount);
-
- final int maxCount = Math.min(limit, sitesCount);
- // History IDS: real history is positive, -1 is no history id in the combined table
- // hence we can start at -2 for suggested sites
- int id = -1;
- for (Site site : cachedSites.values()) {
- // Decrement ID here: this ensure we have a consistent ID to URL mapping, even if items
- // are removed. If we instead decremented at the point of insertion we'd end up with
- // ID conflicts when a suggested site is removed. (note that cachedSites does not change
- // while we're already showing topsites)
- --id;
- if (cursor.getCount() == maxCount) {
- break;
- }
-
- if (excludeUrls != null && excludeUrls.contains(site.url)) {
- continue;
- }
-
- final boolean restrictedProfile = Restrictions.isRestrictedProfile(context);
-
- if (restrictedProfile == site.restricted) {
- final RowBuilder row = cursor.newRow();
- row.add(id);
- row.add(site.url);
- row.add(site.title);
- row.add(id);
- }
- }
-
- cursor.setNotificationUri(context.getContentResolver(),
- BrowserContract.SuggestedSites.CONTENT_URI);
-
- return cursor;
- }
-
- public boolean contains(String url) {
- return (getSiteForUrl(url) != null);
- }
-
- public String getImageUrlForUrl(String url) {
- final Site site = getSiteForUrl(url);
- return (site != null ? site.imageUrl : null);
- }
-
- public String getBackgroundColorForUrl(String url) {
- final Site site = getSiteForUrl(url);
- return (site != null ? site.bgColor : null);
- }
-
- private Set<String> loadBlacklist() {
- Log.d(LOGTAG, "Loading blacklisted suggested sites from SharedPreferences.");
- final Set<String> blacklist = new HashSet<String>();
-
- final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context);
- String sitesString = prefs.getString(PREF_SUGGESTED_SITES_HIDDEN_OLD, null);
- if (sitesString != null) {
- // Migrate the old pref and remove it
- final Editor editor = prefs.edit();
- editor.remove(PREF_SUGGESTED_SITES_HIDDEN_OLD);
- editor.putString(PREF_SUGGESTED_SITES_HIDDEN, sitesString);
- editor.apply();
- } else {
- sitesString = prefs.getString(PREF_SUGGESTED_SITES_HIDDEN, null);
- }
-
- if (sitesString != null) {
- for (String site : sitesString.trim().split(" ")) {
- blacklist.add(Uri.decode(site));
- }
- }
-
- return blacklist;
- }
-
- private List<String> includeBlacklist(List<String> originalList) {
- if (cachedBlacklist == null) {
- cachedBlacklist = loadBlacklist();
- }
-
- if (cachedBlacklist.isEmpty()) {
- return originalList;
- }
-
- if (originalList == null) {
- originalList = new ArrayList<String>();
- }
-
- originalList.addAll(cachedBlacklist);
- return originalList;
- }
-
- /**
- * Blacklist a suggested site so it will no longer be returned as a suggested site.
- * This method should only be called from a background thread because it may write
- * to SharedPreferences.
- *
- * Urls that are not Suggested Sites are ignored.
- *
- * @param url String url of site to blacklist
- * @return true is blacklisted, false otherwise
- */
- public synchronized boolean hideSite(String url) {
- ThreadUtils.assertNotOnUiThread();
-
- if (cachedSites == null) {
- refresh();
- if (cachedSites == null) {
- Log.w(LOGTAG, "Could not load suggested sites!");
- return false;
- }
- }
-
- if (cachedSites.containsKey(url)) {
- if (cachedBlacklist == null) {
- cachedBlacklist = loadBlacklist();
- }
-
- // Check if site has already been blacklisted, just in case.
- if (!cachedBlacklist.contains(url)) {
-
- saveToBlacklist(url);
- cachedBlacklist.add(url);
-
- return true;
- }
- }
-
- return false;
- }
-
- private void saveToBlacklist(String url) {
- final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context);
- final String prefString = prefs.getString(PREF_SUGGESTED_SITES_HIDDEN, "");
- final String siteString = prefString.concat(" " + Uri.encode(url));
- prefs.edit().putString(PREF_SUGGESTED_SITES_HIDDEN, siteString).apply();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/Table.java b/mobile/android/base/java/org/mozilla/gecko/db/Table.java
deleted file mode 100644
index 37a605ee1..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/Table.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.net.Uri;
-
-// Tables provide a basic wrapper around ContentProvider methods to make it simpler to add new tables into storage.
-// If you create a new Table type, make sure to add it to the sTables list in BrowserProvider to ensure it is queried.
-interface Table {
- // Provides information to BrowserProvider about the type of URIs this Table can handle.
- public static class ContentProviderInfo {
- public final int id; // A number of ID for this table. Used by the UriMatcher in BrowserProvider
- public final String name; // A name for this table. Will be appended onto uris querying this table
- // This is also used to define the mimetype of data returned from this db, i.e.
- // BrowserProvider will return "vnd.android.cursor.item/" + name
-
- public ContentProviderInfo(int id, String name) {
- if (name == null) {
- throw new IllegalArgumentException("Content provider info must specify a name");
- }
- this.id = id;
- this.name = name;
- }
- }
-
- // Return a list of Info about the ContentProvider URIs this will match
- ContentProviderInfo[] getContentProviderInfo();
-
- // Called by BrowserDBHelper whenever the database is created or upgraded.
- // Order in which tables are created/upgraded isn't guaranteed (yet), so be careful if your Table depends on something in a
- // separate table.
- void onCreate(SQLiteDatabase db);
- void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
-
- // Called by BrowserProvider when this database queried/modified
- // The dbId here should match the dbId's you returned in your getContentProviderInfo() call
- Cursor query(SQLiteDatabase db, Uri uri, int dbId, String[] projection, String selection, String[] selectionArgs, String sortOrder, String groupBy, String limit);
- int update(SQLiteDatabase db, Uri uri, int dbId, ContentValues values, String selection, String[] selectionArgs);
- long insert(SQLiteDatabase db, Uri uri, int dbId, ContentValues values);
- int delete(SQLiteDatabase db, Uri uri, int dbId, String selection, String[] selectionArgs);
-};
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/TabsAccessor.java b/mobile/android/base/java/org/mozilla/gecko/db/TabsAccessor.java
deleted file mode 100644
index 1be004ca7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/TabsAccessor.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.Cursor;
-
-import org.mozilla.gecko.Tab;
-
-import java.util.List;
-
-public interface TabsAccessor {
- public interface OnQueryTabsCompleteListener {
- public void onQueryTabsComplete(List<RemoteClient> clients);
- }
-
- public Cursor getRemoteClientsByRecencyCursor(Context context);
- public Cursor getRemoteTabsCursor(Context context);
- public Cursor getRemoteTabsCursor(Context context, int limit);
- public List<RemoteClient> getClientsWithoutTabsByRecencyFromCursor(final Cursor cursor);
- public List<RemoteClient> getClientsFromCursor(final Cursor cursor);
- public void getTabs(final Context context, final OnQueryTabsCompleteListener listener);
- public void getTabs(final Context context, final int limit, final OnQueryTabsCompleteListener listener);
- public void persistLocalTabs(final ContentResolver cr, final Iterable<Tab> tabs);
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/TabsProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/TabsProvider.java
deleted file mode 100644
index 09e4d9cf5..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/TabsProvider.java
+++ /dev/null
@@ -1,361 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-
-import org.mozilla.gecko.db.BrowserContract.Clients;
-import org.mozilla.gecko.db.BrowserContract.Tabs;
-
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.UriMatcher;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteQueryBuilder;
-import android.net.Uri;
-import android.text.TextUtils;
-
-public class TabsProvider extends SharedBrowserDatabaseProvider {
- private static final long ONE_DAY_IN_MILLISECONDS = 1000 * 60 * 60 * 24;
- private static final long ONE_WEEK_IN_MILLISECONDS = 7 * ONE_DAY_IN_MILLISECONDS;
- private static final long THREE_WEEKS_IN_MILLISECONDS = 3 * ONE_WEEK_IN_MILLISECONDS;
-
- static final String TABLE_TABS = "tabs";
- static final String TABLE_CLIENTS = "clients";
-
- static final int TABS = 600;
- static final int TABS_ID = 601;
- static final int CLIENTS = 602;
- static final int CLIENTS_ID = 603;
- static final int CLIENTS_RECENCY = 604;
-
- // Exclude clients that are more than three weeks old and also any duplicates that are older than one week old.
- static final String EXCLUDE_STALE_CLIENTS_SUBQUERY =
- "(SELECT " + Clients.GUID +
- ", " + Clients.NAME +
- ", " + Clients.LAST_MODIFIED +
- ", " + Clients.DEVICE_TYPE +
- " FROM " + TABLE_CLIENTS +
- " WHERE " + Clients.LAST_MODIFIED + " > %1$s " +
- " GROUP BY " + Clients.NAME +
- " UNION ALL " +
- " SELECT c." + Clients.GUID + " AS " + Clients.GUID +
- ", c." + Clients.NAME + " AS " + Clients.NAME +
- ", c." + Clients.LAST_MODIFIED + " AS " + Clients.LAST_MODIFIED +
- ", c." + Clients.DEVICE_TYPE + " AS " + Clients.DEVICE_TYPE +
- " FROM " + TABLE_CLIENTS + " AS c " +
- " JOIN (" +
- " SELECT " + Clients.GUID +
- ", " + "MAX( " + Clients.LAST_MODIFIED + ") AS " + Clients.LAST_MODIFIED +
- " FROM " + TABLE_CLIENTS +
- " WHERE (" + Clients.LAST_MODIFIED + " < %1$s" + " AND " + Clients.LAST_MODIFIED + " > %2$s) AND " +
- Clients.NAME + " NOT IN " + "( SELECT " + Clients.NAME + " FROM " + TABLE_CLIENTS + " WHERE " + Clients.LAST_MODIFIED + " > %1$s)" +
- " GROUP BY " + Clients.NAME +
- ") AS c2" +
- " ON c." + Clients.GUID + " = c2." + Clients.GUID + ")";
-
- static final String DEFAULT_TABS_SORT_ORDER = Clients.LAST_MODIFIED + " DESC, " + Tabs.LAST_USED + " DESC";
- static final String DEFAULT_CLIENTS_SORT_ORDER = Clients.LAST_MODIFIED + " DESC";
- static final String DEFAULT_CLIENTS_RECENCY_SORT_ORDER = "COALESCE(MAX(" + Tabs.LAST_USED + "), " + Clients.LAST_MODIFIED + ") DESC";
-
- static final String INDEX_TABS_GUID = "tabs_guid_index";
- static final String INDEX_TABS_POSITION = "tabs_position_index";
-
- static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
-
- static final Map<String, String> TABS_PROJECTION_MAP;
- static final Map<String, String> CLIENTS_PROJECTION_MAP;
- static final Map<String, String> CLIENTS_RECENCY_PROJECTION_MAP;
-
- static {
- URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "tabs", TABS);
- URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "tabs/#", TABS_ID);
- URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "clients", CLIENTS);
- URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "clients/#", CLIENTS_ID);
- URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "clients_recency", CLIENTS_RECENCY);
-
- HashMap<String, String> map;
-
- map = new HashMap<String, String>();
- map.put(Tabs._ID, Tabs._ID);
- map.put(Tabs.TITLE, Tabs.TITLE);
- map.put(Tabs.URL, Tabs.URL);
- map.put(Tabs.HISTORY, Tabs.HISTORY);
- map.put(Tabs.FAVICON, Tabs.FAVICON);
- map.put(Tabs.LAST_USED, Tabs.LAST_USED);
- map.put(Tabs.POSITION, Tabs.POSITION);
- map.put(Clients.GUID, Clients.GUID);
- map.put(Clients.NAME, Clients.NAME);
- map.put(Clients.LAST_MODIFIED, Clients.LAST_MODIFIED);
- map.put(Clients.DEVICE_TYPE, Clients.DEVICE_TYPE);
- TABS_PROJECTION_MAP = Collections.unmodifiableMap(map);
-
- map = new HashMap<String, String>();
- map.put(Clients.GUID, Clients.GUID);
- map.put(Clients.NAME, Clients.NAME);
- map.put(Clients.LAST_MODIFIED, Clients.LAST_MODIFIED);
- map.put(Clients.DEVICE_TYPE, Clients.DEVICE_TYPE);
- CLIENTS_PROJECTION_MAP = Collections.unmodifiableMap(map);
-
- map = new HashMap<>();
- map.put(Clients.GUID, projectColumn(TABLE_CLIENTS, Clients.GUID) + " AS guid");
- map.put(Clients.NAME, projectColumn(TABLE_CLIENTS, Clients.NAME) + " AS name");
- map.put(Clients.LAST_MODIFIED, projectColumn(TABLE_CLIENTS, Clients.LAST_MODIFIED) + " AS last_modified");
- map.put(Clients.DEVICE_TYPE, projectColumn(TABLE_CLIENTS, Clients.DEVICE_TYPE) + " AS device_type");
- // last_used is the max of the tab last_used times, or if there are no tabs,
- // the client's last_modified time.
- map.put(Tabs.LAST_USED, "COALESCE(MAX(" + projectColumn(TABLE_TABS, Tabs.LAST_USED) + "), " + projectColumn(TABLE_CLIENTS, Clients.LAST_MODIFIED) + ") AS last_used");
- CLIENTS_RECENCY_PROJECTION_MAP = Collections.unmodifiableMap(map);
- }
-
- private static final String projectColumn(String table, String column) {
- return table + "." + column;
- }
-
- private static final String selectColumn(String table, String column) {
- return projectColumn(table, column) + " = ?";
- }
-
- @Override
- public String getType(Uri uri) {
- final int match = URI_MATCHER.match(uri);
-
- trace("Getting URI type: " + uri);
-
- switch (match) {
- case TABS:
- trace("URI is TABS: " + uri);
- return Tabs.CONTENT_TYPE;
-
- case TABS_ID:
- trace("URI is TABS_ID: " + uri);
- return Tabs.CONTENT_ITEM_TYPE;
-
- case CLIENTS:
- trace("URI is CLIENTS: " + uri);
- return Clients.CONTENT_TYPE;
-
- case CLIENTS_ID:
- trace("URI is CLIENTS_ID: " + uri);
- return Clients.CONTENT_ITEM_TYPE;
- }
-
- debug("URI has unrecognized type: " + uri);
-
- return null;
- }
-
- @Override
- @SuppressWarnings("fallthrough")
- public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
- trace("Calling delete in transaction on URI: " + uri);
-
- final int match = URI_MATCHER.match(uri);
- int deleted = 0;
-
- switch (match) {
- case CLIENTS_ID:
- trace("Delete on CLIENTS_ID: " + uri);
- selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients._ID));
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { Long.toString(ContentUris.parseId(uri)) });
- // fall through
- case CLIENTS:
- trace("Delete on CLIENTS: " + uri);
- deleted = deleteValues(uri, selection, selectionArgs, TABLE_CLIENTS);
- break;
-
- case TABS_ID:
- trace("Delete on TABS_ID: " + uri);
- selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_TABS, Tabs._ID));
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { Long.toString(ContentUris.parseId(uri)) });
- // fall through
- case TABS:
- trace("Deleting on TABS: " + uri);
- deleted = deleteValues(uri, selection, selectionArgs, TABLE_TABS);
- break;
-
- default:
- throw new UnsupportedOperationException("Unknown delete URI " + uri);
- }
-
- debug("Deleted " + deleted + " rows for URI: " + uri);
-
- return deleted;
- }
-
- @Override
- public Uri insertInTransaction(Uri uri, ContentValues values) {
- trace("Calling insert in transaction on URI: " + uri);
-
- final SQLiteDatabase db = getWritableDatabase(uri);
- int match = URI_MATCHER.match(uri);
- long id = -1;
-
- switch (match) {
- case CLIENTS:
- String guid = values.getAsString(Clients.GUID);
- debug("Inserting client in database with GUID: " + guid);
- id = db.insertOrThrow(TABLE_CLIENTS, Clients.GUID, values);
- break;
-
- case TABS:
- String url = values.getAsString(Tabs.URL);
- debug("Inserting tab in database with URL: " + url);
- id = db.insertOrThrow(TABLE_TABS, Tabs.TITLE, values);
- break;
-
- default:
- throw new UnsupportedOperationException("Unknown insert URI " + uri);
- }
-
- debug("Inserted ID in database: " + id);
-
- if (id >= 0)
- return ContentUris.withAppendedId(uri, id);
-
- return null;
- }
-
- @Override
- public int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
- trace("Calling update in transaction on URI: " + uri);
-
- int match = URI_MATCHER.match(uri);
- int updated = 0;
-
- switch (match) {
- case CLIENTS_ID:
- trace("Update on CLIENTS_ID: " + uri);
- selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients._ID));
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { Long.toString(ContentUris.parseId(uri)) });
- // fall through
- case CLIENTS:
- trace("Update on CLIENTS: " + uri);
- updated = updateValues(uri, values, selection, selectionArgs, TABLE_CLIENTS);
- break;
-
- case TABS_ID:
- trace("Update on TABS_ID: " + uri);
- selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_TABS, Tabs._ID));
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { Long.toString(ContentUris.parseId(uri)) });
- // fall through
- case TABS:
- trace("Update on TABS: " + uri);
- updated = updateValues(uri, values, selection, selectionArgs, TABLE_TABS);
- break;
-
- default:
- throw new UnsupportedOperationException("Unknown update URI " + uri);
- }
-
- debug("Updated " + updated + " rows for URI: " + uri);
-
- return updated;
- }
-
- @Override
- @SuppressWarnings("fallthrough")
- public Cursor query(Uri uri, String[] projection, String selection,
- String[] selectionArgs, String sortOrder) {
- SQLiteDatabase db = getReadableDatabase(uri);
- final int match = URI_MATCHER.match(uri);
-
- String groupBy = null;
- SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
-
- switch (match) {
- case TABS_ID:
- trace("Query is on TABS_ID: " + uri);
- selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_TABS, Tabs._ID));
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { Long.toString(ContentUris.parseId(uri)) });
- // fall through
- case TABS:
- trace("Query is on TABS: " + uri);
- if (TextUtils.isEmpty(sortOrder)) {
- sortOrder = DEFAULT_TABS_SORT_ORDER;
- } else {
- debug("Using sort order " + sortOrder + ".");
- }
-
- qb.setProjectionMap(TABS_PROJECTION_MAP);
- qb.setTables(TABLE_TABS + " LEFT OUTER JOIN " + TABLE_CLIENTS + " ON (" + TABLE_TABS + "." + Tabs.CLIENT_GUID + " = " + TABLE_CLIENTS + "." + Clients.GUID + ")");
- break;
-
- case CLIENTS_ID:
- trace("Query is on CLIENTS_ID: " + uri);
- selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients._ID));
- selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
- new String[] { Long.toString(ContentUris.parseId(uri)) });
- // fall through
- case CLIENTS:
- trace("Query is on CLIENTS: " + uri);
- if (TextUtils.isEmpty(sortOrder)) {
- sortOrder = DEFAULT_CLIENTS_SORT_ORDER;
- } else {
- debug("Using sort order " + sortOrder + ".");
- }
-
- qb.setProjectionMap(CLIENTS_PROJECTION_MAP);
- qb.setTables(TABLE_CLIENTS);
- break;
-
- case CLIENTS_RECENCY:
- trace("Query is on CLIENTS_RECENCY: " + uri);
- if (TextUtils.isEmpty(sortOrder)) {
- sortOrder = DEFAULT_CLIENTS_RECENCY_SORT_ORDER;
- } else {
- debug("Using sort order " + sortOrder + ".");
- }
-
- final long oneWeekAgo = System.currentTimeMillis() - ONE_WEEK_IN_MILLISECONDS;
- final long threeWeeksAgo = System.currentTimeMillis() - THREE_WEEKS_IN_MILLISECONDS;
-
- final String excludeStaleClientsTable = String.format(EXCLUDE_STALE_CLIENTS_SUBQUERY, oneWeekAgo, threeWeeksAgo);
-
- qb.setProjectionMap(CLIENTS_RECENCY_PROJECTION_MAP);
-
- // Use a subquery to quietly exclude stale duplicate client records.
- qb.setTables(excludeStaleClientsTable + " AS " + TABLE_CLIENTS + " LEFT OUTER JOIN " + TABLE_TABS +
- " ON (" + projectColumn(TABLE_CLIENTS, Clients.GUID) +
- " = " + projectColumn(TABLE_TABS, Tabs.CLIENT_GUID) + ")");
- groupBy = projectColumn(TABLE_CLIENTS, Clients.GUID);
- break;
-
- default:
- throw new UnsupportedOperationException("Unknown query URI " + uri);
- }
-
- trace("Running built query.");
- final Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, null, sortOrder, limit);
- cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.TABS_AUTHORITY_URI);
-
- return cursor;
- }
-
- int updateValues(Uri uri, ContentValues values, String selection, String[] selectionArgs, String table) {
- trace("Updating tabs on URI: " + uri);
-
- final SQLiteDatabase db = getWritableDatabase(uri);
- beginWrite(db);
- return db.update(table, values, selection, selectionArgs);
- }
-
- int deleteValues(Uri uri, String selection, String[] selectionArgs, String table) {
- debug("Deleting tabs for URI: " + uri);
-
- final SQLiteDatabase db = getWritableDatabase(uri);
- beginWrite(db);
- return db.delete(table, selection, selectionArgs);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/URLMetadata.java b/mobile/android/base/java/org/mozilla/gecko/db/URLMetadata.java
deleted file mode 100644
index 7973839e2..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/URLMetadata.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
- */
-package org.mozilla.gecko.db;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-
-import org.json.JSONObject;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-
-import android.content.ContentResolver;
-
-@RobocopTarget
-public interface URLMetadata {
- public Map<String, Object> fromJSON(JSONObject obj);
- public Map<String, Map<String, Object>> getForURLs(final ContentResolver cr,
- final Collection<String> urls,
- final List<String> columns);
- public void save(final ContentResolver cr, final Map<String, Object> data);
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/URLMetadataTable.java b/mobile/android/base/java/org/mozilla/gecko/db/URLMetadataTable.java
deleted file mode 100644
index 49bbb74e7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/URLMetadataTable.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-package org.mozilla.gecko.db;
-
-import org.mozilla.gecko.db.BrowserContract.Bookmarks;
-import org.mozilla.gecko.db.BrowserContract.History;
-
-import android.database.sqlite.SQLiteDatabase;
-import android.net.Uri;
-
-// Holds metadata info about urls. Supports some helper functions for getting back a HashMap of key value data.
-public class URLMetadataTable extends BaseTable {
- private static final String LOGTAG = "GeckoURLMetadataTable";
-
- private static final String TABLE = "metadata"; // Name of the table in the db
- private static final int TABLE_ID_NUMBER = BrowserProvider.METADATA;
-
- // Uri for querying this table
- public static final Uri CONTENT_URI = Uri.withAppendedPath(BrowserContract.AUTHORITY_URI, "metadata");
-
- // Columns in the table
- public static final String ID_COLUMN = "id";
- public static final String URL_COLUMN = "url";
- public static final String TILE_IMAGE_URL_COLUMN = "tileImage";
- public static final String TILE_COLOR_COLUMN = "tileColor";
- public static final String TOUCH_ICON_COLUMN = "touchIcon";
-
- URLMetadataTable() { }
-
- @Override
- protected String getTable() {
- return TABLE;
- }
-
- @Override
- public void onCreate(SQLiteDatabase db) {
- String create = "CREATE TABLE " + TABLE + " (" +
- ID_COLUMN + " INTEGER PRIMARY KEY, " +
- URL_COLUMN + " TEXT NON NULL UNIQUE, " +
- TILE_IMAGE_URL_COLUMN + " STRING, " +
- TILE_COLOR_COLUMN + " STRING, " +
- TOUCH_ICON_COLUMN + " STRING);";
- db.execSQL(create);
- }
-
- private void upgradeDatabaseFrom26To27(SQLiteDatabase db) {
- db.execSQL("ALTER TABLE " + TABLE +
- " ADD COLUMN " + TOUCH_ICON_COLUMN + " STRING");
- }
-
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- // This table was added in v21 of the db. Force its creation if we're coming from an earlier version
- if (newVersion >= 21 && oldVersion < 21) {
- onCreate(db);
- return;
- }
-
- // Removed the redundant metadata_url_idx index in version 26
- if (newVersion >= 26 && oldVersion < 26) {
- db.execSQL("DROP INDEX IF EXISTS metadata_url_idx");
- }
- if (newVersion >= 27 && oldVersion < 27) {
- upgradeDatabaseFrom26To27(db);
- }
- }
-
- @Override
- public Table.ContentProviderInfo[] getContentProviderInfo() {
- return new Table.ContentProviderInfo[] {
- new Table.ContentProviderInfo(TABLE_ID_NUMBER, TABLE)
- };
- }
-
- public int deleteUnused(final SQLiteDatabase db) {
- final String selection = URL_COLUMN + " NOT IN " +
- "(SELECT " + History.URL +
- " FROM " + History.TABLE_NAME +
- " WHERE " + History.IS_DELETED + " = 0" +
- " UNION " +
- " SELECT " + Bookmarks.URL +
- " FROM " + Bookmarks.TABLE_NAME +
- " WHERE " + Bookmarks.IS_DELETED + " = 0 " +
- " AND " + Bookmarks.URL + " IS NOT NULL)";
-
- return db.delete(getTable(), selection, null);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/UrlAnnotations.java b/mobile/android/base/java/org/mozilla/gecko/db/UrlAnnotations.java
deleted file mode 100644
index dcae6ee79..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/db/UrlAnnotations.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import android.content.ContentResolver;
-import android.database.Cursor;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
-
-public interface UrlAnnotations {
- @RobocopTarget void insertAnnotation(ContentResolver cr, String url, String key, String value);
-
- Cursor getScreenshots(ContentResolver cr);
- void insertScreenshot(ContentResolver cr, String pageUrl, String screenshotPath);
-
- Cursor getFeedSubscriptions(ContentResolver cr);
- Cursor getWebsitesWithFeedUrl(ContentResolver cr);
- void deleteFeedUrl(ContentResolver cr, String websiteUrl);
- boolean hasWebsiteForFeedUrl(ContentResolver cr, String feedUrl);
- void deleteFeedSubscription(ContentResolver cr, FeedSubscription subscription);
- void updateFeedSubscription(ContentResolver cr, FeedSubscription subscription);
- boolean hasFeedSubscription(ContentResolver cr, String feedUrl);
- void insertFeedSubscription(ContentResolver cr, FeedSubscription subscription);
- boolean hasFeedUrlForWebsite(ContentResolver cr, String websiteUrl);
- void insertFeedUrl(ContentResolver cr, String originUrl, String feedUrl);
-
- void insertReaderViewUrl(ContentResolver cr, String pageURL);
- void deleteReaderViewUrl(ContentResolver cr, String pageURL);
-
- /**
- * Did the user ever interact with this URL in regards to home screen shortcuts?
- *
- * @return true if the user has created a home screen shortcut or declined to create one in the
- * past. This method will still return true if the shortcut has been removed from the
- * home screen by the user.
- */
- boolean hasAcceptedOrDeclinedHomeScreenShortcut(ContentResolver cr, String url);
-
- /**
- * Insert an indication that the user has interacted with this URL in regards to home screen
- * shortcuts.
- *
- * @param hasCreatedShortCut True if a home screen shortcut has been created for this URL. False
- * if the user has actively declined to create a shortcut for this URL.
- */
- void insertHomeScreenShortcut(ContentResolver cr, String url, boolean hasCreatedShortCut);
-
- int getAnnotationCount(ContentResolver cr, BrowserContract.UrlAnnotations.Key key);
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/delegates/BookmarkStateChangeDelegate.java b/mobile/android/base/java/org/mozilla/gecko/delegates/BookmarkStateChangeDelegate.java
deleted file mode 100644
index a1c54d3c3..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/delegates/BookmarkStateChangeDelegate.java
+++ /dev/null
@@ -1,237 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.delegates;
-
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.res.Resources;
-import android.graphics.Color;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.support.design.widget.Snackbar;
-import android.support.v4.content.ContextCompat;
-import android.util.Log;
-import android.view.View;
-import android.widget.ListView;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.AboutPages;
-import org.mozilla.gecko.BrowserApp;
-import org.mozilla.gecko.EditBookmarkDialog;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.SnackbarBuilder;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.home.HomeConfig;
-import org.mozilla.gecko.promotion.SimpleHelperUI;
-import org.mozilla.gecko.prompts.Prompt;
-import org.mozilla.gecko.prompts.PromptListItem;
-import org.mozilla.gecko.util.DrawableUtil;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import java.lang.ref.WeakReference;
-
-/**
- * Delegate to watch for bookmark state changes.
- *
- * This is responsible for showing snackbars and helper UIs related to the addition/removal
- * of bookmarks, or reader view bookmarks.
- */
-public class BookmarkStateChangeDelegate extends BrowserAppDelegateWithReference implements Tabs.OnTabsChangedListener {
- private static final String LOGTAG = "BookmarkDelegate";
-
- @Override
- public void onResume(BrowserApp browserApp) {
- Tabs.registerOnTabsChangedListener(this);
- }
-
- @Override
- public void onPause(BrowserApp browserApp) {
- Tabs.unregisterOnTabsChangedListener(this);
- }
-
- @Override
- public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
- switch (msg) {
- case BOOKMARK_ADDED:
- // We always show the special offline snackbar whenever we bookmark a reader page.
- // It's possible that the page is already stored offline, however this is highly
- // unlikely, and even so it is probably nicer to show the same offline notification
- // every time we bookmark an about:reader page.
- if (!AboutPages.isAboutReader(tab.getURL())) {
- showBookmarkAddedSnackbar();
- } else {
- if (!promoteReaderViewBookmarkAdded()) {
- showReaderModeBookmarkAddedSnackbar();
- }
- }
- break;
-
- case BOOKMARK_REMOVED:
- showBookmarkRemovedSnackbar();
- break;
- }
- }
-
- @Override
- public void onActivityResult(BrowserApp browserApp, int requestCode, int resultCode, Intent data) {
- if (requestCode == BrowserApp.ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK) {
- if (resultCode == BrowserApp.ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS) {
- browserApp.openUrlAndStopEditing("about:home?panel=" + HomeConfig.getIdForBuiltinPanelType(HomeConfig.PanelType.BOOKMARKS));
- } else if (resultCode == BrowserApp.ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE) {
- showReaderModeBookmarkAddedSnackbar();
- }
- }
- }
-
- private boolean promoteReaderViewBookmarkAdded() {
- final BrowserApp browserApp = getBrowserApp();
- if (browserApp == null) {
- return false;
- }
-
- final SharedPreferences prefs = GeckoSharedPrefs.forProfile(browserApp);
-
- final boolean hasFirstReaderViewPromptBeenShownBefore = prefs.getBoolean(SimpleHelperUI.PREF_FIRST_RVBP_SHOWN, false);
-
- if (hasFirstReaderViewPromptBeenShownBefore) {
- return false;
- }
-
- SimpleHelperUI.show(browserApp,
- SimpleHelperUI.FIRST_RVBP_SHOWN_TELEMETRYEXTRA,
- BrowserApp.ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK,
- R.string.helper_first_offline_bookmark_title, R.string.helper_first_offline_bookmark_message,
- R.drawable.helper_readerview_bookmark, R.string.helper_first_offline_bookmark_button,
- BrowserApp.ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS,
- BrowserApp.ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE);
-
- GeckoSharedPrefs.forProfile(browserApp)
- .edit()
- .putBoolean(SimpleHelperUI.PREF_FIRST_RVBP_SHOWN, true)
- .apply();
-
- return true;
- }
-
- private void showBookmarkAddedSnackbar() {
- final BrowserApp browserApp = getBrowserApp();
- if (browserApp == null) {
- return;
- }
-
- // This flow is from the option menu which has check to see if a bookmark was already added.
- // So, it is safe here to show the snackbar that bookmark_added without any checks.
- final SnackbarBuilder.SnackbarCallback callback = new SnackbarBuilder.SnackbarCallback() {
- @Override
- public void onClick(View v) {
- Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.TOAST, "bookmark_options");
- showBookmarkDialog(browserApp);
- }
- };
-
- SnackbarBuilder.builder(browserApp)
- .message(R.string.bookmark_added)
- .duration(Snackbar.LENGTH_LONG)
- .action(R.string.bookmark_options)
- .callback(callback)
- .buildAndShow();
- }
-
- private void showBookmarkRemovedSnackbar() {
- final BrowserApp browserApp = getBrowserApp();
- if (browserApp == null) {
- return;
- }
-
- SnackbarBuilder.builder(browserApp)
- .message(R.string.bookmark_removed)
- .duration(Snackbar.LENGTH_LONG)
- .buildAndShow();
- }
-
- private static void showBookmarkDialog(final BrowserApp browserApp) {
- final Resources res = browserApp.getResources();
- final Tab tab = Tabs.getInstance().getSelectedTab();
-
- final Prompt ps = new Prompt(browserApp, new Prompt.PromptCallback() {
- @Override
- public void onPromptFinished(String result) {
- int itemId = -1;
- try {
- itemId = new JSONObject(result).getInt("button");
- } catch (JSONException ex) {
- Log.e(LOGTAG, "Exception reading bookmark prompt result", ex);
- }
-
- if (tab == null) {
- return;
- }
-
- if (itemId == 0) {
- final String extrasId = res.getResourceEntryName(R.string.contextmenu_edit_bookmark);
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION,
- TelemetryContract.Method.DIALOG, extrasId);
-
- new EditBookmarkDialog(browserApp).show(tab.getURL());
- } else if (itemId == 1) {
- final String extrasId = res.getResourceEntryName(R.string.contextmenu_add_to_launcher);
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION,
- TelemetryContract.Method.DIALOG, extrasId);
-
- final String url = tab.getURL();
- final String title = tab.getDisplayTitle();
-
- if (url != null && title != null) {
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- GeckoAppShell.createShortcut(title, url);
- }
- });
- }
- }
- }
- });
-
- final PromptListItem[] items = new PromptListItem[2];
- items[0] = new PromptListItem(res.getString(R.string.contextmenu_edit_bookmark));
- items[1] = new PromptListItem(res.getString(R.string.contextmenu_add_to_launcher));
-
- ps.show("", "", items, ListView.CHOICE_MODE_NONE);
- }
-
- private void showReaderModeBookmarkAddedSnackbar() {
- final BrowserApp browserApp = getBrowserApp();
- if (browserApp == null) {
- return;
- }
-
- final Drawable iconDownloaded = DrawableUtil.tintDrawable(browserApp, R.drawable.status_icon_readercache, Color.WHITE);
-
- final SnackbarBuilder.SnackbarCallback callback = new SnackbarBuilder.SnackbarCallback() {
- @Override
- public void onClick(View v) {
- browserApp.openUrlAndStopEditing("about:home?panel=" + HomeConfig.getIdForBuiltinPanelType(HomeConfig.PanelType.BOOKMARKS));
- }
- };
-
- SnackbarBuilder.builder(browserApp)
- .message(R.string.reader_saved_offline)
- .duration(Snackbar.LENGTH_LONG)
- .action(R.string.reader_switch_to_bookmarks)
- .callback(callback)
- .icon(iconDownloaded)
- .backgroundColor(ContextCompat.getColor(browserApp, R.color.link_blue))
- .actionColor(Color.WHITE)
- .buildAndShow();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/delegates/BrowserAppDelegate.java b/mobile/android/base/java/org/mozilla/gecko/delegates/BrowserAppDelegate.java
deleted file mode 100644
index 70b134992..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/delegates/BrowserAppDelegate.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.delegates;
-
-import android.content.Intent;
-import android.os.Bundle;
-
-import org.mozilla.gecko.BrowserApp;
-import org.mozilla.gecko.mozglue.SafeIntent;
-import org.mozilla.gecko.tabs.TabsPanel;
-
-/**
- * Abstract class for extending the behavior of BrowserApp without adding additional code to the
- * already huge class.
- */
-public abstract class BrowserAppDelegate {
- /**
- * Called when the BrowserApp activity is first created.
- */
- public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {}
-
- /**
- * Called after the BrowserApp activity has been stopped, prior to it being started again.
- */
- public void onRestart(BrowserApp browserApp) {}
-
- /**
- * Called when the BrowserApp activity is becoming visible to the user.
- */
- public void onStart(BrowserApp browserApp) {}
-
- /**
- * Called when the BrowserApp activity will start interacting with the user.
- */
- public void onResume(BrowserApp browserApp) {}
-
- /**
- * Called when the system is about to start resuming a previous activity.
- */
- public void onPause(BrowserApp browserApp) {}
-
- /**
- * Called when BrowserApp activity is no longer visible to the user.
- */
- public void onStop(BrowserApp browserApp) {}
-
- /**
- * The final call before the BrowserApp activity is destroyed.
- */
- public void onDestroy(BrowserApp browserApp) {}
-
- /**
- * Called when BrowserApp already exists and a new Intent to re-launch it was fired.
- */
- public void onNewIntent(BrowserApp browserApp, SafeIntent intent) {}
-
- /**
- * Called when the tabs tray is opened.
- */
- public void onTabsTrayShown(BrowserApp browserApp, TabsPanel tabsPanel) {}
-
- /**
- * Called when the tabs tray is closed.
- */
- public void onTabsTrayHidden(BrowserApp browserApp, TabsPanel tabsPanel) {}
-
- /**
- * Called when an activity started using startActivityForResult() returns.
- *
- * Delegates should only use request and result codes declared in BrowserApp itself (as opposed
- * to declarations in the delegate), in order to avoid conflicts.
- */
- public void onActivityResult(BrowserApp browserApp, int requestCode, int resultCode, Intent data) {}
-}
-
diff --git a/mobile/android/base/java/org/mozilla/gecko/delegates/BrowserAppDelegateWithReference.java b/mobile/android/base/java/org/mozilla/gecko/delegates/BrowserAppDelegateWithReference.java
deleted file mode 100644
index c67b8a18a..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/delegates/BrowserAppDelegateWithReference.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package org.mozilla.gecko.delegates;
-
-import android.os.Bundle;
-import android.support.annotation.CallSuper;
-
-import org.mozilla.gecko.BrowserApp;
-
-import java.lang.ref.WeakReference;
-
-/**
- * BrowserAppDelegate that stores a reference to the parent BrowserApp.
- */
-public abstract class BrowserAppDelegateWithReference extends BrowserAppDelegate {
- private WeakReference<BrowserApp> browserApp;
-
- @Override
- @CallSuper
- public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {
- this.browserApp = new WeakReference<>(browserApp);
- }
-
- /**
- * Obtain the referenced BrowserApp. May return <code>null</code> if the BrowserApp no longer
- * exists.
- */
- protected BrowserApp getBrowserApp() {
- return browserApp.get();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/delegates/OfflineTabStatusDelegate.java b/mobile/android/base/java/org/mozilla/gecko/delegates/OfflineTabStatusDelegate.java
deleted file mode 100644
index 5f3aa9c59..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/delegates/OfflineTabStatusDelegate.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.delegates;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.support.annotation.CallSuper;
-import android.support.design.widget.Snackbar;
-import android.support.v4.content.ContextCompat;
-
-import org.mozilla.gecko.AboutPages;
-import org.mozilla.gecko.BrowserApp;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.SnackbarBuilder;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.WeakHashMap;
-
-/**
- * Displays "Showing offline version" message when tabs are loaded from cache while offline.
- */
-public class OfflineTabStatusDelegate extends TabsTrayVisibilityAwareDelegate implements Tabs.OnTabsChangedListener {
- private WeakReference<Activity> activityReference;
- private WeakHashMap<Tab, Void> tabsQueuedForOfflineSnackbar = new WeakHashMap<>();
-
- @CallSuper
- @Override
- public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {
- super.onCreate(browserApp, savedInstanceState);
- activityReference = new WeakReference<Activity>(browserApp);
- }
-
- @Override
- public void onResume(BrowserApp browserApp) {
- Tabs.registerOnTabsChangedListener(this);
- }
-
- @Override
- public void onPause(BrowserApp browserApp) {
- Tabs.unregisterOnTabsChangedListener(this);
- }
-
- public void onTabChanged(final Tab tab, Tabs.TabEvents event, String data) {
- if (tab == null) {
- return;
- }
-
- // Ignore tabs loaded regularly.
- if (!tab.hasLoadedFromCache()) {
- return;
- }
-
- // Ignore tabs displaying about pages
- if (AboutPages.isAboutPage(tab.getURL())) {
- return;
- }
-
- // We only want to show these notifications for tabs that were loaded successfully.
- if (tab.getState() != Tab.STATE_SUCCESS) {
- return;
- }
-
- switch (event) {
- // We listen specifically for the STOP event (as opposed to PAGE_SHOW), because we need
- // to know if page load actually succeeded. When STOP is triggered, tab.getState()
- // will return definitive STATE_SUCCESS or STATE_ERROR. When PAGE_SHOW is triggered,
- // tab.getState() will return STATE_LOADING, which is ambiguous for our purposes.
- // We don't want to show these notifications for 404 pages, for example. See Bug 1304914.
- case STOP:
- // Show offline notification if tab is visible, or queue it for display later.
- if (!isTabsTrayVisible() && Tabs.getInstance().isSelectedTab(tab)) {
- showLoadedOfflineSnackbar(activityReference.get());
- } else {
- tabsQueuedForOfflineSnackbar.put(tab, null);
- }
- break;
- // Fallthrough; see Bug 1278980 for details on why this event is here.
- case OPENED_FROM_TABS_TRAY:
- // When tab is selected and offline notification was queued, display it if possible.
- // SELECTED event might also fire when we're on a TabStrip, so check first.
- case SELECTED:
- if (isTabsTrayVisible()) {
- break;
- }
- if (tabsQueuedForOfflineSnackbar.containsKey(tab)) {
- showLoadedOfflineSnackbar(activityReference.get());
- tabsQueuedForOfflineSnackbar.remove(tab);
- }
- break;
- }
- }
-
- /**
- * Displays the notification snackbar and logs a telemetry event.
- *
- * @param activity which will be used for displaying the snackbar.
- */
- private static void showLoadedOfflineSnackbar(final Activity activity) {
- if (activity == null) {
- return;
- }
-
- Telemetry.sendUIEvent(TelemetryContract.Event.NETERROR, TelemetryContract.Method.TOAST, "usecache");
-
- SnackbarBuilder.builder(activity)
- .message(R.string.tab_offline_version)
- .duration(Snackbar.LENGTH_INDEFINITE)
- .backgroundColor(ContextCompat.getColor(activity, R.color.link_blue))
- .buildAndShow();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/delegates/ScreenshotDelegate.java b/mobile/android/base/java/org/mozilla/gecko/delegates/ScreenshotDelegate.java
deleted file mode 100644
index f048372f7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/delegates/ScreenshotDelegate.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.delegates;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.support.design.widget.Snackbar;
-import android.util.Log;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.BrowserApp;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.ScreenshotObserver;
-import org.mozilla.gecko.SnackbarBuilder;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.db.BrowserDB;
-
-import java.lang.ref.WeakReference;
-
-/**
- * Delegate for observing screenshots being taken.
- */
-public class ScreenshotDelegate extends BrowserAppDelegateWithReference implements ScreenshotObserver.OnScreenshotListener {
- private static final String LOGTAG = "GeckoScreenshotDelegate";
-
- private final ScreenshotObserver mScreenshotObserver = new ScreenshotObserver();
-
- @Override
- public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {
- super.onCreate(browserApp, savedInstanceState);
-
- mScreenshotObserver.setListener(browserApp, this);
- }
-
- @Override
- public void onScreenshotTaken(String screenshotPath, String title) {
- // Treat screenshots as a sharing method.
- Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.BUTTON, "screenshot");
-
- if (!AppConstants.SCREENSHOTS_IN_BOOKMARKS_ENABLED) {
- return;
- }
-
- final Tab selectedTab = Tabs.getInstance().getSelectedTab();
- if (selectedTab == null) {
- Log.w(LOGTAG, "Selected tab is null: could not page info to store screenshot.");
- return;
- }
-
- final Activity activity = getBrowserApp();
- if (activity == null) {
- return;
- }
-
- BrowserDB.from(activity).getUrlAnnotations().insertScreenshot(
- activity.getContentResolver(), selectedTab.getURL(), screenshotPath);
-
- SnackbarBuilder.builder(activity)
- .message(R.string.screenshot_added_to_bookmarks)
- .duration(Snackbar.LENGTH_SHORT)
- .buildAndShow();
- }
-
- @Override
- public void onResume(BrowserApp browserApp) {
- mScreenshotObserver.start();
- }
-
- @Override
- public void onPause(BrowserApp browserApp) {
- mScreenshotObserver.stop();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/delegates/TabsTrayVisibilityAwareDelegate.java b/mobile/android/base/java/org/mozilla/gecko/delegates/TabsTrayVisibilityAwareDelegate.java
deleted file mode 100644
index ebd3991ea..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/delegates/TabsTrayVisibilityAwareDelegate.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.delegates;
-
-import android.os.Bundle;
-import android.support.annotation.CallSuper;
-
-import org.mozilla.gecko.BrowserApp;
-import org.mozilla.gecko.tabs.TabsPanel;
-
-public abstract class TabsTrayVisibilityAwareDelegate extends BrowserAppDelegate {
- private boolean tabsTrayVisible;
-
- @Override
- @CallSuper
- public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {
- tabsTrayVisible = false;
- }
-
- @Override
- @CallSuper
- public void onTabsTrayShown(BrowserApp browserApp, TabsPanel tabsPanel) {
- tabsTrayVisible = true;
- }
-
- @Override
- @CallSuper
- public void onTabsTrayHidden(BrowserApp browserApp, TabsPanel tabsPanel) {
- tabsTrayVisible = false;
- }
-
- protected boolean isTabsTrayVisible() {
- return tabsTrayVisible;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/distribution/Distribution.java b/mobile/android/base/java/org/mozilla/gecko/distribution/Distribution.java
deleted file mode 100644
index a7b0fe32d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/distribution/Distribution.java
+++ /dev/null
@@ -1,1046 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.distribution;
-
-import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.ProtocolException;
-import java.net.SocketException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.UnknownHostException;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Queue;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.jar.JarEntry;
-import java.util.jar.JarInputStream;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
-
-import javax.net.ssl.SSLException;
-
-import ch.boye.httpclientandroidlib.protocol.HTTP;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.annotation.JNITarget;
-import org.mozilla.gecko.util.FileUtils;
-import org.mozilla.gecko.util.HardwareUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.os.SystemClock;
-import android.support.annotation.WorkerThread;
-import android.telephony.TelephonyManager;
-import android.util.Log;
-
-/**
- * Handles distribution file loading and fetching,
- * and the corresponding hand-offs to Gecko.
- */
-@RobocopTarget
-public class Distribution {
- private static final String LOGTAG = "GeckoDistribution";
-
- private static final int STATE_UNKNOWN = 0;
- private static final int STATE_NONE = 1;
- private static final int STATE_SET = 2;
-
- private static final String FETCH_PROTOCOL = "https";
- private static final String FETCH_HOSTNAME = "mobile.cdn.mozilla.net";
- private static final String FETCH_PATH = "/distributions/1/";
- private static final String FETCH_EXTENSION = ".jar";
-
- private static final String EXPECTED_CONTENT_TYPE = "application/java-archive";
-
- private static final String DISTRIBUTION_PATH = "distribution/";
-
- /**
- * Telemetry constants.
- */
- private static final String HISTOGRAM_REFERRER_INVALID = "FENNEC_DISTRIBUTION_REFERRER_INVALID";
- private static final String HISTOGRAM_DOWNLOAD_TIME_MS = "FENNEC_DISTRIBUTION_DOWNLOAD_TIME_MS";
- private static final String HISTOGRAM_CODE_CATEGORY = "FENNEC_DISTRIBUTION_CODE_CATEGORY";
-
- /**
- * Success/failure codes. Don't exceed the maximum listed in Histograms.json.
- */
- private static final int CODE_CATEGORY_STATUS_OUT_OF_RANGE = 0;
- // HTTP status 'codes' run from 1 to 5.
- private static final int CODE_CATEGORY_OFFLINE = 6;
- private static final int CODE_CATEGORY_FETCH_EXCEPTION = 7;
-
- // It's a post-fetch exception if we were able to download, but not
- // able to extract.
- private static final int CODE_CATEGORY_POST_FETCH_EXCEPTION = 8;
- private static final int CODE_CATEGORY_POST_FETCH_SECURITY_EXCEPTION = 9;
-
- // It's a malformed distribution if we could extract, but couldn't
- // process the contents.
- private static final int CODE_CATEGORY_MALFORMED_DISTRIBUTION = 10;
-
- // Specific fetch errors.
- private static final int CODE_CATEGORY_FETCH_SOCKET_ERROR = 11;
- private static final int CODE_CATEGORY_FETCH_SSL_ERROR = 12;
- private static final int CODE_CATEGORY_FETCH_NON_SUCCESS_RESPONSE = 13;
- private static final int CODE_CATEGORY_FETCH_INVALID_CONTENT_TYPE = 14;
-
- // Corresponds to the high value in Histograms.json.
- private static final long MAX_DOWNLOAD_TIME_MSEC = 40000; // 40 seconds.
-
- // If this is true, ready callbacks that arrive after our state is initially determined
- // will be queued for delayed running.
- // This should only be the case on first run, when we're in STATE_NONE.
- // Implicitly accessed from any non-UI threads via Distribution.doInit, but in practice only one
- // will actually perform initialization, and "non-UI thread" really means "background thread".
- private volatile boolean shouldDelayLateCallbacks = false;
-
- /**
- * These tasks can be queued to run when a distribution is available.
- *
- * If <code>distributionFound</code> is called, it will be the only call.
- * If <code>distributionNotFound</code> is called, it might be followed by
- * a call to <code>distributionArrivedLate</code>.
- *
- * When <code>distributionNotFound</code> is called,
- * {@link org.mozilla.gecko.distribution.Distribution#exists()} will return
- * false. In the other two callbacks, it will return true.
- */
- public interface ReadyCallback {
- @WorkerThread
- void distributionNotFound();
-
- @WorkerThread
- void distributionFound(Distribution distribution);
-
- @WorkerThread
- void distributionArrivedLate(Distribution distribution);
- }
-
- /**
- * Used as a drop-off point for ReferrerReceiver. Checked when we process
- * first-run distribution.
- *
- * This is `protected` so that test code can clear it between runs.
- */
- @RobocopTarget
- protected static volatile ReferrerDescriptor referrer;
-
- private static Distribution instance;
-
- private final Context context;
- private final String packagePath;
- private final String prefsBranch;
-
- volatile int state = STATE_UNKNOWN;
- private File distributionDir;
-
- private final Queue<ReadyCallback> onDistributionReady = new ConcurrentLinkedQueue<>();
-
- // Callbacks in this queue have been invoked once as distributionNotFound.
- // If they're invoked again, it'll be with distributionArrivedLate.
- private final Queue<ReadyCallback> onLateReady = new ConcurrentLinkedQueue<>();
-
- /**
- * This is a little bit of a bad singleton, because in principle a Distribution
- * can be created with arbitrary paths. So we only have one path to get here, and
- * it uses the default arguments. Watch out if you're creating your own instances!
- */
- public static synchronized Distribution getInstance(Context context) {
- if (instance == null) {
- instance = new Distribution(context);
- }
- return instance;
- }
-
- @RobocopTarget
- public static class DistributionDescriptor {
- public final boolean valid;
- public final String id;
- public final String version; // Example uses a float, but that's a crazy idea.
-
- // Default UI-visible description of the distribution.
- public final String about;
-
- // Each distribution file can include multiple localized versions of
- // the 'about' string. These are represented as, e.g., "about.en-US"
- // keys in the Global object.
- // Here we map locale to description.
- public final Map<String, String> localizedAbout;
-
- @SuppressWarnings("unchecked")
- public DistributionDescriptor(JSONObject obj) {
- this.id = obj.optString("id");
- this.version = obj.optString("version");
- this.about = obj.optString("about");
- Map<String, String> loc = new HashMap<String, String>();
- try {
- Iterator<String> keys = obj.keys();
- while (keys.hasNext()) {
- String key = keys.next();
- if (key.startsWith("about.")) {
- String locale = key.substring(6);
- if (!obj.isNull(locale)) {
- loc.put(locale, obj.getString(key));
- }
- }
- }
- } catch (JSONException ex) {
- Log.w(LOGTAG, "Unable to completely process distribution JSON.", ex);
- }
-
- this.localizedAbout = Collections.unmodifiableMap(loc);
- this.valid = (null != this.id) &&
- (null != this.version) &&
- (null != this.about);
- }
- }
-
- private static Distribution init(final Distribution distribution) {
- // Read/write preferences and files on the background thread.
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- boolean distributionSet = distribution.doInit();
- if (distributionSet) {
- String preferencesJSON = "";
- try {
- final File descFile = distribution.getDistributionFile("preferences.json");
- preferencesJSON = FileUtils.readStringFromFile(descFile);
- } catch (IOException e) {
- Log.e(LOGTAG, "Error getting distribution descriptor file.", e);
- }
- GeckoAppShell.notifyObservers("Distribution:Set", preferencesJSON);
- }
- }
- });
-
- return distribution;
- }
-
- /**
- * Initializes distribution if it hasn't already been initialized. Sends
- * messages to Gecko as appropriate.
- *
- * @param packagePath where to look for the distribution directory.
- */
- @RobocopTarget
- public static Distribution init(final Context context, final String packagePath, final String prefsPath) {
- return init(new Distribution(context, packagePath, prefsPath));
- }
-
- /**
- * Use <code>Context.getPackageResourcePath</code> to find an implicit
- * package path. Reuses the existing Distribution if one exists.
- */
- @RobocopTarget
- public static Distribution init(final Context context) {
- return init(Distribution.getInstance(context));
- }
-
- /**
- * Returns parsed contents of bookmarks.json.
- * This method should only be called from a background thread.
- */
- public static JSONArray getBookmarks(final Context context) {
- Distribution dist = new Distribution(context);
- return dist.getBookmarks();
- }
-
- /**
- * @param packagePath where to look for the distribution directory.
- */
- public Distribution(final Context context, final String packagePath, final String prefsBranch) {
- this.context = context;
- this.packagePath = packagePath;
- this.prefsBranch = prefsBranch;
- }
-
- public Distribution(final Context context) {
- this(context, context.getPackageResourcePath(), null);
- }
-
- /**
- * This method is called by ReferrerReceiver when we receive a post-install
- * notification from Google Play.
- *
- * @param ref a parsed referrer value from the store-supplied intent.
- */
- public static void onReceivedReferrer(final Context context, final ReferrerDescriptor ref) {
- // Track the referrer object for distribution handling.
- referrer = ref;
-
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- final Distribution distribution = Distribution.getInstance(context);
-
- // This will bail if we aren't delayed, or we already have a distribution.
- distribution.processDelayedReferrer(ref);
-
- // On Android 5+ we might receive the referrer intent
- // and never actually launch the browser, which is the usual signal
- // for the distribution init process to complete.
- // Attempt to init here to handle that case.
- // Profile setup that relies on the distribution will occur
- // when the browser is eventually launched, via `addOnDistributionReadyCallback`.
- distribution.doInit();
- }
- });
- }
-
- /**
- * Handle a referrer intent that arrives after first use of the distribution.
- */
- private void processDelayedReferrer(final ReferrerDescriptor ref) {
- ThreadUtils.assertOnBackgroundThread();
- if (state != STATE_NONE) {
- return;
- }
-
- Log.i(LOGTAG, "Processing delayed referrer.");
-
- if (!checkIntentDistribution(ref)) {
- // Oh well. No sense keeping these tasks around.
- this.onLateReady.clear();
- return;
- }
-
- // Persist our new state.
- this.state = STATE_SET;
- getSharedPreferences().edit().putInt(getKeyName(), this.state).apply();
-
- // Just in case this isn't empty but doInit has finished.
- runReadyQueue();
-
- // Now process any tasks that already ran while we were in STATE_NONE
- // to tell them of our good news.
- runLateReadyQueue();
-
- // Make sure that changes to search defaults are applied immediately.
- GeckoAppShell.notifyObservers("Distribution:Changed", "");
- }
-
- /**
- * Helper to grab a file in the distribution directory.
- *
- * Returns null if there is no distribution directory or the file
- * doesn't exist. Ensures init first.
- */
- public File getDistributionFile(String name) {
- Log.d(LOGTAG, "Getting file from distribution.");
-
- if (this.state == STATE_UNKNOWN) {
- if (!this.doInit()) {
- return null;
- }
- }
-
- File dist = ensureDistributionDir();
- if (dist == null) {
- return null;
- }
-
- File descFile = new File(dist, name);
- if (!descFile.exists()) {
- Log.e(LOGTAG, "Distribution directory exists, but no file named " + name);
- return null;
- }
-
- return descFile;
- }
-
- public DistributionDescriptor getDescriptor() {
- File descFile = getDistributionFile("preferences.json");
- if (descFile == null) {
- // Logging and existence checks are handled in getDistributionFile.
- return null;
- }
-
- try {
- JSONObject all = FileUtils.readJSONObjectFromFile(descFile);
-
- if (!all.has("Global")) {
- Log.e(LOGTAG, "Distribution preferences.json has no Global entry!");
- return null;
- }
-
- return new DistributionDescriptor(all.getJSONObject("Global"));
-
- } catch (IOException e) {
- Log.e(LOGTAG, "Error getting distribution descriptor file.", e);
- Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION);
- return null;
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error parsing preferences.json", e);
- Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION);
- return null;
- }
- }
-
- /**
- * Get the Android preferences from the preferences.json file, if any exist.
- * @return The preferences in a JSONObject, or an empty JSONObject if no preferences are defined.
- */
- public JSONObject getAndroidPreferences() {
- final File descFile = getDistributionFile("preferences.json");
- if (descFile == null) {
- // Logging and existence checks are handled in getDistributionFile.
- return new JSONObject();
- }
-
- try {
- final JSONObject all = FileUtils.readJSONObjectFromFile(descFile);
-
- if (!all.has("AndroidPreferences")) {
- return new JSONObject();
- }
-
- return all.getJSONObject("AndroidPreferences");
-
- } catch (IOException e) {
- Log.e(LOGTAG, "Error getting distribution descriptor file.", e);
- Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION);
- return new JSONObject();
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error parsing preferences.json", e);
- Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION);
- return new JSONObject();
- }
- }
-
- public JSONArray getBookmarks() {
- File bookmarks = getDistributionFile("bookmarks.json");
- if (bookmarks == null) {
- // Logging and existence checks are handled in getDistributionFile.
- return null;
- }
-
- try {
- return new JSONArray(FileUtils.readStringFromFile(bookmarks));
- } catch (IOException e) {
- Log.e(LOGTAG, "Error getting bookmarks", e);
- Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION);
- return null;
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error parsing bookmarks.json", e);
- Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION);
- return null;
- }
- }
-
- /**
- * Don't call from the main thread.
- *
- * Postcondition: if this returns true, distributionDir will have been
- * set and populated.
- *
- * This method is *only* protected for use from testDistribution.
- *
- * @return true if we've set a distribution.
- */
- @RobocopTarget
- protected boolean doInit() {
- ThreadUtils.assertNotOnUiThread();
-
- // Bail if we've already tried to initialize the distribution, and
- // there wasn't one.
- final SharedPreferences settings = getSharedPreferences();
-
- final String keyName = getKeyName();
- this.state = settings.getInt(keyName, STATE_UNKNOWN);
-
- if (this.state == STATE_NONE) {
- runReadyQueue();
- return false;
- }
-
- // We've done the work once; don't do it again.
- if (this.state == STATE_SET) {
- // Note that we don't compute the distribution directory.
- // Call `ensureDistributionDir` if you need it.
- runReadyQueue();
- return true;
- }
-
- // We try to find the install intent, then the APK, then the system directory, and finally
- // an already copied distribution. Already copied might originate from the bouncer APK.
- final boolean distributionSet =
- checkIntentDistribution(referrer) ||
- copyAndCheckAPKDistribution() ||
- checkSystemDistribution() ||
- checkDataDistribution();
-
- // If this is our first run -- and thus we weren't already in STATE_NONE or STATE_SET above --
- // and we didn't find a distribution already, then we should hold on to callbacks in case we
- // get a late distribution.
- this.shouldDelayLateCallbacks = !distributionSet;
- this.state = distributionSet ? STATE_SET : STATE_NONE;
- settings.edit().putInt(keyName, this.state).apply();
-
- runReadyQueue();
- return distributionSet;
- }
-
- /**
- * If applicable, download and select the distribution specified in
- * the referrer intent.
- *
- * @return true if a referrer-supplied distribution was selected.
- */
- private boolean checkIntentDistribution(final ReferrerDescriptor referrer) {
- if (referrer == null) {
- return false;
- }
-
- URI uri = getReferredDistribution(referrer);
- if (uri == null) {
- return false;
- }
-
- long start = SystemClock.uptimeMillis();
- Log.v(LOGTAG, "Downloading referred distribution: " + uri);
-
- try {
- final HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
-
- // If the Search Activity starts, and we handle the referrer intent, this'll return
- // null. Recover gracefully in this case.
- final GeckoAppShell.GeckoInterface geckoInterface = GeckoAppShell.getGeckoInterface();
- final String ua;
- if (geckoInterface == null) {
- // Fall back to GeckoApp's default implementation.
- ua = HardwareUtils.isTablet() ? AppConstants.USER_AGENT_FENNEC_TABLET :
- AppConstants.USER_AGENT_FENNEC_MOBILE;
- } else {
- ua = geckoInterface.getDefaultUAString();
- }
-
- connection.setRequestProperty(HTTP.USER_AGENT, ua);
- connection.setRequestProperty("Accept", EXPECTED_CONTENT_TYPE);
-
- try {
- final JarInputStream distro;
- try {
- distro = fetchDistribution(uri, connection);
- } catch (Exception e) {
- Log.e(LOGTAG, "Error fetching distribution from network.", e);
- recordFetchTelemetry(e);
- return false;
- }
-
- long end = SystemClock.uptimeMillis();
- final long duration = end - start;
- Log.d(LOGTAG, "Distro fetch took " + duration + "ms; result? " + (distro != null));
- Telemetry.addToHistogram(HISTOGRAM_DOWNLOAD_TIME_MS, clamp(MAX_DOWNLOAD_TIME_MSEC, duration));
-
- if (distro == null) {
- // Nothing to do.
- return false;
- }
-
- // Try to copy distribution files from the fetched stream.
- try {
- Log.d(LOGTAG, "Copying files from fetched zip.");
- if (copyFilesFromStream(distro)) {
- // We always copy to the data dir, and we only copy files from
- // a 'distribution' subdirectory. Now determine our actual distribution directory.
- return checkDataDistribution();
- }
- } catch (SecurityException e) {
- Log.e(LOGTAG, "Security exception copying files. Corrupt or malicious?", e);
- Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_POST_FETCH_SECURITY_EXCEPTION);
- } catch (Exception e) {
- Log.e(LOGTAG, "Error copying files from distribution.", e);
- Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_POST_FETCH_EXCEPTION);
- } finally {
- distro.close();
- }
- } finally {
- connection.disconnect();
- }
- } catch (IOException e) {
- Log.e(LOGTAG, "Error copying distribution files from network.", e);
- recordFetchTelemetry(e);
- }
-
- return false;
- }
-
- private static final int clamp(long v, long c) {
- return (int) Math.min(c, v);
- }
-
- /**
- * Fetch the provided URI, returning a {@link JarInputStream} if the response body
- * is appropriate.
- *
- * Protected to allow for mocking.
- *
- * @return the entity body as a stream, or null on failure.
- */
- @SuppressWarnings("static-method")
- @RobocopTarget
- protected JarInputStream fetchDistribution(URI uri, HttpURLConnection connection) throws IOException {
- final int status = connection.getResponseCode();
-
- Log.d(LOGTAG, "Distribution fetch: " + status);
- // We record HTTP statuses as 2xx, 3xx, 4xx, 5xx => 2, 3, 4, 5.
- final int value;
- if (status > 599 || status < 100) {
- Log.wtf(LOGTAG, "Unexpected HTTP status code: " + status);
- value = CODE_CATEGORY_STATUS_OUT_OF_RANGE;
- } else {
- value = status / 100;
- }
-
- Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, value);
-
- if (status != 200) {
- Log.w(LOGTAG, "Got status " + status + " fetching distribution.");
- Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_NON_SUCCESS_RESPONSE);
- return null;
- }
-
- final String contentType = connection.getContentType();
- if (contentType == null || !contentType.startsWith(EXPECTED_CONTENT_TYPE)) {
- Log.w(LOGTAG, "Malformed response: invalid Content-Type.");
- Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_INVALID_CONTENT_TYPE);
- return null;
- }
-
- return new JarInputStream(new BufferedInputStream(connection.getInputStream()), true);
- }
-
- private static void recordFetchTelemetry(final Exception exception) {
- if (exception == null) {
- // Should never happen.
- Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_EXCEPTION);
- return;
- }
-
- if (exception instanceof UnknownHostException) {
- // Unknown host => we're offline.
- Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_OFFLINE);
- return;
- }
-
- if (exception instanceof SSLException) {
- Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_SSL_ERROR);
- return;
- }
-
- if (exception instanceof ProtocolException ||
- exception instanceof SocketException) {
- Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_SOCKET_ERROR);
- return;
- }
-
- Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_EXCEPTION);
- }
-
- /**
- * @return true if we copied files out of the APK. Sets distributionDir in that case.
- */
- private boolean copyAndCheckAPKDistribution() {
- try {
- // First, try copying distribution files out of the APK.
- if (copyFilesFromPackagedAssets()) {
- // We always copy to the data dir, and we only copy files from
- // a 'distribution' subdirectory. Now determine our actual distribution directory.
- return checkDataDistribution();
- }
- } catch (IOException e) {
- Log.e(LOGTAG, "Error copying distribution files from APK.", e);
- }
- return false;
- }
-
- /**
- * @return true if we found a data distribution (copied from APK or OTA). Sets distributionDir in that case.
- */
- private boolean checkDataDistribution() {
- return checkDirectories(getDataDistributionDirectories(context));
- }
-
- /**
- * @return true if we found a system distribution. Sets distributionDir in that case.
- */
- private boolean checkSystemDistribution() {
- return checkDirectories(getSystemDistributionDirectories(context));
- }
-
- /**
- * @return true if one of the specified distribution directories exists. Sets distributionDir in that case.
- */
- private boolean checkDirectories(String[] directories) {
- for (String path : directories) {
- File directory = new File(path);
- if (directory.exists()) {
- distributionDir = directory;
- return true;
- }
- }
- return false;
- }
-
- /**
- * Unpack distribution files from a downloaded jar stream.
- *
- * The caller is responsible for closing the provided stream.
- */
- private boolean copyFilesFromStream(JarInputStream jar) throws FileNotFoundException, IOException {
- final byte[] buffer = new byte[1024];
- boolean distributionSet = false;
- JarEntry entry;
- while ((entry = jar.getNextJarEntry()) != null) {
- final String name = entry.getName();
-
- if (entry.isDirectory()) {
- // We'll let getDataFile deal with creating the directory hierarchy.
- // Yes, we can do better, but it can wait.
- continue;
- }
-
- if (!name.startsWith(DISTRIBUTION_PATH)) {
- continue;
- }
-
- File outFile = getDataFile(name);
- if (outFile == null) {
- continue;
- }
-
- distributionSet = true;
-
- writeStream(jar, outFile, entry.getTime(), buffer);
- }
-
- return distributionSet;
- }
-
- /**
- * Copies the /assets/distribution folder out of the APK and into the app's data directory.
- * Returns true if distribution files were found and copied.
- */
- private boolean copyFilesFromPackagedAssets() throws IOException {
- final File applicationPackage = new File(packagePath);
- final ZipFile zip = new ZipFile(applicationPackage);
-
- final String assetsPrefix = "assets/";
- final String fullPrefix = assetsPrefix + DISTRIBUTION_PATH;
-
- boolean distributionSet = false;
- try {
- final byte[] buffer = new byte[1024];
-
- final Enumeration<? extends ZipEntry> zipEntries = zip.entries();
- while (zipEntries.hasMoreElements()) {
- final ZipEntry fileEntry = zipEntries.nextElement();
- final String name = fileEntry.getName();
-
- if (fileEntry.isDirectory()) {
- // We'll let getDataFile deal with creating the directory hierarchy.
- continue;
- }
-
- // Read from "assets/distribution/**".
- if (!name.startsWith(fullPrefix)) {
- continue;
- }
-
- // Write to "distribution/**".
- final String nameWithoutPrefix = name.substring(assetsPrefix.length());
- final File outFile = getDataFile(nameWithoutPrefix);
- if (outFile == null) {
- continue;
- }
-
- distributionSet = true;
-
- final InputStream fileStream = zip.getInputStream(fileEntry);
- try {
- writeStream(fileStream, outFile, fileEntry.getTime(), buffer);
- } finally {
- fileStream.close();
- }
- }
- } finally {
- zip.close();
- }
-
- return distributionSet;
- }
-
- private void writeStream(InputStream fileStream, File outFile, final long modifiedTime, byte[] buffer)
- throws FileNotFoundException, IOException {
- final OutputStream outStream = new FileOutputStream(outFile);
- try {
- int count;
- while ((count = fileStream.read(buffer)) > 0) {
- outStream.write(buffer, 0, count);
- }
-
- outFile.setLastModified(modifiedTime);
- } finally {
- outStream.close();
- }
- }
-
- /**
- * Return a File instance in the data directory, ensuring
- * that the parent exists.
- *
- * @return null if the parents could not be created.
- */
- private File getDataFile(final String name) {
- File outFile = new File(getDataDir(), name);
- File dir = outFile.getParentFile();
-
- if (!dir.exists()) {
- Log.d(LOGTAG, "Creating " + dir.getAbsolutePath());
- if (!dir.mkdirs()) {
- Log.e(LOGTAG, "Unable to create directories: " + dir.getAbsolutePath());
- return null;
- }
- }
-
- return outFile;
- }
-
- private URI getReferredDistribution(ReferrerDescriptor descriptor) {
- final String content = descriptor.content;
- if (content == null) {
- return null;
- }
-
- // We restrict here to avoid injection attacks. After all,
- // we're downloading a distribution payload based on intent input.
- if (!content.matches("^[a-zA-Z0-9]+$")) {
- Log.e(LOGTAG, "Invalid referrer content: " + content);
- Telemetry.addToHistogram(HISTOGRAM_REFERRER_INVALID, 1);
- return null;
- }
-
- try {
- return new URI(FETCH_PROTOCOL, FETCH_HOSTNAME, FETCH_PATH + content + FETCH_EXTENSION, null);
- } catch (URISyntaxException e) {
- // This should never occur.
- Log.wtf(LOGTAG, "Invalid URI with content " + content + "!");
- return null;
- }
- }
-
- /**
- * After calling this method, either <code>distributionDir</code>
- * will be set, or there is no distribution in use.
- *
- * Only call after init.
- */
- private File ensureDistributionDir() {
- if (this.distributionDir != null) {
- return this.distributionDir;
- }
-
- if (this.state != STATE_SET) {
- return null;
- }
-
- // After init, we know that either we've copied a distribution out of
- // the APK, or it exists in /system/.
- // Look in each location in turn.
- // (This could be optimized by caching the path in shared prefs.)
- if (checkDataDistribution() || checkSystemDistribution()) {
- return distributionDir;
- }
-
- return null;
- }
-
- private String getDataDir() {
- return context.getApplicationInfo().dataDir;
- }
-
- @JNITarget
- public static String[] getDistributionDirectories() {
- final Context context = GeckoAppShell.getApplicationContext();
-
- final String[] dataDirectories = getDataDistributionDirectories(context);
- final String[] systemDirectories = getSystemDistributionDirectories(context);
-
- final String[] directories = new String[dataDirectories.length + systemDirectories.length];
-
- System.arraycopy(dataDirectories, 0, directories, 0, dataDirectories.length);
- System.arraycopy(systemDirectories, 0, directories, dataDirectories.length, systemDirectories.length);
-
- return directories;
- }
-
- /**
- * Get a list of system distribution folder candidates.
- *
- * /system/<package>/distribution/<mcc>/<mnc> - For bundled distributions for specific network providers
- * /system/<package>/distribution/<mcc> - For bundled distributions for specific countries
- * /system/<package>/distribution/default - For bundled distributions with no matching mcc/mnc
- * /system/<package>/distribution - Default non-bundled system distribution
- */
- private static String[] getSystemDistributionDirectories(Context context) {
- final String baseDirectory = "/system/" + context.getPackageName() + "/distribution";
- return getDistributionDirectoriesFromBaseDirectory(context, baseDirectory);
- }
-
- /**
- * Get a list of data distribution folder candidates.
- *
- * <dataDir>/distribution/<mcc>/<mnc> - For bundled distributions for specific network providers
- * <dataDir>/distribution/<mcc> - For bundled distributions for specific countries
- * <dataDir>/distribution/default - For bundled distributions with no matching mcc/mnc
- * <dataDir>/distribution - Default non-bundled system distribution
- */
- private static String[] getDataDistributionDirectories(Context context) {
- final String baseDirectory = new File(context.getApplicationInfo().dataDir, DISTRIBUTION_PATH).getAbsolutePath();
- return getDistributionDirectoriesFromBaseDirectory(context, baseDirectory);
- }
-
- /**
- * Get a list of distribution folder candidates inside the specified base directory.
- */
- private static String[] getDistributionDirectoriesFromBaseDirectory(Context context, String baseDirectory) {
- final TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
- if (telephonyManager != null && telephonyManager.getSimState() == TelephonyManager.SIM_STATE_READY) {
- final String simOperator = telephonyManager.getSimOperator();
-
- if (simOperator != null && simOperator.length() >= 5) {
- final String mcc = simOperator.substring(0, 3);
- final String mnc = simOperator.substring(3);
-
- return new String[] {
- baseDirectory + "/" + mcc + "/" + mnc,
- baseDirectory + "/" + mcc,
- baseDirectory + "/default",
- baseDirectory
- };
- }
- }
-
- return new String[] {
- baseDirectory + "/default",
- baseDirectory
- };
- }
-
- /**
- * The provided <code>ReadyCallback</code> will be queued for execution after
- * the distribution is ready, or queued for immediate execution if the
- * distribution has already been processed.
- *
- * Each <code>ReadyCallback</code> will be executed on the background thread.
- */
- public void addOnDistributionReadyCallback(final ReadyCallback callback) {
- if (state == STATE_UNKNOWN) {
- // Queue for later.
- onDistributionReady.add(callback);
- } else {
- invokeCallbackDelayed(callback);
- }
- }
-
- /**
- * Run our delayed queue, after a delayed distribution arrives.
- */
- private void runLateReadyQueue() {
- ReadyCallback task;
- while ((task = onLateReady.poll()) != null) {
- invokeLateCallbackDelayed(task);
- }
- }
-
- /**
- * Execute tasks that wanted to run when we were done loading
- * the distribution.
- */
- private void runReadyQueue() {
- ReadyCallback task;
- while ((task = onDistributionReady.poll()) != null) {
- invokeCallbackDelayed(task);
- }
- }
-
- private void invokeLateCallbackDelayed(final ReadyCallback callback) {
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- // Sanity.
- if (state != STATE_SET) {
- Log.w(LOGTAG, "Refusing to invoke late distro callback in state " + state);
- return;
- }
- callback.distributionArrivedLate(Distribution.this);
- }
- });
- }
-
- private void invokeCallbackDelayed(final ReadyCallback callback) {
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @WorkerThread
- @Override
- public void run() {
- switch (state) {
- case STATE_SET:
- callback.distributionFound(Distribution.this);
- break;
- case STATE_NONE:
- callback.distributionNotFound();
- if (shouldDelayLateCallbacks) {
- onLateReady.add(callback);
- }
- break;
- default:
- throw new IllegalStateException("Expected STATE_NONE or STATE_SET, got " + state);
- }
- }
- });
- }
-
- /**
- * A safe way for callers to determine if this Distribution instance
- * represents a real live distribution.
- */
- public boolean exists() {
- return state == STATE_SET;
- }
-
- private String getKeyName() {
- return context.getPackageName() + ".distribution_state";
- }
-
- private SharedPreferences getSharedPreferences() {
- final SharedPreferences settings;
- if (prefsBranch == null) {
- settings = GeckoSharedPrefs.forApp(context);
- } else {
- settings = context.getSharedPreferences(prefsBranch, Activity.MODE_PRIVATE);
- }
- return settings;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/distribution/DistributionStoreCallback.java b/mobile/android/base/java/org/mozilla/gecko/distribution/DistributionStoreCallback.java
deleted file mode 100644
index 11ed4811f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/distribution/DistributionStoreCallback.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, you can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-package org.mozilla.gecko.distribution;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.util.Log;
-import org.mozilla.gecko.GeckoSharedPrefs;
-
-import java.lang.ref.WeakReference;
-
-/**
- * A distribution ready callback that will store the distribution ID to profile-specific shared preferences.
- */
-public class DistributionStoreCallback implements Distribution.ReadyCallback {
- private static final String LOGTAG = "Gecko" + DistributionStoreCallback.class.getSimpleName();
-
- public static final String PREF_DISTRIBUTION_ID = "distribution.id";
-
- private final WeakReference<Context> contextReference;
- private final String profileName;
-
- public DistributionStoreCallback(final Context context, final String profileName) {
- this.contextReference = new WeakReference<>(context);
- this.profileName = profileName;
- }
-
- public void distributionNotFound() { /* nothing to do here */ }
-
- @Override
- public void distributionFound(final Distribution distribution) {
- storeDistribution(distribution);
- }
-
- @Override
- public void distributionArrivedLate(final Distribution distribution) {
- storeDistribution(distribution);
- }
-
- private void storeDistribution(final Distribution distribution) {
- final Context context = contextReference.get();
- if (context == null) {
- Log.w(LOGTAG, "Context is no longer alive, could retrieve shared prefs to store distribution");
- return;
- }
-
- // While the distribution preferences are per install and not per profile, it's okay to use the
- // profile-specific prefs because:
- // 1) We don't really support mulitple profiles for end-users
- // 2) The TelemetryUploadService already accesses profile-specific shared prefs so this keeps things simple.
- final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfileName(context, profileName);
- final Distribution.DistributionDescriptor desc = distribution.getDescriptor();
- if (desc != null) {
- sharedPrefs.edit().putString(PREF_DISTRIBUTION_ID, desc.id).apply();
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBookmarksProviderProxy.java b/mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBookmarksProviderProxy.java
deleted file mode 100644
index 78a77221d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBookmarksProviderProxy.java
+++ /dev/null
@@ -1,322 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.distribution;
-
-import android.content.ContentProvider;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.UriMatcher;
-import android.database.Cursor;
-import android.database.CursorWrapper;
-import android.net.Uri;
-import android.support.annotation.NonNull;
-import android.text.TextUtils;
-
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.db.BrowserContract;
-
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * A proxy for the partner bookmarks provider. Bookmark and folder ids of the partner bookmarks providers
- * will be transformed so that they do not overlap with the ids from the local database.
- *
- * Bookmarks in folder:
- * content://{PACKAGE_ID}.partnerbookmarks/bookmarks/{folderId}
- * Icon of bookmark:
- * content://{PACKAGE_ID}.partnerbookmarks/icons/{bookmarkId}
- */
-public class PartnerBookmarksProviderProxy extends ContentProvider {
- /**
- * The contract between the partner bookmarks provider and applications. Contains the definition
- * for the supported URIs and columns.
- */
- public static class PartnerContract {
- public static final Uri CONTENT_URI = Uri.parse("content://com.android.partnerbookmarks/bookmarks");
-
- public static final int TYPE_BOOKMARK = 1;
- public static final int TYPE_FOLDER = 2;
-
- public static final int PARENT_ROOT_ID = 0;
-
- public static final String ID = "_id";
- public static final String TYPE = "type";
- public static final String URL = "url";
- public static final String TITLE = "title";
- public static final String FAVICON = "favicon";
- public static final String TOUCHICON = "touchicon";
- public static final String PARENT = "parent";
- }
-
- private static final String AUTHORITY_PREFIX = ".partnerbookmarks";
-
- private static final int URI_MATCH_BOOKMARKS = 1000;
- private static final int URI_MATCH_ICON = 1001;
- private static final int URI_MATCH_BOOKMARK = 1002;
-
- private static final String PREF_DELETED_PARTNER_BOOKMARKS = "distribution.partner.bookmark.deleted";
-
- /**
- * Cursor wrapper for filtering empty folders.
- */
- private static class FilteredCursor extends CursorWrapper {
- private HashSet<Integer> emptyFolderPositions;
- private int count;
-
- public FilteredCursor(PartnerBookmarksProviderProxy proxy, Cursor cursor) {
- super(cursor);
-
- emptyFolderPositions = new HashSet<>();
- count = cursor.getCount();
-
- for (int i = 0; i < cursor.getCount(); i++) {
- cursor.moveToPosition(i);
-
- final long id = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks._ID));
- final int type = cursor.getInt(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.TYPE));
-
- if (type == BrowserContract.Bookmarks.TYPE_FOLDER && proxy.isFolderEmpty(id)) {
- // We do not support deleting folders. So at least hide partner folders that are
- // empty because all bookmarks inside it are deleted/hidden.
- // Note that this will still show folders with empty folders in them. But multi-level
- // partner bookmarks are very unlikely.
-
- count--;
- emptyFolderPositions.add(i);
- }
- }
- }
-
- @Override
- public int getCount() {
- return count;
- }
-
- @Override
- public boolean moveToPosition(int position) {
- final Cursor cursor = getWrappedCursor();
- final int actualCount = cursor.getCount();
-
- // Find the next position pointing to a bookmark or a non-empty folder
- while (position < actualCount && emptyFolderPositions.contains(position)) {
- position++;
- }
-
- return position < actualCount && cursor.moveToPosition(position);
- }
- }
-
- private static String getAuthority(Context context) {
- return context.getPackageName() + AUTHORITY_PREFIX;
- }
-
- public static Uri getUriForBookmarks(Context context, long folderId) {
- return new Uri.Builder()
- .scheme("content")
- .authority(getAuthority(context))
- .appendPath("bookmarks")
- .appendPath(String.valueOf(folderId))
- .build();
- }
-
- public static Uri getUriForIcon(Context context, long bookmarkId) {
- return new Uri.Builder()
- .scheme("content")
- .authority(getAuthority(context))
- .appendPath("icons")
- .appendPath(String.valueOf(bookmarkId))
- .build();
- }
-
- public static Uri getUriForBookmark(Context context, long bookmarkId) {
- return new Uri.Builder()
- .scheme("content")
- .authority(getAuthority(context))
- .appendPath("bookmark")
- .appendPath(String.valueOf(bookmarkId))
- .build();
- }
-
- private final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
-
- @Override
- public boolean onCreate() {
- String authority = getAuthority(assertAndGetContext());
-
- uriMatcher.addURI(authority, "bookmarks/*", URI_MATCH_BOOKMARKS);
- uriMatcher.addURI(authority, "icons/*", URI_MATCH_ICON);
- uriMatcher.addURI(authority, "bookmark/*", URI_MATCH_BOOKMARK);
-
- return true;
- }
-
- @Override
- public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
- final Context context = assertAndGetContext();
- final int match = uriMatcher.match(uri);
-
- final ContentResolver contentResolver = context.getContentResolver();
-
- switch (match) {
- case URI_MATCH_BOOKMARKS:
- final long bookmarkId = ContentUris.parseId(uri);
- if (bookmarkId == -1) {
- throw new IllegalArgumentException("Bookmark id is not a number");
- }
- final Cursor cursor = getBookmarksInFolder(contentResolver, bookmarkId);
- cursor.setNotificationUri(context.getContentResolver(), uri);
- return new FilteredCursor(this, cursor);
-
- case URI_MATCH_ICON:
- return getIcon(contentResolver, ContentUris.parseId(uri));
-
- default:
- throw new UnsupportedOperationException("Unknown URI " + uri.toString());
- }
- }
-
- @Override
- public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
- final int match = uriMatcher.match(uri);
-
- switch (match) {
- case URI_MATCH_BOOKMARK:
- rememberRemovedBookmark(ContentUris.parseId(uri));
- notifyBookmarkChange();
- return 1;
-
- default:
- throw new UnsupportedOperationException("Unknown URI " + uri.toString());
- }
- }
-
- private void notifyBookmarkChange() {
- final Context context = assertAndGetContext();
-
- context.getContentResolver().notifyChange(
- new Uri.Builder()
- .scheme("content")
- .authority(getAuthority(context))
- .appendPath("bookmarks")
- .build(),
- null);
- }
-
- private synchronized void rememberRemovedBookmark(long bookmarkId) {
- Set<String> deletedIds = getRemovedBookmarkIds();
-
- deletedIds.add(String.valueOf(bookmarkId));
-
- GeckoSharedPrefs.forProfile(assertAndGetContext())
- .edit()
- .putStringSet(PREF_DELETED_PARTNER_BOOKMARKS, deletedIds)
- .apply();
- }
-
- private synchronized Set<String> getRemovedBookmarkIds() {
- SharedPreferences preferences = GeckoSharedPrefs.forProfile(assertAndGetContext());
- return preferences.getStringSet(PREF_DELETED_PARTNER_BOOKMARKS, new HashSet<String>());
- }
-
- private Cursor getBookmarksInFolder(ContentResolver contentResolver, long folderId) {
- // Use root folder id or transform negative id into actual (positive) folder id.
- final long actualFolderId = folderId == BrowserContract.Bookmarks.FIXED_ROOT_ID
- ? PartnerContract.PARENT_ROOT_ID
- : BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START - folderId;
-
- final String removedBookmarkIds = TextUtils.join(",", getRemovedBookmarkIds());
-
- return contentResolver.query(
- PartnerContract.CONTENT_URI,
- new String[] {
- // Transform ids into negative values starting with FAKE_PARTNER_BOOKMARKS_START.
- "(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.ID + ") as " + BrowserContract.Bookmarks._ID,
- "(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.ID + ") as " + BrowserContract.Combined.BOOKMARK_ID,
- PartnerContract.TITLE + " as " + BrowserContract.Bookmarks.TITLE,
- PartnerContract.URL + " as " + BrowserContract.Bookmarks.URL,
- // Transform parent ids to negative ids as well
- "(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.PARENT + ") as " + BrowserContract.Bookmarks.PARENT,
- // Convert types (we use 0-1 and the partner provider 1-2)
- "(2 - " + PartnerContract.TYPE + ") as " + BrowserContract.Bookmarks.TYPE,
- // Use the ID of the entry as GUID
- PartnerContract.ID + " as " + BrowserContract.Bookmarks.GUID
- },
- PartnerContract.PARENT + " = ?"
- // We only want to read bookmarks or folders from the content provider
- + " AND " + BrowserContract.Bookmarks.TYPE + " IN (?,?)"
- // Only select entries with non empty title
- + " AND " + BrowserContract.Bookmarks.TITLE + " <> ''"
- // Filter all "deleted" ids
- + " AND " + BrowserContract.Combined.BOOKMARK_ID + " NOT IN (" + removedBookmarkIds + ")",
- new String[] {
- String.valueOf(actualFolderId),
- String.valueOf(PartnerContract.TYPE_BOOKMARK),
- String.valueOf(PartnerContract.TYPE_FOLDER)
- },
- // Same order we use in our content provider (without position)
- BrowserContract.Bookmarks.TYPE + " ASC, " + BrowserContract.Bookmarks._ID + " ASC");
- }
-
- private boolean isFolderEmpty(long folderId) {
- final Context context = assertAndGetContext();
- final Cursor cursor = getBookmarksInFolder(context.getContentResolver(), folderId);
-
- if (cursor == null) {
- return true;
- }
-
- try {
- return cursor.getCount() == 0;
- } finally {
- cursor.close();
- }
- }
-
- private Cursor getIcon(ContentResolver contentResolver, long bookmarkId) {
- final long actualId = BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START - bookmarkId;
-
- return contentResolver.query(
- PartnerContract.CONTENT_URI,
- new String[] {
- PartnerContract.TOUCHICON,
- PartnerContract.FAVICON
- },
- PartnerContract.ID + " = ?",
- new String[] {
- String.valueOf(actualId)
- },
- null);
- }
-
- private Context assertAndGetContext() {
- final Context context = super.getContext();
-
- if (context == null) {
- throw new AssertionError("Context is null");
- }
-
- return context;
- }
-
- @Override
- public String getType(@NonNull Uri uri) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public Uri insert(@NonNull Uri uri, ContentValues values) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
- throw new UnsupportedOperationException();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBrowserCustomizationsClient.java b/mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBrowserCustomizationsClient.java
deleted file mode 100644
index 2dad21a48..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBrowserCustomizationsClient.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.distribution;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.net.Uri;
-
-/**
- * Client for accessing data from Android's "partner browser customizations" content provider.
- */
-public class PartnerBrowserCustomizationsClient {
- private static final Uri CONTENT_URI = Uri.parse("content://com.android.partnerbrowsercustomizations");
-
- private static final Uri HOMEPAGE_URI = CONTENT_URI.buildUpon().path("homepage").build();
-
- private static final String COLUMN_HOMEPAGE = "homepage";
-
- /**
- * Returns the partner homepage or null if it could not be read from the content provider.
- */
- public static String getHomepage(Context context) {
- Cursor cursor = context.getContentResolver().query(
- HOMEPAGE_URI, new String[] { COLUMN_HOMEPAGE }, null, null, null);
-
- if (cursor == null) {
- return null;
- }
-
- try {
- if (!cursor.moveToFirst()) {
- return null;
- }
-
- return cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_HOMEPAGE));
- } finally {
- cursor.close();
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerDescriptor.java b/mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerDescriptor.java
deleted file mode 100644
index 4a1be656b..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerDescriptor.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.distribution;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-
-import android.net.Uri;
-
-import java.net.URLDecoder;
-import java.io.UnsupportedEncodingException;
-
-/**
- * Encapsulates access to values encoded in the "referrer" extra of an install intent.
- *
- * This object is immutable.
- *
- * Example input:
- *
- * "utm_source=campsource&utm_medium=campmed&utm_term=term%2Bhere&utm_content=content&utm_campaign=name"
- */
-@RobocopTarget
-public class ReferrerDescriptor {
- public final String source;
- public final String medium;
- public final String term;
- public final String content;
- public final String campaign;
-
- public ReferrerDescriptor(String referrer) {
- if (referrer == null) {
- source = null;
- medium = null;
- term = null;
- content = null;
- campaign = null;
- return;
- }
-
- try {
- referrer = URLDecoder.decode(referrer, "UTF-8");
- } catch (UnsupportedEncodingException e) {
- // UTF-8 is always supported
- }
-
- final Uri u = new Uri.Builder()
- .scheme("http")
- .authority("local")
- .path("/")
- .encodedQuery(referrer).build();
-
- source = u.getQueryParameter("utm_source");
- medium = u.getQueryParameter("utm_medium");
- term = u.getQueryParameter("utm_term");
- content = u.getQueryParameter("utm_content");
- campaign = u.getQueryParameter("utm_campaign");
- }
-
- @Override
- public String toString() {
- return "{s: " + source + ", m: " + medium + ", t: " + term + ", c: " + content + ", c: " + campaign + "}";
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerReceiver.java b/mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerReceiver.java
deleted file mode 100644
index 3651d6068..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerReceiver.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.distribution;
-
-import org.mozilla.gecko.AdjustConstants;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoAppShell;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.support.v4.content.LocalBroadcastManager;
-import android.text.TextUtils;
-import android.util.Log;
-
-public class ReferrerReceiver extends BroadcastReceiver {
- private static final String LOGTAG = "GeckoReferrerReceiver";
-
- private static final String ACTION_INSTALL_REFERRER = "com.android.vending.INSTALL_REFERRER";
-
- // Sent when we're done.
- @RobocopTarget
- public static final String ACTION_REFERRER_RECEIVED = "org.mozilla.fennec.REFERRER_RECEIVED";
-
- /**
- * If the install intent has this source, it is a Mozilla specific or over
- * the air distribution referral. We'll track the campaign ID using
- * Mozilla's metrics systems.
- *
- * If the install intent has a source different than this one, it is a
- * referral from an advertising network. We may track these campaigns using
- * third-party tracking and metrics systems.
- */
- private static final String MOZILLA_UTM_SOURCE = "mozilla";
-
- /**
- * If the install intent has this campaign, we'll load the specified distribution.
- */
- private static final String DISTRIBUTION_UTM_CAMPAIGN = "distribution";
-
- @Override
- public void onReceive(Context context, Intent intent) {
- Log.v(LOGTAG, "Received intent " + intent);
- if (!ACTION_INSTALL_REFERRER.equals(intent.getAction())) {
- // This should never happen.
- return;
- }
-
- // Track the referrer object for distribution handling.
- ReferrerDescriptor referrer = new ReferrerDescriptor(intent.getStringExtra("referrer"));
-
- if (!TextUtils.equals(referrer.source, MOZILLA_UTM_SOURCE)) {
- // Allow the Adjust handler to process the intent.
- try {
- AdjustConstants.getAdjustHelper().onReceive(context, intent);
- } catch (Exception e) {
- Log.e(LOGTAG, "Got exception in Adjust's onReceive; ignoring referrer intent.", e);
- }
- return;
- }
-
- if (TextUtils.equals(referrer.campaign, DISTRIBUTION_UTM_CAMPAIGN)) {
- Distribution.onReceivedReferrer(context, referrer);
- // We want Adjust information for OTA distributions as well
- try {
- AdjustConstants.getAdjustHelper().onReceive(context, intent);
- } catch (Exception e) {
- Log.e(LOGTAG, "Got exception in Adjust's onReceive for distribution.", e);
- }
- } else {
- Log.d(LOGTAG, "Not downloading distribution: non-matching campaign.");
- // If this is a Mozilla campaign, pass the campaign along to Gecko.
- // It'll pretend to be a "playstore" distribution for BLP purposes.
- propagateMozillaCampaign(referrer);
- }
-
- // Broadcast a secondary, local intent to allow test code to respond.
- final Intent receivedIntent = new Intent(ACTION_REFERRER_RECEIVED);
- LocalBroadcastManager.getInstance(context).sendBroadcast(receivedIntent);
- }
-
-
- private void propagateMozillaCampaign(ReferrerDescriptor referrer) {
- if (referrer.campaign == null) {
- return;
- }
-
- try {
- final JSONObject data = new JSONObject();
- data.put("id", "playstore");
- data.put("version", referrer.campaign);
- String payload = data.toString();
-
- // Try to make sure the prefs are written as a group.
- GeckoAppShell.notifyObservers("Campaign:Set", payload);
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error propagating campaign identifier.", e);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/BaseAction.java b/mobile/android/base/java/org/mozilla/gecko/dlc/BaseAction.java
deleted file mode 100644
index 28d6b238d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/dlc/BaseAction.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.dlc;
-
-import android.content.Context;
-import android.support.annotation.IntDef;
-import android.util.Log;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.background.nativecode.NativeCrypto;
-import org.mozilla.gecko.dlc.catalog.DownloadContent;
-import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
-import org.mozilla.gecko.sync.Utils;
-import org.mozilla.gecko.util.HardwareUtils;
-import org.mozilla.gecko.util.IOUtils;
-import org.mozilla.gecko.util.ProxySelector;
-
-import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
-import java.net.URI;
-import java.net.URISyntaxException;
-
-public abstract class BaseAction {
- private static final String LOGTAG = "GeckoDLCBaseAction";
-
- /**
- * Exception indicating a recoverable error has happened. Download of the content will be retried later.
- */
- /* package-private */ static class RecoverableDownloadContentException extends Exception {
- private static final long serialVersionUID = -2246772819507370734L;
-
- @IntDef({MEMORY, DISK_IO, SERVER, NETWORK})
- public @interface ErrorType {}
- public static final int MEMORY = 1;
- public static final int DISK_IO = 2;
- public static final int SERVER = 3;
- public static final int NETWORK = 4;
-
- private int errorType;
-
- public RecoverableDownloadContentException(@ErrorType int errorType, String message) {
- super(message);
- this.errorType = errorType;
- }
-
- public RecoverableDownloadContentException(@ErrorType int errorType, Throwable cause) {
- super(cause);
- this.errorType = errorType;
- }
-
- @ErrorType
- public int getErrorType() {
- return errorType;
- }
-
- /**
- * Should this error be counted as failure? If this type of error will happen multiple times in a row then this
- * error will be treated as permanently and the operation will not be tried again until the content changes.
- */
- public boolean shouldBeCountedAsFailure() {
- if (NETWORK == errorType) {
- return false; // Always retry after network errors
- }
-
- return true;
- }
- }
-
- /**
- * If this exception is thrown the content will be marked as unrecoverable, permanently failed and we will not try
- * downloading it again - until a newer version of the content is available.
- */
- /* package-private */ static class UnrecoverableDownloadContentException extends Exception {
- private static final long serialVersionUID = 8956080754787367105L;
-
- public UnrecoverableDownloadContentException(String message) {
- super(message);
- }
-
- public UnrecoverableDownloadContentException(Throwable cause) {
- super(cause);
- }
- }
-
- public abstract void perform(Context context, DownloadContentCatalog catalog);
-
- protected File getDestinationFile(Context context, DownloadContent content)
- throws UnrecoverableDownloadContentException, RecoverableDownloadContentException {
- if (content.isFont()) {
- File destinationDirectory = new File(context.getApplicationInfo().dataDir, "fonts");
-
- if (!destinationDirectory.exists() && !destinationDirectory.mkdirs()) {
- throw new RecoverableDownloadContentException(RecoverableDownloadContentException.DISK_IO,
- "Destination directory does not exist and cannot be created");
- }
-
- return new File(destinationDirectory, content.getFilename());
- }
-
- // Unrecoverable: We downloaded a file and we don't know what to do with it (Should not happen)
- throw new UnrecoverableDownloadContentException("Can't determine destination for kind: " + content.getKind());
- }
-
- protected boolean verify(File file, String expectedChecksum)
- throws RecoverableDownloadContentException, UnrecoverableDownloadContentException {
- InputStream inputStream = null;
-
- try {
- inputStream = new BufferedInputStream(new FileInputStream(file));
-
- byte[] ctx = NativeCrypto.sha256init();
- if (ctx == null) {
- throw new RecoverableDownloadContentException(RecoverableDownloadContentException.MEMORY,
- "Could not create SHA-256 context");
- }
-
- byte[] buffer = new byte[4096];
- int read;
-
- while ((read = inputStream.read(buffer)) != -1) {
- NativeCrypto.sha256update(ctx, buffer, read);
- }
-
- String actualChecksum = Utils.byte2Hex(NativeCrypto.sha256finalize(ctx));
-
- if (!actualChecksum.equalsIgnoreCase(expectedChecksum)) {
- Log.w(LOGTAG, "Checksums do not match. Expected=" + expectedChecksum + ", Actual=" + actualChecksum);
- return false;
- }
-
- return true;
- } catch (IOException e) {
- // Recoverable: Just I/O discontinuation
- throw new RecoverableDownloadContentException(RecoverableDownloadContentException.DISK_IO, e);
- } finally {
- IOUtils.safeStreamClose(inputStream);
- }
- }
-
- protected HttpURLConnection buildHttpURLConnection(String url)
- throws UnrecoverableDownloadContentException, IOException {
- try {
- System.setProperty("http.keepAlive", "true");
-
- HttpURLConnection connection = (HttpURLConnection) ProxySelector.openConnectionWithProxy(new URI(url));
- connection.setRequestProperty("User-Agent", HardwareUtils.isTablet() ?
- AppConstants.USER_AGENT_FENNEC_TABLET :
- AppConstants.USER_AGENT_FENNEC_MOBILE);
- connection.setRequestMethod("GET");
- connection.setInstanceFollowRedirects(true);
- return connection;
- } catch (MalformedURLException e) {
- throw new UnrecoverableDownloadContentException(e);
- } catch (URISyntaxException e) {
- throw new UnrecoverableDownloadContentException(e);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/CleanupAction.java b/mobile/android/base/java/org/mozilla/gecko/dlc/CleanupAction.java
deleted file mode 100644
index e44704c6c..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/dlc/CleanupAction.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.dlc;
-
-import android.content.Context;
-
-import org.mozilla.gecko.dlc.catalog.DownloadContent;
-import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
-
-import java.io.File;
-
-/**
- * CleanupAction: Remove content that is no longer needed.
- */
-public class CleanupAction extends BaseAction {
- @Override
- public void perform(Context context, DownloadContentCatalog catalog) {
- for (DownloadContent content : catalog.getContentToDelete()) {
- if (!content.isAssetArchive()) {
- continue; // We do not know how to clean up this content. But this means we didn't
- // download it anyways.
- }
-
- try {
- File file = getDestinationFile(context, content);
-
- if (!file.exists()) {
- // File does not exist. As good as deleting.
- catalog.remove(content);
- return;
- }
-
- if (file.delete()) {
- // File has been deleted. Now remove it from the catalog.
- catalog.remove(content);
- }
- } catch (UnrecoverableDownloadContentException e) {
- // We can't recover. Pretend the content is removed. It probably never existed in
- // the first place.
- catalog.remove(content);
- } catch (RecoverableDownloadContentException e) {
- // Try again next time.
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadAction.java b/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadAction.java
deleted file mode 100644
index 8618d4699..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadAction.java
+++ /dev/null
@@ -1,325 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.dlc;
-
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.support.v4.net.ConnectivityManagerCompat;
-import android.util.Log;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.dlc.catalog.DownloadContent;
-import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
-import org.mozilla.gecko.util.HardwareUtils;
-import org.mozilla.gecko.util.IOUtils;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.zip.GZIPInputStream;
-
-/**
- * Download content that has been scheduled during "study" or "verify".
- */
-public class DownloadAction extends BaseAction {
- private static final String LOGTAG = "DLCDownloadAction";
-
- private static final String CACHE_DIRECTORY = "downloadContent";
-
- private static final String CDN_BASE_URL = "https://fennec-catalog.cdn.mozilla.net/";
-
- public interface Callback {
- void onContentDownloaded(DownloadContent content);
- }
-
- private Callback callback;
-
- public DownloadAction(Callback callback) {
- this.callback = callback;
- }
-
- public void perform(Context context, DownloadContentCatalog catalog) {
- Log.d(LOGTAG, "Downloading content..");
-
- if (!isConnectedToNetwork(context)) {
- Log.d(LOGTAG, "No connected network available. Postponing download.");
- // TODO: Reschedule download (bug 1209498)
- return;
- }
-
- if (isActiveNetworkMetered(context)) {
- Log.d(LOGTAG, "Network is metered. Postponing download.");
- // TODO: Reschedule download (bug 1209498)
- return;
- }
-
- for (DownloadContent content : catalog.getScheduledDownloads()) {
- Log.d(LOGTAG, "Downloading: " + content);
-
- File temporaryFile = null;
-
- try {
- File destinationFile = getDestinationFile(context, content);
- if (destinationFile.exists() && verify(destinationFile, content.getChecksum())) {
- Log.d(LOGTAG, "Content already exists and is up-to-date.");
- catalog.markAsDownloaded(content);
- continue;
- }
-
- temporaryFile = createTemporaryFile(context, content);
-
- if (!hasEnoughDiskSpace(content, destinationFile, temporaryFile)) {
- Log.d(LOGTAG, "Not enough disk space to save content. Skipping download.");
- continue;
- }
-
- // TODO: Check space on disk before downloading content (bug 1220145)
- final String url = createDownloadURL(content);
-
- if (!temporaryFile.exists() || temporaryFile.length() < content.getSize()) {
- download(url, temporaryFile);
- }
-
- if (!verify(temporaryFile, content.getDownloadChecksum())) {
- Log.w(LOGTAG, "Wrong checksum after download, content=" + content.getId());
- temporaryFile.delete();
- continue;
- }
-
- if (!content.isAssetArchive()) {
- Log.e(LOGTAG, "Downloaded content is not of type 'asset-archive': " + content.getType());
- temporaryFile.delete();
- continue;
- }
-
- extract(temporaryFile, destinationFile, content.getChecksum());
-
- catalog.markAsDownloaded(content);
-
- Log.d(LOGTAG, "Successfully downloaded: " + content);
-
- if (callback != null) {
- callback.onContentDownloaded(content);
- }
-
- if (temporaryFile != null && temporaryFile.exists()) {
- temporaryFile.delete();
- }
- } catch (RecoverableDownloadContentException e) {
- Log.w(LOGTAG, "Downloading content failed (Recoverable): " + content, e);
-
- if (e.shouldBeCountedAsFailure()) {
- catalog.rememberFailure(content, e.getErrorType());
- }
-
- // TODO: Reschedule download (bug 1209498)
- } catch (UnrecoverableDownloadContentException e) {
- Log.w(LOGTAG, "Downloading content failed (Unrecoverable): " + content, e);
-
- catalog.markAsPermanentlyFailed(content);
-
- if (temporaryFile != null && temporaryFile.exists()) {
- temporaryFile.delete();
- }
- }
- }
-
- Log.v(LOGTAG, "Done");
- }
-
- protected void download(String source, File temporaryFile)
- throws RecoverableDownloadContentException, UnrecoverableDownloadContentException {
- InputStream inputStream = null;
- OutputStream outputStream = null;
-
- HttpURLConnection connection = null;
-
- try {
- connection = buildHttpURLConnection(source);
-
- final long offset = temporaryFile.exists() ? temporaryFile.length() : 0;
- if (offset > 0) {
- connection.setRequestProperty("Range", "bytes=" + offset + "-");
- }
-
- final int status = connection.getResponseCode();
- if (status != HttpURLConnection.HTTP_OK && status != HttpURLConnection.HTTP_PARTIAL) {
- // We are trying to be smart and only retry if this is an error that might resolve in the future.
- // TODO: This is guesstimating at best. We want to implement failure counters (Bug 1215106).
- if (status >= 500) {
- // Recoverable: Server errors 5xx
- throw new RecoverableDownloadContentException(RecoverableDownloadContentException.SERVER,
- "(Recoverable) Download failed. Status code: " + status);
- } else if (status >= 400) {
- // Unrecoverable: Client errors 4xx - Unlikely that this version of the client will ever succeed.
- throw new UnrecoverableDownloadContentException("(Unrecoverable) Download failed. Status code: " + status);
- } else {
- // HttpsUrlConnection: -1 (No valid response code)
- // Informational 1xx: They have no meaning to us.
- // Successful 2xx: We don't know how to handle anything but 200.
- // Redirection 3xx: HttpClient should have followed redirects if possible. We should not see those errors here.
- throw new UnrecoverableDownloadContentException("(Unrecoverable) Download failed. Status code: " + status);
- }
- }
-
- inputStream = new BufferedInputStream(connection.getInputStream());
- outputStream = openFile(temporaryFile, status == HttpURLConnection.HTTP_PARTIAL);
-
- IOUtils.copy(inputStream, outputStream);
-
- inputStream.close();
- outputStream.close();
- } catch (IOException e) {
- // Recoverable: Just I/O discontinuation
- throw new RecoverableDownloadContentException(RecoverableDownloadContentException.NETWORK, e);
- } finally {
- IOUtils.safeStreamClose(inputStream);
- IOUtils.safeStreamClose(outputStream);
-
- if (connection != null) {
- connection.disconnect();
- }
- }
- }
-
- protected OutputStream openFile(File file, boolean append) throws FileNotFoundException {
- return new BufferedOutputStream(new FileOutputStream(file, append));
- }
-
- protected void extract(File sourceFile, File destinationFile, String checksum)
- throws UnrecoverableDownloadContentException, RecoverableDownloadContentException {
- InputStream inputStream = null;
- OutputStream outputStream = null;
- File temporaryFile = null;
-
- try {
- File destinationDirectory = destinationFile.getParentFile();
- if (!destinationDirectory.exists() && !destinationDirectory.mkdirs()) {
- throw new IOException("Destination directory does not exist and cannot be created");
- }
-
- temporaryFile = new File(destinationDirectory, destinationFile.getName() + ".tmp");
-
- inputStream = new GZIPInputStream(new BufferedInputStream(new FileInputStream(sourceFile)));
- outputStream = new BufferedOutputStream(new FileOutputStream(temporaryFile));
-
- IOUtils.copy(inputStream, outputStream);
-
- inputStream.close();
- outputStream.close();
-
- if (!verify(temporaryFile, checksum)) {
- Log.w(LOGTAG, "Checksum of extracted file does not match.");
- return;
- }
-
- move(temporaryFile, destinationFile);
- } catch (IOException e) {
- // We could not extract to the destination: Keep temporary file and try again next time we run.
- throw new RecoverableDownloadContentException(RecoverableDownloadContentException.DISK_IO, e);
- } finally {
- IOUtils.safeStreamClose(inputStream);
- IOUtils.safeStreamClose(outputStream);
-
- if (temporaryFile != null && temporaryFile.exists()) {
- temporaryFile.delete();
- }
- }
- }
-
- protected boolean isConnectedToNetwork(Context context) {
- ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
- NetworkInfo networkInfo = manager.getActiveNetworkInfo();
-
- return networkInfo != null && networkInfo.isConnected();
- }
-
- protected boolean isActiveNetworkMetered(Context context) {
- return ConnectivityManagerCompat.isActiveNetworkMetered(
- (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));
- }
-
- protected String createDownloadURL(DownloadContent content) {
- final String location = content.getLocation();
-
- return CDN_BASE_URL + content.getLocation();
- }
-
- protected File createTemporaryFile(Context context, DownloadContent content)
- throws RecoverableDownloadContentException {
- File cacheDirectory = new File(context.getCacheDir(), CACHE_DIRECTORY);
-
- if (!cacheDirectory.exists() && !cacheDirectory.mkdirs()) {
- // Recoverable: File system might not be mounted NOW and we didn't download anything yet anyways.
- throw new RecoverableDownloadContentException(RecoverableDownloadContentException.DISK_IO,
- "Could not create cache directory: " + cacheDirectory);
- }
-
- return new File(cacheDirectory, content.getDownloadChecksum() + "-" + content.getId());
- }
-
- protected void move(File temporaryFile, File destinationFile)
- throws RecoverableDownloadContentException, UnrecoverableDownloadContentException {
- if (!temporaryFile.renameTo(destinationFile)) {
- Log.d(LOGTAG, "Could not move temporary file to destination. Trying to copy..");
- copy(temporaryFile, destinationFile);
- temporaryFile.delete();
- }
- }
-
- protected void copy(File temporaryFile, File destinationFile)
- throws RecoverableDownloadContentException, UnrecoverableDownloadContentException {
- InputStream inputStream = null;
- OutputStream outputStream = null;
-
- try {
- File destinationDirectory = destinationFile.getParentFile();
- if (!destinationDirectory.exists() && !destinationDirectory.mkdirs()) {
- throw new IOException("Destination directory does not exist and cannot be created");
- }
-
- inputStream = new BufferedInputStream(new FileInputStream(temporaryFile));
- outputStream = new BufferedOutputStream(new FileOutputStream(destinationFile));
-
- IOUtils.copy(inputStream, outputStream);
-
- inputStream.close();
- outputStream.close();
- } catch (IOException e) {
- // We could not copy the temporary file to its destination: Keep the temporary file and
- // try again the next time we run.
- throw new RecoverableDownloadContentException(RecoverableDownloadContentException.DISK_IO, e);
- } finally {
- IOUtils.safeStreamClose(inputStream);
- IOUtils.safeStreamClose(outputStream);
- }
- }
-
- protected boolean hasEnoughDiskSpace(DownloadContent content, File destinationFile, File temporaryFile) {
- final File temporaryDirectory = temporaryFile.getParentFile();
- if (temporaryDirectory.getUsableSpace() < content.getSize()) {
- return false;
- }
-
- final File destinationDirectory = destinationFile.getParentFile();
- // We need some more space to extract the file (getSize() returns the uncompressed size)
- if (destinationDirectory.getUsableSpace() < content.getSize() * 2) {
- return false;
- }
-
- return true;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadContentService.java b/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadContentService.java
deleted file mode 100644
index 3729cf2e0..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadContentService.java
+++ /dev/null
@@ -1,144 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.dlc;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.dlc.catalog.DownloadContent;
-import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
-import org.mozilla.gecko.util.HardwareUtils;
-
-import android.app.IntentService;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-
-/**
- * Service to handle downloadable content that did not ship with the APK.
- */
-public class DownloadContentService extends IntentService {
- private static final String LOGTAG = "GeckoDLCService";
-
- /**
- * Study: Scan the catalog for "new" content available for download.
- */
- private static final String ACTION_STUDY_CATALOG = AppConstants.ANDROID_PACKAGE_NAME + ".DLC.STUDY";
-
- /**
- * Verify: Validate downloaded content. Does it still exist and does it have the correct checksum?
- */
- private static final String ACTION_VERIFY_CONTENT = AppConstants.ANDROID_PACKAGE_NAME + ".DLC.VERIFY";
-
- /**
- * Download content that has been scheduled during "study" or "verify".
- */
- private static final String ACTION_DOWNLOAD_CONTENT = AppConstants.ANDROID_PACKAGE_NAME + ".DLC.DOWNLOAD";
-
- /**
- * Sync: Synchronize catalog from a Kinto instance.
- */
- private static final String ACTION_SYNCHRONIZE_CATALOG = AppConstants.ANDROID_PACKAGE_NAME + ".DLC.SYNC";
-
- /**
- * CleanupAction: Remove content that is no longer needed (e.g. Removed from the catalog after a sync).
- */
- private static final String ACTION_CLEANUP_FILES = AppConstants.ANDROID_PACKAGE_NAME + ".DLC.CLEANUP";
-
- public static void startStudy(Context context) {
- Intent intent = new Intent(ACTION_STUDY_CATALOG);
- intent.setComponent(new ComponentName(context, DownloadContentService.class));
- context.startService(intent);
- }
-
- public static void startVerification(Context context) {
- Intent intent = new Intent(ACTION_VERIFY_CONTENT);
- intent.setComponent(new ComponentName(context, DownloadContentService.class));
- context.startService(intent);
- }
-
- public static void startDownloads(Context context) {
- Intent intent = new Intent(ACTION_DOWNLOAD_CONTENT);
- intent.setComponent(new ComponentName(context, DownloadContentService.class));
- context.startService(intent);
- }
-
- public static void startSync(Context context) {
- Intent intent = new Intent(ACTION_SYNCHRONIZE_CATALOG);
- intent.setComponent(new ComponentName(context, DownloadContentService.class));
- context.startService(intent);
- }
-
- public static void startCleanup(Context context) {
- Intent intent = new Intent(ACTION_CLEANUP_FILES);
- intent.setComponent(new ComponentName(context, DownloadContentService.class));
- context.startService(intent);
- }
-
- private DownloadContentCatalog catalog;
-
- public DownloadContentService() {
- super(LOGTAG);
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
-
- catalog = new DownloadContentCatalog(this);
- }
-
- protected void onHandleIntent(Intent intent) {
- if (!AppConstants.MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE) {
- Log.w(LOGTAG, "Download content is not enabled. Stop.");
- return;
- }
-
- if (!HardwareUtils.isSupportedSystem()) {
- // This service is running very early before checks in BrowserApp can prevent us from running.
- Log.w(LOGTAG, "System is not supported. Stop.");
- return;
- }
-
- if (intent == null) {
- return;
- }
-
- final BaseAction action;
-
- switch (intent.getAction()) {
- case ACTION_STUDY_CATALOG:
- action = new StudyAction();
- break;
-
- case ACTION_DOWNLOAD_CONTENT:
- action = new DownloadAction(new DownloadAction.Callback() {
- @Override
- public void onContentDownloaded(DownloadContent content) {
- if (content.isFont()) {
- GeckoAppShell.notifyObservers("Fonts:Reload", "");
- }
- }
- });
- break;
-
- case ACTION_VERIFY_CONTENT:
- action = new VerifyAction();
- break;
-
- case ACTION_SYNCHRONIZE_CATALOG:
- action = new SyncAction();
- break;
-
- default:
- Log.e(LOGTAG, "Unknown action: " + intent.getAction());
- return;
- }
-
- action.perform(this, catalog);
- catalog.persistChanges();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/StudyAction.java b/mobile/android/base/java/org/mozilla/gecko/dlc/StudyAction.java
deleted file mode 100644
index e15a17bbe..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/dlc/StudyAction.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.dlc;
-
-import android.content.Context;
-import android.os.Build;
-import android.text.TextUtils;
-import android.util.Log;
-
-import org.mozilla.gecko.dlc.catalog.DownloadContent;
-import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
-import org.mozilla.gecko.util.ContextUtils;
-
-/**
- * Study: Scan the catalog for "new" content available for download.
- */
-public class StudyAction extends BaseAction {
- private static final String LOGTAG = "DLCStudyAction";
-
- public void perform(Context context, DownloadContentCatalog catalog) {
- Log.d(LOGTAG, "Studying catalog..");
-
- for (DownloadContent content : catalog.getContentToStudy()) {
- if (!isMatching(context, content)) {
- // This content is not for this particular version of the application or system
- continue;
- }
-
- if (content.isAssetArchive() && content.isFont()) {
- catalog.scheduleDownload(content);
-
- Log.d(LOGTAG, "Scheduled download: " + content);
- }
- }
-
- if (catalog.hasScheduledDownloads()) {
- startDownloads(context);
- }
-
- Log.v(LOGTAG, "Done");
- }
-
- protected boolean isMatching(Context context, DownloadContent content) {
- final String androidApiPattern = content.getAndroidApiPattern();
- if (!TextUtils.isEmpty(androidApiPattern)) {
- final String apiVersion = String.valueOf(Build.VERSION.SDK_INT);
- if (apiVersion.matches(androidApiPattern)) {
- Log.d(LOGTAG, String.format("Android API (%s) does not match pattern: %s", apiVersion, androidApiPattern));
- return false;
- }
- }
-
- final String appIdPattern = content.getAppIdPattern();
- if (!TextUtils.isEmpty(appIdPattern)) {
- final String appId = context.getPackageName();
- if (!appId.matches(appIdPattern)) {
- Log.d(LOGTAG, String.format("App ID (%s) does not match pattern: %s", appId, appIdPattern));
- return false;
- }
- }
-
- final String appVersionPattern = content.getAppVersionPattern();
- if (!TextUtils.isEmpty(appVersionPattern)) {
- final String appVersion = ContextUtils.getCurrentPackageInfo(context).versionName;
- if (!appVersion.matches(appVersionPattern)) {
- Log.d(LOGTAG, String.format("App version (%s) does not match pattern: %s", appVersion, appVersionPattern));
- return false;
- }
- }
-
- // There are no patterns or all patterns have matched.
- return true;
- }
-
- protected void startDownloads(Context context) {
- DownloadContentService.startDownloads(context);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/SyncAction.java b/mobile/android/base/java/org/mozilla/gecko/dlc/SyncAction.java
deleted file mode 100644
index 104bdad18..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/dlc/SyncAction.java
+++ /dev/null
@@ -1,263 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.dlc;
-
-import android.content.Context;
-import android.net.Uri;
-import android.util.Log;
-
-import com.keepsafe.switchboard.SwitchBoard;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.dlc.catalog.DownloadContent;
-import org.mozilla.gecko.dlc.catalog.DownloadContentBuilder;
-import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
-import org.mozilla.gecko.Experiments;
-import org.mozilla.gecko.util.IOUtils;
-
-import java.io.BufferedInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-
-/**
- * Sync: Synchronize catalog from a Kinto instance.
- */
-public class SyncAction extends BaseAction {
- private static final String LOGTAG = "DLCSyncAction";
-
- private static final String KINTO_KEY_ID = "id";
- private static final String KINTO_KEY_DELETED = "deleted";
- private static final String KINTO_KEY_DATA = "data";
- private static final String KINTO_KEY_ATTACHMENT = "attachment";
- private static final String KINTO_KEY_ORIGINAL = "original";
-
- private static final String KINTO_PARAMETER_SINCE = "_since";
- private static final String KINTO_PARAMETER_FIELDS = "_fields";
- private static final String KINTO_PARAMETER_SORT = "_sort";
-
- /**
- * Kinto endpoint with online version of downloadable content catalog
- *
- * Dev instance:
- * https://kinto-ota.dev.mozaws.net/v1/buckets/dlc/collections/catalog/records
- */
- private static final String CATALOG_ENDPOINT = "https://firefox.settings.services.mozilla.com/v1/buckets/fennec/collections/catalog/records";
-
- @Override
- public void perform(Context context, DownloadContentCatalog catalog) {
- Log.d(LOGTAG, "Synchronizing catalog.");
-
- if (!isSyncEnabledForClient(context)) {
- Log.d(LOGTAG, "Sync is not enabled for client. Skipping.");
- return;
- }
-
- boolean cleanupRequired = false;
- boolean studyRequired = false;
-
- try {
- long lastModified = catalog.getLastModified();
-
- // TODO: Consider using ETag here (Bug 1257459)
- JSONArray rawCatalog = fetchRawCatalog(lastModified);
-
- Log.d(LOGTAG, "Server returned " + rawCatalog.length() + " records (since " + lastModified + ")");
-
- for (int i = 0; i < rawCatalog.length(); i++) {
- JSONObject object = rawCatalog.getJSONObject(i);
- String id = object.getString(KINTO_KEY_ID);
-
- final boolean isDeleted = object.optBoolean(KINTO_KEY_DELETED, false);
-
- if (!isDeleted) {
- JSONObject attachment = object.getJSONObject(KINTO_KEY_ATTACHMENT);
- if (attachment.isNull(KINTO_KEY_ORIGINAL))
- throw new JSONException(String.format("Old Attachment Format"));
- }
-
- DownloadContent existingContent = catalog.getContentById(id);
-
- if (isDeleted) {
- cleanupRequired |= deleteContent(catalog, id);
- } else if (existingContent != null) {
- studyRequired |= updateContent(catalog, object, existingContent);
- } else {
- studyRequired |= createContent(catalog, object);
- }
- }
- } catch (UnrecoverableDownloadContentException e) {
- Log.e(LOGTAG, "UnrecoverableDownloadContentException", e);
- } catch (RecoverableDownloadContentException e) {
- Log.e(LOGTAG, "RecoverableDownloadContentException");
- } catch (JSONException e) {
- Log.e(LOGTAG, "JSONException", e);
- }
-
- if (studyRequired) {
- startStudyAction(context);
- }
-
- if (cleanupRequired) {
- startCleanupAction(context);
- }
-
- Log.v(LOGTAG, "Done");
- }
-
- protected void startStudyAction(Context context) {
- DownloadContentService.startStudy(context);
- }
-
- protected void startCleanupAction(Context context) {
- DownloadContentService.startCleanup(context);
- }
-
- protected JSONArray fetchRawCatalog(long lastModified)
- throws RecoverableDownloadContentException, UnrecoverableDownloadContentException {
- HttpURLConnection connection = null;
-
- try {
- Uri.Builder builder = Uri.parse(CATALOG_ENDPOINT).buildUpon();
-
- if (lastModified > 0) {
- builder.appendQueryParameter(KINTO_PARAMETER_SINCE, String.valueOf(lastModified));
- }
- // Only select the fields we are actually going to read.
- builder.appendQueryParameter(KINTO_PARAMETER_FIELDS,
- "attachment.location,attachment.original.filename,attachment.original.hash,attachment.hash,type,kind,attachment.original.size,match");
-
- // We want to process items in the order they have been modified. This is to ensure that
- // our last_modified values are correct if we processing is interrupted and not all items
- // have been processed.
- builder.appendQueryParameter(KINTO_PARAMETER_SORT, "last_modified");
-
- connection = buildHttpURLConnection(builder.build().toString());
-
- // TODO: Read 'Alert' header and EOL message if existing (Bug 1249248)
-
- // TODO: Read and use 'Backoff' header if available (Bug 1249251)
-
- // TODO: Add support for Next-Page header (Bug 1257495)
-
- final int responseCode = connection.getResponseCode();
-
- if (responseCode != HttpURLConnection.HTTP_OK) {
- if (responseCode >= 500) {
- // A Retry-After header will be added to error responses (>=500), telling the
- // client how many seconds it should wait before trying again.
-
- // TODO: Read and obey value in "Retry-After" header (Bug 1249249)
- throw new RecoverableDownloadContentException(RecoverableDownloadContentException.SERVER, "Server error (" + responseCode + ")");
- } else if (responseCode == 410) {
- // A 410 Gone error response can be returned if the client version is too old,
- // or the service had been replaced with a new and better service using a new
- // protocol version.
-
- // TODO: The server is gone. Stop synchronizing the catalog from this server (Bug 1249248).
- throw new UnrecoverableDownloadContentException("Server is gone (410)");
- } else if (responseCode >= 400) {
- // If the HTTP status is >=400 the response contains a JSON response.
- logErrorResponse(connection);
-
- // Unrecoverable: Client errors 4xx - Unlikely that this version of the client will ever succeed.
- throw new UnrecoverableDownloadContentException("(Unrecoverable) Catalog sync failed. Status code: " + responseCode);
- } else if (responseCode < 200) {
- // If the HTTP status is <200 the response contains a JSON response.
- logErrorResponse(connection);
-
- throw new RecoverableDownloadContentException(RecoverableDownloadContentException.SERVER, "Response code: " + responseCode);
- } else {
- // HttpsUrlConnection: -1 (No valid response code)
- // Successful 2xx: We don't know how to handle anything but 200.
- // Redirection 3xx: We should have followed redirects if possible. We should not see those errors here.
-
- throw new UnrecoverableDownloadContentException("(Unrecoverable) Download failed. Response code: " + responseCode);
- }
- }
-
- return fetchJSONResponse(connection).getJSONArray(KINTO_KEY_DATA);
- } catch (JSONException | IOException e) {
- throw new RecoverableDownloadContentException(RecoverableDownloadContentException.NETWORK, e);
- } finally {
- if (connection != null) {
- connection.disconnect();
- }
- }
- }
-
- private JSONObject fetchJSONResponse(HttpURLConnection connection) throws IOException, JSONException {
- InputStream inputStream = null;
-
- try {
- inputStream = new BufferedInputStream(connection.getInputStream());
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- IOUtils.copy(inputStream, outputStream);
- return new JSONObject(outputStream.toString("UTF-8"));
- } finally {
- IOUtils.safeStreamClose(inputStream);
- }
- }
-
- protected boolean updateContent(DownloadContentCatalog catalog, JSONObject object, DownloadContent existingContent)
- throws JSONException {
- DownloadContent content = existingContent.buildUpon()
- .updateFromKinto(object)
- .build();
-
- if (existingContent.getLastModified() >= content.getLastModified()) {
- Log.d(LOGTAG, "Item has not changed: " + content);
- return false;
- }
-
- catalog.update(content);
-
- return true;
- }
-
- protected boolean createContent(DownloadContentCatalog catalog, JSONObject object) throws JSONException {
- DownloadContent content = new DownloadContentBuilder()
- .updateFromKinto(object)
- .build();
-
- catalog.add(content);
-
- return true;
- }
-
- protected boolean deleteContent(DownloadContentCatalog catalog, String id) {
- DownloadContent content = catalog.getContentById(id);
- if (content == null) {
- return false;
- }
-
- catalog.markAsDeleted(content);
-
- return true;
- }
-
- protected boolean isSyncEnabledForClient(Context context) {
- // Sync action is behind a switchboard flag for staged rollout.
- return SwitchBoard.isInExperiment(context, Experiments.DOWNLOAD_CONTENT_CATALOG_SYNC);
- }
-
- private void logErrorResponse(HttpURLConnection connection) {
- try {
- JSONObject error = fetchJSONResponse(connection);
-
- Log.w(LOGTAG, "Server returned error response:");
- Log.w(LOGTAG, "- Code: " + error.getInt("code"));
- Log.w(LOGTAG, "- Errno: " + error.getInt("errno"));
- Log.w(LOGTAG, "- Error: " + error.optString("error", "-"));
- Log.w(LOGTAG, "- Message: " + error.optString("message", "-"));
- Log.w(LOGTAG, "- Info: " + error.optString("info", "-"));
- } catch (JSONException | IOException e) {
- Log.w(LOGTAG, "Could not fetch error response", e);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/VerifyAction.java b/mobile/android/base/java/org/mozilla/gecko/dlc/VerifyAction.java
deleted file mode 100644
index e96a62eae..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/dlc/VerifyAction.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.dlc;
-
-import android.content.Context;
-import android.util.Log;
-
-import org.mozilla.gecko.dlc.catalog.DownloadContent;
-import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
-
-import java.io.File;
-
-/**
- * Verify: Validate downloaded content. Does it still exist and does it have the correct checksum?
- */
-public class VerifyAction extends BaseAction {
- private static final String LOGTAG = "DLCVerifyAction";
-
- @Override
- public void perform(Context context, DownloadContentCatalog catalog) {
- Log.d(LOGTAG, "Verifying catalog..");
-
- for (DownloadContent content : catalog.getDownloadedContent()) {
- try {
- File destinationFile = getDestinationFile(context, content);
-
- if (!destinationFile.exists()) {
- Log.d(LOGTAG, "Downloaded content does not exist anymore: " + content);
-
- // This file does not exist even though it is marked as downloaded in the catalog. Scheduling a
- // download to fetch it again.
- catalog.scheduleDownload(content);
- continue;
- }
-
- if (!verify(destinationFile, content.getChecksum())) {
- catalog.scheduleDownload(content);
- Log.d(LOGTAG, "Wrong checksum. Scheduling download: " + content);
- continue;
- }
-
- Log.v(LOGTAG, "Content okay: " + content);
- } catch (UnrecoverableDownloadContentException e) {
- Log.w(LOGTAG, "Unrecoverable exception while verifying downloaded file", e);
- } catch (RecoverableDownloadContentException e) {
- // That's okay, we are just verifying already existing content. No log.
- }
- }
-
- if (catalog.hasScheduledDownloads()) {
- startDownloads(context);
- }
-
- Log.v(LOGTAG, "Done");
- }
-
- protected void startDownloads(Context context) {
- DownloadContentService.startDownloads(context);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContent.java b/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContent.java
deleted file mode 100644
index 61f7992ca..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContent.java
+++ /dev/null
@@ -1,189 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.dlc.catalog;
-
-import android.support.annotation.IntDef;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.StringDef;
-
-public class DownloadContent {
- @IntDef({STATE_NONE, STATE_SCHEDULED, STATE_DOWNLOADED, STATE_FAILED, STATE_UPDATED, STATE_DELETED})
- public @interface State {}
- public static final int STATE_NONE = 0;
- public static final int STATE_SCHEDULED = 1;
- public static final int STATE_DOWNLOADED = 2;
- public static final int STATE_FAILED = 3; // Permanently failed for this version of the content
- public static final int STATE_UPDATED = 4;
- public static final int STATE_DELETED = 5;
-
- @StringDef({TYPE_ASSET_ARCHIVE})
- public @interface Type {}
- public static final String TYPE_ASSET_ARCHIVE = "asset-archive";
-
- @StringDef({KIND_FONT, KIND_HYPHENATION_DICTIONARY})
- public @interface Kind {}
- public static final String KIND_FONT = "font";
- public static final String KIND_HYPHENATION_DICTIONARY = "hyphenation";
-
- private final String id;
- private final String location;
- private final String filename;
- private final String checksum;
- private final String downloadChecksum;
- private final long lastModified;
- private final String type;
- private final String kind;
- private final long size;
- private final String appVersionPattern;
- private final String androidApiPattern;
- private final String appIdPattern;
- private int state;
- private int failures;
- private int lastFailureType;
-
- /* package-private */ DownloadContent(@NonNull String id, @NonNull String location, @NonNull String filename,
- @NonNull String checksum, @NonNull String downloadChecksum, @NonNull long lastModified,
- @NonNull String type, @NonNull String kind, long size, int failures, int lastFailureType,
- @Nullable String appVersionPattern, @Nullable String androidApiPattern, @Nullable String appIdPattern) {
- this.id = id;
- this.location = location;
- this.filename = filename;
- this.checksum = checksum;
- this.downloadChecksum = downloadChecksum;
- this.lastModified = lastModified;
- this.type = type;
- this.kind = kind;
- this.size = size;
- this.state = STATE_NONE;
- this.failures = failures;
- this.lastFailureType = lastFailureType;
- this.appVersionPattern = appVersionPattern;
- this.androidApiPattern = androidApiPattern;
- this.appIdPattern = appIdPattern;
- }
-
- public String getId() {
- return id;
- }
-
- /* package-private */ void setState(@State int state) {
- this.state = state;
- }
-
- @State
- public int getState() {
- return state;
- }
-
- public boolean isStateIn(@State int... states) {
- for (int state : states) {
- if (this.state == state) {
- return true;
- }
- }
-
- return false;
- }
-
- @Kind
- public String getKind() {
- return kind;
- }
-
- @Type
- public String getType() {
- return type;
- }
-
- public String getLocation() {
- return location;
- }
-
- public long getLastModified() {
- return lastModified;
- }
-
- public String getFilename() {
- return filename;
- }
-
- public String getChecksum() {
- return checksum;
- }
-
- public String getDownloadChecksum() {
- return downloadChecksum;
- }
-
- public long getSize() {
- return size;
- }
-
- public boolean isFont() {
- return KIND_FONT.equals(kind);
- }
-
- public boolean isHyphenationDictionary() {
- return KIND_HYPHENATION_DICTIONARY.equals(kind);
- }
-
- /**
- *Checks whether the content to be downloaded is a known content.
- *Currently it checks whether the type is "Asset Archive" and is of kind
- *"Font" or "Hyphenation Dictionary".
- */
- public boolean isKnownContent() {
- return ((isFont() || isHyphenationDictionary()) && isAssetArchive());
- }
-
- public boolean isAssetArchive() {
- return TYPE_ASSET_ARCHIVE.equals(type);
- }
-
- /* package-private */ int getFailures() {
- return failures;
- }
-
- /* package-private */ int getLastFailureType() {
- return lastFailureType;
- }
-
- /* package-private */ void rememberFailure(int failureType) {
- if (lastFailureType != failureType) {
- lastFailureType = failureType;
- failures = 1;
- } else {
- failures++;
- }
- }
-
- /* package-private */ void resetFailures() {
- failures = 0;
- lastFailureType = 0;
- }
-
- public String getAppVersionPattern() {
- return appVersionPattern;
- }
-
- public String getAndroidApiPattern() {
- return androidApiPattern;
- }
-
- public String getAppIdPattern() {
- return appIdPattern;
- }
-
- public DownloadContentBuilder buildUpon() {
- return DownloadContentBuilder.buildUpon(this);
- }
-
-
- public String toString() {
- return String.format("[%s,%s] %s (%d bytes) %s", getType(), getKind(), getId(), getSize(), getChecksum());
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentBootstrap.java b/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentBootstrap.java
deleted file mode 100644
index 40c804573..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentBootstrap.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.dlc.catalog;
-
-import android.support.v4.util.ArrayMap;
-
-import org.mozilla.gecko.AppConstants;
-
-import java.util.Arrays;
-import java.util.List;
-
-/* package-private */ class DownloadContentBootstrap {
- public static ArrayMap<String, DownloadContent> createInitialDownloadContentList() {
- if (!AppConstants.MOZ_ANDROID_EXCLUDE_FONTS) {
- // We are packaging fonts. There's nothing we want to download;
- return new ArrayMap<>();
- }
-
- List<DownloadContent> initialList = Arrays.asList(
- new DownloadContentBuilder()
- .setId("c40929cf-7f4c-fa72-3dc9-12cadf56905d")
- .setLocation("fennec/catalog/f63e5f92-793c-4574-a2d7-fbc50242b8cb.gz")
- .setFilename("CharisSILCompact-B.ttf")
- .setChecksum("699d958b492eda0cc2823535f8567d0393090e3842f6df3c36dbe7239cb80b6d")
- .setDownloadChecksum("a9f9b34fed353169a88cc159b8f298cb285cce0b8b0f979c22a7d85de46f0532")
- .setSize(1676072)
- .setKind("font")
- .setType("asset-archive")
- .build(),
-
- new DownloadContentBuilder()
- .setId("6d265876-85ed-0917-fdc8-baf583ca2cba")
- .setLocation("fennec/catalog/19af6c88-09d9-4d6c-805e-cfebb8699a6c.gz")
- .setFilename("CharisSILCompact-BI.ttf")
- .setChecksum("82465e747b4f41471dbfd942842b2ee810749217d44b55dbc43623b89f9c7d9b")
- .setDownloadChecksum("2be26671039a5e2e4d0360a948b4fa42048171133076a3bb6173d93d4b9cd55b")
- .setSize(1667812)
- .setKind("font")
- .setType("asset-archive")
- .build(),
-
- new DownloadContentBuilder()
- .setId("8460dc6d-d129-fd1a-24b6-343dbf6531dd")
- .setLocation("fennec/catalog/f35a384a-90ea-41c6-a957-bb1845de97eb.gz")
- .setFilename("CharisSILCompact-I.ttf")
- .setChecksum("ab3ed6f2a4d4c2095b78227bd33155d7ccd05a879c107a291912640d4d64f767")
- .setDownloadChecksum("38a6469041c02624d43dfd41d2dd745e3e3211655e616188f65789a90952a1e9")
- .setSize(1693988)
- .setKind("font")
- .setType("asset-archive")
- .build(),
-
- new DownloadContentBuilder()
- .setId("c906275c-3747-fe27-426f-6187526a6f06")
- .setLocation("fennec/catalog/8c3bec92-d2df-4789-8c4a-0f523f026d96.gz")
- .setFilename("CharisSILCompact-R.ttf")
- .setChecksum("4ed509317f1bb441b185ea13bf1c9d19d1a0b396962efa3b5dc3190ad88f2067")
- .setDownloadChecksum("7c2ec1f550c2005b75383b878f737266b5f0b1c82679dd886c8bbe30c82e340e")
- .setSize(1727656)
- .setKind("font")
- .setType("asset-archive")
- .build(),
-
- new DownloadContentBuilder()
- .setId("ff5deecc-6ecc-d816-bb51-65face460119")
- .setLocation("fennec/catalog/ea115d71-e2ac-4609-853e-c978780776b1.gz")
- .setFilename("ClearSans-Bold.ttf")
- .setChecksum("385d0a293c1714770e198f7c762ab32f7905a0ed9d2993f69d640bd7232b4b70")
- .setDownloadChecksum("0d3c22bef90e7096f75b331bb7391de3aa43017e10d61041cd3085816db4919a")
- .setSize(140136)
- .setKind("font")
- .setType("asset-archive")
- .build(),
-
- new DownloadContentBuilder()
- .setId("a173d1db-373b-ce42-1335-6b3285cfdebd")
- .setLocation("fennec/catalog/0838e513-2d99-4e53-b58f-6b970f6548c6.gz")
- .setFilename("ClearSans-BoldItalic.ttf")
- .setChecksum("7bce66864e38eecd7c94b6657b9b38c35ebfacf8046bfb1247e08f07fe933198")
- .setDownloadChecksum("de0903164dde1ad3768d0bd6dec949871d6ab7be08f573d9d70f38c138a22e37")
- .setSize(156124)
- .setKind("font")
- .setType("asset-archive")
- .build(),
-
- new DownloadContentBuilder()
- .setId("e65c66df-0088-940d-ca5c-207c22118c0e")
- .setLocation("fennec/catalog/7550fa42-0947-478c-a5f0-5ea1bbb6ba27.gz")
- .setFilename("ClearSans-Italic.ttf")
- .setChecksum("87c13c5fbae832e4f85c3bd46fcbc175978234f39be5fe51c4937be4cbff3b68")
- .setDownloadChecksum("6e323db3115005dd0e96d2422db87a520f9ae426de28a342cd6cc87b55601d87")
- .setSize(155672)
- .setKind("font")
- .setType("asset-archive")
- .build(),
-
- new DownloadContentBuilder()
- .setId("25610abb-5dc8-fd75-40e7-990507f010c4")
- .setLocation("fennec/catalog/dd9bee7d-d784-476b-a3dd-69af8e516487.gz")
- .setFilename("ClearSans-Light.ttf")
- .setChecksum("e4885f6188e7a8587f5621c077c6c1f5e8d3739dffc8f4d055c2ba87568c750a")
- .setDownloadChecksum("19d4f7c67176e9e254c61420da9c7363d9fe5e6b4bb9d61afa4b3b574280714f")
- .setSize(145976)
- .setKind("font")
- .setType("asset-archive")
- .build(),
-
- new DownloadContentBuilder()
- .setId("ffe40339-a096-2262-c3f8-54af75c81fe6")
- .setLocation("fennec/catalog/bc5ada8c-8cfc-443d-93d7-dc5f98138a07.gz")
- .setFilename("ClearSans-Medium.ttf")
- .setChecksum("5d0e0115f3a3ed4be3eda6d7eabb899bb9a361292802e763d53c72e00f629da1")
- .setDownloadChecksum("edec86dab3ad2a97561cb41b584670262a48bed008c57bb587ee05ca47fb067f")
- .setSize(148892)
- .setKind("font")
- .setType("asset-archive")
- .build(),
-
- new DownloadContentBuilder()
- .setId("139a94be-ac69-0264-c9cc-8f2d071fd29d")
- .setLocation("fennec/catalog/0490c768-6178-49c2-af88-9f8769ff3167.gz")
- .setFilename("ClearSans-MediumItalic.ttf")
- .setChecksum("937dda88b26469306258527d38e42c95e27e7ebb9f05bd1d7c5d706a3c9541d7")
- .setDownloadChecksum("34edbd1b325dbffe7791fba8dd2d19852eb3c2fe00cff517ea2161ddc424ee22")
- .setSize(155228)
- .setKind("font")
- .setType("asset-archive")
- .build(),
-
- new DownloadContentBuilder()
- .setId("b887012a-01e1-7c94-fdcb-ca44d5b974a2")
- .setLocation("fennec/catalog/78205bf8-c668-41b1-b68f-afd54f98713b.gz")
- .setFilename("ClearSans-Regular.ttf")
- .setChecksum("9b91bbdb95ffa6663da24fdaa8ee06060cd0a4d2dceaf1ffbdda00e04915ee5b")
- .setDownloadChecksum("a72f1420b4da1ba9e6797adac34f08e72f94128a85e56542d5e6a8080af5f08a")
- .setSize(142572)
- .setKind("font")
- .setType("asset-archive")
- .build(),
-
- new DownloadContentBuilder()
- .setId("c8703652-d317-0356-0bf8-95441a5b2c9b")
- .setLocation("fennec/catalog/3570f44f-9440-4aa0-abd0-642eaf2a1aa0.gz")
- .setFilename("ClearSans-Thin.ttf")
- .setChecksum("07b0db85a3ad99afeb803f0f35631436a7b4c67ac66d0c7f77d26a47357c592a")
- .setDownloadChecksum("d9f23fd8687d6743f5c281c33539fb16f163304795039959b8caf159e6d62822")
- .setSize(147004)
- .setKind("font")
- .setType("asset-archive")
- .build());
-
- ArrayMap<String, DownloadContent> content = new ArrayMap<>();
- for (DownloadContent currentContent : initialList) {
- content.put(currentContent.getId(), currentContent);
- }
- return content;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentBuilder.java b/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentBuilder.java
deleted file mode 100644
index 243e2d4eb..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentBuilder.java
+++ /dev/null
@@ -1,238 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.dlc.catalog;
-
-import android.text.TextUtils;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-public class DownloadContentBuilder {
- private static final String LOCAL_KEY_ID = "id";
- private static final String LOCAL_KEY_LOCATION = "location";
- private static final String LOCAL_KEY_FILENAME = "filename";
- private static final String LOCAL_KEY_CHECKSUM = "checksum";
- private static final String LOCAL_KEY_DOWNLOAD_CHECKSUM = "download_checksum";
- private static final String LOCAL_KEY_LAST_MODIFIED = "last_modified";
- private static final String LOCAL_KEY_TYPE = "type";
- private static final String LOCAL_KEY_KIND = "kind";
- private static final String LOCAL_KEY_SIZE = "size";
- private static final String LOCAL_KEY_STATE = "state";
- private static final String LOCAL_KEY_FAILURES = "failures";
- private static final String LOCAL_KEY_LAST_FAILURE_TYPE = "last_failure_type";
- private static final String LOCAL_KEY_PATTERN_APP_ID = "pattern_app_id";
- private static final String LOCAL_KEY_PATTERN_ANDROID_API = "pattern_android_api";
- private static final String LOCAL_KEY_PATTERN_APP_VERSION = "pattern_app_version";
-
- private static final String KINTO_KEY_ID = "id";
- private static final String KINTO_KEY_ATTACHMENT = "attachment";
- private static final String KINTO_KEY_ORIGINAL = "original";
- private static final String KINTO_KEY_LOCATION = "location";
- private static final String KINTO_KEY_FILENAME = "filename";
- private static final String KINTO_KEY_HASH = "hash";
- private static final String KINTO_KEY_LAST_MODIFIED = "last_modified";
- private static final String KINTO_KEY_TYPE = "type";
- private static final String KINTO_KEY_KIND = "kind";
- private static final String KINTO_KEY_SIZE = "size";
- private static final String KINTO_KEY_MATCH = "match";
- private static final String KINTO_KEY_APP_ID = "appId";
- private static final String KINTO_KEY_ANDROID_API = "androidApi";
- private static final String KINTO_KEY_APP_VERSION = "appVersion";
-
- private String id;
- private String location;
- private String filename;
- private String checksum;
- private String downloadChecksum;
- private long lastModified;
- private String type;
- private String kind;
- private long size;
- private int state;
- private int failures;
- private int lastFailureType;
- private String appVersionPattern;
- private String androidApiPattern;
- private String appIdPattern;
-
- public static DownloadContentBuilder buildUpon(DownloadContent content) {
- DownloadContentBuilder builder = new DownloadContentBuilder();
-
- builder.id = content.getId();
- builder.location = content.getLocation();
- builder.filename = content.getFilename();
- builder.checksum = content.getChecksum();
- builder.downloadChecksum = content.getDownloadChecksum();
- builder.lastModified = content.getLastModified();
- builder.type = content.getType();
- builder.kind = content.getKind();
- builder.size = content.getSize();
- builder.state = content.getState();
- builder.failures = content.getFailures();
- builder.lastFailureType = content.getLastFailureType();
-
- return builder;
- }
-
- public static DownloadContent fromJSON(JSONObject object) throws JSONException {
- return new DownloadContentBuilder()
- .setId(object.getString(LOCAL_KEY_ID))
- .setLocation(object.getString(LOCAL_KEY_LOCATION))
- .setFilename(object.getString(LOCAL_KEY_FILENAME))
- .setChecksum(object.getString(LOCAL_KEY_CHECKSUM))
- .setDownloadChecksum(object.getString(LOCAL_KEY_DOWNLOAD_CHECKSUM))
- .setLastModified(object.getLong(LOCAL_KEY_LAST_MODIFIED))
- .setType(object.getString(LOCAL_KEY_TYPE))
- .setKind(object.getString(LOCAL_KEY_KIND))
- .setSize(object.getLong(LOCAL_KEY_SIZE))
- .setState(object.getInt(LOCAL_KEY_STATE))
- .setFailures(object.optInt(LOCAL_KEY_FAILURES), object.optInt(LOCAL_KEY_LAST_FAILURE_TYPE))
- .setAppVersionPattern(object.optString(LOCAL_KEY_PATTERN_APP_VERSION))
- .setAppIdPattern(object.optString(LOCAL_KEY_PATTERN_APP_ID))
- .setAndroidApiPattern(object.optString(LOCAL_KEY_PATTERN_ANDROID_API))
- .build();
- }
-
- public static JSONObject toJSON(DownloadContent content) throws JSONException {
- final JSONObject object = new JSONObject();
- object.put(LOCAL_KEY_ID, content.getId());
- object.put(LOCAL_KEY_LOCATION, content.getLocation());
- object.put(LOCAL_KEY_FILENAME, content.getFilename());
- object.put(LOCAL_KEY_CHECKSUM, content.getChecksum());
- object.put(LOCAL_KEY_DOWNLOAD_CHECKSUM, content.getDownloadChecksum());
- object.put(LOCAL_KEY_LAST_MODIFIED, content.getLastModified());
- object.put(LOCAL_KEY_TYPE, content.getType());
- object.put(LOCAL_KEY_KIND, content.getKind());
- object.put(LOCAL_KEY_SIZE, content.getSize());
- object.put(LOCAL_KEY_STATE, content.getState());
- object.put(LOCAL_KEY_PATTERN_APP_VERSION, content.getAppVersionPattern());
- object.put(LOCAL_KEY_PATTERN_APP_ID, content.getAppIdPattern());
- object.put(LOCAL_KEY_PATTERN_ANDROID_API, content.getAndroidApiPattern());
-
- final int failures = content.getFailures();
- if (failures > 0) {
- object.put(LOCAL_KEY_FAILURES, failures);
- object.put(LOCAL_KEY_LAST_FAILURE_TYPE, content.getLastFailureType());
- }
-
- return object;
- }
-
- public DownloadContent build() {
- DownloadContent content = new DownloadContent(id, location, filename, checksum,
- downloadChecksum, lastModified, type, kind, size, failures, lastFailureType,
- appVersionPattern, androidApiPattern, appIdPattern);
- content.setState(state);
-
- return content;
- }
-
- public DownloadContentBuilder setId(String id) {
- this.id = id;
- return this;
- }
-
- public DownloadContentBuilder setLocation(String location) {
- this.location = location;
- return this;
- }
-
- public DownloadContentBuilder setFilename(String filename) {
- this.filename = filename;
- return this;
- }
-
- public DownloadContentBuilder setChecksum(String checksum) {
- this.checksum = checksum;
- return this;
- }
-
- public DownloadContentBuilder setDownloadChecksum(String downloadChecksum) {
- this.downloadChecksum = downloadChecksum;
- return this;
- }
-
- public DownloadContentBuilder setLastModified(long lastModified) {
- this.lastModified = lastModified;
- return this;
- }
-
- public DownloadContentBuilder setType(String type) {
- this.type = type;
- return this;
- }
-
- public DownloadContentBuilder setKind(String kind) {
- this.kind = kind;
- return this;
- }
-
- public DownloadContentBuilder setSize(long size) {
- this.size = size;
- return this;
- }
-
- public DownloadContentBuilder setState(int state) {
- this.state = state;
- return this;
- }
-
- /* package-private */ DownloadContentBuilder setFailures(int failures, int lastFailureType) {
- this.failures = failures;
- this.lastFailureType = lastFailureType;
-
- return this;
- }
-
- public DownloadContentBuilder setAppVersionPattern(String appVersionPattern) {
- this.appVersionPattern = appVersionPattern;
- return this;
- }
-
- public DownloadContentBuilder setAndroidApiPattern(String androidApiPattern) {
- this.androidApiPattern = androidApiPattern;
- return this;
- }
-
- public DownloadContentBuilder setAppIdPattern(String appIdPattern) {
- this.appIdPattern = appIdPattern;
- return this;
- }
-
- public DownloadContentBuilder updateFromKinto(JSONObject object) throws JSONException {
- final String objectId = object.getString(KINTO_KEY_ID);
-
- if (TextUtils.isEmpty(id)) {
- // New object without an id yet
- id = objectId;
- } else if (!id.equals(objectId)) {
- throw new JSONException(String.format("Record ids do not match: Expected=%s, Actual=%s", id, objectId));
- }
-
- setType(object.getString(KINTO_KEY_TYPE));
- setKind(object.getString(KINTO_KEY_KIND));
- setLastModified(object.getLong(KINTO_KEY_LAST_MODIFIED));
-
- JSONObject attachment = object.getJSONObject(KINTO_KEY_ATTACHMENT);
- JSONObject original = attachment.getJSONObject(KINTO_KEY_ORIGINAL);
-
- setFilename(original.getString(KINTO_KEY_FILENAME));
- setChecksum(original.getString(KINTO_KEY_HASH));
- setSize(original.getLong(KINTO_KEY_SIZE));
-
- setLocation(attachment.getString(KINTO_KEY_LOCATION));
- setDownloadChecksum(attachment.getString(KINTO_KEY_HASH));
-
- JSONObject match = object.optJSONObject(KINTO_KEY_MATCH);
- if (match != null) {
- setAndroidApiPattern(match.optString(KINTO_KEY_ANDROID_API));
- setAppIdPattern(match.optString(KINTO_KEY_APP_ID));
- setAppVersionPattern(match.optString(KINTO_KEY_APP_VERSION));
- }
-
- return this;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentCatalog.java b/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentCatalog.java
deleted file mode 100644
index 43ba4e82e..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentCatalog.java
+++ /dev/null
@@ -1,303 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.dlc.catalog;
-
-import android.content.Context;
-import android.support.annotation.Nullable;
-import android.support.v4.util.ArrayMap;
-import android.support.v4.util.AtomicFile;
-import android.util.Log;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Catalog of downloadable content (DLC).
- *
- * Changing elements returned by the catalog should be guarded by the catalog instance to guarantee visibility when
- * persisting changes.
- */
-public class DownloadContentCatalog {
- private static final String LOGTAG = "GeckoDLCCatalog";
- private static final String FILE_NAME = "download_content_catalog";
-
- private static final String JSON_KEY_CONTENT = "content";
-
- private static final int MAX_FAILURES_UNTIL_PERMANENTLY_FAILED = 10;
-
- private final AtomicFile file; // Guarded by 'file'
-
- private ArrayMap<String, DownloadContent> content; // Guarded by 'this'
- private boolean hasLoadedCatalog; // Guarded by 'this
- private boolean hasCatalogChanged; // Guarded by 'this'
-
- public DownloadContentCatalog(Context context) {
- this(new AtomicFile(new File(context.getApplicationInfo().dataDir, FILE_NAME)));
-
- startLoadFromDisk();
- }
-
- // For injecting mocked AtomicFile objects during test
- protected DownloadContentCatalog(AtomicFile file) {
- this.content = new ArrayMap<>();
- this.file = file;
- }
-
- public List<DownloadContent> getContentToStudy() {
- return filterByState(DownloadContent.STATE_NONE, DownloadContent.STATE_UPDATED);
- }
-
- public List<DownloadContent> getContentToDelete() {
- return filterByState(DownloadContent.STATE_DELETED);
- }
-
- public List<DownloadContent> getDownloadedContent() {
- return filterByState(DownloadContent.STATE_DOWNLOADED);
- }
-
- public List<DownloadContent> getScheduledDownloads() {
- return filterByState(DownloadContent.STATE_SCHEDULED);
- }
-
- private synchronized List<DownloadContent> filterByState(@DownloadContent.State int... filterStates) {
- awaitLoadingCatalogLocked();
-
- List<DownloadContent> filteredContent = new ArrayList<>();
-
- for (DownloadContent currentContent : content.values()) {
- if (currentContent.isStateIn(filterStates)) {
- filteredContent.add(currentContent);
- }
- }
-
- return filteredContent;
- }
-
- public boolean hasScheduledDownloads() {
- return !filterByState(DownloadContent.STATE_SCHEDULED).isEmpty();
- }
-
- public synchronized void add(DownloadContent newContent) {
- awaitLoadingCatalogLocked();
-
- content.put(newContent.getId(), newContent);
- hasCatalogChanged = true;
- }
-
- public synchronized void update(DownloadContent changedContent) {
- awaitLoadingCatalogLocked();
-
- if (!content.containsKey(changedContent.getId())) {
- Log.w(LOGTAG, "Did not find content with matching id (" + changedContent.getId() + ") to update");
- return;
- }
-
- changedContent.setState(DownloadContent.STATE_UPDATED);
- changedContent.resetFailures();
-
- content.put(changedContent.getId(), changedContent);
- hasCatalogChanged = true;
- }
-
- public synchronized void remove(DownloadContent removedContent) {
- awaitLoadingCatalogLocked();
-
- if (!content.containsKey(removedContent.getId())) {
- Log.w(LOGTAG, "Did not find content with matching id (" + removedContent.getId() + ") to remove");
- return;
- }
-
- content.remove(removedContent.getId());
- }
-
- @Nullable
- public synchronized DownloadContent getContentById(String id) {
- return content.get(id);
- }
-
- public synchronized long getLastModified() {
- awaitLoadingCatalogLocked();
-
- long lastModified = 0;
-
- for (DownloadContent currentContent : content.values()) {
- if (currentContent.getLastModified() > lastModified) {
- lastModified = currentContent.getLastModified();
- }
- }
-
- return lastModified;
- }
-
- public synchronized void scheduleDownload(DownloadContent content) {
- content.setState(DownloadContent.STATE_SCHEDULED);
- hasCatalogChanged = true;
- }
-
- public synchronized void markAsDownloaded(DownloadContent content) {
- content.setState(DownloadContent.STATE_DOWNLOADED);
- content.resetFailures();
- hasCatalogChanged = true;
- }
-
- public synchronized void markAsPermanentlyFailed(DownloadContent content) {
- content.setState(DownloadContent.STATE_FAILED);
- hasCatalogChanged = true;
- }
-
- public synchronized void markAsDeleted(DownloadContent content) {
- content.setState(DownloadContent.STATE_DELETED);
- hasCatalogChanged = true;
- }
-
- public synchronized void rememberFailure(DownloadContent content, int failureType) {
- if (content.getFailures() >= MAX_FAILURES_UNTIL_PERMANENTLY_FAILED) {
- Log.d(LOGTAG, "Maximum number of failures reached. Marking content has permanently failed.");
-
- markAsPermanentlyFailed(content);
- } else {
- content.rememberFailure(failureType);
- hasCatalogChanged = true;
- }
- }
-
- public void persistChanges() {
- new Thread(LOGTAG + "-Persist") {
- public void run() {
- writeToDisk();
- }
- }.start();
- }
-
- private void startLoadFromDisk() {
- new Thread(LOGTAG + "-Load") {
- public void run() {
- loadFromDisk();
- }
- }.start();
- }
-
- private void awaitLoadingCatalogLocked() {
- while (!hasLoadedCatalog) {
- try {
- Log.v(LOGTAG, "Waiting for catalog to be loaded");
-
- wait();
- } catch (InterruptedException e) {
- // Ignore
- }
- }
- }
-
- protected synchronized boolean hasCatalogChanged() {
- return hasCatalogChanged;
- }
-
- protected synchronized void loadFromDisk() {
- Log.d(LOGTAG, "Loading from disk");
-
- if (hasLoadedCatalog) {
- return;
- }
-
- ArrayMap<String, DownloadContent> loadedContent = new ArrayMap<>();
-
- try {
- JSONObject catalog;
-
- synchronized (file) {
- catalog = new JSONObject(new String(file.readFully(), "UTF-8"));
- }
-
- JSONArray array = catalog.getJSONArray(JSON_KEY_CONTENT);
- for (int i = 0; i < array.length(); i++) {
- DownloadContent currentContent = DownloadContentBuilder.fromJSON(array.getJSONObject(i));
- loadedContent.put(currentContent.getId(), currentContent);
- }
- } catch (FileNotFoundException e) {
- Log.d(LOGTAG, "Catalog file does not exist: Bootstrapping initial catalog");
- loadedContent = DownloadContentBootstrap.createInitialDownloadContentList();
- } catch (JSONException e) {
- Log.w(LOGTAG, "Unable to parse catalog JSON. Re-creating catalog.", e);
- // Catalog seems to be broken. Re-create catalog:
- loadedContent = DownloadContentBootstrap.createInitialDownloadContentList();
- hasCatalogChanged = true; // Indicate that we want to persist the new catalog
- } catch (NullPointerException e) {
- // Bad content can produce an NPE in JSON code -- bug 1300139
- Log.w(LOGTAG, "Unable to parse catalog JSON. Re-creating catalog.", e);
- // Catalog seems to be broken. Re-create catalog:
- loadedContent = DownloadContentBootstrap.createInitialDownloadContentList();
- hasCatalogChanged = true; // Indicate that we want to persist the new catalog
- } catch (UnsupportedEncodingException e) {
- AssertionError error = new AssertionError("Should not happen: This device does not speak UTF-8");
- error.initCause(e);
- throw error;
- } catch (IOException e) {
- Log.d(LOGTAG, "Can't read catalog due to IOException", e);
- }
-
- onCatalogLoaded(loadedContent);
-
- notifyAll();
-
- Log.d(LOGTAG, "Loaded " + content.size() + " elements");
- }
-
- protected void onCatalogLoaded(ArrayMap<String, DownloadContent> content) {
- this.content = content;
- this.hasLoadedCatalog = true;
- }
-
- protected synchronized void writeToDisk() {
- if (!hasCatalogChanged) {
- Log.v(LOGTAG, "Not persisting: Catalog has not changed");
- return;
- }
-
- Log.d(LOGTAG, "Writing to disk");
-
- FileOutputStream outputStream = null;
-
- synchronized (file) {
- try {
- outputStream = file.startWrite();
-
- JSONArray array = new JSONArray();
- for (DownloadContent currentContent : content.values()) {
- array.put(DownloadContentBuilder.toJSON(currentContent));
- }
-
- JSONObject catalog = new JSONObject();
- catalog.put(JSON_KEY_CONTENT, array);
-
- outputStream.write(catalog.toString().getBytes("UTF-8"));
-
- file.finishWrite(outputStream);
-
- hasCatalogChanged = false;
- } catch (UnsupportedEncodingException e) {
- AssertionError error = new AssertionError("Should not happen: This device does not speak UTF-8");
- error.initCause(e);
- throw error;
- } catch (IOException | JSONException e) {
- Log.e(LOGTAG, "IOException during writing catalog", e);
-
- if (outputStream != null) {
- file.failWrite(outputStream);
- }
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/ContentNotificationsDelegate.java b/mobile/android/base/java/org/mozilla/gecko/feeds/ContentNotificationsDelegate.java
deleted file mode 100644
index d317a21ee..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/ContentNotificationsDelegate.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.feeds;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.v4.app.NotificationManagerCompat;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.BrowserApp;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.delegates.BrowserAppDelegate;
-import org.mozilla.gecko.mozglue.SafeIntent;
-
-import java.util.List;
-
-/**
- * BrowserAppDelegate implementation that takes care of handling intents from content notifications.
- */
-public class ContentNotificationsDelegate extends BrowserAppDelegate {
- // The application is opened from a content notification
- public static final String ACTION_CONTENT_NOTIFICATION = AppConstants.ANDROID_PACKAGE_NAME + ".action.CONTENT_NOTIFICATION";
-
- public static final String EXTRA_READ_BUTTON = "read_button";
- public static final String EXTRA_URLS = "urls";
-
- private static final String TELEMETRY_EXTRA_CONTENT_UPDATE = "content_update";
- private static final String TELEMETRY_EXTRA_READ_NOW_BUTTON = TELEMETRY_EXTRA_CONTENT_UPDATE + "_read_now";
-
- @Override
- public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {
- if (savedInstanceState != null) {
- // This activity is getting restored: We do not want to handle the URLs in the Intent again. The browser
- // will take care of restoring the tabs we already created.
- return;
- }
-
-
- final Intent unsafeIntent = browserApp.getIntent();
-
- // Nothing to do.
- if (unsafeIntent == null) {
- return;
- }
-
- final SafeIntent intent = new SafeIntent(unsafeIntent);
-
- if (ACTION_CONTENT_NOTIFICATION.equals(intent.getAction())) {
- openURLsFromIntent(browserApp, intent);
- }
- }
-
- @Override
- public void onNewIntent(BrowserApp browserApp, @NonNull final SafeIntent intent) {
- if (ACTION_CONTENT_NOTIFICATION.equals(intent.getAction())) {
- openURLsFromIntent(browserApp, intent);
- }
- }
-
- private void openURLsFromIntent(BrowserApp browserApp, @NonNull final SafeIntent intent) {
- final List<String> urls = intent.getStringArrayListExtra(EXTRA_URLS);
- if (urls != null) {
- browserApp.openUrls(urls);
- }
-
- Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(browserApp));
-
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, TELEMETRY_EXTRA_CONTENT_UPDATE);
-
- if (intent.getBooleanExtra(EXTRA_READ_BUTTON, false)) {
- // "READ NOW" button in notification was clicked
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, TELEMETRY_EXTRA_READ_NOW_BUTTON);
-
- // Android's "auto cancel" won't remove the notification when an action button is pressed. So we do it ourselves here.
- NotificationManagerCompat.from(browserApp).cancel(R.id.websiteContentNotification);
- } else {
- // Notification was clicked
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, TELEMETRY_EXTRA_CONTENT_UPDATE);
- }
-
- Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(browserApp));
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/FeedAlarmReceiver.java b/mobile/android/base/java/org/mozilla/gecko/feeds/FeedAlarmReceiver.java
deleted file mode 100644
index d943b4f81..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/FeedAlarmReceiver.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.feeds;
-
-import android.content.Context;
-import android.content.Intent;
-import android.support.v4.content.WakefulBroadcastReceiver;
-import android.util.Log;
-
-/**
- * Broadcast receiver that will receive broadcasts from the AlarmManager and start the FeedService
- * with the given action.
- */
-public class FeedAlarmReceiver extends WakefulBroadcastReceiver {
- private static final String LOGTAG = "FeedCheckAction";
-
- @Override
- public void onReceive(Context context, Intent intent) {
- final String action = intent.getAction();
-
- Log.d(LOGTAG, "Received alarm with action: " + action);
-
- final Intent serviceIntent = new Intent(context, FeedService.class);
- serviceIntent.setAction(action);
-
- startWakefulService(context, serviceIntent);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/FeedFetcher.java b/mobile/android/base/java/org/mozilla/gecko/feeds/FeedFetcher.java
deleted file mode 100644
index 76c1b7e30..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/FeedFetcher.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.feeds;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-import org.mozilla.gecko.feeds.parser.Feed;
-import org.mozilla.gecko.feeds.parser.SimpleFeedParser;
-import org.mozilla.gecko.util.IOUtils;
-
-import java.io.BufferedInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-
-import ch.boye.httpclientandroidlib.util.TextUtils;
-
-/**
- * Helper class for fetching and parsing a feed.
- */
-public class FeedFetcher {
- private static final int CONNECT_TIMEOUT = 15000;
- private static final int READ_TIMEOUT = 15000;
-
- public static class FeedResponse {
- public final Feed feed;
- public final String etag;
- public final String lastModified;
-
- public FeedResponse(Feed feed, String etag, String lastModified) {
- this.feed = feed;
- this.etag = etag;
- this.lastModified = lastModified;
- }
- }
-
- /**
- * Fetch and parse a feed from the given URL. Will return null if fetching or parsing failed.
- */
- public static FeedResponse fetchAndParseFeed(String url) {
- return fetchAndParseFeedIfModified(url, null, null);
- }
-
- /**
- * Fetch and parse a feed from the given URL using the given ETag and "Last modified" value.
- *
- * Will return null if fetching or parsing failed. Will also return null if the feed has not
- * changed (ETag / Last-Modified-Since).
- *
- * @param eTag The ETag from the last fetch or null if no ETag is available (will always fetch feed)
- * @param lastModified The "Last modified" header from the last time the feed has been fetch or
- * null if no value is available (will always fetch feed)
- * @return A FeedResponse or null if no feed could be fetched (error or no new version available)
- */
- @Nullable
- public static FeedResponse fetchAndParseFeedIfModified(@NonNull String url, @Nullable String eTag, @Nullable String lastModified) {
- HttpURLConnection connection = null;
- InputStream stream = null;
-
- try {
- connection = (HttpURLConnection) new URL(url).openConnection();
- connection.setInstanceFollowRedirects(true);
- connection.setConnectTimeout(CONNECT_TIMEOUT);
- connection.setReadTimeout(READ_TIMEOUT);
-
- if (!TextUtils.isEmpty(eTag)) {
- connection.setRequestProperty("If-None-Match", eTag);
- }
-
- if (!TextUtils.isEmpty(lastModified)) {
- connection.setRequestProperty("If-Modified-Since", lastModified);
- }
-
- final int statusCode = connection.getResponseCode();
-
- if (statusCode != HttpURLConnection.HTTP_OK) {
- return null;
- }
-
- String responseEtag = connection.getHeaderField("ETag");
- if (!TextUtils.isEmpty(responseEtag) && responseEtag.startsWith("W/")) {
- // Weak ETag, get actual ETag value
- responseEtag = responseEtag.substring(2);
- }
-
- final String updatedLastModified = connection.getHeaderField("Last-Modified");
-
- stream = new BufferedInputStream(connection.getInputStream());
-
- final SimpleFeedParser parser = new SimpleFeedParser();
- final Feed feed = parser.parse(stream);
-
- return new FeedResponse(feed, responseEtag, updatedLastModified);
- } catch (IOException e) {
- return null;
- } catch (SimpleFeedParser.ParserException e) {
- return null;
- } finally {
- if (connection != null) {
- connection.disconnect();
- }
- IOUtils.safeStreamClose(stream);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/FeedService.java b/mobile/android/base/java/org/mozilla/gecko/feeds/FeedService.java
deleted file mode 100644
index 374486215..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/FeedService.java
+++ /dev/null
@@ -1,168 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.feeds;
-
-import android.app.IntentService;
-import android.content.Context;
-import android.content.Intent;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.support.annotation.Nullable;
-import android.support.v4.net.ConnectivityManagerCompat;
-import android.util.Log;
-
-import com.keepsafe.switchboard.SwitchBoard;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.feeds.action.FeedAction;
-import org.mozilla.gecko.feeds.action.CheckForUpdatesAction;
-import org.mozilla.gecko.feeds.action.EnrollSubscriptionsAction;
-import org.mozilla.gecko.feeds.action.SetupAlarmsAction;
-import org.mozilla.gecko.feeds.action.SubscribeToFeedAction;
-import org.mozilla.gecko.feeds.action.WithdrawSubscriptionsAction;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-import org.mozilla.gecko.Experiments;
-
-/**
- * Background service for subscribing to and checking website feeds to notify the user about updates.
- */
-public class FeedService extends IntentService {
- private static final String LOGTAG = "GeckoFeedService";
-
- public static final String ACTION_SETUP = AppConstants.ANDROID_PACKAGE_NAME + ".FEEDS.SETUP";
- public static final String ACTION_SUBSCRIBE = AppConstants.ANDROID_PACKAGE_NAME + ".FEEDS.SUBSCRIBE";
- public static final String ACTION_CHECK = AppConstants.ANDROID_PACKAGE_NAME + ".FEEDS.CHECK";
- public static final String ACTION_ENROLL = AppConstants.ANDROID_PACKAGE_NAME + ".FEEDS.ENROLL";
- public static final String ACTION_WITHDRAW = AppConstants.ANDROID_PACKAGE_NAME + ".FEEDS.WITHDRAW";
-
- public static void setup(Context context) {
- Intent intent = new Intent(context, FeedService.class);
- intent.setAction(ACTION_SETUP);
- context.startService(intent);
- }
-
- public static void subscribe(Context context, String feedUrl) {
- Intent intent = new Intent(context, FeedService.class);
- intent.setAction(ACTION_SUBSCRIBE);
- intent.putExtra(SubscribeToFeedAction.EXTRA_FEED_URL, feedUrl);
- context.startService(intent);
- }
-
- public FeedService() {
- super(LOGTAG);
- }
-
- private BrowserDB browserDB;
-
- @Override
- public void onCreate() {
- super.onCreate();
-
- browserDB = BrowserDB.from(this);
- }
-
- @Override
- protected void onHandleIntent(Intent intent) {
- try {
- if (intent == null) {
- return;
- }
-
- Log.d(LOGTAG, "Service started with action: " + intent.getAction());
-
- if (!isInExperiment(this)) {
- Log.d(LOGTAG, "Not in content notifications experiment. Skipping.");
- return;
- }
-
- FeedAction action = createActionForIntent(intent);
- if (action == null) {
- Log.d(LOGTAG, "No action to process");
- return;
- }
-
- if (action.requiresPreferenceEnabled() && !isPreferenceEnabled()) {
- Log.d(LOGTAG, "Preference is disabled. Skipping.");
- return;
- }
-
- if (action.requiresNetwork() && !isConnectedToUnmeteredNetwork()) {
- // For now just skip if we are not connected or the network is metered. We do not want
- // to use precious mobile traffic.
- Log.d(LOGTAG, "Not connected to a network or network is metered. Skipping.");
- return;
- }
-
- action.perform(browserDB, intent);
- } finally {
- FeedAlarmReceiver.completeWakefulIntent(intent);
- }
-
- Log.d(LOGTAG, "Done.");
- }
-
- @Nullable
- private FeedAction createActionForIntent(Intent intent) {
- final Context context = getApplicationContext();
-
- switch (intent.getAction()) {
- case ACTION_SETUP:
- return new SetupAlarmsAction(context);
-
- case ACTION_SUBSCRIBE:
- return new SubscribeToFeedAction(context);
-
- case ACTION_CHECK:
- return new CheckForUpdatesAction(context);
-
- case ACTION_ENROLL:
- return new EnrollSubscriptionsAction(context);
-
- case ACTION_WITHDRAW:
- return new WithdrawSubscriptionsAction(context);
-
- default:
- throw new AssertionError("Unknown action: " + intent.getAction());
- }
- }
-
- private boolean isConnectedToUnmeteredNetwork() {
- ConnectivityManager manager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
- NetworkInfo networkInfo = manager.getActiveNetworkInfo();
- if (networkInfo == null || !networkInfo.isConnected()) {
- return false;
- }
-
- return !ConnectivityManagerCompat.isActiveNetworkMetered(manager);
- }
-
- public static boolean isInExperiment(Context context) {
- return SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_12HRS) ||
- SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_5PM) ||
- SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_8AM);
- }
-
- public static String getEnabledExperiment(Context context) {
- String experiment = null;
-
- if (SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_12HRS)) {
- experiment = Experiments.CONTENT_NOTIFICATIONS_12HRS;
- } else if (SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_8AM)) {
- experiment = Experiments.CONTENT_NOTIFICATIONS_8AM;
- } else if (SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_5PM)) {
- experiment = Experiments.CONTENT_NOTIFICATIONS_5PM;
- }
-
- return experiment;
- }
-
- private boolean isPreferenceEnabled() {
- return GeckoSharedPrefs.forApp(this).getBoolean(GeckoPreferences.PREFS_NOTIFICATIONS_CONTENT, true);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/action/CheckForUpdatesAction.java b/mobile/android/base/java/org/mozilla/gecko/feeds/action/CheckForUpdatesAction.java
deleted file mode 100644
index 09a2b12b6..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/CheckForUpdatesAction.java
+++ /dev/null
@@ -1,281 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.feeds.action;
-
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.database.Cursor;
-import android.support.v4.app.NotificationCompat;
-import android.support.v4.app.NotificationManagerCompat;
-import android.support.v4.content.ContextCompat;
-import android.text.format.DateFormat;
-
-import org.json.JSONException;
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.BrowserApp;
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.db.UrlAnnotations;
-import org.mozilla.gecko.feeds.ContentNotificationsDelegate;
-import org.mozilla.gecko.feeds.FeedFetcher;
-import org.mozilla.gecko.feeds.FeedService;
-import org.mozilla.gecko.feeds.parser.Feed;
-import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-import org.mozilla.gecko.util.StringUtils;
-
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-
-/**
- * CheckForUpdatesAction: Check if feeds we subscribed to have new content available.
- */
-public class CheckForUpdatesAction extends FeedAction {
- /**
- * This extra will be added to Intents fired by the notification.
- */
- public static final String EXTRA_CONTENT_NOTIFICATION = "content-notification";
-
- private final Context context;
-
- public CheckForUpdatesAction(Context context) {
- this.context = context;
- }
-
- @Override
- public void perform(BrowserDB browserDB, Intent intent) {
- final UrlAnnotations urlAnnotations = browserDB.getUrlAnnotations();
- final ContentResolver resolver = context.getContentResolver();
- final List<Feed> updatedFeeds = new ArrayList<>();
-
- log("Checking feeds for updates..");
-
- Cursor cursor = urlAnnotations.getFeedSubscriptions(resolver);
- if (cursor == null) {
- return;
- }
-
- try {
- while (cursor.moveToNext()) {
- FeedSubscription subscription = FeedSubscription.fromCursor(cursor);
-
- FeedFetcher.FeedResponse response = checkFeedForUpdates(subscription);
- if (response != null) {
- final Feed feed = response.feed;
-
- if (!hasBeenVisited(browserDB, feed.getLastItem().getURL())) {
- // Only notify about this update if the last item hasn't been visited yet.
- updatedFeeds.add(feed);
- } else {
- Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
- Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL,
- TelemetryContract.Method.SERVICE,
- "content_update");
- Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
- }
-
- urlAnnotations.updateFeedSubscription(resolver, subscription);
- }
- }
- } catch (JSONException e) {
- log("Could not deserialize subscription", e);
- } finally {
- cursor.close();
- }
-
- showNotification(updatedFeeds);
- }
-
- private FeedFetcher.FeedResponse checkFeedForUpdates(FeedSubscription subscription) {
- log("Checking feed: " + subscription.getFeedTitle());
-
- FeedFetcher.FeedResponse response = fetchFeed(subscription);
- if (response == null) {
- return null;
- }
-
- if (subscription.hasBeenUpdated(response)) {
- log("* Feed has changed. New item: " + response.feed.getLastItem().getTitle());
-
- subscription.update(response);
-
- return response;
-
- }
-
- return null;
- }
-
- /**
- * Returns true if this URL has been visited before.
- *
- * We do an exact match. So this can fail if the feed uses a different URL and redirects to
- * content. But it's better than no checks at all.
- */
- private boolean hasBeenVisited(final BrowserDB browserDB, final String url) {
- final Cursor cursor = browserDB.getHistoryForURL(context.getContentResolver(), url);
- if (cursor == null) {
- return false;
- }
-
- try {
- if (cursor.moveToFirst()) {
- return cursor.getInt(cursor.getColumnIndex(BrowserContract.History.VISITS)) > 0;
- }
- } finally {
- cursor.close();
- }
-
- return false;
- }
-
- private void showNotification(List<Feed> updatedFeeds) {
- final int feedCount = updatedFeeds.size();
- if (feedCount == 0) {
- return;
- }
-
- if (feedCount == 1) {
- showNotificationForSingleUpdate(updatedFeeds.get(0));
- } else {
- showNotificationForMultipleUpdates(updatedFeeds);
- }
-
- Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
- Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.NOTIFICATION, "content_update");
- Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
- }
-
- private void showNotificationForSingleUpdate(Feed feed) {
- final String date = DateFormat.getMediumDateFormat(context).format(new Date(feed.getLastItem().getTimestamp()));
-
- final NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle()
- .bigText(feed.getLastItem().getTitle())
- .setBigContentTitle(feed.getTitle())
- .setSummaryText(context.getString(R.string.content_notification_updated_on, date));
-
- final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, createOpenIntent(feed), PendingIntent.FLAG_UPDATE_CURRENT);
-
- final Notification notification = new NotificationCompat.Builder(context)
- .setSmallIcon(R.drawable.ic_status_logo)
- .setContentTitle(feed.getTitle())
- .setContentText(feed.getLastItem().getTitle())
- .setStyle(style)
- .setColor(ContextCompat.getColor(context, R.color.fennec_ui_orange))
- .setContentIntent(pendingIntent)
- .setAutoCancel(true)
- .addAction(createOpenAction(feed))
- .addAction(createNotificationSettingsAction())
- .build();
-
- NotificationManagerCompat.from(context).notify(R.id.websiteContentNotification, notification);
- }
-
- private void showNotificationForMultipleUpdates(List<Feed> feeds) {
- final NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
- for (Feed feed : feeds) {
- inboxStyle.addLine(StringUtils.stripScheme(feed.getLastItem().getURL(), StringUtils.UrlFlags.STRIP_HTTPS));
- }
- inboxStyle.setSummaryText(context.getString(R.string.content_notification_summary));
-
- final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, createOpenIntent(feeds), PendingIntent.FLAG_UPDATE_CURRENT);
-
- Notification notification = new NotificationCompat.Builder(context)
- .setSmallIcon(R.drawable.ic_status_logo)
- .setContentTitle(context.getString(R.string.content_notification_title_plural, feeds.size()))
- .setContentText(context.getString(R.string.content_notification_summary))
- .setStyle(inboxStyle)
- .setColor(ContextCompat.getColor(context, R.color.fennec_ui_orange))
- .setContentIntent(pendingIntent)
- .setAutoCancel(true)
- .addAction(createOpenAction(feeds))
- .setNumber(feeds.size())
- .addAction(createNotificationSettingsAction())
- .build();
-
- NotificationManagerCompat.from(context).notify(R.id.websiteContentNotification, notification);
- }
-
- private Intent createOpenIntent(Feed feed) {
- final List<Feed> feeds = new ArrayList<>();
- feeds.add(feed);
-
- return createOpenIntent(feeds);
- }
-
- private Intent createOpenIntent(List<Feed> feeds) {
- final ArrayList<String> urls = new ArrayList<>();
- for (Feed feed : feeds) {
- urls.add(feed.getLastItem().getURL());
- }
-
- final Intent intent = new Intent(context, BrowserApp.class);
- intent.setAction(ContentNotificationsDelegate.ACTION_CONTENT_NOTIFICATION);
- intent.putStringArrayListExtra(ContentNotificationsDelegate.EXTRA_URLS, urls);
-
- return intent;
- }
-
- private NotificationCompat.Action createOpenAction(Feed feed) {
- final List<Feed> feeds = new ArrayList<>();
- feeds.add(feed);
-
- return createOpenAction(feeds);
- }
-
- private NotificationCompat.Action createOpenAction(List<Feed> feeds) {
- Intent intent = createOpenIntent(feeds);
- intent.putExtra(ContentNotificationsDelegate.EXTRA_READ_BUTTON, true);
-
- PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT);
-
- return new NotificationCompat.Action(
- R.drawable.open_in_browser,
- context.getString(R.string.content_notification_action_read_now),
- pendingIntent);
- }
-
- private NotificationCompat.Action createNotificationSettingsAction() {
- final Intent intent = new Intent(GeckoApp.ACTION_LAUNCH_SETTINGS);
- intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
- intent.putExtra(EXTRA_CONTENT_NOTIFICATION, true);
-
- GeckoPreferences.setResourceToOpen(intent, "preferences_notifications");
-
- PendingIntent settingsIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
-
- return new NotificationCompat.Action(
- R.drawable.settings_notifications,
- context.getString(R.string.content_notification_action_settings),
- settingsIntent);
- }
-
- private FeedFetcher.FeedResponse fetchFeed(FeedSubscription subscription) {
- return FeedFetcher.fetchAndParseFeedIfModified(
- subscription.getFeedUrl(),
- subscription.getETag(),
- subscription.getLastModified()
- );
- }
-
- @Override
- public boolean requiresNetwork() {
- return true;
- }
-
- @Override
- public boolean requiresPreferenceEnabled() {
- return true;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/action/EnrollSubscriptionsAction.java b/mobile/android/base/java/org/mozilla/gecko/feeds/action/EnrollSubscriptionsAction.java
deleted file mode 100644
index b778938fd..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/EnrollSubscriptionsAction.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.feeds.action;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.database.Cursor;
-import android.text.TextUtils;
-
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.db.UrlAnnotations;
-import org.mozilla.gecko.feeds.FeedService;
-import org.mozilla.gecko.feeds.knownsites.KnownSiteBlogger;
-import org.mozilla.gecko.feeds.knownsites.KnownSite;
-import org.mozilla.gecko.feeds.knownsites.KnownSiteMedium;
-import org.mozilla.gecko.feeds.knownsites.KnownSiteTumblr;
-import org.mozilla.gecko.feeds.knownsites.KnownSiteWordpress;
-
-/**
- * EnrollSubscriptionsAction: Search for bookmarks of known sites we can subscribe to.
- */
-public class EnrollSubscriptionsAction extends FeedAction {
- private static final String LOGTAG = "FeedEnrollAction";
-
- private static final KnownSite[] knownSites = {
- new KnownSiteMedium(),
- new KnownSiteBlogger(),
- new KnownSiteWordpress(),
- new KnownSiteTumblr(),
- };
-
- private Context context;
-
- public EnrollSubscriptionsAction(Context context) {
- this.context = context;
- }
-
- @Override
- public void perform(BrowserDB db, Intent intent) {
- log("Searching for bookmarks to enroll in updates");
-
- final ContentResolver contentResolver = context.getContentResolver();
-
- for (KnownSite knownSite : knownSites) {
- searchFor(db, contentResolver, knownSite);
- }
- }
-
- @Override
- public boolean requiresNetwork() {
- return false;
- }
-
- @Override
- public boolean requiresPreferenceEnabled() {
- return true;
- }
-
- private void searchFor(BrowserDB db, ContentResolver contentResolver, KnownSite knownSite) {
- final UrlAnnotations urlAnnotations = db.getUrlAnnotations();
-
- final Cursor cursor = db.getBookmarksForPartialUrl(contentResolver, knownSite.getURLSearchString());
- if (cursor == null) {
- log("Nothing found (" + knownSite.getClass().getSimpleName() + ")");
- return;
- }
-
- try {
- log("Found " + cursor.getCount() + " websites");
-
- while (cursor.moveToNext()) {
-
- final String url = cursor.getString(cursor.getColumnIndex(BrowserContract.Bookmarks.URL));
-
- log(" URL: " + url);
-
- String feedUrl = knownSite.getFeedFromURL(url);
- if (TextUtils.isEmpty(feedUrl)) {
- log("Could not determine feed for URL: " + url);
- return;
- }
-
- if (!urlAnnotations.hasFeedUrlForWebsite(contentResolver, url)) {
- urlAnnotations.insertFeedUrl(contentResolver, url, feedUrl);
- }
-
- if (!urlAnnotations.hasFeedSubscription(contentResolver, feedUrl)) {
- FeedService.subscribe(context, feedUrl);
- }
- }
- } finally {
- cursor.close();
- }
- }
-
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/action/FeedAction.java b/mobile/android/base/java/org/mozilla/gecko/feeds/action/FeedAction.java
deleted file mode 100644
index acfaa8b4d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/FeedAction.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.feeds.action;
-
-import android.content.Intent;
-import android.util.Log;
-
-import org.mozilla.gecko.db.BrowserDB;
-
-/**
- * Interface for actions run by FeedService.
- */
-public abstract class FeedAction {
- public static final boolean DEBUG_LOG = false;
-
- /**
- * Perform this action.
- *
- * @param browserDB database instance to perform the action.
- * @param intent used to start the service.
- */
- public abstract void perform(BrowserDB browserDB, Intent intent);
-
- /**
- * Does this action require an active network connection?
- */
- public abstract boolean requiresNetwork();
-
- /**
- * Should this action only run if the preference is enabled?
- */
- public abstract boolean requiresPreferenceEnabled();
-
- /**
- * This method will swallow all log messages to avoid logging potential personal information.
- *
- * For debugging purposes set {@code DEBUG_LOG} to true.
- */
- public void log(String message) {
- if (DEBUG_LOG) {
- Log.d("Gecko" + getClass().getSimpleName(), message);
- }
- }
-
- /**
- * This method will swallow all log messages to avoid logging potential personal information.
- *
- * For debugging purposes set {@code DEBUG_LOG} to true.
- */
- public void log(String message, Throwable throwable) {
- if (DEBUG_LOG) {
- Log.d("Gecko" + getClass().getSimpleName(), message, throwable);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/action/SetupAlarmsAction.java b/mobile/android/base/java/org/mozilla/gecko/feeds/action/SetupAlarmsAction.java
deleted file mode 100644
index f5bf39997..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/SetupAlarmsAction.java
+++ /dev/null
@@ -1,146 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.feeds.action;
-
-import android.app.AlarmManager;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.os.SystemClock;
-
-import com.keepsafe.switchboard.SwitchBoard;
-
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.feeds.FeedAlarmReceiver;
-import org.mozilla.gecko.feeds.FeedService;
-import org.mozilla.gecko.Experiments;
-
-import java.text.DateFormat;
-import java.util.Calendar;
-
-/**
- * SetupAlarmsAction: Set up alarms to run various actions every now and then.
- */
-public class SetupAlarmsAction extends FeedAction {
- private static final String LOGTAG = "FeedSetupAction";
-
- private Context context;
-
- public SetupAlarmsAction(Context context) {
- this.context = context;
- }
-
- @Override
- public void perform(BrowserDB browserDB, Intent intent) {
- final AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
-
- cancelPreviousAlarms(alarmManager);
- scheduleAlarms(alarmManager);
- }
-
- @Override
- public boolean requiresNetwork() {
- return false;
- }
-
- @Override
- public boolean requiresPreferenceEnabled() {
- return false;
- }
-
- private void cancelPreviousAlarms(AlarmManager alarmManager) {
- final PendingIntent withdrawIntent = getWithdrawPendingIntent();
- alarmManager.cancel(withdrawIntent);
-
- final PendingIntent enrollIntent = getEnrollPendingIntent();
- alarmManager.cancel(enrollIntent);
-
- final PendingIntent checkIntent = getCheckPendingIntent();
- alarmManager.cancel(checkIntent);
-
- log("Cancelled previous alarms");
- }
-
- private void scheduleAlarms(AlarmManager alarmManager) {
- alarmManager.setInexactRepeating(
- AlarmManager.ELAPSED_REALTIME,
- SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_FIFTEEN_MINUTES,
- AlarmManager.INTERVAL_DAY,
- getWithdrawPendingIntent());
-
- alarmManager.setInexactRepeating(
- AlarmManager.ELAPSED_REALTIME,
- SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HALF_HOUR,
- AlarmManager.INTERVAL_DAY,
- getEnrollPendingIntent()
- );
-
- if (SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_12HRS)) {
- scheduleUpdateCheckEvery12Hours(alarmManager);
- }
-
- if (SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_8AM)) {
- scheduleUpdateAtFullHour(alarmManager, 8);
- }
-
- if (SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_5PM)) {
- scheduleUpdateAtFullHour(alarmManager, 17);
- }
-
-
- log("Scheduled alarms");
- }
-
- private void scheduleUpdateCheckEvery12Hours(AlarmManager alarmManager) {
- alarmManager.setInexactRepeating(
- AlarmManager.ELAPSED_REALTIME,
- SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HOUR,
- AlarmManager.INTERVAL_HALF_DAY,
- getCheckPendingIntent()
- );
- }
-
- private void scheduleUpdateAtFullHour(AlarmManager alarmManager, int hourOfDay) {
- final Calendar calendar = Calendar.getInstance();
-
- if (calendar.get(Calendar.HOUR_OF_DAY) >= hourOfDay) {
- // This time has already passed today. Try again tomorrow.
- calendar.add(Calendar.DAY_OF_MONTH, 1);
- }
-
- calendar.set(Calendar.HOUR_OF_DAY, hourOfDay);
- calendar.set(Calendar.MINUTE, 0);
- calendar.set(Calendar.SECOND, 0);
- calendar.set(Calendar.MILLISECOND, 0);
-
- alarmManager.setInexactRepeating(
- AlarmManager.RTC,
- calendar.getTimeInMillis(),
- AlarmManager.INTERVAL_DAY,
- getCheckPendingIntent()
- );
-
- log("Scheduled update alarm at " + DateFormat.getDateTimeInstance().format(calendar.getTime()));
- }
-
- private PendingIntent getWithdrawPendingIntent() {
- Intent intent = new Intent(context, FeedAlarmReceiver.class);
- intent.setAction(FeedService.ACTION_WITHDRAW);
- return PendingIntent.getBroadcast(context, 0, intent, 0);
- }
-
- private PendingIntent getEnrollPendingIntent() {
- Intent intent = new Intent(context, FeedAlarmReceiver.class);
- intent.setAction(FeedService.ACTION_ENROLL);
- return PendingIntent.getBroadcast(context, 0, intent, 0);
- }
-
- private PendingIntent getCheckPendingIntent() {
- Intent intent = new Intent(context, FeedAlarmReceiver.class);
- intent.setAction(FeedService.ACTION_CHECK);
- return PendingIntent.getBroadcast(context, 0, intent, 0);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/action/SubscribeToFeedAction.java b/mobile/android/base/java/org/mozilla/gecko/feeds/action/SubscribeToFeedAction.java
deleted file mode 100644
index fbfce1af2..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/SubscribeToFeedAction.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.feeds.action;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.db.UrlAnnotations;
-import org.mozilla.gecko.feeds.FeedFetcher;
-import org.mozilla.gecko.feeds.FeedService;
-import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
-
-/**
- * SubscribeToFeedAction: Try to fetch a feed and create a subscription if successful.
- */
-public class SubscribeToFeedAction extends FeedAction {
- private static final String LOGTAG = "FeedSubscribeAction";
-
- public static final String EXTRA_FEED_URL = "feed_url";
-
- private Context context;
-
- public SubscribeToFeedAction(Context context) {
- this.context = context;
- }
-
- @Override
- public void perform(BrowserDB browserDB, Intent intent) {
- final UrlAnnotations urlAnnotations = browserDB.getUrlAnnotations();
-
- final Bundle extras = intent.getExtras();
- final String feedUrl = extras.getString(EXTRA_FEED_URL);
-
- if (urlAnnotations.hasFeedSubscription(context.getContentResolver(), feedUrl)) {
- log("Already subscribed to " + feedUrl + ". Skipping.");
- return;
- }
-
- log("Subscribing to feed: " + feedUrl);
-
- subscribe(urlAnnotations, feedUrl);
- }
-
- @Override
- public boolean requiresNetwork() {
- return true;
- }
-
- @Override
- public boolean requiresPreferenceEnabled() {
- return true;
- }
-
- private void subscribe(UrlAnnotations urlAnnotations, String feedUrl) {
- FeedFetcher.FeedResponse response = FeedFetcher.fetchAndParseFeed(feedUrl);
- if (response == null) {
- log(String.format("Could not fetch feed (%s). Not subscribing for now.", feedUrl));
- return;
- }
-
- log("Subscribing to feed: " + response.feed.getTitle());
- log(" Last item: " + response.feed.getLastItem().getTitle());
-
- final FeedSubscription subscription = FeedSubscription.create(feedUrl, response);
-
- urlAnnotations.insertFeedSubscription(context.getContentResolver(), subscription);
-
- Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
- Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.SERVICE, "content_update");
- Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/action/WithdrawSubscriptionsAction.java b/mobile/android/base/java/org/mozilla/gecko/feeds/action/WithdrawSubscriptionsAction.java
deleted file mode 100644
index 6f955c185..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/WithdrawSubscriptionsAction.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.feeds.action;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.database.Cursor;
-
-import org.json.JSONException;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.db.UrlAnnotations;
-import org.mozilla.gecko.feeds.FeedService;
-import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
-
-/**
- * WithdrawSubscriptionsAction: Look for feeds to unsubscribe from.
- */
-public class WithdrawSubscriptionsAction extends FeedAction {
- private static final String LOGTAG = "FeedWithdrawAction";
-
- private Context context;
-
- public WithdrawSubscriptionsAction(Context context) {
- this.context = context;
- }
-
- @Override
- public void perform(BrowserDB browserDB, Intent intent) {
- log("Searching for subscriptions to remove..");
-
- final UrlAnnotations urlAnnotations = browserDB.getUrlAnnotations();
- final ContentResolver resolver = context.getContentResolver();
-
- removeFeedsOfUnknownUrls(browserDB, urlAnnotations, resolver);
- removeSubscriptionsOfRemovedFeeds(urlAnnotations, resolver);
- }
-
- /**
- * Search for website URLs with a feed assigned. Remove entry if website URL is not known anymore:
- * For now this means the website is not bookmarked.
- */
- private void removeFeedsOfUnknownUrls(BrowserDB browserDB, UrlAnnotations urlAnnotations, ContentResolver resolver) {
- Cursor cursor = urlAnnotations.getWebsitesWithFeedUrl(resolver);
- if (cursor == null) {
- return;
- }
-
- try {
- while (cursor.moveToNext()) {
- final String url = cursor.getString(cursor.getColumnIndex(BrowserContract.UrlAnnotations.URL));
-
- if (!browserDB.isBookmark(resolver, url)) {
- log("Removing feed for unknown URL: " + url);
-
- urlAnnotations.deleteFeedUrl(resolver, url);
- }
- }
- } finally {
- cursor.close();
- }
- }
-
- /**
- * Remove subscriptions of feed URLs that are not assigned to a website URL (anymore).
- */
- private void removeSubscriptionsOfRemovedFeeds(UrlAnnotations urlAnnotations, ContentResolver resolver) {
- Cursor cursor = urlAnnotations.getFeedSubscriptions(resolver);
- if (cursor == null) {
- return;
- }
-
- try {
- while (cursor.moveToNext()) {
- final FeedSubscription subscription = FeedSubscription.fromCursor(cursor);
-
- if (!urlAnnotations.hasWebsiteForFeedUrl(resolver, subscription.getFeedUrl())) {
- log("Removing subscription for feed: " + subscription.getFeedUrl());
-
- urlAnnotations.deleteFeedSubscription(resolver, subscription);
-
- Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
- Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.SERVICE, "content_update");
- Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
- }
- }
- } catch (JSONException e) {
- log("Could not deserialize subscription", e);
- } finally {
- cursor.close();
- }
- }
-
- @Override
- public boolean requiresNetwork() {
- return false;
- }
-
- @Override
- public boolean requiresPreferenceEnabled() {
- return true;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSite.java b/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSite.java
deleted file mode 100644
index febfbb0c7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSite.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.feeds.knownsites;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-/**
- * A site we know and for which we can guess the feed URL from an arbitrary URL.
- */
-public interface KnownSite {
- /**
- * Get a search string to find URLs of this site in our database. This search string is usually
- * a partial domain / URL.
- *
- * For example we could return "medium.com" to find all URLs that contain this string. This could
- * obviously find URLs that are not actually medium.com sites. This is acceptable as long as
- * getFeedFromURL() can handle these inputs and either returns a feed for valid URLs or null for
- * other matches that are not related to this site.
- */
- @NonNull String getURLSearchString();
-
- /**
- * Get the Feed URL for this URL. For a known site we can "guess" the feed URL from an URL
- * pointing to any page. The input URL will be a result from the database found with the value
- * returned by getURLSearchString().
- *
- * Example:
- * - Input: https://medium.com/@antlam/ux-thoughts-for-2016-1fc1d6e515e8
- * - Output: https://medium.com/feed/@antlam
- *
- * @return the url representing a feed, or null if a feed could not be determined.
- */
- @Nullable String getFeedFromURL(String url);
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteBlogger.java b/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteBlogger.java
deleted file mode 100644
index 6bb3629bf..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteBlogger.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.feeds.knownsites;
-
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Blogger.com
- */
-public class KnownSiteBlogger implements KnownSite {
- @Override
- public String getURLSearchString() {
- return ".blogspot.com";
- }
-
- @Override
- public String getFeedFromURL(String url) {
- Pattern pattern = Pattern.compile("https?://(www\\.)?(.*?)\\.blogspot\\.com(/.*)?");
- Matcher matcher = pattern.matcher(url);
- if (matcher.matches()) {
- return String.format("https://%s.blogspot.com/feeds/posts/default", matcher.group(2));
- }
- return null;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteMedium.java b/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteMedium.java
deleted file mode 100644
index a96e83fcd..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteMedium.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.feeds.knownsites;
-
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Medium.com
- */
-public class KnownSiteMedium implements KnownSite {
- @Override
- public String getURLSearchString() {
- return "://medium.com/";
- }
-
- @Override
- public String getFeedFromURL(String url) {
- Pattern pattern = Pattern.compile("https?://medium.com/([^/]+)(/.*)?");
- Matcher matcher = pattern.matcher(url);
- if (matcher.matches()) {
- return String.format("https://medium.com/feed/%s", matcher.group(1));
- }
- return null;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteTumblr.java b/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteTumblr.java
deleted file mode 100644
index c9f480013..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteTumblr.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.feeds.knownsites;
-
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Tumblr.com
- */
-public class KnownSiteTumblr implements KnownSite {
- @Override
- public String getURLSearchString() {
- return ".tumblr.com";
- }
-
- @Override
- public String getFeedFromURL(String url) {
- final Pattern pattern = Pattern.compile("https?://(.*?).tumblr.com(/.*)?");
- final Matcher matcher = pattern.matcher(url);
- if (matcher.matches()) {
- final String username = matcher.group(1);
- if (username.equals("www")) {
- return null;
- }
- return "http://" + username + ".tumblr.com/rss";
- }
- return null;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteWordpress.java b/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteWordpress.java
deleted file mode 100644
index a74b41a74..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteWordpress.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package org.mozilla.gecko.feeds.knownsites;
-
-import android.support.annotation.NonNull;
-
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Wordpress.com
- */
-public class KnownSiteWordpress implements KnownSite {
- @Override
- public String getURLSearchString() {
- return ".wordpress.com";
- }
-
- @Override
- public String getFeedFromURL(String url) {
- Pattern pattern = Pattern.compile("https?://(.*?).wordpress.com(/.*)?");
- Matcher matcher = pattern.matcher(url);
- if (matcher.matches()) {
- return "https://" + matcher.group(1) + ".wordpress.com/feed/";
- }
- return null;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/parser/Feed.java b/mobile/android/base/java/org/mozilla/gecko/feeds/parser/Feed.java
deleted file mode 100644
index aefc72aa7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/parser/Feed.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.feeds.parser;
-
-import ch.boye.httpclientandroidlib.util.TextUtils;
-
-public class Feed {
- private String title;
- private String websiteURL;
- private String feedURL;
- private Item lastItem;
-
- public static Feed create(String title, String websiteURL, String feedURL, Item lastItem) {
- Feed feed = new Feed();
-
- feed.setTitle(title);
- feed.setWebsiteURL(websiteURL);
- feed.setFeedURL(feedURL);
- feed.setLastItem(lastItem);
-
- return feed;
- }
-
- /* package-private */ Feed() {}
-
- /* package-private */ void setTitle(String title) {
- this.title = title;
- }
-
- /* package-private */ void setWebsiteURL(String websiteURL) {
- this.websiteURL = websiteURL;
- }
-
- /* package-private */ void setFeedURL(String feedURL) {
- this.feedURL = feedURL;
- }
-
- /* package-private */ void setLastItem(Item lastItem) {
- this.lastItem = lastItem;
- }
-
- /**
- * Is this feed object sufficiently complete so that we can use it?
- */
- /* package-private */ boolean isSufficientlyComplete() {
- return !TextUtils.isEmpty(title) &&
- lastItem != null &&
- !TextUtils.isEmpty(lastItem.getURL()) &&
- !TextUtils.isEmpty(lastItem.getTitle());
- }
-
- public String getTitle() {
- return title;
- }
-
- public String getWebsiteURL() {
- return websiteURL;
- }
-
- public String getFeedURL() {
- return feedURL;
- }
-
- public Item getLastItem() {
- return lastItem;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/parser/Item.java b/mobile/android/base/java/org/mozilla/gecko/feeds/parser/Item.java
deleted file mode 100644
index 8d8f6d44e..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/parser/Item.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.feeds.parser;
-
-public class Item {
- private String title;
- private String url;
- private long timestamp;
-
- public static Item create(String title, String url, long timestamp) {
- Item item = new Item();
-
- item.setTitle(title);
- item.setURL(url);
- item.setTimestamp(timestamp);
-
- return item;
- }
-
- /* package-private */ void setTitle(String title) {
- this.title = title;
- }
-
- /* package-private */ void setURL(String url) {
- this.url = url;
- }
-
- /* package-private */ void setTimestamp(long timestamp) {
- this.timestamp = timestamp;
- }
-
- public String getTitle() {
- return title;
- }
-
- public String getURL() {
- return url;
- }
-
- /**
- * @return the number of milliseconds since Jan. 1, 1970, midnight GMT.
- */
- public long getTimestamp() {
- return timestamp;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/parser/SimpleFeedParser.java b/mobile/android/base/java/org/mozilla/gecko/feeds/parser/SimpleFeedParser.java
deleted file mode 100644
index afb1b7cb2..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/parser/SimpleFeedParser.java
+++ /dev/null
@@ -1,367 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.feeds.parser;
-
-import android.util.Log;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-import org.xmlpull.v1.XmlPullParserFactory;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
-
-import ch.boye.httpclientandroidlib.util.TextUtils;
-
-/**
- * A super simple feed parser written for implementing "content notifications". This XML Pull Parser
- * can read ATOM and RSS feeds and returns an object describing the feed and the latest entry.
- */
-public class SimpleFeedParser {
- /**
- * Generic exception that's thrown by the parser whenever a stream cannot be parsed.
- */
- public static class ParserException extends Exception {
- private static final long serialVersionUID = -6119538440219805603L;
-
- public ParserException(Throwable cause) {
- super(cause);
- }
-
- public ParserException(String message) {
- super(message);
- }
- }
-
- private static final String LOGTAG = "Gecko/FeedParser";
-
- private static final String TAG_RSS = "rss";
- private static final String TAG_FEED = "feed";
- private static final String TAG_RDF = "RDF";
- private static final String TAG_TITLE = "title";
- private static final String TAG_ITEM = "item";
- private static final String TAG_LINK = "link";
- private static final String TAG_ENTRY = "entry";
- private static final String TAG_PUBDATE = "pubDate";
- private static final String TAG_UPDATED = "updated";
- private static final String TAG_DATE = "date";
- private static final String TAG_SOURCE = "source";
- private static final String TAG_IMAGE = "image";
- private static final String TAG_CONTENT = "content";
-
- private class ParserState {
- public Feed feed;
- public Item currentItem;
- public boolean isRSS;
- public boolean isATOM;
- public boolean inSource;
- public boolean inImage;
- public boolean inContent;
- }
-
- public Feed parse(InputStream in) throws ParserException, IOException {
- final ParserState state = new ParserState();
-
- try {
- final XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
- factory.setNamespaceAware(true);
-
- XmlPullParser parser = factory.newPullParser();
- parser.setInput(in, null);
-
- int eventType = parser.getEventType();
-
- while (eventType != XmlPullParser.END_DOCUMENT) {
- switch (eventType) {
- case XmlPullParser.START_DOCUMENT:
- handleStartDocument(state);
- break;
-
- case XmlPullParser.START_TAG:
- handleStartTag(parser, state);
- break;
-
- case XmlPullParser.END_TAG:
- handleEndTag(parser, state);
- break;
- }
-
- eventType = parser.next();
- }
- } catch (XmlPullParserException e) {
- throw new ParserException(e);
- }
-
- if (!state.feed.isSufficientlyComplete()) {
- throw new ParserException("Feed is not sufficiently complete");
- }
-
- return state.feed;
- }
-
- private void handleStartDocument(ParserState state) {
- state.feed = new Feed();
- }
-
- private void handleStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException {
- switch (parser.getName()) {
- case TAG_RSS:
- state.isRSS = true;
- break;
-
- case TAG_FEED:
- state.isATOM = true;
- break;
-
- case TAG_RDF:
- // This is a RSS 1.0 feed
- state.isRSS = true;
- break;
-
- case TAG_ITEM:
- case TAG_ENTRY:
- state.currentItem = new Item();
- break;
-
- case TAG_TITLE:
- handleTitleStartTag(parser, state);
- break;
-
- case TAG_LINK:
- handleLinkStartTag(parser, state);
- break;
-
- case TAG_PUBDATE:
- handlePubDateStartTag(parser, state);
- break;
-
- case TAG_UPDATED:
- handleUpdatedStartTag(parser, state);
- break;
-
- case TAG_DATE:
- handleDateStartTag(parser, state);
- break;
-
- case TAG_SOURCE:
- state.inSource = true;
- break;
-
- case TAG_IMAGE:
- state.inImage = true;
- break;
-
- case TAG_CONTENT:
- state.inContent = true;
- break;
- }
- }
-
- private void handleEndTag(XmlPullParser parser, ParserState state) {
- switch (parser.getName()) {
- case TAG_ITEM:
- case TAG_ENTRY:
- handleItemOrEntryREndTag(state);
- break;
-
- case TAG_SOURCE:
- state.inSource = false;
- break;
-
- case TAG_IMAGE:
- state.inImage = false;
- break;
-
- case TAG_CONTENT:
- state.inContent = false;
- break;
- }
- }
-
- private void handleTitleStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException {
- if (state.inSource || state.inImage || state.inContent) {
- // We do not care about titles in <source>, <image> or <media> tags.
- return;
- }
-
- String title = getTextUntilEndTag(parser, TAG_TITLE);
-
- title = title.replaceAll("[\r\n]", " ");
- title = title.replaceAll(" +", " ");
-
- if (state.currentItem != null) {
- state.currentItem.setTitle(title);
- } else {
- state.feed.setTitle(title);
- }
- }
-
- private void handleLinkStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException {
- if (state.inSource || state.inImage) {
- // We do not care about links in <source> or <image> tags.
- return;
- }
-
- Map<String, String> attributes = fetchAttributes(parser);
-
- if (attributes.size() > 0) {
- String rel = attributes.get("rel");
-
- if (state.currentItem == null && "self".equals(rel)) {
- state.feed.setFeedURL(attributes.get("href"));
- return;
- }
-
- if (rel == null || "alternate".equals(rel)) {
- String type = attributes.get("type");
- if (type == null || type.equals("text/html")) {
- String link = attributes.get("href");
- if (TextUtils.isEmpty(link)) {
- return;
- }
-
- if (state.currentItem != null) {
- state.currentItem.setURL(link);
- } else {
- state.feed.setWebsiteURL(link);
- }
-
- return;
- }
- }
- }
-
- if (state.isRSS) {
- String link = getTextUntilEndTag(parser, TAG_LINK);
- if (TextUtils.isEmpty(link)) {
- return;
- }
-
- if (state.currentItem != null) {
- state.currentItem.setURL(link);
- } else {
- state.feed.setWebsiteURL(link);
- }
- }
- }
-
- private void handleItemOrEntryREndTag(ParserState state) {
- if (state.feed.getLastItem() == null || state.feed.getLastItem().getTimestamp() < state.currentItem.getTimestamp()) {
- // Only set this item as "last item" if we do not have an item yet or this item is newer.
- state.feed.setLastItem(state.currentItem);
- }
-
- state.currentItem = null;
- }
-
- private void handlePubDateStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException {
- if (state.currentItem == null) {
- return;
- }
-
- String pubDate = getTextUntilEndTag(parser, TAG_PUBDATE);
- if (TextUtils.isEmpty(pubDate)) {
- return;
- }
-
- // RFC-822
- SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
-
- updateCurrentItemTimestamp(state, pubDate, format);
- }
-
- private void handleUpdatedStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException {
- if (state.inSource) {
- // We do not care about stuff in <source> tags.
- return;
- }
-
- if (state.currentItem == null) {
- // We are only interested in <updated> values of feed items.
- return;
- }
-
- String updated = getTextUntilEndTag(parser, TAG_UPDATED);
- if (TextUtils.isEmpty(updated)) {
- return;
- }
-
- SimpleDateFormat[] formats = new SimpleDateFormat[] {
- new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US),
- new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US)
- };
-
- // Fix timezones SimpleDateFormat can't parse:
- // 2016-01-26T18:56:54Z -> 2016-01-26T18:56:54+0000 (Timezone: Z -> +0000)
- updated = updated.replaceFirst("Z$", "+0000");
- // 2016-01-26T18:56:54+01:00 -> 2016-01-26T18:56:54+0100 (Timezone: +01:00 -> +0100)
- updated = updated.replaceFirst("([0-9]{2})([\\+\\-])([0-9]{2}):([0-9]{2})$", "$1$2$3$4");
-
- updateCurrentItemTimestamp(state, updated, formats);
- }
-
- private void handleDateStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException {
- if (state.currentItem == null) {
- // We are only interested in <updated> values of feed items.
- return;
- }
-
- String text = getTextUntilEndTag(parser, TAG_DATE);
- if (TextUtils.isEmpty(text)) {
- return;
- }
-
- // Fix timezones SimpleDateFormat can't parse:
- // 2016-01-26T18:56:54+00:00 -> 2016-01-26T18:56:54+0000
- text = text.replaceFirst("([0-9]{2})([\\+\\-])([0-9]{2}):([0-9]{2})$", "$1$2$3$4");
-
- SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);
-
- updateCurrentItemTimestamp(state, text, format);
- }
-
- private void updateCurrentItemTimestamp(ParserState state, String text, SimpleDateFormat... formats) {
- for (SimpleDateFormat format : formats) {
- try {
- Date date = format.parse(text);
- state.currentItem.setTimestamp(date.getTime());
- return;
- } catch (ParseException e) {
- Log.w(LOGTAG, "Could not parse 'updated': " + text);
- }
- }
- }
-
- private Map<String, String> fetchAttributes(XmlPullParser parser) {
- Map<String, String> attributes = new HashMap<>();
-
- for (int i = 0; i < parser.getAttributeCount(); i++) {
- attributes.put(parser.getAttributeName(i), parser.getAttributeValue(i));
- }
-
- return attributes;
- }
-
- private String getTextUntilEndTag(XmlPullParser parser, String tag) throws IOException, XmlPullParserException {
- StringBuilder builder = new StringBuilder();
-
- while (parser.next() != XmlPullParser.END_DOCUMENT) {
- if (parser.getEventType() == XmlPullParser.TEXT) {
- builder.append(parser.getText());
- } else if (parser.getEventType() == XmlPullParser.END_TAG && tag.equals(parser.getName())) {
- break;
- }
- }
-
- return builder.toString().trim();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/feeds/subscriptions/FeedSubscription.java b/mobile/android/base/java/org/mozilla/gecko/feeds/subscriptions/FeedSubscription.java
deleted file mode 100644
index 7ce7f193f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/subscriptions/FeedSubscription.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.feeds.subscriptions;
-
-import android.database.Cursor;
-import android.text.TextUtils;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.feeds.FeedFetcher;
-import org.mozilla.gecko.feeds.parser.Item;
-
-/**
- * An object describing a subscription and containing some meta data about the last time we fetched
- * the feed.
- */
-public class FeedSubscription {
- private static final String JSON_KEY_FEED_TITLE = "feed_title";
- private static final String JSON_KEY_LAST_ITEM_TITLE = "last_item_title";
- private static final String JSON_KEY_LAST_ITEM_URL = "last_item_url";
- private static final String JSON_KEY_LAST_ITEM_TIMESTAMP = "last_item_timestamp";
- private static final String JSON_KEY_ETAG = "etag";
- private static final String JSON_KEY_LAST_MODIFIED = "last_modified";
-
- private String feedUrl;
- private String feedTitle;
- private String lastItemTitle;
- private String lastItemUrl;
- private long lastItemTimestamp;
- private String etag;
- private String lastModified;
-
- public static FeedSubscription create(String feedUrl, FeedFetcher.FeedResponse response) {
- FeedSubscription subscription = new FeedSubscription();
- subscription.feedUrl = feedUrl;
-
- subscription.update(response);
-
- return subscription;
- }
-
- public static FeedSubscription fromCursor(Cursor cursor) throws JSONException {
- final FeedSubscription subscription = new FeedSubscription();
- subscription.feedUrl = cursor.getString(cursor.getColumnIndex(BrowserContract.UrlAnnotations.URL));
-
- final String value = cursor.getString(cursor.getColumnIndex(BrowserContract.UrlAnnotations.VALUE));
- subscription.fromJSON(new JSONObject(value));
-
- return subscription;
- }
-
- private void fromJSON(JSONObject object) throws JSONException {
- feedTitle = object.getString(JSON_KEY_FEED_TITLE);
- lastItemTitle = object.getString(JSON_KEY_LAST_ITEM_TITLE);
- lastItemUrl = object.getString(JSON_KEY_LAST_ITEM_URL);
- lastItemTimestamp = object.getLong(JSON_KEY_LAST_ITEM_TIMESTAMP);
- etag = object.optString(JSON_KEY_ETAG);
- lastModified = object.optString(JSON_KEY_LAST_MODIFIED);
- }
-
- public void update(FeedFetcher.FeedResponse response) {
- feedTitle = response.feed.getTitle();
- lastItemTitle = response.feed.getLastItem().getTitle();
- lastItemUrl = response.feed.getLastItem().getURL();
- lastItemTimestamp = response.feed.getLastItem().getTimestamp();
- etag = response.etag;
- lastModified = response.lastModified;
- }
-
- /**
- * Guesstimate if this response is a newer representation of the feed.
- */
- public boolean hasBeenUpdated(FeedFetcher.FeedResponse response) {
- final Item responseItem = response.feed.getLastItem();
-
- if (responseItem.getTimestamp() > lastItemTimestamp) {
- // The timestamp is from a newer date so we expect that this item is a new item. But this
- // could also mean that the timestamp of an already existing item has been updated. We
- // accept that and assume that the content will have changed too in this case.
- return true;
- }
-
- if (responseItem.getTimestamp() == lastItemTimestamp && responseItem.getTimestamp() != 0) {
- // We have a timestamp that is not zero and this item has still the timestamp: It's very
- // likely that we are looking at the same item. We assume this is not new content.
- return false;
- }
-
- if (!responseItem.getURL().equals(lastItemUrl)) {
- // The URL changed: It is very likely that this is a new item. At least it has been updated
- // in a way that we just treat it as new content here.
- return true;
- }
-
- return false;
- }
-
- public String getFeedUrl() {
- return feedUrl;
- }
-
- public String getFeedTitle() {
- return feedTitle;
- }
-
- public String getETag() {
- return etag;
- }
-
- public String getLastModified() {
- return lastModified;
- }
-
- public JSONObject toJSON() throws JSONException {
- JSONObject object = new JSONObject();
-
- object.put(JSON_KEY_FEED_TITLE, feedTitle);
- object.put(JSON_KEY_LAST_ITEM_TITLE, lastItemTitle);
- object.put(JSON_KEY_LAST_ITEM_URL, lastItemUrl);
- object.put(JSON_KEY_LAST_ITEM_TIMESTAMP, lastItemTimestamp);
- object.put(JSON_KEY_ETAG, etag);
- object.put(JSON_KEY_LAST_MODIFIED, lastModified);
-
- return object;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/firstrun/DataPanel.java b/mobile/android/base/java/org/mozilla/gecko/firstrun/DataPanel.java
deleted file mode 100644
index d5940d758..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/DataPanel.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.firstrun;
-
-import android.os.Bundle;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.PrefsHelper;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-
-public class DataPanel extends FirstrunPanel {
- private boolean isEnabled = false;
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
- final View root = super.onCreateView(inflater, container, savedInstance);
- final ImageView clickableImage = (ImageView) root.findViewById(R.id.firstrun_image);
- clickableImage.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- // Set new state.
- isEnabled = !isEnabled;
- int newResource = isEnabled ? R.drawable.firstrun_data_on : R.drawable.firstrun_data_off;
- ((ImageView) view).setImageResource(newResource);
- if (isEnabled) {
- // Always block images.
- PrefsHelper.setPref("browser.image_blocking", 0);
- } else {
- // Default: always load images.
- PrefsHelper.setPref("browser.image_blocking", 1);
- }
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-datasaving-" + isEnabled);
- }
- });
-
- return root;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunAnimationContainer.java b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunAnimationContainer.java
deleted file mode 100644
index 93dd0c254..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunAnimationContainer.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.firstrun;
-
-import android.content.Context;
-import android.support.v4.app.FragmentManager;
-import android.util.AttributeSet;
-
-import android.view.View;
-import android.widget.LinearLayout;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.Experiments;
-
-/**
- * A container for the pager and the entire first run experience.
- * This is used for animation purposes.
- */
-public class FirstrunAnimationContainer extends LinearLayout {
- public static final String PREF_FIRSTRUN_ENABLED = "startpane_enabled";
-
- public static interface OnFinishListener {
- public void onFinish();
- }
-
- private FirstrunPager pager;
- private boolean visible;
- private OnFinishListener onFinishListener;
-
- public FirstrunAnimationContainer(Context context) {
- this(context, null);
- }
- public FirstrunAnimationContainer(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public void load(Context appContext, FragmentManager fm) {
- visible = true;
- pager = (FirstrunPager) findViewById(R.id.firstrun_pager);
- pager.load(appContext, fm, new OnFinishListener() {
- @Override
- public void onFinish() {
- hide();
- }
- });
- }
-
- public boolean isVisible() {
- return visible;
- }
-
- public void hide() {
- visible = false;
- if (onFinishListener != null) {
- onFinishListener.onFinish();
- }
- animateHide();
-
- // Stop all versions of firstrun A/B sessions.
- Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING3_B);
- Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING3_C);
- }
-
- private void animateHide() {
- final Animator alphaAnimator = ObjectAnimator.ofFloat(this, "alpha", 0);
- alphaAnimator.setDuration(150);
- alphaAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- FirstrunAnimationContainer.this.setVisibility(View.GONE);
- }
- });
-
- alphaAnimator.start();
- }
-
- public boolean showBrowserHint() {
- final int currentPage = pager.getCurrentItem();
- FirstrunPanel currentPanel = (FirstrunPanel) ((FirstrunPager.ViewPagerAdapter) pager.getAdapter()).getItem(currentPage);
- pager.cleanup();
- return currentPanel.shouldShowBrowserHint();
- }
-
- public void registerOnFinishListener(OnFinishListener listener) {
- this.onFinishListener = listener;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPager.java b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPager.java
deleted file mode 100644
index c2838ee3e..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPager.java
+++ /dev/null
@@ -1,174 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.firstrun;
-
-import android.content.Context;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentPagerAdapter;
-import android.support.v4.view.ViewPager;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewGroup;
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.home.HomePager.Decor;
-import org.mozilla.gecko.home.TabMenuStrip;
-import org.mozilla.gecko.restrictions.Restrictions;
-
-import java.util.List;
-
-/**
- * ViewPager containing for our first run pages.
- *
- * @see FirstrunPanel for the first run pages that are used in this pager.
- */
-public class FirstrunPager extends ViewPager {
-
- private Context context;
- protected FirstrunPanel.PagerNavigation pagerNavigation;
- private Decor mDecor;
-
- public FirstrunPager(Context context) {
- this(context, null);
- }
-
- public FirstrunPager(Context context, AttributeSet attrs) {
- super(context, attrs);
- this.context = context;
- }
-
- @Override
- public void addView(View child, int index, ViewGroup.LayoutParams params) {
- if (child instanceof Decor) {
- ((ViewPager.LayoutParams) params).isDecor = true;
- mDecor = (Decor) child;
- mDecor.setOnTitleClickListener(new TabMenuStrip.OnTitleClickListener() {
- @Override
- public void onTitleClicked(int index) {
- setCurrentItem(index, true);
- }
- });
- }
-
- super.addView(child, index, params);
- }
-
- public void load(Context appContext, FragmentManager fm, final FirstrunAnimationContainer.OnFinishListener onFinishListener) {
- final List<FirstrunPagerConfig.FirstrunPanelConfig> panels;
-
- if (Restrictions.isRestrictedProfile(context)) {
- panels = FirstrunPagerConfig.getRestricted();
- } else {
- panels = FirstrunPagerConfig.getDefault(appContext);
- }
-
- setAdapter(new ViewPagerAdapter(fm, panels));
- this.pagerNavigation = new FirstrunPanel.PagerNavigation() {
- @Override
- public void next() {
- final int currentPage = FirstrunPager.this.getCurrentItem();
- if (currentPage < FirstrunPager.this.getAdapter().getCount() - 1) {
- FirstrunPager.this.setCurrentItem(currentPage + 1);
- }
- }
-
- @Override
- public void finish() {
- if (onFinishListener != null) {
- onFinishListener.onFinish();
- }
- }
- };
- addOnPageChangeListener(new OnPageChangeListener() {
- @Override
- public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
- mDecor.onPageScrolled(position, positionOffset, positionOffsetPixels);
- }
-
- @Override
- public void onPageSelected(int i) {
- mDecor.onPageSelected(i);
- Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.PANEL, "onboarding." + i);
- }
-
- @Override
- public void onPageScrollStateChanged(int i) {}
- });
-
- animateLoad();
-
- // Record telemetry for first onboarding panel, for baseline.
- Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.PANEL, "onboarding.0");
- }
-
- public void cleanup() {
- setAdapter(null);
- }
-
- private void animateLoad() {
- setTranslationY(500);
- setAlpha(0);
-
- final Animator translateAnimator = ObjectAnimator.ofFloat(this, "translationY", 0);
- translateAnimator.setDuration(400);
-
- final Animator alphaAnimator = ObjectAnimator.ofFloat(this, "alpha", 1);
- alphaAnimator.setStartDelay(200);
- alphaAnimator.setDuration(600);
-
- final AnimatorSet set = new AnimatorSet();
- set.playTogether(alphaAnimator, translateAnimator);
- set.setStartDelay(400);
-
- set.start();
- }
-
- protected class ViewPagerAdapter extends FragmentPagerAdapter {
- private final List<FirstrunPagerConfig.FirstrunPanelConfig> panels;
- private final Fragment[] fragments;
-
- public ViewPagerAdapter(FragmentManager fm, List<FirstrunPagerConfig.FirstrunPanelConfig> panels) {
- super(fm);
- this.panels = panels;
- this.fragments = new Fragment[panels.size()];
- for (FirstrunPagerConfig.FirstrunPanelConfig panel : panels) {
- mDecor.onAddPagerView(context.getString(panel.getTitleRes()));
- }
-
- if (panels.size() > 0) {
- mDecor.onPageSelected(0);
- }
- }
-
- @Override
- public Fragment getItem(int i) {
- Fragment fragment = this.fragments[i];
- if (fragment == null) {
- FirstrunPagerConfig.FirstrunPanelConfig panelConfig = panels.get(i);
- fragment = Fragment.instantiate(context, panelConfig.getClassname(), panelConfig.getArgs());
- ((FirstrunPanel) fragment).setPagerNavigation(pagerNavigation);
- fragments[i] = fragment;
- }
- return fragment;
- }
-
- @Override
- public int getCount() {
- return panels.size();
- }
-
- @Override
- public CharSequence getPageTitle(int i) {
- // Unused now that we use TabMenuStrip.
- return context.getString(panels.get(i).getTitleRes()).toUpperCase();
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPagerConfig.java b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPagerConfig.java
deleted file mode 100644
index 3f901d07b..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPagerConfig.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.firstrun;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.util.Log;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.Experiments;
-
-import java.util.LinkedList;
-import java.util.List;
-
-public class FirstrunPagerConfig {
- public static final String LOGTAG = "FirstrunPagerConfig";
-
- public static final String KEY_IMAGE = "imageRes";
- public static final String KEY_TEXT = "textRes";
- public static final String KEY_SUBTEXT = "subtextRes";
-
- public static List<FirstrunPanelConfig> getDefault(Context context) {
- final List<FirstrunPanelConfig> panels = new LinkedList<>();
-
- if (Experiments.isInExperimentLocal(context, Experiments.ONBOARDING3_B)) {
- panels.add(SimplePanelConfigs.urlbarPanelConfig);
- panels.add(SimplePanelConfigs.bookmarksPanelConfig);
- panels.add(SimplePanelConfigs.dataPanelConfig);
- panels.add(SimplePanelConfigs.syncPanelConfig);
- panels.add(SimplePanelConfigs.signInPanelConfig);
- Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING3_B);
- GeckoSharedPrefs.forProfile(context).edit().putString(Experiments.PREF_ONBOARDING_VERSION, Experiments.ONBOARDING3_B).apply();
- } else if (Experiments.isInExperimentLocal(context, Experiments.ONBOARDING3_C)) {
- panels.add(SimplePanelConfigs.tabqueuePanelConfig);
- panels.add(SimplePanelConfigs.readerviewPanelConfig);
- panels.add(SimplePanelConfigs.accountPanelConfig);
- Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING3_C);
- GeckoSharedPrefs.forProfile(context).edit().putString(Experiments.PREF_ONBOARDING_VERSION, Experiments.ONBOARDING3_C).apply();
- } else {
- Log.e(LOGTAG, "Not in an experiment!");
- panels.add(SimplePanelConfigs.signInPanelConfig);
- }
- return panels;
- }
-
- public static List<FirstrunPanelConfig> getRestricted() {
- final List<FirstrunPanelConfig> panels = new LinkedList<>();
- panels.add(new FirstrunPanelConfig(RestrictedWelcomePanel.class.getName(), RestrictedWelcomePanel.TITLE_RES));
- return panels;
- }
-
- public static class FirstrunPanelConfig {
-
- private String classname;
- private int titleRes;
- private Bundle args;
-
- public FirstrunPanelConfig(String resource, int titleRes) {
- this(resource, titleRes, -1, -1, -1, true);
- }
-
- public FirstrunPanelConfig(String classname, int titleRes, int imageRes, int textRes, int subtextRes) {
- this(classname, titleRes, imageRes, textRes, subtextRes, false);
- }
-
- private FirstrunPanelConfig(String classname, int titleRes, int imageRes, int textRes, int subtextRes, boolean isCustom) {
- this.classname = classname;
- this.titleRes = titleRes;
-
- if (!isCustom) {
- this.args = new Bundle();
- this.args.putInt(KEY_IMAGE, imageRes);
- this.args.putInt(KEY_TEXT, textRes);
- this.args.putInt(KEY_SUBTEXT, subtextRes);
- }
- }
-
- public String getClassname() {
- return this.classname;
- }
-
- public int getTitleRes() {
- return this.titleRes;
- }
-
- public Bundle getArgs() {
- return args;
- }
- }
-
- private static class SimplePanelConfigs {
- public static final FirstrunPanelConfig urlbarPanelConfig = new FirstrunPanelConfig(FirstrunPanel.class.getName(), R.string.firstrun_panel_title_welcome, R.drawable.firstrun_urlbar, R.string.firstrun_urlbar_message, R.string.firstrun_urlbar_subtext);
- public static final FirstrunPanelConfig bookmarksPanelConfig = new FirstrunPanelConfig(FirstrunPanel.class.getName(), R.string.firstrun_bookmarks_title, R.drawable.firstrun_bookmarks, R.string.firstrun_bookmarks_message, R.string.firstrun_bookmarks_subtext);
- public static final FirstrunPanelConfig dataPanelConfig = new FirstrunPanelConfig(DataPanel.class.getName(), R.string.firstrun_data_title, R.drawable.firstrun_data_off, R.string.firstrun_data_message, R.string.firstrun_data_subtext);
- public static final FirstrunPanelConfig syncPanelConfig = new FirstrunPanelConfig(FirstrunPanel.class.getName(), R.string.firstrun_sync_title, R.drawable.firstrun_sync, R.string.firstrun_sync_message, R.string.firstrun_sync_subtext);
- public static final FirstrunPanelConfig signInPanelConfig = new FirstrunPanelConfig(SyncPanel.class.getName(), R.string.pref_sync, R.drawable.firstrun_signin, R.string.firstrun_signin_message, R.string.firstrun_welcome_button_browser);
-
- public static final FirstrunPanelConfig tabqueuePanelConfig = new FirstrunPanelConfig(TabQueuePanel.class.getName(), R.string.firstrun_tabqueue_title, R.drawable.firstrun_tabqueue_off, R.string.firstrun_tabqueue_message_off, R.string.firstrun_tabqueue_subtext_off);
- public static final FirstrunPanelConfig readerviewPanelConfig = new FirstrunPanelConfig(FirstrunPanel.class.getName(), R.string.firstrun_readerview_title, R.drawable.firstrun_readerview, R.string.firstrun_readerview_message, R.string.firstrun_readerview_subtext);
- public static final FirstrunPanelConfig accountPanelConfig = new FirstrunPanelConfig(SyncPanel.class.getName(), R.string.firstrun_account_title, R.drawable.firstrun_account, R.string.firstrun_account_message, R.string.firstrun_button_notnow);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPanel.java b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPanel.java
deleted file mode 100644
index 4b27dbc73..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPanel.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.firstrun;
-
-import android.os.Bundle;
-import android.support.v4.app.Fragment;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-
-/**
- * Base class for our first run pages. We call these FirstrunPanel for consistency
- * with HomePager/HomePanel.
- *
- * @see FirstrunPager for the containing pager.
- */
-public class FirstrunPanel extends Fragment {
-
- public static final int TITLE_RES = -1;
- protected boolean showBrowserHint = true;
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
- final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.firstrun_basepanel_checkable_fragment, container, false);
- Bundle args = getArguments();
- if (args != null) {
- final int imageRes = args.getInt(FirstrunPagerConfig.KEY_IMAGE);
- final int textRes = args.getInt(FirstrunPagerConfig.KEY_TEXT);
- final int subtextRes = args.getInt(FirstrunPagerConfig.KEY_SUBTEXT);
-
- ((ImageView) root.findViewById(R.id.firstrun_image)).setImageResource(imageRes);
- ((TextView) root.findViewById(R.id.firstrun_text)).setText(textRes);
- ((TextView) root.findViewById(R.id.firstrun_subtext)).setText(subtextRes);
- }
-
- root.findViewById(R.id.firstrun_link).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-next");
- pagerNavigation.next();
- }
- });
-
- return root;
- }
-
- public interface PagerNavigation {
- void next();
- void finish();
- }
- protected PagerNavigation pagerNavigation;
-
- public void setPagerNavigation(PagerNavigation listener) {
- this.pagerNavigation = listener;
- }
-
- protected void next() {
- if (pagerNavigation != null) {
- pagerNavigation.next();
- }
- }
-
- protected void close() {
- if (pagerNavigation != null) {
- pagerNavigation.finish();
- }
- }
-
- protected boolean shouldShowBrowserHint() {
- return showBrowserHint;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/firstrun/RestrictedWelcomePanel.java b/mobile/android/base/java/org/mozilla/gecko/firstrun/RestrictedWelcomePanel.java
deleted file mode 100644
index efc91d20f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/RestrictedWelcomePanel.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.firstrun;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.home.HomePager;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import java.util.EnumSet;
-
-public class RestrictedWelcomePanel extends FirstrunPanel {
- public static final int TITLE_RES = R.string.firstrun_panel_title_welcome;
-
- private static final String LEARN_MORE_URL = "https://support.mozilla.org/kb/controlledaccess";
-
- private HomePager.OnUrlOpenListener onUrlOpenListener;
-
- @Override
- public void onAttach(Activity activity) {
- super.onAttach(activity);
-
- try {
- onUrlOpenListener = (HomePager.OnUrlOpenListener) activity;
- } catch (ClassCastException e) {
- throw new ClassCastException(activity.toString() + " must implement HomePager.OnUrlOpenListener");
- }
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
- final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.restricted_firstrun_welcome_fragment, container, false);
-
- root.findViewById(R.id.welcome_browse).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- close();
- }
- });
-
- root.findViewById(R.id.learn_more_link).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- onUrlOpenListener.onUrlOpen(LEARN_MORE_URL, EnumSet.of(HomePager.OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
-
- close();
- }
- });
-
- return root;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/firstrun/SyncPanel.java b/mobile/android/base/java/org/mozilla/gecko/firstrun/SyncPanel.java
deleted file mode 100644
index 2f489c84e..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/SyncPanel.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.firstrun;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.fxa.FxAccountConstants;
-import org.mozilla.gecko.fxa.activities.FxAccountWebFlowActivity;
-
-public class SyncPanel extends FirstrunPanel {
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
- final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.firstrun_sync_fragment, container, false);
- final Bundle args = getArguments();
- if (args != null) {
- final int imageRes = args.getInt(FirstrunPagerConfig.KEY_IMAGE);
- final int textRes = args.getInt(FirstrunPagerConfig.KEY_TEXT);
- final int subtextRes = args.getInt(FirstrunPagerConfig.KEY_SUBTEXT);
-
- ((ImageView) root.findViewById(R.id.firstrun_image)).setImageResource(imageRes);
- ((TextView) root.findViewById(R.id.firstrun_text)).setText(textRes);
- ((TextView) root.findViewById(R.id.welcome_browse)).setText(subtextRes);
- }
-
- root.findViewById(R.id.welcome_account).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-sync");
- showBrowserHint = false;
-
- final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
- intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_FIRSTRUN);
- intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
- startActivity(intent);
-
- close();
- }
- });
-
- root.findViewById(R.id.welcome_browse).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-browser");
- close();
- }
- });
-
- return root;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/firstrun/TabQueuePanel.java b/mobile/android/base/java/org/mozilla/gecko/firstrun/TabQueuePanel.java
deleted file mode 100644
index 3c2ed8312..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/TabQueuePanel.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.firstrun;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.graphics.Typeface;
-import android.os.Bundle;
-import android.support.v4.content.ContextCompat;
-import android.support.v7.widget.SwitchCompat;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CompoundButton;
-import android.widget.ImageView;
-import android.widget.TextView;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-import org.mozilla.gecko.tabqueue.TabQueueHelper;
-import org.mozilla.gecko.tabqueue.TabQueuePrompt;
-
-public class TabQueuePanel extends FirstrunPanel {
- private static final int REQUEST_CODE_TAB_QUEUE = 1;
- private SwitchCompat toggleSwitch;
- private ImageView imageView;
- private TextView messageTextView;
- private TextView subtextTextView;
- private Context context;
-
- @Override
- public View onCreateView(LayoutInflater inflater, final ViewGroup container, Bundle savedInstance) {
- context = getContext();
- final View root = super.onCreateView(inflater, container, savedInstance);
-
- imageView = (ImageView) root.findViewById(R.id.firstrun_image);
- messageTextView = (TextView) root.findViewById(R.id.firstrun_text);
- subtextTextView = (TextView) root.findViewById(R.id.firstrun_subtext);
-
- toggleSwitch = (SwitchCompat) root.findViewById(R.id.firstrun_switch);
- toggleSwitch.setVisibility(View.VISIBLE);
- toggleSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.DIALOG, "firstrun_tabqueue-permissions");
- if (b && !TabQueueHelper.canDrawOverlays(context)) {
- Intent promptIntent = new Intent(context, TabQueuePrompt.class);
- startActivityForResult(promptIntent, REQUEST_CODE_TAB_QUEUE);
- return;
- }
-
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-tabqueue-" + b);
-
- final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
- final SharedPreferences.Editor editor = prefs.edit();
- editor.putBoolean(GeckoPreferences.PREFS_TAB_QUEUE, b).apply();
-
- // Set image, text, and typeface changes.
- imageView.setImageResource(b ? R.drawable.firstrun_tabqueue_on : R.drawable.firstrun_tabqueue_off);
- messageTextView.setText(b ? R.string.firstrun_tabqueue_message_on : R.string.firstrun_tabqueue_message_off);
- messageTextView.setTypeface(b ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT);
- subtextTextView.setText(b ? R.string.firstrun_tabqueue_subtext_on : R.string.firstrun_tabqueue_subtext_off);
- subtextTextView.setTypeface(b ? Typeface.defaultFromStyle(Typeface.ITALIC) : Typeface.DEFAULT);
- subtextTextView.setTextColor(b ? ContextCompat.getColor(context, R.color.fennec_ui_orange) : ContextCompat.getColor(context, R.color.placeholder_grey));
- }
- });
-
- return root;
- }
-
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- switch (requestCode) {
- case REQUEST_CODE_TAB_QUEUE:
- final boolean accepted = TabQueueHelper.processTabQueuePromptResponse(resultCode, context);
- if (accepted) {
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.DIALOG, "firstrun_tabqueue-permissions-yes");
- toggleSwitch.setChecked(true);
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-tabqueue-true");
- }
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.DIALOG, "firstrun_tabqueue-permissions-" + (accepted ? "accepted" : "rejected"));
- break;
- }
- }
-
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/gcm/GcmInstanceIDListenerService.java b/mobile/android/base/java/org/mozilla/gecko/gcm/GcmInstanceIDListenerService.java
deleted file mode 100644
index 0616cd229..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/gcm/GcmInstanceIDListenerService.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gcm;
-
-import android.util.Log;
-
-import com.google.android.gms.iid.InstanceIDListenerService;
-
-import org.mozilla.gecko.push.PushService;
-import org.mozilla.gecko.util.ThreadUtils;
-
-/**
- * This service is notified by the on-device Google Play Services library if an
- * in-use token needs to be updated. We simply pass through to AndroidPushService.
- */
-public class GcmInstanceIDListenerService extends InstanceIDListenerService {
- /**
- * Called if InstanceID token is updated. This may occur if the security of
- * the previous token had been compromised. This call is initiated by the
- * InstanceID provider.
- */
- @Override
- public void onTokenRefresh() {
- Log.d("GeckoPushGCM", "Token refresh request received. Processing on background thread.");
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- PushService.getInstance(GcmInstanceIDListenerService.this).onRefresh();
- }
- });
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/gcm/GcmMessageListenerService.java b/mobile/android/base/java/org/mozilla/gecko/gcm/GcmMessageListenerService.java
deleted file mode 100644
index 7962d7dc3..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/gcm/GcmMessageListenerService.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gcm;
-
-import android.os.Bundle;
-import android.util.Log;
-
-import com.google.android.gms.gcm.GcmListenerService;
-
-import org.mozilla.gecko.push.PushService;
-import org.mozilla.gecko.util.ThreadUtils;
-
-/**
- * This service actually handles messages directed from the on-device Google
- * Play Services package. We simply route them to the AndroidPushService.
- */
-public class GcmMessageListenerService extends GcmListenerService {
- /**
- * Called when message is received.
- *
- * @param from SenderID of the sender.
- * @param bundle Data bundle containing message data as key/value pairs.
- */
- @Override
- public void onMessageReceived(final String from, final Bundle bundle) {
- Log.d("GeckoPushGCM", "Message received. Processing on background thread.");
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- PushService.getInstance(GcmMessageListenerService.this).onMessageReceived(
- GcmMessageListenerService.this, bundle);
- }
- });
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/gcm/GcmTokenClient.java b/mobile/android/base/java/org/mozilla/gecko/gcm/GcmTokenClient.java
deleted file mode 100644
index 024905eb0..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/gcm/GcmTokenClient.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.gcm;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.support.annotation.NonNull;
-import android.util.Log;
-
-import com.google.android.gms.common.ConnectionResult;
-import com.google.android.gms.common.GoogleApiAvailability;
-import com.google.android.gms.gcm.GoogleCloudMessaging;
-import com.google.android.gms.iid.InstanceID;
-
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.push.Fetched;
-
-import java.io.IOException;
-
-/**
- * Fetch and cache GCM tokens.
- * <p/>
- * GCM tokens are stable and long lived. Google Play Services will periodically request that
- * they are rotated, however: see
- * <a href="https://developers.google.com/instance-id/guides/android-implementation">https://developers.google.com/instance-id/guides/android-implementation</a>.
- * <p/>
- * The GCM token is cached in the App-wide shared preferences. There's no particular harm in
- * requesting new tokens, so if the user clears the App data, that's fine -- we'll get a fresh
- * token and Push will react accordingly.
- */
-public class GcmTokenClient {
- private static final String LOG_TAG = "GeckoPushGCM";
-
- private static final String KEY_GCM_TOKEN = "gcm_token";
- private static final String KEY_GCM_TOKEN_TIMESTAMP = "gcm_token_timestamp";
-
- private final Context context;
-
- public GcmTokenClient(Context context) {
- this.context = context;
- }
-
- /**
- * Check the device to make sure it has the Google Play Services APK.
- * @param context Android context.
- */
- protected void ensurePlayServices(Context context) throws NeedsGooglePlayServicesException {
- final GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance();
- int resultCode = apiAvailability.isGooglePlayServicesAvailable(context);
- if (resultCode != ConnectionResult.SUCCESS) {
- Log.w(LOG_TAG, "This device does not support GCM! isGooglePlayServicesAvailable returned: " + resultCode);
- Log.w(LOG_TAG, "isGooglePlayServicesAvailable message: " + apiAvailability.getErrorString(resultCode));
- throw new NeedsGooglePlayServicesException(resultCode);
- }
- }
-
- /**
- * Get a GCM token (possibly cached).
- *
- * @param senderID to request token for.
- * @param debug whether to log debug details.
- * @return token and timestamp.
- * @throws NeedsGooglePlayServicesException if user action is needed to use Google Play Services.
- * @throws IOException if the token fetch failed.
- */
- public @NonNull Fetched getToken(@NonNull String senderID, boolean debug) throws NeedsGooglePlayServicesException, IOException {
- ensurePlayServices(this.context);
-
- final SharedPreferences sharedPrefs = GeckoSharedPrefs.forApp(context);
- String token = sharedPrefs.getString(KEY_GCM_TOKEN, null);
- long timestamp = sharedPrefs.getLong(KEY_GCM_TOKEN_TIMESTAMP, 0L);
- if (token != null && timestamp > 0L) {
- if (debug) {
- Log.i(LOG_TAG, "Cached GCM token exists: " + token);
- } else {
- Log.i(LOG_TAG, "Cached GCM token exists.");
- }
- return new Fetched(token, timestamp);
- }
-
- Log.i(LOG_TAG, "Cached GCM token does not exist; requesting new token with sender ID: " + senderID);
-
- final InstanceID instanceID = InstanceID.getInstance(context);
- token = instanceID.getToken(senderID, GoogleCloudMessaging.INSTANCE_ID_SCOPE, null);
- timestamp = System.currentTimeMillis();
-
- if (debug) {
- Log.i(LOG_TAG, "Got fresh GCM token; caching: " + token);
- } else {
- Log.i(LOG_TAG, "Got fresh GCM token; caching.");
- }
- sharedPrefs
- .edit()
- .putString(KEY_GCM_TOKEN, token)
- .putLong(KEY_GCM_TOKEN_TIMESTAMP, timestamp)
- .apply();
-
- return new Fetched(token, timestamp);
- }
-
- /**
- * Remove any cached GCM token.
- */
- public void invalidateToken() {
- final SharedPreferences sharedPrefs = GeckoSharedPrefs.forApp(context);
- sharedPrefs
- .edit()
- .remove(KEY_GCM_TOKEN)
- .remove(KEY_GCM_TOKEN_TIMESTAMP)
- .apply();
- }
-
- public class NeedsGooglePlayServicesException extends Exception {
- private static final long serialVersionUID = 4132853166L;
-
- private final int resultCode;
-
- NeedsGooglePlayServicesException(int resultCode) {
- super();
- this.resultCode = resultCode;
- }
-
- public void showErrorNotification() {
- final GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance();
- apiAvailability.showErrorNotification(context, resultCode);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/health/HealthRecorder.java b/mobile/android/base/java/org/mozilla/gecko/health/HealthRecorder.java
deleted file mode 100644
index a9f5b72f3..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/health/HealthRecorder.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.health;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-
-import org.json.JSONObject;
-
-/**
- * HealthRecorder is an interface into the Firefox Health Report storage system.
- */
-public interface HealthRecorder {
- /**
- * Returns whether the Health Recorder is actively recording events.
- */
- public boolean isEnabled();
-
- public void setCurrentSession(SessionInformation session);
- public void checkForOrphanSessions();
-
- public void recordGeckoStartupTime(long duration);
- public void recordJavaStartupTime(long duration);
- public void recordSearch(final String engineID, final String location);
- public void recordSessionEnd(String reason, SharedPreferences.Editor editor);
- public void recordSessionEnd(String reason, SharedPreferences.Editor editor, final int environment);
-
- public void onAppLocaleChanged(String to);
- public void onAddonChanged(String id, JSONObject json);
- public void onAddonUninstalling(String id);
- public void onEnvironmentChanged();
- public void onEnvironmentChanged(final boolean startNewSession, final String sessionEndReason);
-
- public void close(final Context context);
-
- public void processDelayed();
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/health/SessionInformation.java b/mobile/android/base/java/org/mozilla/gecko/health/SessionInformation.java
deleted file mode 100644
index ad65918e1..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/health/SessionInformation.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.health;
-
-import android.content.SharedPreferences;
-import android.util.Log;
-
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.GeckoAppShell;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-public class SessionInformation {
- private static final String LOG_TAG = "GeckoSessInfo";
-
- public static final String PREFS_SESSION_START = "sessionStart";
-
- public final long wallStartTime; // System wall clock.
- public final long realStartTime; // Realtime clock.
-
- private final boolean wasOOM;
- private final boolean wasStopped;
-
- private volatile long timedGeckoStartup = -1;
- private volatile long timedJavaStartup = -1;
-
- // Current sessions don't (right now) care about wasOOM/wasStopped.
- // Eventually we might want to lift that logic out of GeckoApp.
- public SessionInformation(long wallTime, long realTime) {
- this(wallTime, realTime, false, false);
- }
-
- // Previous sessions do...
- public SessionInformation(long wallTime, long realTime, boolean wasOOM, boolean wasStopped) {
- this.wallStartTime = wallTime;
- this.realStartTime = realTime;
- this.wasOOM = wasOOM;
- this.wasStopped = wasStopped;
- }
-
- /**
- * Initialize a new SessionInformation instance from the supplied prefs object.
- *
- * This includes retrieving OOM/crash data, as well as timings.
- *
- * If no wallStartTime was found, that implies that the previous
- * session was correctly recorded, and an object with a zero
- * wallStartTime is returned.
- */
- public static SessionInformation fromSharedPrefs(SharedPreferences prefs) {
- boolean wasOOM = prefs.getBoolean(GeckoAppShell.PREFS_OOM_EXCEPTION, false);
- boolean wasStopped = prefs.getBoolean(GeckoApp.PREFS_WAS_STOPPED, true);
- long wallStartTime = prefs.getLong(PREFS_SESSION_START, 0L);
- long realStartTime = 0L;
- Log.d(LOG_TAG, "Building SessionInformation from prefs: " +
- wallStartTime + ", " + realStartTime + ", " +
- wasStopped + ", " + wasOOM);
- return new SessionInformation(wallStartTime, realStartTime, wasOOM, wasStopped);
- }
-
- /**
- * Initialize a new SessionInformation instance to 'split' the current
- * session.
- */
- public static SessionInformation forRuntimeTransition() {
- final boolean wasOOM = false;
- final boolean wasStopped = true;
- final long wallStartTime = System.currentTimeMillis();
- final long realStartTime = android.os.SystemClock.elapsedRealtime();
- Log.v(LOG_TAG, "Recording runtime session transition: " +
- wallStartTime + ", " + realStartTime);
- return new SessionInformation(wallStartTime, realStartTime, wasOOM, wasStopped);
- }
-
- public boolean wasKilled() {
- return wasOOM || !wasStopped;
- }
-
- /**
- * Record the beginning of this session to SharedPreferences by
- * recording our start time. If a session was already recorded, it is
- * overwritten (there can only be one running session at a time). Does
- * not commit the editor.
- */
- public void recordBegin(SharedPreferences.Editor editor) {
- Log.d(LOG_TAG, "Recording start of session: " + this.wallStartTime);
- editor.putLong(PREFS_SESSION_START, this.wallStartTime);
- }
-
- /**
- * Record the completion of this session to SharedPreferences by
- * deleting our start time. Does not commit the editor.
- */
- public void recordCompletion(SharedPreferences.Editor editor) {
- Log.d(LOG_TAG, "Recording session done: " + this.wallStartTime);
- editor.remove(PREFS_SESSION_START);
- }
-
- /**
- * Return the JSON that we'll put in the DB for this session.
- */
- public JSONObject getCompletionJSON(String reason, long realEndTime) throws JSONException {
- long durationSecs = (realEndTime - this.realStartTime) / 1000;
- JSONObject out = new JSONObject();
- out.put("r", reason);
- out.put("d", durationSecs);
- if (this.timedGeckoStartup > 0) {
- out.put("sg", this.timedGeckoStartup);
- }
- if (this.timedJavaStartup > 0) {
- out.put("sj", this.timedJavaStartup);
- }
- return out;
- }
-
- public JSONObject getCrashedJSON() throws JSONException {
- JSONObject out = new JSONObject();
- // We use ints here instead of booleans, because we're packing
- // stuff into JSON, and saving bytes in the DB is a worthwhile
- // goal.
- out.put("oom", this.wasOOM ? 1 : 0);
- out.put("stopped", this.wasStopped ? 1 : 0);
- out.put("r", "A");
- return out;
- }
-
- public void setTimedGeckoStartup(final long duration) {
- timedGeckoStartup = duration;
- }
-
- public void setTimedJavaStartup(final long duration) {
- timedJavaStartup = duration;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/health/StubbedHealthRecorder.java b/mobile/android/base/java/org/mozilla/gecko/health/StubbedHealthRecorder.java
deleted file mode 100644
index 65a972985..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/health/StubbedHealthRecorder.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.health;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-
-import org.json.JSONObject;
-
-/**
- * StubbedHealthRecorder is an implementation of HealthRecorder that does (you guessed it!)
- * nothing.
- */
-public class StubbedHealthRecorder implements HealthRecorder {
- @Override
- public boolean isEnabled() { return false; }
-
- @Override
- public void setCurrentSession(SessionInformation session) { }
- @Override
- public void checkForOrphanSessions() { }
-
- @Override
- public void recordGeckoStartupTime(long duration) { }
- @Override
- public void recordJavaStartupTime(long duration) { }
- @Override
- public void recordSearch(final String engineID, final String location) { }
- @Override
- public void recordSessionEnd(String reason, SharedPreferences.Editor editor) { }
- @Override
- public void recordSessionEnd(String reason, SharedPreferences.Editor editor, final int environment) { }
-
- @Override
- public void onAppLocaleChanged(String to) { }
- @Override
- public void onAddonChanged(String id, JSONObject json) { }
- @Override
- public void onAddonUninstalling(String id) { }
- @Override
- public void onEnvironmentChanged() { }
- @Override
- public void onEnvironmentChanged(final boolean startNewSession, final String sessionEndReason) { }
-
- @Override
- public void close(final Context context) { }
-
- @Override
- public void processDelayed() { }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/BookmarkFolderView.java b/mobile/android/base/java/org/mozilla/gecko/home/BookmarkFolderView.java
deleted file mode 100644
index 566422faf..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/BookmarkFolderView.java
+++ /dev/null
@@ -1,147 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.UIAsyncTask;
-
-import android.content.Context;
-import android.support.annotation.NonNull;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import java.lang.ref.WeakReference;
-import java.util.Collections;
-import java.util.Set;
-import java.util.TreeSet;
-
-public class BookmarkFolderView extends LinearLayout {
- private static final Set<Integer> FOLDERS_WITH_COUNT;
-
- static {
- final Set<Integer> folders = new TreeSet<>();
- folders.add(BrowserContract.Bookmarks.FAKE_READINGLIST_SMARTFOLDER_ID);
-
- FOLDERS_WITH_COUNT = Collections.unmodifiableSet(folders);
- }
-
- public enum FolderState {
- /**
- * A standard folder, i.e. a folder in a list of bookmarks and folders.
- */
- FOLDER(R.drawable.folder_closed),
-
- /**
- * The parent folder: this indicates that you are able to return to the previous
- * folder ("Back to {name}").
- */
- PARENT(R.drawable.bookmark_folder_arrow_up),
-
- /**
- * The reading list smartfolder: this displays a reading list icon instead of the
- * normal folder icon.
- */
- READING_LIST(R.drawable.reading_list_folder);
-
- public final int image;
-
- FolderState(final int image) { this.image = image; }
- }
-
- private final TextView mTitle;
- private final TextView mSubtitle;
-
- private final ImageView mIcon;
-
- public BookmarkFolderView(Context context) {
- this(context, null);
- }
-
- public BookmarkFolderView(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- LayoutInflater.from(context).inflate(R.layout.two_line_folder_row, this);
-
- mTitle = (TextView) findViewById(R.id.title);
- mSubtitle = (TextView) findViewById(R.id.subtitle);
- mIcon = (ImageView) findViewById(R.id.icon);
- }
-
- public void update(String title, int folderID) {
- setTitle(title);
- setID(folderID);
- }
-
- private void setTitle(String title) {
- mTitle.setText(title);
- }
-
- private static class ItemCountUpdateTask extends UIAsyncTask.WithoutParams<Integer> {
- private final WeakReference<TextView> mTextViewReference;
- private final int mFolderID;
-
- public ItemCountUpdateTask(final WeakReference<TextView> textViewReference,
- final int folderID) {
- super(ThreadUtils.getBackgroundHandler());
-
- mTextViewReference = textViewReference;
- mFolderID = folderID;
- }
-
- @Override
- protected Integer doInBackground() {
- final TextView textView = mTextViewReference.get();
-
- if (textView == null) {
- return null;
- }
-
- final BrowserDB db = BrowserDB.from(textView.getContext());
- return db.getBookmarkCountForFolder(textView.getContext().getContentResolver(), mFolderID);
- }
-
- @Override
- protected void onPostExecute(Integer count) {
- final TextView textView = mTextViewReference.get();
-
- if (textView == null) {
- return;
- }
-
- final String text;
- if (count == 1) {
- text = textView.getContext().getResources().getString(R.string.bookmark_folder_one_item);
- } else {
- text = textView.getContext().getResources().getString(R.string.bookmark_folder_items, count);
- }
-
- textView.setText(text);
- textView.setVisibility(View.VISIBLE);
- }
- }
-
- private void setID(final int folderID) {
- if (FOLDERS_WITH_COUNT.contains(folderID)) {
- final WeakReference<TextView> subTitleReference = new WeakReference<TextView>(mSubtitle);
-
- new ItemCountUpdateTask(subTitleReference, folderID).execute();
- } else {
- mSubtitle.setVisibility(View.GONE);
- }
- }
-
- public void setState(@NonNull FolderState state) {
- mIcon.setImageResource(state.image);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/BookmarkScreenshotRow.java b/mobile/android/base/java/org/mozilla/gecko/home/BookmarkScreenshotRow.java
deleted file mode 100644
index a1efff049..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/BookmarkScreenshotRow.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.util.AttributeSet;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.BrowserContract.UrlAnnotations;
-
-import java.text.DateFormat;
-import java.text.FieldPosition;
-import java.util.Date;
-
-/**
- * An entry of the screenshot list in the bookmarks panel.
- */
-class BookmarkScreenshotRow extends LinearLayout {
- private TextView titleView;
- private TextView dateView;
-
- // This DateFormat uses the current locale at instantiation time, which won't get updated if the locale is changed.
- // Since it's just a date, it's probably not worth the code complexity to fix that.
- private static final DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.LONG);
- private static final DateFormat timeFormat = DateFormat.getTimeInstance(DateFormat.SHORT);
-
- // This parameter to DateFormat.format has no impact on the result but rather gets mutated by the method to
- // identify where a certain field starts and ends (by index). This is useful if you want to later modify the String;
- // I'm not sure why this argument isn't optional.
- private static final FieldPosition dummyFieldPosition = new FieldPosition(-1);
-
- public BookmarkScreenshotRow(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public void onFinishInflate() {
- super.onFinishInflate();
- titleView = (TextView) findViewById(R.id.title);
- dateView = (TextView) findViewById(R.id.date);
- }
-
- public void updateFromCursor(final Cursor c) {
- titleView.setText(getTitleFromCursor(c));
- dateView.setText(getDateFromCursor(c));
- }
-
- private static String getTitleFromCursor(final Cursor c) {
- final int index = c.getColumnIndexOrThrow(UrlAnnotations.URL);
- return c.getString(index);
- }
-
- private static String getDateFromCursor(final Cursor c) {
- final long timestamp = c.getLong(c.getColumnIndexOrThrow(UrlAnnotations.DATE_CREATED));
- final Date date = new Date(timestamp);
- final StringBuffer sb = new StringBuffer();
- dateFormat.format(date, sb, dummyFieldPosition)
- .append(" - ");
- timeFormat.format(date, sb, dummyFieldPosition);
- return sb.toString();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListAdapter.java
deleted file mode 100644
index b31116693..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListAdapter.java
+++ /dev/null
@@ -1,352 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.BrowserContract.Bookmarks;
-import org.mozilla.gecko.home.BookmarkFolderView.FolderState;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.view.View;
-
-/**
- * Adapter to back the BookmarksListView with a list of bookmarks.
- */
-class BookmarksListAdapter extends MultiTypeCursorAdapter {
- private static final int VIEW_TYPE_BOOKMARK_ITEM = 0;
- private static final int VIEW_TYPE_FOLDER = 1;
- private static final int VIEW_TYPE_SCREENSHOT = 2;
-
- private static final int[] VIEW_TYPES = new int[] { VIEW_TYPE_BOOKMARK_ITEM, VIEW_TYPE_FOLDER, VIEW_TYPE_SCREENSHOT };
- private static final int[] LAYOUT_TYPES =
- new int[] { R.layout.bookmark_item_row, R.layout.bookmark_folder_row, R.layout.bookmark_screenshot_row };
-
- public enum RefreshType implements Parcelable {
- PARENT,
- CHILD;
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(ordinal());
- }
-
- public static final Creator<RefreshType> CREATOR = new Creator<RefreshType>() {
- @Override
- public RefreshType createFromParcel(final Parcel source) {
- return RefreshType.values()[source.readInt()];
- }
-
- @Override
- public RefreshType[] newArray(final int size) {
- return new RefreshType[size];
- }
- };
- }
-
- public static class FolderInfo implements Parcelable {
- public final int id;
- public final String title;
-
- public FolderInfo(int id) {
- this(id, "");
- }
-
- public FolderInfo(Parcel in) {
- this(in.readInt(), in.readString());
- }
-
- public FolderInfo(int id, String title) {
- this.id = id;
- this.title = title;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(id);
- dest.writeString(title);
- }
-
- public static final Creator<FolderInfo> CREATOR = new Creator<FolderInfo>() {
- @Override
- public FolderInfo createFromParcel(Parcel in) {
- return new FolderInfo(in);
- }
-
- @Override
- public FolderInfo[] newArray(int size) {
- return new FolderInfo[size];
- }
- };
- }
-
- // A listener that knows how to refresh the list for a given folder id.
- // This is usually implemented by the enclosing fragment/activity.
- public static interface OnRefreshFolderListener {
- // The folder id to refresh the list with.
- public void onRefreshFolder(FolderInfo folderInfo, RefreshType refreshType);
- }
-
- /**
- * The type of data a bookmarks folder can display. This can be used to
- * distinguish bookmark folders from "smart folders" that contain non-bookmark
- * entries but still appear in the Bookmarks panel.
- */
- public enum FolderType {
- BOOKMARKS,
- SCREENSHOTS,
- }
-
- // mParentStack holds folder info instances (id + title) that allow
- // us to navigate back up the folder hierarchy.
- private LinkedList<FolderInfo> mParentStack;
-
- // Refresh folder listener.
- private OnRefreshFolderListener mListener;
-
- private FolderType openFolderType = FolderType.BOOKMARKS;
-
- public BookmarksListAdapter(Context context, Cursor cursor, List<FolderInfo> parentStack) {
- // Initializing with a null cursor.
- super(context, cursor, VIEW_TYPES, LAYOUT_TYPES);
-
- if (parentStack == null) {
- mParentStack = new LinkedList<FolderInfo>();
- } else {
- mParentStack = new LinkedList<FolderInfo>(parentStack);
- }
- }
-
- public void restoreData(List<FolderInfo> parentStack) {
- mParentStack = new LinkedList<FolderInfo>(parentStack);
- notifyDataSetChanged();
- }
-
- public List<FolderInfo> getParentStack() {
- return Collections.unmodifiableList(mParentStack);
- }
-
- public FolderType getOpenFolderType() {
- return openFolderType;
- }
-
- /**
- * Moves to parent folder, if one exists.
- *
- * @return Whether the adapter successfully moved to a parent folder.
- */
- public boolean moveToParentFolder() {
- // If we're already at the root, we can't move to a parent folder.
- // An empty parent stack here means we're still waiting for the
- // initial list of bookmarks and can't go to a parent folder.
- if (mParentStack.size() <= 1) {
- return false;
- }
-
- if (mListener != null) {
- // We pick the second folder in the stack as it represents
- // the parent folder.
- mListener.onRefreshFolder(mParentStack.get(1), RefreshType.PARENT);
- }
-
- return true;
- }
-
- /**
- * Moves to child folder, given a folderId.
- *
- * @param folderId The id of the folder to show.
- * @param folderTitle The title of the folder to show.
- */
- public void moveToChildFolder(int folderId, String folderTitle) {
- FolderInfo folderInfo = new FolderInfo(folderId, folderTitle);
-
- if (mListener != null) {
- mListener.onRefreshFolder(folderInfo, RefreshType.CHILD);
- }
- }
-
- /**
- * Set a listener that can refresh this adapter.
- *
- * @param listener The listener that can refresh the adapter.
- */
- public void setOnRefreshFolderListener(OnRefreshFolderListener listener) {
- mListener = listener;
- }
-
- private boolean isCurrentFolder(FolderInfo folderInfo) {
- return (mParentStack.size() > 0 &&
- mParentStack.peek().id == folderInfo.id);
- }
-
- public void swapCursor(Cursor c, FolderInfo folderInfo, RefreshType refreshType) {
- updateOpenFolderType(folderInfo);
- switch (refreshType) {
- case PARENT:
- if (!isCurrentFolder(folderInfo)) {
- mParentStack.removeFirst();
- }
- break;
-
- case CHILD:
- if (!isCurrentFolder(folderInfo)) {
- mParentStack.addFirst(folderInfo);
- }
- break;
-
- default:
- // Do nothing;
- }
-
- swapCursor(c);
- }
-
- private void updateOpenFolderType(final FolderInfo folderInfo) {
- if (folderInfo.id == Bookmarks.FIXED_SCREENSHOT_FOLDER_ID) {
- openFolderType = FolderType.SCREENSHOTS;
- } else {
- openFolderType = FolderType.BOOKMARKS;
- }
- }
-
- @Override
- public int getItemViewType(int position) {
- // The position also reflects the opened child folder row.
- if (isShowingChildFolder()) {
- if (position == 0) {
- return VIEW_TYPE_FOLDER;
- }
-
- // Accounting for the folder view.
- position--;
- }
-
- if (openFolderType == FolderType.SCREENSHOTS) {
- return VIEW_TYPE_SCREENSHOT;
- }
-
- final Cursor c = getCursor(position);
- if (c.getInt(c.getColumnIndexOrThrow(Bookmarks.TYPE)) == Bookmarks.TYPE_FOLDER) {
- return VIEW_TYPE_FOLDER;
- }
-
- // Default to returning normal item type.
- return VIEW_TYPE_BOOKMARK_ITEM;
- }
-
- /**
- * Get the title of the folder given a cursor moved to the position.
- *
- * @param context The context of the view.
- * @param cursor A cursor moved to the required position.
- * @return The title of the folder at the position.
- */
- public String getFolderTitle(Context context, Cursor c) {
- String guid = c.getString(c.getColumnIndexOrThrow(Bookmarks.GUID));
-
- // If we don't have a special GUID, just return the folder title from the DB.
- if (guid == null || guid.length() == 12) {
- return c.getString(c.getColumnIndexOrThrow(Bookmarks.TITLE));
- }
-
- Resources res = context.getResources();
-
- // Use localized strings for special folder names.
- if (guid.equals(Bookmarks.FAKE_DESKTOP_FOLDER_GUID)) {
- return res.getString(R.string.bookmarks_folder_desktop);
- } else if (guid.equals(Bookmarks.MENU_FOLDER_GUID)) {
- return res.getString(R.string.bookmarks_folder_menu);
- } else if (guid.equals(Bookmarks.TOOLBAR_FOLDER_GUID)) {
- return res.getString(R.string.bookmarks_folder_toolbar);
- } else if (guid.equals(Bookmarks.UNFILED_FOLDER_GUID)) {
- return res.getString(R.string.bookmarks_folder_unfiled);
- } else if (guid.equals(Bookmarks.SCREENSHOT_FOLDER_GUID)) {
- return res.getString(R.string.screenshot_folder_label_in_bookmarks);
- } else if (guid.equals(Bookmarks.FAKE_READINGLIST_SMARTFOLDER_GUID)) {
- return res.getString(R.string.readinglist_smartfolder_label_in_bookmarks);
- }
-
- // If for some reason we have a folder with a special GUID, but it's not one of
- // the special folders we expect in the UI, just return the title from the DB.
- return c.getString(c.getColumnIndexOrThrow(Bookmarks.TITLE));
- }
-
- /**
- * @return true, if currently showing a child folder, false otherwise.
- */
- public boolean isShowingChildFolder() {
- if (mParentStack.size() == 0) {
- return false;
- }
-
- return (mParentStack.peek().id != Bookmarks.FIXED_ROOT_ID);
- }
-
- @Override
- public int getCount() {
- return super.getCount() + (isShowingChildFolder() ? 1 : 0);
- }
-
- @Override
- public void bindView(View view, Context context, int position) {
- final int viewType = getItemViewType(position);
-
- final Cursor cursor;
- if (isShowingChildFolder()) {
- if (position == 0) {
- cursor = null;
- } else {
- // Accounting for the folder view.
- position--;
- cursor = getCursor(position);
- }
- } else {
- cursor = getCursor(position);
- }
-
- if (viewType == VIEW_TYPE_SCREENSHOT) {
- ((BookmarkScreenshotRow) view).updateFromCursor(cursor);
- } else if (viewType == VIEW_TYPE_BOOKMARK_ITEM) {
- final TwoLinePageRow row = (TwoLinePageRow) view;
- row.updateFromCursor(cursor);
- } else {
- final BookmarkFolderView row = (BookmarkFolderView) view;
- if (cursor == null) {
- final Resources res = context.getResources();
- row.update(res.getString(R.string.home_move_back_to_filter, mParentStack.get(1).title), -1);
- row.setState(FolderState.PARENT);
- } else {
- int id = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID));
-
- row.update(getFolderTitle(context, cursor), id);
-
- if (id == Bookmarks.FAKE_READINGLIST_SMARTFOLDER_ID) {
- row.setState(FolderState.READING_LIST);
- } else {
- row.setState(FolderState.FOLDER);
- }
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListView.java b/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListView.java
deleted file mode 100644
index 94157be10..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListView.java
+++ /dev/null
@@ -1,218 +0,0 @@
- /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import java.util.EnumSet;
-import java.util.List;
-
-import android.util.Log;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.BrowserContract.Bookmarks;
-import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.util.AttributeSet;
-import android.view.KeyEvent;
-import android.view.View;
-import android.widget.AdapterView;
-import android.widget.HeaderViewListAdapter;
-import android.widget.ListAdapter;
-
-import org.mozilla.gecko.reader.SavedReaderViewHelper;
-import org.mozilla.gecko.util.NetworkUtils;
-
-/**
- * A ListView of bookmarks.
- */
-public class BookmarksListView extends HomeListView
- implements AdapterView.OnItemClickListener {
- public static final String LOGTAG = "GeckoBookmarksListView";
-
- public BookmarksListView(Context context) {
- this(context, null);
- }
-
- public BookmarksListView(Context context, AttributeSet attrs) {
- this(context, attrs, R.attr.bookmarksListViewStyle);
- }
-
- public BookmarksListView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- setOnItemClickListener(this);
-
- setOnKeyListener(new View.OnKeyListener() {
- @Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- final int action = event.getAction();
-
- // If the user hit the BACK key, try to move to the parent folder.
- if (action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
- return getBookmarksListAdapter().moveToParentFolder();
- }
- return false;
- }
- });
- }
-
- /**
- * Get the appropriate telemetry extra for a given folder.
- *
- * baseFolderID is the ID of the first-level folder in the parent stack, i.e. the first folder
- * that was selected from the root hierarchy (e.g. Desktop, Reading List, or any mobile first-level
- * subfolder). If the current folder is a first-level folder, then the fixed root ID may be used
- * instead.
- *
- * We use baseFolderID only to distinguish whether or not we're currently in a desktop subfolder.
- * If it isn't equal to FAKE_DESKTOP_FOLDER_ID we know we're in a mobile subfolder, or one
- * of the smartfolders.
- */
- private String getTelemetryExtraForFolder(int folderID, int baseFolderID) {
- if (folderID == Bookmarks.FAKE_DESKTOP_FOLDER_ID) {
- return "folder_desktop";
- } else if (folderID == Bookmarks.FIXED_SCREENSHOT_FOLDER_ID) {
- return "folder_screenshots";
- } else if (folderID == Bookmarks.FAKE_READINGLIST_SMARTFOLDER_ID) {
- return "folder_reading_list";
- } else {
- // The stack depth is 2 for either the fake desktop folder, or any subfolder of mobile
- // bookmarks, we subtract these offsets so that any direct subfolder of mobile
- // has a level equal to 1. (Desktop folders will be one level deeper due to the
- // fake desktop folder, hence subtract 2.)
- if (baseFolderID == Bookmarks.FAKE_DESKTOP_FOLDER_ID) {
- return "folder_desktop_subfolder";
- } else {
- return "folder_mobile_subfolder";
- }
- }
- }
-
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- final BookmarksListAdapter adapter = getBookmarksListAdapter();
- if (adapter.isShowingChildFolder()) {
- if (position == 0) {
- // If we tap on an opened folder, move back to parent folder.
-
- final List<BookmarksListAdapter.FolderInfo> parentStack = ((BookmarksListAdapter) getAdapter()).getParentStack();
- if (parentStack.size() < 2) {
- throw new IllegalStateException("Cannot move to parent folder if we are already in the root folder");
- }
-
- // The first item (top of stack) is the current folder, we're returning to the next one
- BookmarksListAdapter.FolderInfo folder = parentStack.get(1);
- final int parentID = folder.id;
- final int baseFolderID;
- if (parentStack.size() > 2) {
- baseFolderID = parentStack.get(parentStack.size() - 2).id;
- } else {
- baseFolderID = Bookmarks.FIXED_ROOT_ID;
- }
-
- final String extra = getTelemetryExtraForFolder(parentID, baseFolderID);
-
- // Move to parent _after_ retrieving stack information
- adapter.moveToParentFolder();
-
- Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.LIST_ITEM, extra);
- return;
- }
-
- // Accounting for the folder view.
- position--;
- }
-
- final Cursor cursor = adapter.getCursor();
- if (cursor == null) {
- return;
- }
-
- cursor.moveToPosition(position);
-
- if (adapter.getOpenFolderType() == BookmarksListAdapter.FolderType.SCREENSHOTS) {
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, "bookmarks-screenshot");
-
- final String fileUrl = "file://" + cursor.getString(cursor.getColumnIndex(BrowserContract.UrlAnnotations.VALUE));
- getOnUrlOpenListener().onUrlOpen(fileUrl, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
- return;
- }
-
- int type = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks.TYPE));
- if (type == Bookmarks.TYPE_FOLDER) {
- // If we're clicking on a folder, update adapter to move to that folder
- final int folderId = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID));
- final String folderTitle = adapter.getFolderTitle(parent.getContext(), cursor);
- adapter.moveToChildFolder(folderId, folderTitle);
-
- final List<BookmarksListAdapter.FolderInfo> parentStack = ((BookmarksListAdapter) getAdapter()).getParentStack();
-
- final int baseFolderID;
- if (parentStack.size() > 2) {
- baseFolderID = parentStack.get(parentStack.size() - 2).id;
- } else {
- baseFolderID = Bookmarks.FIXED_ROOT_ID;
- }
-
- final String extra = getTelemetryExtraForFolder(folderId, baseFolderID);
- Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.LIST_ITEM, extra);
- } else {
- // Otherwise, just open the URL
- final String url = cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.URL));
-
- final SavedReaderViewHelper rvh = SavedReaderViewHelper.getSavedReaderViewHelper(getContext());
-
- final String extra;
- if (rvh.isURLCached(url)) {
- extra = "bookmarks-reader";
- } else {
- extra = "bookmarks";
- }
-
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, extra);
- Telemetry.addToHistogram("FENNEC_LOAD_SAVED_PAGE", NetworkUtils.isConnected(getContext()) ? 2 : 3);
-
- // This item is a TwoLinePageRow, so we allow switch-to-tab.
- getOnUrlOpenListener().onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
- }
- }
-
- @Override
- public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
- // Adjust the item position to account for the parent folder row that is inserted
- // at the top of the list when viewing the contents of a folder.
- final BookmarksListAdapter adapter = getBookmarksListAdapter();
- if (adapter.isShowingChildFolder()) {
- position--;
- }
-
- // Temporarily prevent crashes until we figure out what we actually want to do here (bug 1252316).
- if (adapter.getOpenFolderType() == BookmarksListAdapter.FolderType.SCREENSHOTS) {
- return false;
- }
-
- return super.onItemLongClick(parent, view, position, id);
- }
-
- private BookmarksListAdapter getBookmarksListAdapter() {
- BookmarksListAdapter adapter;
- ListAdapter listAdapter = getAdapter();
- if (listAdapter instanceof HeaderViewListAdapter) {
- adapter = (BookmarksListAdapter) ((HeaderViewListAdapter) listAdapter).getWrappedAdapter();
- } else {
- adapter = (BookmarksListAdapter) listAdapter;
- }
- return adapter;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksPanel.java b/mobile/android/base/java/org/mozilla/gecko/home/BookmarksPanel.java
deleted file mode 100644
index 4b4781996..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksPanel.java
+++ /dev/null
@@ -1,316 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.List;
-
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.BrowserContract.Bookmarks;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
-import org.mozilla.gecko.home.BookmarksListAdapter.FolderInfo;
-import org.mozilla.gecko.home.BookmarksListAdapter.OnRefreshFolderListener;
-import org.mozilla.gecko.home.BookmarksListAdapter.RefreshType;
-import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
-import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-
-import android.app.Activity;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.database.Cursor;
-import android.database.MergeCursor;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.support.annotation.NonNull;
-import android.support.v4.app.LoaderManager;
-import android.support.v4.content.Loader;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewStub;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-/**
- * A page in about:home that displays a ListView of bookmarks.
- */
-public class BookmarksPanel extends HomeFragment {
- public static final String LOGTAG = "GeckoBookmarksPanel";
-
- // Cursor loader ID for list of bookmarks.
- private static final int LOADER_ID_BOOKMARKS_LIST = 0;
-
- // Information about the target bookmarks folder.
- private static final String BOOKMARKS_FOLDER_INFO = "folder_info";
-
- // Refresh type for folder refreshing loader.
- private static final String BOOKMARKS_REFRESH_TYPE = "refresh_type";
-
- // List of bookmarks.
- private BookmarksListView mList;
-
- // Adapter for list of bookmarks.
- private BookmarksListAdapter mListAdapter;
-
- // Adapter's parent stack.
- private List<FolderInfo> mSavedParentStack;
-
- // Reference to the View to display when there are no results.
- private View mEmptyView;
-
- // Callback for cursor loaders.
- private CursorLoaderCallbacks mLoaderCallbacks;
-
- @Override
- public void restoreData(@NonNull Bundle data) {
- final ArrayList<FolderInfo> stack = data.getParcelableArrayList("parentStack");
- if (stack == null) {
- return;
- }
-
- if (mListAdapter == null) {
- mSavedParentStack = new LinkedList<FolderInfo>(stack);
- } else {
- mListAdapter.restoreData(stack);
- }
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- final View view = inflater.inflate(R.layout.home_bookmarks_panel, container, false);
-
- mList = (BookmarksListView) view.findViewById(R.id.bookmarks_list);
-
- mList.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() {
- @Override
- public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
- final int type = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks.TYPE));
- if (type == Bookmarks.TYPE_FOLDER) {
- // We don't show a context menu for folders
- return null;
- }
- final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
- info.url = cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.URL));
- info.title = cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.TITLE));
- info.bookmarkId = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID));
- info.itemType = RemoveItemType.BOOKMARKS;
- return info;
- }
- });
-
- return view;
- }
-
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
-
- OnUrlOpenListener listener = null;
- try {
- listener = (OnUrlOpenListener) getActivity();
- } catch (ClassCastException e) {
- throw new ClassCastException(getActivity().toString()
- + " must implement HomePager.OnUrlOpenListener");
- }
-
- mList.setTag(HomePager.LIST_TAG_BOOKMARKS);
- mList.setOnUrlOpenListener(listener);
-
- registerForContextMenu(mList);
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- final Activity activity = getActivity();
-
- // Setup the list adapter.
- mListAdapter = new BookmarksListAdapter(activity, null, mSavedParentStack);
- mListAdapter.setOnRefreshFolderListener(new OnRefreshFolderListener() {
- @Override
- public void onRefreshFolder(FolderInfo folderInfo, RefreshType refreshType) {
- // Restart the loader with folder as the argument.
- Bundle bundle = new Bundle();
- bundle.putParcelable(BOOKMARKS_FOLDER_INFO, folderInfo);
- bundle.putParcelable(BOOKMARKS_REFRESH_TYPE, refreshType);
- getLoaderManager().restartLoader(LOADER_ID_BOOKMARKS_LIST, bundle, mLoaderCallbacks);
- }
- });
- mList.setAdapter(mListAdapter);
-
- // Create callbacks before the initial loader is started.
- mLoaderCallbacks = new CursorLoaderCallbacks();
- loadIfVisible();
- }
-
- @Override
- public void onDestroyView() {
- mList = null;
- mListAdapter = null;
- mEmptyView = null;
- super.onDestroyView();
- }
-
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
-
- if (isVisible()) {
- // The parent stack is saved just so that the folder state can be
- // restored on rotation.
- mSavedParentStack = mListAdapter.getParentStack();
- }
- }
-
- @Override
- protected void load() {
- final Bundle bundle;
- if (mSavedParentStack != null && mSavedParentStack.size() > 1) {
- bundle = new Bundle();
- bundle.putParcelable(BOOKMARKS_FOLDER_INFO, mSavedParentStack.get(0));
- bundle.putParcelable(BOOKMARKS_REFRESH_TYPE, RefreshType.CHILD);
- } else {
- bundle = null;
- }
-
- getLoaderManager().initLoader(LOADER_ID_BOOKMARKS_LIST, bundle, mLoaderCallbacks);
- }
-
- private void updateUiFromCursor(Cursor c) {
- if ((c == null || c.getCount() == 0) && mEmptyView == null) {
- // Set empty page view. We delay this so that the empty view won't flash.
- final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.home_empty_view_stub);
- mEmptyView = emptyViewStub.inflate();
-
- final ImageView emptyIcon = (ImageView) mEmptyView.findViewById(R.id.home_empty_image);
- emptyIcon.setImageResource(R.drawable.icon_bookmarks_empty);
-
- final TextView emptyText = (TextView) mEmptyView.findViewById(R.id.home_empty_text);
- emptyText.setText(R.string.home_bookmarks_empty);
-
- mList.setEmptyView(mEmptyView);
- }
- }
-
- /**
- * Loader for the list for bookmarks.
- */
- private static class BookmarksLoader extends SimpleCursorLoader {
- private final FolderInfo mFolderInfo;
- private final RefreshType mRefreshType;
- private final BrowserDB mDB;
-
- public BookmarksLoader(Context context) {
- this(context,
- new FolderInfo(Bookmarks.FIXED_ROOT_ID, context.getResources().getString(R.string.bookmarks_title)),
- RefreshType.CHILD);
- }
-
- public BookmarksLoader(Context context, FolderInfo folderInfo, RefreshType refreshType) {
- super(context);
- mFolderInfo = folderInfo;
- mRefreshType = refreshType;
- mDB = BrowserDB.from(context);
- }
-
- @Override
- public Cursor loadCursor() {
- final boolean isRootFolder = mFolderInfo.id == BrowserContract.Bookmarks.FIXED_ROOT_ID;
-
- final ContentResolver contentResolver = getContext().getContentResolver();
-
- Cursor partnerCursor = null;
- Cursor userCursor = null;
-
- if (GeckoSharedPrefs.forProfile(getContext()).getBoolean(GeckoPreferences.PREFS_READ_PARTNER_BOOKMARKS_PROVIDER, false)
- && (isRootFolder || mFolderInfo.id <= Bookmarks.FAKE_PARTNER_BOOKMARKS_START)) {
- partnerCursor = contentResolver.query(PartnerBookmarksProviderProxy.getUriForBookmarks(getContext(), mFolderInfo.id), null, null, null, null, null);
- }
-
- if (isRootFolder || mFolderInfo.id > Bookmarks.FAKE_PARTNER_BOOKMARKS_START) {
- userCursor = mDB.getBookmarksInFolder(contentResolver, mFolderInfo.id);
- }
-
-
- if (partnerCursor == null && userCursor == null) {
- return null;
- } else if (partnerCursor == null) {
- return userCursor;
- } else if (userCursor == null) {
- return partnerCursor;
- } else {
- return new MergeCursor(new Cursor[] { partnerCursor, userCursor });
- }
- }
-
- @Override
- public void onContentChanged() {
- // Invalidate the cached value that keeps track of whether or
- // not desktop bookmarks exist.
- mDB.invalidate();
- super.onContentChanged();
- }
-
- public FolderInfo getFolderInfo() {
- return mFolderInfo;
- }
-
- public RefreshType getRefreshType() {
- return mRefreshType;
- }
- }
-
- /**
- * Loader callbacks for the LoaderManager of this fragment.
- */
- private class CursorLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
- @Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- if (args == null) {
- return new BookmarksLoader(getActivity());
- } else {
- FolderInfo folderInfo = (FolderInfo) args.getParcelable(BOOKMARKS_FOLDER_INFO);
- RefreshType refreshType = (RefreshType) args.getParcelable(BOOKMARKS_REFRESH_TYPE);
- return new BookmarksLoader(getActivity(), folderInfo, refreshType);
- }
- }
-
- @Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
- BookmarksLoader bl = (BookmarksLoader) loader;
- mListAdapter.swapCursor(c, bl.getFolderInfo(), bl.getRefreshType());
-
- if (mPanelStateChangeListener != null) {
- final List<FolderInfo> parentStack = mListAdapter.getParentStack();
- final Bundle bundle = new Bundle();
-
- // Bundle likes to store ArrayLists or Arrays, but we've got a generic List (which
- // is actually an unmodifiable wrapper around a LinkedList). We'll need to do a
- // LinkedList conversion at the other end, when saving we need to use this awkward
- // syntax to create an Array.
- bundle.putParcelableArrayList("parentStack", new ArrayList<FolderInfo>(parentStack));
-
- mPanelStateChangeListener.onStateChanged(bundle);
- }
- updateUiFromCursor(c);
- }
-
- @Override
- public void onLoaderReset(Loader<Cursor> loader) {
- if (mList != null) {
- mListAdapter.swapCursor(null);
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/BrowserSearch.java b/mobile/android/base/java/org/mozilla/gecko/home/BrowserSearch.java
deleted file mode 100644
index 7732932fe..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/BrowserSearch.java
+++ /dev/null
@@ -1,1316 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import java.io.BufferedReader;
-import java.io.InputStreamReader;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Locale;
-
-import android.content.SharedPreferences;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.PrefsHelper;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.SuggestClient;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.BrowserContract.History;
-import org.mozilla.gecko.db.BrowserContract.URLColumns;
-import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
-import org.mozilla.gecko.home.SearchLoader.SearchCursorLoader;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-import org.mozilla.gecko.toolbar.AutocompleteHandler;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.StringUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.app.Activity;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.view.ContextMenu.ContextMenuInfo;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.AsyncTaskLoader;
-import android.support.v4.content.Loader;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.ContextMenu;
-import android.view.LayoutInflater;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.ViewGroup;
-import android.view.ViewStub;
-import android.view.WindowManager.LayoutParams;
-import android.view.animation.AccelerateInterpolator;
-import android.view.animation.Animation;
-import android.view.animation.TranslateAnimation;
-import android.widget.AdapterView;
-import android.widget.LinearLayout;
-import android.widget.ListView;
-import android.widget.TextView;
-
-/**
- * Fragment that displays frecency search results in a ListView.
- */
-public class BrowserSearch extends HomeFragment
- implements GeckoEventListener,
- SearchEngineBar.OnSearchBarClickListener {
-
- @RobocopTarget
- public interface SuggestClientFactory {
- public SuggestClient getSuggestClient(Context context, String template, int timeout, int max);
- }
-
- @RobocopTarget
- public static class DefaultSuggestClientFactory implements SuggestClientFactory {
- @Override
- public SuggestClient getSuggestClient(Context context, String template, int timeout, int max) {
- return new SuggestClient(context, template, timeout, max, true);
- }
- }
-
- /**
- * Set this to mock the suggestion mechanism. Public for access from tests.
- */
- @RobocopTarget
- public static volatile SuggestClientFactory sSuggestClientFactory = new DefaultSuggestClientFactory();
-
- // Logging tag name
- private static final String LOGTAG = "GeckoBrowserSearch";
-
- // Cursor loader ID for search query
- private static final int LOADER_ID_SEARCH = 0;
-
- // AsyncTask loader ID for suggestion query
- private static final int LOADER_ID_SUGGESTION = 1;
- private static final int LOADER_ID_SAVED_SUGGESTION = 2;
-
- // Timeout for the suggestion client to respond
- private static final int SUGGESTION_TIMEOUT = 3000;
-
- // Maximum number of suggestions from the search engine's suggestion client. This impacts network traffic and device
- // data consumption whereas R.integer.max_saved_suggestions controls how many suggestion to show in the UI.
- private static final int NETWORK_SUGGESTION_MAX = 3;
-
- // The maximum number of rows deep in a search we'll dig
- // for an autocomplete result
- private static final int MAX_AUTOCOMPLETE_SEARCH = 20;
-
- // Length of https:// + 1 required to make autocomplete
- // fill in the domain, for both http:// and https://
- private static final int HTTPS_PREFIX_LENGTH = 9;
-
- // Duration for fade-in animation
- private static final int ANIMATION_DURATION = 250;
-
- // Holds the current search term to use in the query
- private volatile String mSearchTerm;
-
- // Adapter for the list of search results
- private SearchAdapter mAdapter;
-
- // The view shown by the fragment
- private LinearLayout mView;
-
- // The list showing search results
- private HomeListView mList;
-
- // The bar on the bottom of the screen displaying search engine options.
- private SearchEngineBar mSearchEngineBar;
-
- // Client that performs search suggestion queries.
- // Public for testing.
- @RobocopTarget
- public volatile SuggestClient mSuggestClient;
-
- // List of search engines from Gecko.
- // Do not mutate this list.
- // Access to this member must only occur from the UI thread.
- private List<SearchEngine> mSearchEngines;
-
- // Search history suggestions
- private ArrayList<String> mSearchHistorySuggestions;
-
- // Track the locale that was last in use when we filled mSearchEngines.
- // Access to this member must only occur from the UI thread.
- private Locale mLastLocale;
-
- // Whether search suggestions are enabled or not
- private boolean mSuggestionsEnabled;
-
- // Whether history suggestions are enabled or not
- private boolean mSavedSearchesEnabled;
-
- // Callbacks used for the search loader
- private CursorLoaderCallbacks mCursorLoaderCallbacks;
-
- // Callbacks used for the search suggestion loader
- private SearchEngineSuggestionLoaderCallbacks mSearchEngineSuggestionLoaderCallbacks;
- private SearchHistorySuggestionLoaderCallbacks mSearchHistorySuggestionLoaderCallback;
-
- // Autocomplete handler used when filtering results
- private AutocompleteHandler mAutocompleteHandler;
-
- // On search listener
- private OnSearchListener mSearchListener;
-
- // On edit suggestion listener
- private OnEditSuggestionListener mEditSuggestionListener;
-
- // Whether the suggestions will fade in when shown
- private boolean mAnimateSuggestions;
-
- // Opt-in prompt view for search suggestions
- private View mSuggestionsOptInPrompt;
-
- public interface OnSearchListener {
- void onSearch(SearchEngine engine, String text, TelemetryContract.Method method);
- }
-
- public interface OnEditSuggestionListener {
- public void onEditSuggestion(String suggestion);
- }
-
- public static BrowserSearch newInstance() {
- BrowserSearch browserSearch = new BrowserSearch();
-
- final Bundle args = new Bundle();
- args.putBoolean(HomePager.CAN_LOAD_ARG, true);
- browserSearch.setArguments(args);
-
- return browserSearch;
- }
-
- public BrowserSearch() {
- mSearchTerm = "";
- }
-
- @Override
- public void onAttach(Activity activity) {
- super.onAttach(activity);
-
- try {
- mSearchListener = (OnSearchListener) activity;
- } catch (ClassCastException e) {
- throw new ClassCastException(activity.toString()
- + " must implement BrowserSearch.OnSearchListener");
- }
-
- try {
- mEditSuggestionListener = (OnEditSuggestionListener) activity;
- } catch (ClassCastException e) {
- throw new ClassCastException(activity.toString()
- + " must implement BrowserSearch.OnEditSuggestionListener");
- }
- }
-
- @Override
- public void onDetach() {
- super.onDetach();
-
- mAutocompleteHandler = null;
- mSearchListener = null;
- mEditSuggestionListener = null;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- mSearchEngines = new ArrayList<SearchEngine>();
- mSearchHistorySuggestions = new ArrayList<>();
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
-
- mSearchEngines = null;
- }
-
- @Override
- public void onHiddenChanged(boolean hidden) {
- if (!hidden) {
- final Tab tab = Tabs.getInstance().getSelectedTab();
- final boolean isPrivate = (tab != null && tab.isPrivate());
-
- // Removes Search Suggestions Loader if in private browsing mode
- // Loader may have been inserted when browsing in normal tab
- if (isPrivate) {
- getLoaderManager().destroyLoader(LOADER_ID_SUGGESTION);
- }
-
- GeckoAppShell.notifyObservers("SearchEngines:GetVisible", null);
- }
- super.onHiddenChanged(hidden);
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- final SharedPreferences prefs = GeckoSharedPrefs.forApp(getContext());
- mSavedSearchesEnabled = prefs.getBoolean(GeckoPreferences.PREFS_HISTORY_SAVED_SEARCH, true);
-
- // Fetch engines if we need to.
- if (mSearchEngines.isEmpty() || !Locale.getDefault().equals(mLastLocale)) {
- GeckoAppShell.notifyObservers("SearchEngines:GetVisible", null);
- } else {
- updateSearchEngineBar();
- }
-
- Telemetry.startUISession(TelemetryContract.Session.FRECENCY);
- }
-
- @Override
- public void onPause() {
- super.onPause();
-
- Telemetry.stopUISession(TelemetryContract.Session.FRECENCY);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- // All list views are styled to look the same with a global activity theme.
- // If the style of the list changes, inflate it from an XML.
- mView = (LinearLayout) inflater.inflate(R.layout.browser_search, container, false);
- mList = (HomeListView) mView.findViewById(R.id.home_list_view);
- mSearchEngineBar = (SearchEngineBar) mView.findViewById(R.id.search_engine_bar);
-
- return mView;
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
-
- GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
- "SearchEngines:Data");
-
- mSearchEngineBar.setAdapter(null);
- mSearchEngineBar = null;
-
- mList.setAdapter(null);
- mList = null;
-
- mView = null;
- mSuggestionsOptInPrompt = null;
- mSuggestClient = null;
- }
-
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- mList.setTag(HomePager.LIST_TAG_BROWSER_SEARCH);
-
- mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- // Perform the user-entered search if the user clicks on a search engine row.
- // This row will be disabled if suggestions (in addition to the user-entered term) are showing.
- if (view instanceof SearchEngineRow) {
- ((SearchEngineRow) view).performUserEnteredSearch();
- return;
- }
-
- // Account for the search engine rows.
- position -= getPrimaryEngineCount();
- final Cursor c = mAdapter.getCursor(position);
- final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL));
-
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, "frecency");
-
- // This item is a TwoLinePageRow, so we allow switch-to-tab.
- mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
- }
- });
-
- mList.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
- @Override
- public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
- // Don't do anything when the user long-clicks on a search engine row.
- if (view instanceof SearchEngineRow) {
- return true;
- }
-
- // Account for the search engine rows.
- position -= getPrimaryEngineCount();
- return mList.onItemLongClick(parent, view, position, id);
- }
- });
-
- final ListSelectionListener listener = new ListSelectionListener();
- mList.setOnItemSelectedListener(listener);
- mList.setOnFocusChangeListener(listener);
-
- mList.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() {
- @Override
- public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
- final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
- info.url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
- info.title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.TITLE));
-
- int bookmarkId = cursor.getInt(cursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID));
- info.bookmarkId = bookmarkId;
-
- int historyId = cursor.getInt(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID));
- info.historyId = historyId;
-
- boolean isBookmark = bookmarkId != -1;
- boolean isHistory = historyId != -1;
-
- if (isBookmark && isHistory) {
- info.itemType = HomeContextMenuInfo.RemoveItemType.COMBINED;
- } else if (isBookmark) {
- info.itemType = HomeContextMenuInfo.RemoveItemType.BOOKMARKS;
- } else if (isHistory) {
- info.itemType = HomeContextMenuInfo.RemoveItemType.HISTORY;
- }
-
- return info;
- }
- });
-
- mList.setOnKeyListener(new View.OnKeyListener() {
- @Override
- public boolean onKey(View v, int keyCode, android.view.KeyEvent event) {
- final View selected = mList.getSelectedView();
-
- if (selected instanceof SearchEngineRow) {
- return selected.onKeyDown(keyCode, event);
- }
- return false;
- }
- });
-
- registerForContextMenu(mList);
- GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
- "SearchEngines:Data");
-
- mSearchEngineBar.setOnSearchBarClickListener(this);
- }
-
- @Override
- public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
- if (!(menuInfo instanceof HomeContextMenuInfo)) {
- return;
- }
-
- HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo;
-
- MenuInflater inflater = new MenuInflater(view.getContext());
- inflater.inflate(R.menu.browsersearch_contextmenu, menu);
-
- menu.setHeaderTitle(info.getDisplayTitle());
- }
-
- @Override
- public boolean onContextItemSelected(MenuItem item) {
- ContextMenuInfo menuInfo = item.getMenuInfo();
- if (!(menuInfo instanceof HomeContextMenuInfo)) {
- return false;
- }
-
- final HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo;
- final Context context = getActivity();
-
- final int itemId = item.getItemId();
-
- if (itemId == R.id.browsersearch_remove) {
- // Position for Top Sites grid items, but will always be -1 since this is only for BrowserSearch result
- final int position = -1;
-
- new RemoveItemByUrlTask(context, info.url, info.itemType, position).execute();
- return true;
- }
-
- return false;
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- // Initialize the search adapter
- mAdapter = new SearchAdapter(getActivity());
- mList.setAdapter(mAdapter);
-
- // Only create an instance when we need it
- mSearchEngineSuggestionLoaderCallbacks = null;
- mSearchHistorySuggestionLoaderCallback = null;
-
- // Create callbacks before the initial loader is started
- mCursorLoaderCallbacks = new CursorLoaderCallbacks();
- loadIfVisible();
- }
-
- @Override
- public void handleMessage(String event, final JSONObject message) {
- if (event.equals("SearchEngines:Data")) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- setSearchEngines(message);
- }
- });
- }
- }
-
- @Override
- protected void load() {
- SearchLoader.init(getLoaderManager(), LOADER_ID_SEARCH, mCursorLoaderCallbacks, mSearchTerm);
- }
-
- private void handleAutocomplete(String searchTerm, Cursor c) {
- if (c == null ||
- mAutocompleteHandler == null ||
- TextUtils.isEmpty(searchTerm)) {
- return;
- }
-
- // Avoid searching the path if we don't have to. Currently just
- // decided by whether there is a '/' character in the string.
- final boolean searchPath = searchTerm.indexOf('/') > 0;
- final String autocompletion = findAutocompletion(searchTerm, c, searchPath);
-
- if (autocompletion == null || mAutocompleteHandler == null) {
- return;
- }
-
- // Prefetch auto-completed domain since it's a likely target
- GeckoAppShell.notifyObservers("Session:Prefetch", "http://" + autocompletion);
-
- mAutocompleteHandler.onAutocomplete(autocompletion);
- mAutocompleteHandler = null;
- }
-
- /**
- * Returns the substring of a provided URI, starting at the given offset,
- * and extending up to the end of the path segment in which the provided
- * index is found.
- *
- * For example, given
- *
- * "www.reddit.com/r/boop/abcdef", 0, ?
- *
- * this method returns
- *
- * ?=2: "www.reddit.com/"
- * ?=17: "www.reddit.com/r/boop/"
- * ?=21: "www.reddit.com/r/boop/"
- * ?=22: "www.reddit.com/r/boop/abcdef"
- *
- */
- private static String uriSubstringUpToMatchedPath(final String url, final int offset, final int begin) {
- final int afterEnd = url.length();
-
- // We want to include the trailing slash, but not other characters.
- int chop = url.indexOf('/', begin);
- if (chop != -1) {
- ++chop;
- if (chop < offset) {
- // This isn't supposed to happen. Fall back to returning the whole damn thing.
- return url;
- }
- } else {
- chop = url.indexOf('?', begin);
- if (chop == -1) {
- chop = url.indexOf('#', begin);
- }
- if (chop == -1) {
- chop = afterEnd;
- }
- }
-
- return url.substring(offset, chop);
- }
-
- LinkedHashSet<String> domains = null;
- private LinkedHashSet<String> getDomains() {
- if (domains == null) {
- domains = new LinkedHashSet<String>(500);
- BufferedReader buf = null;
- try {
- buf = new BufferedReader(new InputStreamReader(getResources().openRawResource(R.raw.topdomains)));
- String res = null;
-
- do {
- res = buf.readLine();
- if (res != null) {
- domains.add(res);
- }
- } while (res != null);
- } catch (IOException e) {
- Log.e(LOGTAG, "Error reading domains", e);
- } finally {
- if (buf != null) {
- try {
- buf.close();
- } catch (IOException e) { }
- }
- }
- }
- return domains;
- }
-
- private String searchDomains(String search) {
- for (String domain : getDomains()) {
- if (domain.startsWith(search)) {
- return domain;
- }
- }
- return null;
- }
-
- private String findAutocompletion(String searchTerm, Cursor c, boolean searchPath) {
- if (!c.moveToFirst()) {
- // No cursor probably means no history, so let's try the fallback list.
- return searchDomains(searchTerm);
- }
-
- final int searchLength = searchTerm.length();
- final int urlIndex = c.getColumnIndexOrThrow(History.URL);
- int searchCount = 0;
-
- do {
- final String url = c.getString(urlIndex);
-
- if (searchCount == 0) {
- // Prefetch the first item in the list since it's weighted the highest
- GeckoAppShell.notifyObservers("Session:Prefetch", url);
- }
-
- // Does the completion match against the whole URL? This will match
- // about: pages, as well as user input including "http://...".
- if (url.startsWith(searchTerm)) {
- return uriSubstringUpToMatchedPath(url, 0,
- (searchLength > HTTPS_PREFIX_LENGTH) ? searchLength : HTTPS_PREFIX_LENGTH);
- }
-
- final Uri uri = Uri.parse(url);
- final String host = uri.getHost();
-
- // Host may be null for about pages.
- if (host == null) {
- continue;
- }
-
- if (host.startsWith(searchTerm)) {
- return host + "/";
- }
-
- final String strippedHost = StringUtils.stripCommonSubdomains(host);
- if (strippedHost.startsWith(searchTerm)) {
- return strippedHost + "/";
- }
-
- ++searchCount;
-
- if (!searchPath) {
- continue;
- }
-
- // Otherwise, if we're matching paths, let's compare against the string itself.
- final int hostOffset = url.indexOf(strippedHost);
- if (hostOffset == -1) {
- // This was a URL string that parsed to a different host (normalized?).
- // Give up.
- continue;
- }
-
- // We already matched the non-stripped host, so now we're
- // substring-searching in the part of the URL without the common
- // subdomains.
- if (url.startsWith(searchTerm, hostOffset)) {
- // Great! Return including the rest of the path segment.
- return uriSubstringUpToMatchedPath(url, hostOffset, hostOffset + searchLength);
- }
- } while (searchCount < MAX_AUTOCOMPLETE_SEARCH && c.moveToNext());
-
- // If we can't find an autocompletion domain from history, let's try using the fallback list.
- return searchDomains(searchTerm);
- }
-
- public void resetScrollState() {
- mSearchEngineBar.scrollToPosition(0);
- }
-
- private void filterSuggestions() {
- Tab tab = Tabs.getInstance().getSelectedTab();
- final boolean isPrivate = (tab != null && tab.isPrivate());
-
- // mSuggestClient may be null if we haven't received our search engine list yet - hence
- // we need to exit here in that case.
- if (isPrivate || mSuggestClient == null || (!mSuggestionsEnabled && !mSavedSearchesEnabled)) {
- mSearchHistorySuggestions.clear();
- return;
- }
-
- // Suggestions from search engine
- if (mSearchEngineSuggestionLoaderCallbacks == null) {
- mSearchEngineSuggestionLoaderCallbacks = new SearchEngineSuggestionLoaderCallbacks();
- }
- getLoaderManager().restartLoader(LOADER_ID_SUGGESTION, null, mSearchEngineSuggestionLoaderCallbacks);
-
- // Saved suggestions
- if (mSearchHistorySuggestionLoaderCallback == null) {
- mSearchHistorySuggestionLoaderCallback = new SearchHistorySuggestionLoaderCallbacks();
- }
- getLoaderManager().restartLoader(LOADER_ID_SAVED_SUGGESTION, null, mSearchHistorySuggestionLoaderCallback);
- }
-
- private void setSuggestions(ArrayList<String> suggestions) {
- ThreadUtils.assertOnUiThread();
-
- // mSearchEngines may be null if the setSuggestions calls after onDestroy (bug 1310621).
- // So drop the suggestions if search engines are not available
- if (mSearchEngines != null && !mSearchEngines.isEmpty()) {
- mSearchEngines.get(0).setSuggestions(suggestions);
- mAdapter.notifyDataSetChanged();
- }
-
- }
-
- private void setSavedSuggestions(ArrayList<String> savedSuggestions) {
- ThreadUtils.assertOnUiThread();
-
- mSearchHistorySuggestions = savedSuggestions;
- mAdapter.notifyDataSetChanged();
- }
-
- private boolean shouldUpdateSearchEngine(ArrayList<SearchEngine> searchEngines) {
- if (searchEngines.size() != mSearchEngines.size()) {
- return true;
- }
-
- int size = searchEngines.size();
-
- for (int i = 0; i < size; i++) {
- if (!mSearchEngines.get(i).name.equals(searchEngines.get(i).name)) {
- return true;
- }
- }
-
- return false;
- }
-
- private void setSearchEngines(JSONObject data) {
- ThreadUtils.assertOnUiThread();
-
- // This method is called via a Runnable posted from the Gecko thread, so
- // it's possible the fragment and/or its view has been destroyed by the
- // time we get here. If so, just abort.
- if (mView == null) {
- return;
- }
-
- try {
- final JSONObject suggest = data.getJSONObject("suggest");
- final String suggestEngine = suggest.optString("engine", null);
- final String suggestTemplate = suggest.optString("template", null);
- final boolean suggestionsPrompted = suggest.getBoolean("prompted");
- final JSONArray engines = data.getJSONArray("searchEngines");
-
- mSuggestionsEnabled = suggest.getBoolean("enabled");
-
- ArrayList<SearchEngine> searchEngines = new ArrayList<SearchEngine>();
- for (int i = 0; i < engines.length(); i++) {
- final JSONObject engineJSON = engines.getJSONObject(i);
- final SearchEngine engine = new SearchEngine((Context) getActivity(), engineJSON);
-
- if (engine.name.equals(suggestEngine) && suggestTemplate != null) {
- // Suggest engine should be at the front of the list.
- // We're baking in an assumption here that the suggest engine
- // is also the default engine.
- searchEngines.add(0, engine);
-
- ensureSuggestClientIsSet(suggestTemplate);
- } else {
- searchEngines.add(engine);
- }
- }
-
- // checking if the new searchEngine is different from mSearchEngine, will have to re-layout if yes
- boolean change = shouldUpdateSearchEngine(searchEngines);
-
- if (mAdapter != null && change) {
- mSearchEngines = Collections.unmodifiableList(searchEngines);
- mLastLocale = Locale.getDefault();
- updateSearchEngineBar();
-
- mAdapter.notifyDataSetChanged();
- }
-
- final Tab tab = Tabs.getInstance().getSelectedTab();
- final boolean isPrivate = (tab != null && tab.isPrivate());
-
- // Show suggestions opt-in prompt only if suggestions are not enabled yet,
- // user hasn't been prompted and we're not on a private browsing tab.
- // The prompt might have been inflated already when this view was previously called.
- // Remove the opt-in prompt if it has been inflated in the view and dealt with by the user,
- // or if we're on a private browsing tab
- if (!mSuggestionsEnabled && !suggestionsPrompted && !isPrivate) {
- showSuggestionsOptIn();
- } else {
- removeSuggestionsOptIn();
- }
-
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error getting search engine JSON", e);
- }
-
- filterSuggestions();
- }
-
- private void updateSearchEngineBar() {
- final int primaryEngineCount = getPrimaryEngineCount();
-
- if (primaryEngineCount < mSearchEngines.size()) {
- mSearchEngineBar.setSearchEngines(
- mSearchEngines.subList(primaryEngineCount, mSearchEngines.size())
- );
- mSearchEngineBar.setVisibility(View.VISIBLE);
- } else {
- mSearchEngineBar.setVisibility(View.GONE);
- }
- }
-
- @Override
- public void onSearchBarClickListener(final SearchEngine searchEngine) {
- final TelemetryContract.Method method = TelemetryContract.Method.LIST_ITEM;
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, method, "searchenginebar");
- mSearchListener.onSearch(searchEngine, mSearchTerm, method);
- }
-
- private void ensureSuggestClientIsSet(final String suggestTemplate) {
- // Don't update the suggestClient if we already have a client with the correct template
- if (mSuggestClient != null && suggestTemplate.equals(mSuggestClient.getSuggestTemplate())) {
- return;
- }
-
- mSuggestClient = sSuggestClientFactory.getSuggestClient(getActivity(), suggestTemplate, SUGGESTION_TIMEOUT, NETWORK_SUGGESTION_MAX);
- }
-
- private void showSuggestionsOptIn() {
- // Only make the ViewStub visible again if it has already previously been shown.
- // (An inflated ViewStub is removed from the View hierarchy so a second call to findViewById will return null,
- // which also further necessitates handling this separately.)
- if (mSuggestionsOptInPrompt != null) {
- mSuggestionsOptInPrompt.setVisibility(View.VISIBLE);
- return;
- }
-
- mSuggestionsOptInPrompt = ((ViewStub) mView.findViewById(R.id.suggestions_opt_in_prompt)).inflate();
-
- TextView promptText = (TextView) mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_title);
- promptText.setText(getResources().getString(R.string.suggestions_prompt));
-
- final View yesButton = mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_yes);
- final View noButton = mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_no);
-
- final OnClickListener listener = new OnClickListener() {
- @Override
- public void onClick(View v) {
- // Prevent the buttons from being clicked multiple times (bug 816902)
- yesButton.setOnClickListener(null);
- noButton.setOnClickListener(null);
-
- setSuggestionsEnabled(v == yesButton);
- }
- };
-
- yesButton.setOnClickListener(listener);
- noButton.setOnClickListener(listener);
-
- // If the prompt gains focus, automatically pass focus to the
- // yes button in the prompt.
- final View prompt = mSuggestionsOptInPrompt.findViewById(R.id.prompt);
- prompt.setOnFocusChangeListener(new View.OnFocusChangeListener() {
- @Override
- public void onFocusChange(View v, boolean hasFocus) {
- if (hasFocus) {
- yesButton.requestFocus();
- }
- }
- });
- }
-
- private void removeSuggestionsOptIn() {
- if (mSuggestionsOptInPrompt == null) {
- return;
- }
-
- mSuggestionsOptInPrompt.setVisibility(View.GONE);
- }
-
- private void setSuggestionsEnabled(final boolean enabled) {
- // Clicking the yes/no buttons quickly can cause the click events be
- // queued before the listeners are removed above, so it's possible
- // setSuggestionsEnabled() can be called twice. mSuggestionsOptInPrompt
- // can be null if this happens (bug 828480).
- if (mSuggestionsOptInPrompt == null) {
- return;
- }
-
- // Make suggestions appear immediately after the user opts in
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- SuggestClient client = mSuggestClient;
- if (client != null) {
- client.query(mSearchTerm);
- }
- }
- });
-
- PrefsHelper.setPref("browser.search.suggest.prompted", true);
- PrefsHelper.setPref("browser.search.suggest.enabled", enabled);
-
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, (enabled ? "suggestions_optin_yes" : "suggestions_optin_no"));
-
- TranslateAnimation slideAnimation = new TranslateAnimation(0, mSuggestionsOptInPrompt.getWidth(), 0, 0);
- slideAnimation.setDuration(ANIMATION_DURATION);
- slideAnimation.setInterpolator(new AccelerateInterpolator());
- slideAnimation.setFillAfter(true);
- final View prompt = mSuggestionsOptInPrompt.findViewById(R.id.prompt);
-
- TranslateAnimation shrinkAnimation = new TranslateAnimation(0, 0, 0, -1 * mSuggestionsOptInPrompt.getHeight());
- shrinkAnimation.setDuration(ANIMATION_DURATION);
- shrinkAnimation.setFillAfter(true);
- shrinkAnimation.setStartOffset(slideAnimation.getDuration());
- shrinkAnimation.setAnimationListener(new Animation.AnimationListener() {
- @Override
- public void onAnimationStart(Animation a) {
- // Increase the height of the view so a gap isn't shown during animation
- mView.getLayoutParams().height = mView.getHeight() +
- mSuggestionsOptInPrompt.getHeight();
- mView.requestLayout();
- }
-
- @Override
- public void onAnimationRepeat(Animation a) {}
-
- @Override
- public void onAnimationEnd(Animation a) {
- // Removing the view immediately results in a NPE in
- // dispatchDraw(), possibly because this callback executes
- // before drawing is finished. Posting this as a Runnable fixes
- // the issue.
- mView.post(new Runnable() {
- @Override
- public void run() {
- mView.removeView(mSuggestionsOptInPrompt);
- mList.clearAnimation();
- mSuggestionsOptInPrompt = null;
-
- // Reset the view height
- mView.getLayoutParams().height = LayoutParams.MATCH_PARENT;
-
- // Show search suggestions and update them
- if (enabled) {
- mSuggestionsEnabled = enabled;
- mAnimateSuggestions = true;
- mAdapter.notifyDataSetChanged();
- filterSuggestions();
- }
- }
- });
- }
- });
-
- prompt.startAnimation(slideAnimation);
- mSuggestionsOptInPrompt.startAnimation(shrinkAnimation);
- mList.startAnimation(shrinkAnimation);
- }
-
- private int getPrimaryEngineCount() {
- return mSearchEngines.size() > 0 ? 1 : 0;
- }
-
- private void restartSearchLoader() {
- SearchLoader.restart(getLoaderManager(), LOADER_ID_SEARCH, mCursorLoaderCallbacks, mSearchTerm);
- }
-
- private void initSearchLoader() {
- SearchLoader.init(getLoaderManager(), LOADER_ID_SEARCH, mCursorLoaderCallbacks, mSearchTerm);
- }
-
- public void filter(String searchTerm, AutocompleteHandler handler) {
- if (TextUtils.isEmpty(searchTerm)) {
- return;
- }
-
- final boolean isNewFilter = !TextUtils.equals(mSearchTerm, searchTerm);
-
- mSearchTerm = searchTerm;
- mAutocompleteHandler = handler;
-
- if (mAdapter != null) {
- if (isNewFilter) {
- // The adapter depends on the search term to determine its number
- // of items. Make it we notify the view about it.
- mAdapter.notifyDataSetChanged();
-
- // Restart loaders with the new search term
- restartSearchLoader();
- filterSuggestions();
- } else {
- // The search term hasn't changed, simply reuse any existing
- // loader for the current search term. This will ensure autocompletion
- // is consistently triggered (see bug 933739).
- initSearchLoader();
- }
- }
- }
-
- abstract private static class SuggestionAsyncLoader extends AsyncTaskLoader<ArrayList<String>> {
- protected final String mSearchTerm;
- private ArrayList<String> mSuggestions;
-
- public SuggestionAsyncLoader(Context context, String searchTerm) {
- super(context);
- mSearchTerm = searchTerm;
- }
-
- @Override
- public void deliverResult(ArrayList<String> suggestions) {
- mSuggestions = suggestions;
-
- if (isStarted()) {
- super.deliverResult(mSuggestions);
- }
- }
-
- @Override
- protected void onStartLoading() {
- if (mSuggestions != null) {
- deliverResult(mSuggestions);
- }
-
- if (takeContentChanged() || mSuggestions == null) {
- forceLoad();
- }
- }
-
- @Override
- protected void onStopLoading() {
- cancelLoad();
- }
-
- @Override
- protected void onReset() {
- super.onReset();
-
- onStopLoading();
- mSuggestions = null;
- }
- }
-
- private static class SearchEngineSuggestionAsyncLoader extends SuggestionAsyncLoader {
- private final SuggestClient mSuggestClient;
-
- public SearchEngineSuggestionAsyncLoader(Context context, SuggestClient suggestClient, String searchTerm) {
- super(context, searchTerm);
- mSuggestClient = suggestClient;
- }
-
- @Override
- public ArrayList<String> loadInBackground() {
- return mSuggestClient.query(mSearchTerm);
- }
- }
-
- private static class SearchHistorySuggestionAsyncLoader extends SuggestionAsyncLoader {
- public SearchHistorySuggestionAsyncLoader(Context context, String searchTerm) {
- super(context, searchTerm);
- }
-
- @Override
- public ArrayList<String> loadInBackground() {
- final ContentResolver cr = getContext().getContentResolver();
-
- String[] columns = new String[] { BrowserContract.SearchHistory.QUERY };
- String actualQuery = BrowserContract.SearchHistory.QUERY + " LIKE ?";
- String[] queryArgs = new String[] { '%' + mSearchTerm + '%' };
-
- // For deduplication, the worst case is that all the first NETWORK_SUGGESTION_MAX history suggestions are duplicates
- // of search engine suggestions, and the there is a duplicate for the search term itself. A duplicate of the
- // search term can occur if the user has previously searched for the same thing.
- final int maxSavedSuggestions = NETWORK_SUGGESTION_MAX + 1 + getContext().getResources().getInteger(R.integer.max_saved_suggestions);
-
- final String sortOrderAndLimit = BrowserContract.SearchHistory.DATE + " DESC LIMIT " + maxSavedSuggestions;
- final Cursor result = cr.query(BrowserContract.SearchHistory.CONTENT_URI, columns, actualQuery, queryArgs, sortOrderAndLimit);
-
- if (result == null) {
- return new ArrayList<>();
- }
-
- final ArrayList<String> savedSuggestions = new ArrayList<>();
- try {
- if (result.moveToFirst()) {
- final int searchColumn = result.getColumnIndexOrThrow(BrowserContract.SearchHistory.QUERY);
- do {
- final String savedSearch = result.getString(searchColumn);
- savedSuggestions.add(savedSearch);
- } while (result.moveToNext());
- }
- } finally {
- result.close();
- }
-
- return savedSuggestions;
- }
- }
-
- private class SearchAdapter extends MultiTypeCursorAdapter {
- private static final int ROW_SEARCH = 0;
- private static final int ROW_STANDARD = 1;
- private static final int ROW_SUGGEST = 2;
-
- public SearchAdapter(Context context) {
- super(context, null, new int[] { ROW_STANDARD,
- ROW_SEARCH,
- ROW_SUGGEST },
- new int[] { R.layout.home_item_row,
- R.layout.home_search_item_row,
- R.layout.home_search_item_row });
- }
-
- @Override
- public int getItemViewType(int position) {
- if (position < getPrimaryEngineCount()) {
- if (mSuggestionsEnabled && mSearchEngines.get(position).hasSuggestions()) {
- // Give suggestion views their own type to prevent them from
- // sharing other recycled search result views. Using other
- // recycled views for the suggestion row can break animations
- // (bug 815937).
-
- return ROW_SUGGEST;
- } else {
- return ROW_SEARCH;
- }
- }
-
- return ROW_STANDARD;
- }
-
- @Override
- public boolean isEnabled(int position) {
- // If we're using a gamepad or keyboard, allow the row to be
- // focused so it can pass the focus to its child suggestion views.
- if (!mList.isInTouchMode()) {
- return true;
- }
-
- // If the suggestion row only contains one item (the user-entered
- // query), allow the entire row to be clickable; clicking the row
- // has the same effect as clicking the single suggestion. If the
- // row contains multiple items, clicking the row will do nothing.
-
- if (position < getPrimaryEngineCount()) {
- return !mSearchEngines.get(position).hasSuggestions();
- }
-
- return true;
- }
-
- // Add the search engines to the number of reported results.
- @Override
- public int getCount() {
- final int resultCount = super.getCount();
-
- // Don't show search engines or suggestions if search field is empty
- if (TextUtils.isEmpty(mSearchTerm)) {
- return resultCount;
- }
-
- return resultCount + getPrimaryEngineCount();
- }
-
- @Override
- public void bindView(View view, Context context, int position) {
- final int type = getItemViewType(position);
-
- if (type == ROW_SEARCH || type == ROW_SUGGEST) {
- final SearchEngineRow row = (SearchEngineRow) view;
- row.setOnUrlOpenListener(mUrlOpenListener);
- row.setOnSearchListener(mSearchListener);
- row.setOnEditSuggestionListener(mEditSuggestionListener);
- row.setSearchTerm(mSearchTerm);
-
- final SearchEngine engine = mSearchEngines.get(position);
- final boolean haveSuggestions = (engine.hasSuggestions() || !mSearchHistorySuggestions.isEmpty());
- final boolean animate = (mAnimateSuggestions && haveSuggestions);
- row.updateSuggestions(mSuggestionsEnabled, engine, mSearchHistorySuggestions, animate);
- if (animate) {
- // Only animate suggestions the first time they are shown
- mAnimateSuggestions = false;
- }
- } else {
- // Account for the search engines
- position -= getPrimaryEngineCount();
-
- final Cursor c = getCursor(position);
- final TwoLinePageRow row = (TwoLinePageRow) view;
- row.updateFromCursor(c);
- }
- }
- }
-
- private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
- @Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- return SearchLoader.createInstance(getActivity(), args);
- }
-
- @Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
- if (mAdapter != null) {
- mAdapter.swapCursor(c);
-
- // We should handle autocompletion based on the search term
- // associated with the loader that has just provided
- // the results.
- SearchCursorLoader searchLoader = (SearchCursorLoader) loader;
- handleAutocomplete(searchLoader.getSearchTerm(), c);
- }
- }
-
- @Override
- public void onLoaderReset(Loader<Cursor> loader) {
- if (mAdapter != null) {
- mAdapter.swapCursor(null);
- }
- }
- }
-
- private class SearchEngineSuggestionLoaderCallbacks implements LoaderCallbacks<ArrayList<String>> {
- @Override
- public Loader<ArrayList<String>> onCreateLoader(int id, Bundle args) {
- // mSuggestClient is set to null in onDestroyView(), so using it
- // safely here relies on the fact that onCreateLoader() is called
- // synchronously in restartLoader().
- return new SearchEngineSuggestionAsyncLoader(getActivity(), mSuggestClient, mSearchTerm);
- }
-
- @Override
- public void onLoadFinished(Loader<ArrayList<String>> loader, ArrayList<String> suggestions) {
- setSuggestions(suggestions);
- }
-
- @Override
- public void onLoaderReset(Loader<ArrayList<String>> loader) {
- setSuggestions(new ArrayList<String>());
- }
- }
-
- private class SearchHistorySuggestionLoaderCallbacks implements LoaderCallbacks<ArrayList<String>> {
- @Override
- public Loader<ArrayList<String>> onCreateLoader(int id, Bundle args) {
- // mSuggestClient is set to null in onDestroyView(), so using it
- // safely here relies on the fact that onCreateLoader() is called
- // synchronously in restartLoader().
- return new SearchHistorySuggestionAsyncLoader(getActivity(), mSearchTerm);
- }
-
- @Override
- public void onLoadFinished(Loader<ArrayList<String>> loader, ArrayList<String> suggestions) {
- setSavedSuggestions(suggestions);
- }
-
- @Override
- public void onLoaderReset(Loader<ArrayList<String>> loader) {
- setSavedSuggestions(new ArrayList<String>());
- }
- }
-
- private static class ListSelectionListener implements View.OnFocusChangeListener,
- AdapterView.OnItemSelectedListener {
- private SearchEngineRow mSelectedEngineRow;
-
- @Override
- public void onFocusChange(View v, boolean hasFocus) {
- if (hasFocus) {
- View selectedRow = ((ListView) v).getSelectedView();
- if (selectedRow != null) {
- selectRow(selectedRow);
- }
- } else {
- deselectRow();
- }
- }
-
- @Override
- public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
- deselectRow();
- selectRow(view);
- }
-
- @Override
- public void onNothingSelected(AdapterView<?> parent) {
- deselectRow();
- }
-
- private void selectRow(View row) {
- if (row instanceof SearchEngineRow) {
- mSelectedEngineRow = (SearchEngineRow) row;
- mSelectedEngineRow.onSelected();
- }
- }
-
- private void deselectRow() {
- if (mSelectedEngineRow != null) {
- mSelectedEngineRow.onDeselected();
- mSelectedEngineRow = null;
- }
- }
- }
-
- /**
- * HomeSearchListView is a list view for displaying search engine results on the awesome screen.
- */
- public static class HomeSearchListView extends HomeListView {
- public HomeSearchListView(Context context, AttributeSet attrs) {
- this(context, attrs, R.attr.homeListViewStyle);
- }
-
- public HomeSearchListView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
- // Dismiss the soft keyboard.
- requestFocus();
- }
-
- return super.onTouchEvent(event);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/ClientsAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/ClientsAdapter.java
deleted file mode 100644
index f288a2745..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/ClientsAdapter.java
+++ /dev/null
@@ -1,373 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import android.content.Context;
-import android.support.annotation.UiThread;
-import android.support.v4.util.Pair;
-import android.support.v7.widget.RecyclerView;
-import android.text.format.DateUtils;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.RemoteClient;
-import org.mozilla.gecko.db.RemoteTab;
-
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.GregorianCalendar;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-
-import static org.mozilla.gecko.home.CombinedHistoryItem.ItemType.*;
-
-public class ClientsAdapter extends RecyclerView.Adapter<CombinedHistoryItem> implements CombinedHistoryRecyclerView.AdapterContextMenuBuilder {
- public static final String LOGTAG = "GeckoClientsAdapter";
-
- /**
- * If a device claims to have synced before this date, we will assume it has never synced.
- */
- public static final Date EARLIEST_VALID_SYNCED_DATE;
- static {
- final Calendar c = GregorianCalendar.getInstance();
- c.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
- EARLIEST_VALID_SYNCED_DATE = c.getTime();
- }
-
- List<Pair<String, Integer>> adapterList = new LinkedList<>();
-
- // List of hidden remote clients.
- // Only accessed from the UI thread.
- protected final List<RemoteClient> hiddenClients = new ArrayList<>();
- private Map<String, RemoteClient> visibleClients = new HashMap<>();
-
- // Maintain group collapsed and hidden state. Only accessed from the UI thread.
- protected static RemoteTabsExpandableListState sState;
-
- private final Context context;
-
- public ClientsAdapter(Context context) {
- this.context = context;
-
- // This races when multiple Fragments are created. That's okay: one
- // will win, and thereafter, all will be okay. If we create and then
- // drop an instance the shared SharedPreferences backing all the
- // instances will maintain the state for us. Since everything happens on
- // the UI thread, this doesn't even need to be volatile.
- if (sState == null) {
- sState = new RemoteTabsExpandableListState(GeckoSharedPrefs.forProfile(context));
- }
-
- this.setHasStableIds(true);
- }
-
- @Override
- public CombinedHistoryItem onCreateViewHolder(ViewGroup parent, int viewType) {
- final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
- final View view;
-
- final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
-
- switch (itemType) {
- case NAVIGATION_BACK:
- view = inflater.inflate(R.layout.home_combined_back_item, parent, false);
- return new CombinedHistoryItem.HistoryItem(view);
-
- case CLIENT:
- view = inflater.inflate(R.layout.home_remote_tabs_group, parent, false);
- return new CombinedHistoryItem.ClientItem(view);
-
- case CHILD:
- view = inflater.inflate(R.layout.home_item_row, parent, false);
- return new CombinedHistoryItem.HistoryItem(view);
-
- case HIDDEN_DEVICES:
- view = inflater.inflate(R.layout.home_remote_tabs_hidden_devices, parent, false);
- return new CombinedHistoryItem.BasicItem(view);
- }
- return null;
- }
-
- @Override
- public void onBindViewHolder(CombinedHistoryItem holder, final int position) {
- final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
-
- switch (itemType) {
- case CLIENT:
- final CombinedHistoryItem.ClientItem clientItem = (CombinedHistoryItem.ClientItem) holder;
- final String clientGuid = adapterList.get(position).first;
- final RemoteClient client = visibleClients.get(clientGuid);
- clientItem.bind(context, client, sState.isClientCollapsed(clientGuid));
- break;
-
- case CHILD:
- final Pair<String, Integer> pair = adapterList.get(position);
- RemoteTab remoteTab = visibleClients.get(pair.first).tabs.get(pair.second);
- ((CombinedHistoryItem.HistoryItem) holder).bind(remoteTab);
- break;
-
- case HIDDEN_DEVICES:
- final String hiddenDevicesLabel = context.getResources().getString(R.string.home_remote_tabs_many_hidden_devices, hiddenClients.size());
- ((TextView) holder.itemView).setText(hiddenDevicesLabel);
- break;
- }
- }
-
- @Override
- public int getItemCount () {
- return adapterList.size();
- }
-
- private CombinedHistoryItem.ItemType getItemTypeForPosition(int position) {
- if (position == 0) {
- return NAVIGATION_BACK;
- }
-
- final Pair<String, Integer> pair = adapterList.get(position);
- if (pair == null) {
- return HIDDEN_DEVICES;
- } else if (pair.second == -1) {
- return CLIENT;
- } else {
- return CHILD;
- }
- }
-
- @Override
- public int getItemViewType(int position) {
- return CombinedHistoryItem.ItemType.itemTypeToViewType(getItemTypeForPosition(position));
- }
-
- @Override
- public long getItemId(int position) {
- // RecyclerView.NO_ID is -1, so start our hard-coded IDs at -2.
- final int NAVIGATION_BACK_ID = -2;
- final int HIDDEN_DEVICES_ID = -3;
-
- final String clientGuid;
- // adapterList is a list of tuples (clientGuid, tabId).
- final Pair<String, Integer> pair = adapterList.get(position);
-
- switch (getItemTypeForPosition(position)) {
- case NAVIGATION_BACK:
- return NAVIGATION_BACK_ID;
-
- case HIDDEN_DEVICES:
- return HIDDEN_DEVICES_ID;
-
- // For Clients, return hashCode of their GUIDs.
- case CLIENT:
- clientGuid = pair.first;
- return clientGuid.hashCode();
-
- // For Tabs, return hashCode of their URLs.
- case CHILD:
- clientGuid = pair.first;
- final Integer tabId = pair.second;
-
- final RemoteClient remoteClient = visibleClients.get(clientGuid);
- if (remoteClient == null) {
- return RecyclerView.NO_ID;
- }
-
- final RemoteTab remoteTab = remoteClient.tabs.get(tabId);
- if (remoteTab == null) {
- return RecyclerView.NO_ID;
- }
-
- return remoteTab.url.hashCode();
-
- default:
- throw new IllegalStateException("Unexpected Home Panel item type");
- }
- }
-
- public int getClientsCount() {
- return hiddenClients.size() + visibleClients.size();
- }
-
- @UiThread
- public void setClients(List<RemoteClient> clients) {
- adapterList.clear();
- adapterList.add(null);
-
- hiddenClients.clear();
- visibleClients.clear();
-
- for (RemoteClient client : clients) {
- final String guid = client.guid;
- if (sState.isClientHidden(guid)) {
- hiddenClients.add(client);
- } else {
- visibleClients.put(guid, client);
- adapterList.addAll(getVisibleItems(client));
- }
- }
-
- // Add item for unhiding clients.
- if (!hiddenClients.isEmpty()) {
- adapterList.add(null);
- }
-
- notifyDataSetChanged();
- }
-
- private static List<Pair<String, Integer>> getVisibleItems(RemoteClient client) {
- List<Pair<String, Integer>> list = new LinkedList<>();
- final String guid = client.guid;
- list.add(new Pair<>(guid, -1));
- if (!sState.isClientCollapsed(client.guid)) {
- for (int i = 0; i < client.tabs.size(); i++) {
- list.add(new Pair<>(guid, i));
- }
- }
- return list;
- }
-
- public List<RemoteClient> getHiddenClients() {
- return hiddenClients;
- }
-
- public void toggleClient(int position) {
- final Pair<String, Integer> pair = adapterList.get(position);
- if (pair.second != -1) {
- return;
- }
-
- final String clientGuid = pair.first;
- final RemoteClient client = visibleClients.get(clientGuid);
-
- final boolean isCollapsed = sState.isClientCollapsed(clientGuid);
-
- sState.setClientCollapsed(clientGuid, !isCollapsed);
- notifyItemChanged(position);
-
- if (isCollapsed) {
- for (int i = client.tabs.size() - 1; i > -1; i--) {
- // Insert child tabs at the index right after the client item that was clicked.
- adapterList.add(position + 1, new Pair<>(clientGuid, i));
- }
- notifyItemRangeInserted(position + 1, client.tabs.size());
- } else {
- int i = client.tabs.size();
- while (i > 0) {
- adapterList.remove(position + 1);
- i--;
- }
- notifyItemRangeRemoved(position + 1, client.tabs.size());
- }
- }
-
- public void unhideClients(List<RemoteClient> selectedClients) {
- final int numClients = selectedClients.size();
- if (numClients == 0) {
- return;
- }
-
- final int insertionIndex = adapterList.size() - 1;
- int itemCount = numClients;
-
- for (RemoteClient client : selectedClients) {
- final String clientGuid = client.guid;
-
- sState.setClientHidden(clientGuid, false);
- hiddenClients.remove(client);
-
- visibleClients.put(clientGuid, client);
- sState.setClientCollapsed(clientGuid, false);
- adapterList.addAll(adapterList.size() - 1, getVisibleItems(client));
-
- itemCount += client.tabs.size();
- }
-
- notifyItemRangeInserted(insertionIndex, itemCount);
-
- final int hiddenDevicesIndex = adapterList.size() - 1;
- if (hiddenClients.isEmpty()) {
- // No more hidden clients, remove "unhide" item.
- adapterList.remove(hiddenDevicesIndex);
- notifyItemRemoved(hiddenDevicesIndex);
- } else {
- // Update "hidden clients" item because number of hidden clients changed.
- notifyItemChanged(hiddenDevicesIndex);
- }
- }
-
- public void removeItem(int position) {
- final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
- switch (itemType) {
- case CLIENT:
- final String clientGuid = adapterList.get(position).first;
- final RemoteClient client = visibleClients.remove(clientGuid);
- final boolean hadHiddenClients = !hiddenClients.isEmpty();
-
- int removeCount = sState.isClientCollapsed(clientGuid) ? 1 : client.tabs.size() + 1;
- int c = removeCount;
- while (c > 0) {
- adapterList.remove(position);
- c--;
- }
- notifyItemRangeRemoved(position, removeCount);
-
- sState.setClientHidden(clientGuid, true);
- hiddenClients.add(client);
-
- if (!hadHiddenClients) {
- // Add item for unhiding clients;
- adapterList.add(null);
- notifyItemInserted(adapterList.size() - 1);
- } else {
- // Update "hidden clients" item because number of hidden clients changed.
- notifyItemChanged(adapterList.size() - 1);
- }
- break;
- }
- }
-
- @Override
- public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) {
- final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
- HomeContextMenuInfo info;
- final Pair<String, Integer> pair = adapterList.get(position);
- switch (itemType) {
- case CHILD:
- info = new HomeContextMenuInfo(view, position, -1);
- return populateChildInfoFromTab(info, visibleClients.get(pair.first).tabs.get(pair.second));
-
- case CLIENT:
- info = new CombinedHistoryPanel.RemoteTabsClientContextMenuInfo(view, position, -1, visibleClients.get(pair.first));
- return info;
- }
- return null;
- }
-
- protected static HomeContextMenuInfo populateChildInfoFromTab(HomeContextMenuInfo info, RemoteTab tab) {
- info.url = tab.url;
- info.title = tab.title;
- return info;
- }
-
- /**
- * Return a relative "Last synced" time span for the given tab record.
- *
- * @param now local time.
- * @param time to format string for.
- * @return string describing time span
- */
- public static String getLastSyncedString(Context context, long now, long time) {
- if (new Date(time).before(EARLIEST_VALID_SYNCED_DATE)) {
- return context.getString(R.string.remote_tabs_never_synced);
- }
- final CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS);
- return context.getResources().getString(R.string.remote_tabs_last_synced, relativeTimeSpanString);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
deleted file mode 100644
index 402ed26e7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
+++ /dev/null
@@ -1,433 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-package org.mozilla.gecko.home;
-
-import android.content.res.Resources;
-import android.support.annotation.UiThread;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
-
-import android.database.Cursor;
-import android.util.SparseArray;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.util.ThreadUtils;
-
-public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistoryItem> implements CombinedHistoryRecyclerView.AdapterContextMenuBuilder {
- private static final int RECENT_TABS_SMARTFOLDER_INDEX = 0;
-
- // Array for the time ranges in milliseconds covered by each section.
- static final HistorySectionsHelper.SectionDateRange[] sectionDateRangeArray = new HistorySectionsHelper.SectionDateRange[SectionHeader.values().length];
-
- // Semantic names for the time covered by each section
- public enum SectionHeader {
- TODAY,
- YESTERDAY,
- WEEK,
- THIS_MONTH,
- MONTH_AGO,
- TWO_MONTHS_AGO,
- THREE_MONTHS_AGO,
- FOUR_MONTHS_AGO,
- FIVE_MONTHS_AGO,
- OLDER_THAN_SIX_MONTHS
- }
-
- private HomeFragment.PanelStateChangeListener panelStateChangeListener;
-
- private Cursor historyCursor;
- private DevicesUpdateHandler devicesUpdateHandler;
- private int deviceCount = 0;
- private RecentTabsUpdateHandler recentTabsUpdateHandler;
- private int recentTabsCount = 0;
-
- private LinearLayoutManager linearLayoutManager; // Only used on the UI thread, so no need to be volatile.
-
- // We use a sparse array to store each section header's position in the panel [more cheaply than a HashMap].
- private final SparseArray<SectionHeader> sectionHeaders;
-
- public CombinedHistoryAdapter(Resources resources, int cachedRecentTabsCount) {
- super();
- recentTabsCount = cachedRecentTabsCount;
- sectionHeaders = new SparseArray<>();
- HistorySectionsHelper.updateRecentSectionOffset(resources, sectionDateRangeArray);
- this.setHasStableIds(true);
- }
-
- public void setPanelStateChangeListener(
- HomeFragment.PanelStateChangeListener panelStateChangeListener) {
- this.panelStateChangeListener = panelStateChangeListener;
- }
-
- @UiThread
- public void setLinearLayoutManager(LinearLayoutManager linearLayoutManager) {
- this.linearLayoutManager = linearLayoutManager;
- }
-
- public void setHistory(Cursor history) {
- historyCursor = history;
- populateSectionHeaders(historyCursor, sectionHeaders);
- notifyDataSetChanged();
- }
-
- public interface DevicesUpdateHandler {
- void onDeviceCountUpdated(int count);
- }
-
- public DevicesUpdateHandler getDeviceUpdateHandler() {
- if (devicesUpdateHandler == null) {
- devicesUpdateHandler = new DevicesUpdateHandler() {
- @Override
- public void onDeviceCountUpdated(int count) {
- deviceCount = count;
- notifyItemChanged(getSyncedDevicesSmartFolderIndex());
- }
- };
- }
- return devicesUpdateHandler;
- }
-
- public interface RecentTabsUpdateHandler {
- void onRecentTabsCountUpdated(int count, boolean countReliable);
- }
-
- public RecentTabsUpdateHandler getRecentTabsUpdateHandler() {
- if (recentTabsUpdateHandler != null) {
- return recentTabsUpdateHandler;
- }
-
- recentTabsUpdateHandler = new RecentTabsUpdateHandler() {
- @Override
- public void onRecentTabsCountUpdated(final int count, final boolean countReliable) {
- // Now that other items can move around depending on the visibility of the
- // Recent Tabs folder, only update the recentTabsCount on the UI thread.
- ThreadUtils.postToUiThread(new Runnable() {
- @UiThread
- @Override
- public void run() {
- if (!countReliable && count <= recentTabsCount) {
- // The final tab count (where countReliable = true) is normally >= than
- // previous values with countReliable = false. Hence we only want to
- // update the displayed tab count with a preliminary value if it's larger
- // than the previous count, so as to avoid the displayed count jumping
- // downwards and then back up, as well as unnecessary folder animations.
- return;
- }
-
- final boolean prevFolderVisibility = isRecentTabsFolderVisible();
- recentTabsCount = count;
- final boolean folderVisible = isRecentTabsFolderVisible();
-
- if (prevFolderVisibility == folderVisible) {
- if (prevFolderVisibility) {
- notifyItemChanged(RECENT_TABS_SMARTFOLDER_INDEX);
- }
- return;
- }
-
- // If the Recent Tabs smart folder has become hidden/unhidden,
- // we need to recalculate the history section header positions.
- populateSectionHeaders(historyCursor, sectionHeaders);
-
- if (folderVisible) {
- int scrollPos = -1;
- if (linearLayoutManager != null) {
- scrollPos = linearLayoutManager.findFirstCompletelyVisibleItemPosition();
- }
-
- notifyItemInserted(RECENT_TABS_SMARTFOLDER_INDEX);
- // If the list exceeds the display height and we want to show the new
- // item inserted at position 0, we need to scroll up manually
- // (see https://code.google.com/p/android/issues/detail?id=174227#c2).
- // However we only do this if our current scroll position is at the
- // top of the list.
- if (linearLayoutManager != null && scrollPos == 0) {
- linearLayoutManager.scrollToPosition(0);
- }
- } else {
- notifyItemRemoved(RECENT_TABS_SMARTFOLDER_INDEX);
- }
-
- if (countReliable && panelStateChangeListener != null) {
- panelStateChangeListener.setCachedRecentTabsCount(recentTabsCount);
- }
- }
- });
- }
- };
- return recentTabsUpdateHandler;
- }
-
- @UiThread
- private boolean isRecentTabsFolderVisible() {
- return recentTabsCount > 0;
- }
-
- @UiThread
- // Number of smart folders for determining practical empty state.
- public int getNumVisibleSmartFolders() {
- int visibleFolders = 1; // Synced devices folder is always visible.
-
- if (isRecentTabsFolderVisible()) {
- visibleFolders += 1;
- }
-
- return visibleFolders;
- }
-
- @UiThread
- private int getSyncedDevicesSmartFolderIndex() {
- return isRecentTabsFolderVisible() ?
- RECENT_TABS_SMARTFOLDER_INDEX + 1 :
- RECENT_TABS_SMARTFOLDER_INDEX;
- }
-
- @Override
- public CombinedHistoryItem onCreateViewHolder(ViewGroup viewGroup, int viewType) {
- final LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
- final View view;
-
- final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
-
- switch (itemType) {
- case RECENT_TABS:
- case SYNCED_DEVICES:
- view = inflater.inflate(R.layout.home_smartfolder, viewGroup, false);
- return new CombinedHistoryItem.SmartFolder(view);
-
- case SECTION_HEADER:
- view = inflater.inflate(R.layout.home_header_row, viewGroup, false);
- return new CombinedHistoryItem.BasicItem(view);
-
- case HISTORY:
- view = inflater.inflate(R.layout.home_item_row, viewGroup, false);
- return new CombinedHistoryItem.HistoryItem(view);
- default:
- throw new IllegalArgumentException("Unexpected Home Panel item type");
- }
- }
-
- @Override
- public void onBindViewHolder(CombinedHistoryItem viewHolder, int position) {
- final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
- final int localPosition = transformAdapterPositionForDataStructure(itemType, position);
-
- switch (itemType) {
- case RECENT_TABS:
- ((CombinedHistoryItem.SmartFolder) viewHolder).bind(R.drawable.icon_recent, R.string.home_closed_tabs_title2, R.string.home_closed_tabs_one, R.string.home_closed_tabs_number, recentTabsCount);
- break;
-
- case SYNCED_DEVICES:
- ((CombinedHistoryItem.SmartFolder) viewHolder).bind(R.drawable.cloud, R.string.home_synced_devices_smartfolder, R.string.home_synced_devices_one, R.string.home_synced_devices_number, deviceCount);
- break;
-
- case SECTION_HEADER:
- ((TextView) viewHolder.itemView).setText(getSectionHeaderTitle(sectionHeaders.get(localPosition)));
- break;
-
- case HISTORY:
- if (historyCursor == null || !historyCursor.moveToPosition(localPosition)) {
- throw new IllegalStateException("Couldn't move cursor to position " + localPosition);
- }
- ((CombinedHistoryItem.HistoryItem) viewHolder).bind(historyCursor);
- break;
- }
- }
-
- /**
- * Transform an adapter position to the position for the data structure backing the item type.
- *
- * The type is not strictly necessary and could be fetched from <code>getItemTypeForPosition</code>,
- * but is used for explicitness.
- *
- * @param type ItemType of the item
- * @param position position in the adapter
- * @return position of the item in the data structure
- */
- @UiThread
- private int transformAdapterPositionForDataStructure(CombinedHistoryItem.ItemType type, int position) {
- if (type == CombinedHistoryItem.ItemType.SECTION_HEADER) {
- return position;
- } else if (type == CombinedHistoryItem.ItemType.HISTORY) {
- return position - getHeadersBefore(position) - getNumVisibleSmartFolders();
- } else {
- return position;
- }
- }
-
- @UiThread
- private CombinedHistoryItem.ItemType getItemTypeForPosition(int position) {
- if (position == RECENT_TABS_SMARTFOLDER_INDEX && isRecentTabsFolderVisible()) {
- return CombinedHistoryItem.ItemType.RECENT_TABS;
- }
- if (position == getSyncedDevicesSmartFolderIndex()) {
- return CombinedHistoryItem.ItemType.SYNCED_DEVICES;
- }
- final int sectionPosition = transformAdapterPositionForDataStructure(CombinedHistoryItem.ItemType.SECTION_HEADER, position);
- if (sectionHeaders.get(sectionPosition) != null) {
- return CombinedHistoryItem.ItemType.SECTION_HEADER;
- }
- return CombinedHistoryItem.ItemType.HISTORY;
- }
-
- @UiThread
- @Override
- public int getItemViewType(int position) {
- return CombinedHistoryItem.ItemType.itemTypeToViewType(getItemTypeForPosition(position));
- }
-
- @UiThread
- @Override
- public int getItemCount() {
- final int historySize = historyCursor == null ? 0 : historyCursor.getCount();
- return historySize + sectionHeaders.size() + getNumVisibleSmartFolders();
- }
-
- /**
- * Returns stable ID for each position. Data behind historyCursor is a sorted Combined view.
- *
- * @param position view item position for which to generate a stable ID
- * @return stable ID for given position
- */
- @UiThread
- @Override
- public long getItemId(int position) {
- // Two randomly selected large primes used to generate non-clashing IDs.
- final long PRIME_BOOKMARKS = 32416189867L;
- final long PRIME_SECTION_HEADERS = 32416187737L;
-
- // RecyclerView.NO_ID is -1, so let's start from -2 for our hard-coded IDs.
- final int RECENT_TABS_ID = -2;
- final int SYNCED_DEVICES_ID = -3;
-
- switch (getItemTypeForPosition(position)) {
- case RECENT_TABS:
- return RECENT_TABS_ID;
- case SYNCED_DEVICES:
- return SYNCED_DEVICES_ID;
- case SECTION_HEADER:
- // We might have multiple section headers, so we try get unique IDs for them.
- return position * PRIME_SECTION_HEADERS;
- case HISTORY:
- final int historyPosition = transformAdapterPositionForDataStructure(
- CombinedHistoryItem.ItemType.HISTORY, position);
- if (!historyCursor.moveToPosition(historyPosition)) {
- return RecyclerView.NO_ID;
- }
-
- final int historyIdCol = historyCursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID);
- final long historyId = historyCursor.getLong(historyIdCol);
-
- if (historyId != -1) {
- return historyId;
- }
-
- final int bookmarkIdCol = historyCursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID);
- final long bookmarkId = historyCursor.getLong(bookmarkIdCol);
-
- // Avoid clashing with historyId.
- return bookmarkId * PRIME_BOOKMARKS;
- default:
- throw new IllegalStateException("Unexpected Home Panel item type");
- }
- }
-
- /**
- * Add only the SectionHeaders that have history items within their range to a SparseArray, where the
- * array index is the position of the header in the history-only (no clients) ordering.
- * @param c data Cursor
- * @param sparseArray SparseArray to populate
- */
- @UiThread
- private void populateSectionHeaders(Cursor c, SparseArray<SectionHeader> sparseArray) {
- ThreadUtils.assertOnUiThread();
-
- sparseArray.clear();
-
- if (c == null || !c.moveToFirst()) {
- return;
- }
-
- SectionHeader section = null;
-
- do {
- final int historyPosition = c.getPosition();
- final long visitTime = c.getLong(c.getColumnIndexOrThrow(BrowserContract.History.DATE_LAST_VISITED));
- final SectionHeader itemSection = getSectionFromTime(visitTime);
-
- if (section != itemSection) {
- section = itemSection;
- sparseArray.append(historyPosition + sparseArray.size() + getNumVisibleSmartFolders(), section);
- }
-
- if (section == SectionHeader.OLDER_THAN_SIX_MONTHS) {
- break;
- }
- } while (c.moveToNext());
- }
-
- private static String getSectionHeaderTitle(SectionHeader section) {
- return sectionDateRangeArray[section.ordinal()].displayName;
- }
-
- private static SectionHeader getSectionFromTime(long time) {
- for (int i = 0; i < SectionHeader.OLDER_THAN_SIX_MONTHS.ordinal(); i++) {
- if (time > sectionDateRangeArray[i].start) {
- return SectionHeader.values()[i];
- }
- }
-
- return SectionHeader.OLDER_THAN_SIX_MONTHS;
- }
-
- /**
- * Returns the number of section headers before the given history item at the adapter position.
- * @param position position in the adapter
- */
- private int getHeadersBefore(int position) {
- // Skip the first header case because there will always be a header.
- for (int i = 1; i < sectionHeaders.size(); i++) {
- // If the position of the header is greater than the history position,
- // return the number of headers tested.
- if (sectionHeaders.keyAt(i) > position) {
- return i;
- }
- }
- return sectionHeaders.size();
- }
-
- @Override
- public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) {
- final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
- if (itemType == CombinedHistoryItem.ItemType.HISTORY) {
- final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, -1);
-
- historyCursor.moveToPosition(transformAdapterPositionForDataStructure(CombinedHistoryItem.ItemType.HISTORY, position));
- return populateHistoryInfoFromCursor(info, historyCursor);
- }
- return null;
- }
-
- protected static HomeContextMenuInfo populateHistoryInfoFromCursor(HomeContextMenuInfo info, Cursor cursor) {
- info.url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
- info.title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.TITLE));
- info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID));
- info.itemType = HomeContextMenuInfo.RemoveItemType.HISTORY;
- final int bookmarkIdCol = cursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID);
- if (cursor.isNull(bookmarkIdCol)) {
- // If this is a combined cursor, we may get a history item without a
- // bookmark, in which case the bookmarks ID column value will be null.
- info.bookmarkId = -1;
- } else {
- info.bookmarkId = cursor.getInt(bookmarkIdCol);
- }
- return info;
- }
-
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java
deleted file mode 100644
index a2c1b72c2..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-package org.mozilla.gecko.home;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.support.v4.content.ContextCompat;
-import android.support.v7.widget.RecyclerView;
-import android.util.Log;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.TextView;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.RemoteClient;
-import org.mozilla.gecko.db.RemoteTab;
-import org.mozilla.gecko.home.RecentTabsAdapter.ClosedTab;
-
-public abstract class CombinedHistoryItem extends RecyclerView.ViewHolder {
- private static final String LOGTAG = "CombinedHistoryItem";
-
- public CombinedHistoryItem(View view) {
- super(view);
- }
-
- public enum ItemType {
- CLIENT, HIDDEN_DEVICES, SECTION_HEADER, HISTORY, NAVIGATION_BACK, CHILD, SYNCED_DEVICES,
- RECENT_TABS, CLOSED_TAB;
-
- public static ItemType viewTypeToItemType(int viewType) {
- if (viewType >= ItemType.values().length) {
- Log.e(LOGTAG, "No corresponding ItemType!");
- }
- return ItemType.values()[viewType];
- }
-
- public static int itemTypeToViewType(ItemType itemType) {
- return itemType.ordinal();
- }
- }
-
- public static class BasicItem extends CombinedHistoryItem {
- public BasicItem(View view) {
- super(view);
- }
- }
-
- public static class SmartFolder extends CombinedHistoryItem {
- final Context context;
- final ImageView icon;
- final TextView title;
- final TextView subtext;
-
- public SmartFolder(View view) {
- super(view);
- context = view.getContext();
-
- icon = (ImageView) view.findViewById(R.id.device_type);
- title = (TextView) view.findViewById(R.id.title);
- subtext = (TextView) view.findViewById(R.id.subtext);
- }
-
- public void bind(int drawableRes, int titleRes, int singleDeviceRes, int multiDeviceRes, int numDevices) {
- icon.setImageResource(drawableRes);
- title.setText(titleRes);
- final String subtitle = numDevices == 1 ? context.getString(singleDeviceRes) : context.getString(multiDeviceRes, numDevices);
- subtext.setText(subtitle);
- }
- }
-
- public static class HistoryItem extends CombinedHistoryItem {
- public HistoryItem(View view) {
- super(view);
- }
-
- public void bind(Cursor historyCursor) {
- final TwoLinePageRow pageRow = (TwoLinePageRow) this.itemView;
- pageRow.setShowIcons(true);
- pageRow.updateFromCursor(historyCursor);
- }
-
- public void bind(RemoteTab remoteTab) {
- final TwoLinePageRow childPageRow = (TwoLinePageRow) this.itemView;
- childPageRow.setShowIcons(true);
- childPageRow.update(remoteTab.title, remoteTab.url);
- }
-
- public void bind(ClosedTab closedTab) {
- final TwoLinePageRow childPageRow = (TwoLinePageRow) this.itemView;
- childPageRow.setShowIcons(false);
- childPageRow.update(closedTab.title, closedTab.url);
- }
- }
-
- public static class ClientItem extends CombinedHistoryItem {
- final TextView nameView;
- final ImageView deviceTypeView;
- final TextView lastModifiedView;
- final ImageView deviceExpanded;
-
- public ClientItem(View view) {
- super(view);
- nameView = (TextView) view.findViewById(R.id.client);
- deviceTypeView = (ImageView) view.findViewById(R.id.device_type);
- lastModifiedView = (TextView) view.findViewById(R.id.last_synced);
- deviceExpanded = (ImageView) view.findViewById(R.id.device_expanded);
- }
-
- public void bind(Context context, RemoteClient client, boolean isCollapsed) {
- this.nameView.setText(client.name);
- final long now = System.currentTimeMillis();
- this.lastModifiedView.setText(ClientsAdapter.getLastSyncedString(context, now, client.lastModified));
-
- if (client.isDesktop()) {
- deviceTypeView.setImageResource(isCollapsed ? R.drawable.sync_desktop_inactive : R.drawable.sync_desktop);
- } else {
- deviceTypeView.setImageResource(isCollapsed ? R.drawable.sync_mobile_inactive : R.drawable.sync_mobile);
- }
-
- nameView.setTextColor(ContextCompat.getColor(context, isCollapsed ? R.color.tabs_tray_icon_grey : R.color.placeholder_active_grey));
- if (client.tabs.size() > 0) {
- deviceExpanded.setImageResource(isCollapsed ? R.drawable.home_group_collapsed : R.drawable.arrow_down);
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
deleted file mode 100644
index c9afecd63..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
+++ /dev/null
@@ -1,697 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import android.accounts.Account;
-import android.app.AlertDialog;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.database.Cursor;
-import android.os.Bundle;
-import android.support.annotation.UiThread;
-import android.support.v4.app.LoaderManager;
-import android.support.v4.content.Loader;
-import android.support.v4.widget.SwipeRefreshLayout;
-import android.support.v7.widget.DefaultItemAnimator;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.text.SpannableStringBuilder;
-import android.text.TextPaint;
-import android.text.method.LinkMovementMethod;
-import android.text.style.ClickableSpan;
-import android.text.style.UnderlineSpan;
-import android.util.Log;
-import android.view.ContextMenu;
-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.ImageView;
-import android.widget.TextView;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.RemoteClientsDialogFragment;
-import org.mozilla.gecko.fxa.FirefoxAccounts;
-import org.mozilla.gecko.fxa.FxAccountConstants;
-import org.mozilla.gecko.fxa.SyncStatusListener;
-import org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel;
-import org.mozilla.gecko.restrictions.Restrictions;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.db.RemoteClient;
-import org.mozilla.gecko.restrictions.Restrictable;
-import org.mozilla.gecko.widget.HistoryDividerItemDecoration;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.PARENT;
-import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD_SYNC;
-import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD_RECENT_TABS;
-
-public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsDialogFragment.RemoteClientsListener {
- private static final String LOGTAG = "GeckoCombinedHistoryPnl";
-
- private static final String[] STAGES_TO_SYNC_ON_REFRESH = new String[] { "clients", "tabs" };
- private final int LOADER_ID_HISTORY = 0;
- private final int LOADER_ID_REMOTE = 1;
-
- // String placeholders to mark formatting.
- private final static String FORMAT_S1 = "%1$s";
- private final static String FORMAT_S2 = "%2$s";
-
- private CombinedHistoryRecyclerView mRecyclerView;
- private CombinedHistoryAdapter mHistoryAdapter;
- private ClientsAdapter mClientsAdapter;
- private RecentTabsAdapter mRecentTabsAdapter;
- private CursorLoaderCallbacks mCursorLoaderCallbacks;
-
- private Bundle mSavedRestoreBundle;
-
- private PanelLevel mPanelLevel;
- private Button mPanelFooterButton;
-
- private PanelStateUpdateHandler mPanelStateUpdateHandler;
-
- // Child refresh layout view.
- protected SwipeRefreshLayout mRefreshLayout;
-
- // Sync listener that stops refreshing when a sync is completed.
- protected RemoteTabsSyncListener mSyncStatusListener;
-
- // Reference to the View to display when there are no results.
- private View mHistoryEmptyView;
- private View mClientsEmptyView;
- private View mRecentTabsEmptyView;
-
- public interface OnPanelLevelChangeListener {
- enum PanelLevel {
- PARENT, CHILD_SYNC, CHILD_RECENT_TABS
- }
-
- /**
- * Propagates level changes.
- * @param level
- * @return true if level changed, false otherwise.
- */
- boolean changeLevel(PanelLevel level);
- }
-
- @Override
- public void onCreate(Bundle savedInstance) {
- super.onCreate(savedInstance);
-
- int cachedRecentTabsCount = 0;
- if (mPanelStateChangeListener != null ) {
- cachedRecentTabsCount = mPanelStateChangeListener.getCachedRecentTabsCount();
- }
- mHistoryAdapter = new CombinedHistoryAdapter(getResources(), cachedRecentTabsCount);
- if (mPanelStateChangeListener != null) {
- mHistoryAdapter.setPanelStateChangeListener(mPanelStateChangeListener);
- }
-
- mClientsAdapter = new ClientsAdapter(getContext());
- // The RecentTabsAdapter doesn't use a cursor and therefore can't use the CursorLoader's
- // onLoadFinished() callback for updating the panel state when the closed tab count changes.
- // Instead, we provide it with independent callbacks as necessary.
- mRecentTabsAdapter = new RecentTabsAdapter(getContext(),
- mHistoryAdapter.getRecentTabsUpdateHandler(), getPanelStateUpdateHandler());
-
- mSyncStatusListener = new RemoteTabsSyncListener();
- FirefoxAccounts.addSyncStatusListener(mSyncStatusListener);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- return inflater.inflate(R.layout.home_combined_history_panel, container, false);
- }
-
- @UiThread
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
-
- mRecyclerView = (CombinedHistoryRecyclerView) view.findViewById(R.id.combined_recycler_view);
- setUpRecyclerView();
-
- mRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.refresh_layout);
- setUpRefreshLayout();
-
- mClientsEmptyView = view.findViewById(R.id.home_clients_empty_view);
- mHistoryEmptyView = view.findViewById(R.id.home_history_empty_view);
- mRecentTabsEmptyView = view.findViewById(R.id.home_recent_tabs_empty_view);
- setUpEmptyViews();
-
- mPanelFooterButton = (Button) view.findViewById(R.id.history_panel_footer_button);
- mPanelFooterButton.setText(R.string.home_clear_history_button);
- mPanelFooterButton.setOnClickListener(new OnFooterButtonClickListener());
-
- mRecentTabsAdapter.startListeningForClosedTabs();
- mRecentTabsAdapter.startListeningForHistorySanitize();
-
- if (mSavedRestoreBundle != null) {
- setPanelStateFromBundle(mSavedRestoreBundle);
- mSavedRestoreBundle = null;
- }
- }
-
- @UiThread
- private void setUpRecyclerView() {
- if (mPanelLevel == null) {
- mPanelLevel = PARENT;
- }
-
- mRecyclerView.setAdapter(mPanelLevel == PARENT ? mHistoryAdapter :
- mPanelLevel == CHILD_SYNC ? mClientsAdapter : mRecentTabsAdapter);
-
- final RecyclerView.ItemAnimator animator = new DefaultItemAnimator();
- animator.setAddDuration(100);
- animator.setChangeDuration(100);
- animator.setMoveDuration(100);
- animator.setRemoveDuration(100);
- mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
- mHistoryAdapter.setLinearLayoutManager((LinearLayoutManager) mRecyclerView.getLayoutManager());
- mRecyclerView.setItemAnimator(animator);
- mRecyclerView.addItemDecoration(new HistoryDividerItemDecoration(getContext()));
- mRecyclerView.setOnHistoryClickedListener(mUrlOpenListener);
- mRecyclerView.setOnPanelLevelChangeListener(new OnLevelChangeListener());
- mRecyclerView.setHiddenClientsDialogBuilder(new HiddenClientsHelper());
- mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
- @Override
- public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
- super.onScrolled(recyclerView, dx, dy);
- final LinearLayoutManager llm = (LinearLayoutManager) recyclerView.getLayoutManager();
- if ((mPanelLevel == PARENT) && (llm.findLastCompletelyVisibleItemPosition() == HistoryCursorLoader.HISTORY_LIMIT)) {
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.LIST, "history_scroll_max");
- }
-
- }
- });
- registerForContextMenu(mRecyclerView);
- }
-
- private void setUpRefreshLayout() {
- mRefreshLayout.setColorSchemeResources(R.color.fennec_ui_orange, R.color.action_orange);
- mRefreshLayout.setOnRefreshListener(new RemoteTabsRefreshListener());
- mRefreshLayout.setEnabled(false);
- }
-
- private void setUpEmptyViews() {
- // Set up history empty view.
- final ImageView historyIcon = (ImageView) mHistoryEmptyView.findViewById(R.id.home_empty_image);
- historyIcon.setVisibility(View.GONE);
-
- final TextView historyText = (TextView) mHistoryEmptyView.findViewById(R.id.home_empty_text);
- historyText.setText(R.string.home_most_recent_empty);
-
- final TextView historyHint = (TextView) mHistoryEmptyView.findViewById(R.id.home_empty_hint);
-
- if (!Restrictions.isAllowed(getActivity(), Restrictable.PRIVATE_BROWSING)) {
- historyHint.setVisibility(View.GONE);
- } else {
- final String hintText = getResources().getString(R.string.home_most_recent_emptyhint);
- final SpannableStringBuilder hintBuilder = formatHintText(hintText);
- if (hintBuilder != null) {
- historyHint.setText(hintBuilder);
- historyHint.setMovementMethod(LinkMovementMethod.getInstance());
- historyHint.setVisibility(View.VISIBLE);
- }
- }
-
- // Set up Clients empty view.
- final Button syncSetupButton = (Button) mClientsEmptyView.findViewById(R.id.sync_setup_button);
- syncSetupButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "history_syncsetup");
- // This Activity will redirect to the correct Activity as needed.
- final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
- startActivity(intent);
- }
- });
-
- // Set up Recent Tabs empty view.
- final ImageView recentTabsIcon = (ImageView) mRecentTabsEmptyView.findViewById(R.id.home_empty_image);
- recentTabsIcon.setImageResource(R.drawable.icon_remote_tabs_empty);
-
- final TextView recentTabsText = (TextView) mRecentTabsEmptyView.findViewById(R.id.home_empty_text);
- recentTabsText.setText(R.string.home_last_tabs_empty);
- }
-
- @Override
- public void setPanelStateChangeListener(
- PanelStateChangeListener panelStateChangeListener) {
- super.setPanelStateChangeListener(panelStateChangeListener);
- if (mHistoryAdapter != null) {
- mHistoryAdapter.setPanelStateChangeListener(panelStateChangeListener);
- }
- }
-
- @Override
- public void restoreData(Bundle data) {
- if (mRecyclerView != null) {
- setPanelStateFromBundle(data);
- } else {
- mSavedRestoreBundle = data;
- }
- }
-
- private void setPanelStateFromBundle(Bundle data) {
- if (data != null && data.getBoolean("goToRecentTabs", false) && mPanelLevel != CHILD_RECENT_TABS) {
- mPanelLevel = CHILD_RECENT_TABS;
- mRecyclerView.swapAdapter(mRecentTabsAdapter, true);
- updateEmptyView(CHILD_RECENT_TABS);
- updateButtonFromLevel();
- }
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- mCursorLoaderCallbacks = new CursorLoaderCallbacks();
- }
-
- @Override
- protected void load() {
- getLoaderManager().initLoader(LOADER_ID_HISTORY, null, mCursorLoaderCallbacks);
- getLoaderManager().initLoader(LOADER_ID_REMOTE, null, mCursorLoaderCallbacks);
- }
-
- private static class RemoteTabsCursorLoader extends SimpleCursorLoader {
- private final GeckoProfile mProfile;
-
- public RemoteTabsCursorLoader(Context context) {
- super(context);
- mProfile = GeckoProfile.get(context);
- }
-
- @Override
- public Cursor loadCursor() {
- return BrowserDB.from(mProfile).getTabsAccessor().getRemoteTabsCursor(getContext());
- }
- }
-
- private static class HistoryCursorLoader extends SimpleCursorLoader {
- // Max number of history results
- public static final int HISTORY_LIMIT = 100;
- private final BrowserDB mDB;
-
- public HistoryCursorLoader(Context context) {
- super(context);
- mDB = BrowserDB.from(context);
- }
-
- @Override
- public Cursor loadCursor() {
- final ContentResolver cr = getContext().getContentResolver();
- return mDB.getRecentHistory(cr, HISTORY_LIMIT);
- }
- }
-
- private class CursorLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
- private BrowserDB mDB; // Pseudo-final: set in onCreateLoader.
-
- @Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- if (mDB == null) {
- mDB = BrowserDB.from(getActivity());
- }
-
- switch (id) {
- case LOADER_ID_HISTORY:
- return new HistoryCursorLoader(getContext());
- case LOADER_ID_REMOTE:
- return new RemoteTabsCursorLoader(getContext());
- default:
- Log.e(LOGTAG, "Unknown loader id!");
- return null;
- }
- }
-
- @Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
- final int loaderId = loader.getId();
- switch (loaderId) {
- case LOADER_ID_HISTORY:
- mHistoryAdapter.setHistory(c);
- updateEmptyView(PARENT);
- break;
-
- case LOADER_ID_REMOTE:
- final List<RemoteClient> clients = mDB.getTabsAccessor().getClientsFromCursor(c);
- mHistoryAdapter.getDeviceUpdateHandler().onDeviceCountUpdated(clients.size());
- mClientsAdapter.setClients(clients);
- updateEmptyView(CHILD_SYNC);
- break;
- }
-
- updateButtonFromLevel();
- }
-
- @Override
- public void onLoaderReset(Loader<Cursor> loader) {
- mClientsAdapter.setClients(Collections.<RemoteClient>emptyList());
- mHistoryAdapter.setHistory(null);
- }
- }
-
- public interface PanelStateUpdateHandler {
- void onPanelStateUpdated(PanelLevel level);
- }
-
- public PanelStateUpdateHandler getPanelStateUpdateHandler() {
- if (mPanelStateUpdateHandler == null) {
- mPanelStateUpdateHandler = new PanelStateUpdateHandler() {
- @Override
- public void onPanelStateUpdated(PanelLevel level) {
- updateEmptyView(level);
- updateButtonFromLevel();
- }
- };
- }
- return mPanelStateUpdateHandler;
- }
-
- protected class OnLevelChangeListener implements OnPanelLevelChangeListener {
- @Override
- public boolean changeLevel(PanelLevel level) {
- if (level == mPanelLevel) {
- return false;
- }
-
- mPanelLevel = level;
- switch (level) {
- case PARENT:
- mRecyclerView.swapAdapter(mHistoryAdapter, true);
- mRefreshLayout.setEnabled(false);
- break;
- case CHILD_SYNC:
- mRecyclerView.swapAdapter(mClientsAdapter, true);
- mRefreshLayout.setEnabled(mClientsAdapter.getClientsCount() > 0);
- break;
- case CHILD_RECENT_TABS:
- mRecyclerView.swapAdapter(mRecentTabsAdapter, true);
- break;
- }
-
- updateEmptyView(level);
- updateButtonFromLevel();
- return true;
- }
- }
-
- private void updateButtonFromLevel() {
- switch (mPanelLevel) {
- case PARENT:
- final boolean historyRestricted = !Restrictions.isAllowed(getActivity(), Restrictable.CLEAR_HISTORY);
- if (historyRestricted || mHistoryAdapter.getItemCount() == mHistoryAdapter.getNumVisibleSmartFolders()) {
- mPanelFooterButton.setVisibility(View.GONE);
- } else {
- mPanelFooterButton.setText(R.string.home_clear_history_button);
- mPanelFooterButton.setVisibility(View.VISIBLE);
- }
- break;
- case CHILD_RECENT_TABS:
- if (mRecentTabsAdapter.getClosedTabsCount() > 1) {
- mPanelFooterButton.setText(R.string.home_restore_all);
- mPanelFooterButton.setVisibility(View.VISIBLE);
- } else {
- mPanelFooterButton.setVisibility(View.GONE);
- }
- break;
- case CHILD_SYNC:
- mPanelFooterButton.setVisibility(View.GONE);
- break;
- }
- }
-
- private class OnFooterButtonClickListener implements View.OnClickListener {
- @Override
- public void onClick(View view) {
- switch (mPanelLevel) {
- case PARENT:
- final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity());
- dialogBuilder.setMessage(R.string.home_clear_history_confirm);
- dialogBuilder.setNegativeButton(R.string.button_cancel, new AlertDialog.OnClickListener() {
- @Override
- public void onClick(final DialogInterface dialog, final int which) {
- dialog.dismiss();
- }
- });
-
- dialogBuilder.setPositiveButton(R.string.button_ok, new AlertDialog.OnClickListener() {
- @Override
- public void onClick(final DialogInterface dialog, final int which) {
- dialog.dismiss();
-
- // Send message to Java to clear history.
- final JSONObject json = new JSONObject();
- try {
- json.put("history", true);
- } catch (JSONException e) {
- Log.e(LOGTAG, "JSON error", e);
- }
-
- GeckoAppShell.notifyObservers("Sanitize:ClearData", json.toString());
- Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.BUTTON, "history");
- }
- });
-
- dialogBuilder.show();
- break;
- case CHILD_RECENT_TABS:
- final String telemetryExtra = mRecentTabsAdapter.restoreAllTabs();
- if (telemetryExtra != null) {
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.BUTTON, telemetryExtra);
- }
- break;
- }
- }
- }
-
- private void updateEmptyView(PanelLevel level) {
- boolean showEmptyHistoryView = (mPanelLevel == PARENT && mHistoryEmptyView.isShown());
- boolean showEmptyClientsView = (mPanelLevel == CHILD_SYNC && mClientsEmptyView.isShown());
- boolean showEmptyRecentTabsView = (mPanelLevel == CHILD_RECENT_TABS && mRecentTabsEmptyView.isShown());
-
- if (mPanelLevel == level) {
- switch (mPanelLevel) {
- case PARENT:
- showEmptyHistoryView = mHistoryAdapter.getItemCount() == mHistoryAdapter.getNumVisibleSmartFolders();
- break;
-
- case CHILD_SYNC:
- showEmptyClientsView = mClientsAdapter.getItemCount() == 1;
- break;
-
- case CHILD_RECENT_TABS:
- showEmptyRecentTabsView = mRecentTabsAdapter.getClosedTabsCount() == 0;
- break;
- }
- }
-
- final boolean showEmptyView = showEmptyClientsView || showEmptyHistoryView || showEmptyRecentTabsView;
- mRecyclerView.setOverScrollMode(showEmptyView ? View.OVER_SCROLL_NEVER : View.OVER_SCROLL_IF_CONTENT_SCROLLS);
-
- mHistoryEmptyView.setVisibility(showEmptyHistoryView ? View.VISIBLE : View.GONE);
- mClientsEmptyView.setVisibility(showEmptyClientsView ? View.VISIBLE : View.GONE);
- mRecentTabsEmptyView.setVisibility(showEmptyRecentTabsView ? View.VISIBLE : View.GONE);
- }
-
- /**
- * Make Span that is clickable, and underlined
- * between the string markers <code>FORMAT_S1</code> and
- * <code>FORMAT_S2</code>.
- *
- * @param text String to format
- * @return formatted SpannableStringBuilder, or null if there
- * is not any text to format.
- */
- private SpannableStringBuilder formatHintText(String text) {
- // Set formatting as marked by string placeholders.
- final int underlineStart = text.indexOf(FORMAT_S1);
- final int underlineEnd = text.indexOf(FORMAT_S2);
-
- // Check that there is text to be formatted.
- if (underlineStart >= underlineEnd) {
- return null;
- }
-
- final SpannableStringBuilder ssb = new SpannableStringBuilder(text);
-
- // Set clickable text.
- final ClickableSpan clickableSpan = new ClickableSpan() {
- @Override
- public void onClick(View widget) {
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "hint_private_browsing");
- try {
- final JSONObject json = new JSONObject();
- json.put("type", "Menu:Open");
- GeckoApp.getEventDispatcher().dispatchEvent(json, null);
- EventDispatcher.getInstance().dispatchEvent(json, null);
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error forming JSON for Private Browsing contextual hint", e);
- }
- }
- };
-
- ssb.setSpan(clickableSpan, 0, text.length(), 0);
-
- // Remove underlining set by ClickableSpan.
- final UnderlineSpan noUnderlineSpan = new UnderlineSpan() {
- @Override
- public void updateDrawState(TextPaint textPaint) {
- textPaint.setUnderlineText(false);
- }
- };
-
- ssb.setSpan(noUnderlineSpan, 0, text.length(), 0);
-
- // Add underlining for "Private Browsing".
- ssb.setSpan(new UnderlineSpan(), underlineStart, underlineEnd, 0);
-
- ssb.delete(underlineEnd, underlineEnd + FORMAT_S2.length());
- ssb.delete(underlineStart, underlineStart + FORMAT_S1.length());
-
- return ssb;
- }
-
- @Override
- public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
- if (!(menuInfo instanceof RemoteTabsClientContextMenuInfo)) {
- // Long pressed item was not a RemoteTabsGroup item. Superclass
- // can handle this.
- super.onCreateContextMenu(menu, view, menuInfo);
- return;
- }
-
- // Long pressed item was a remote client; provide the appropriate menu.
- final MenuInflater inflater = new MenuInflater(view.getContext());
- inflater.inflate(R.menu.home_remote_tabs_client_contextmenu, menu);
-
- final RemoteTabsClientContextMenuInfo info = (RemoteTabsClientContextMenuInfo) menuInfo;
- menu.setHeaderTitle(info.client.name);
- }
-
- @Override
- public boolean onContextItemSelected(MenuItem item) {
- if (super.onContextItemSelected(item)) {
- // HomeFragment was able to handle to selected item.
- return true;
- }
-
- final ContextMenu.ContextMenuInfo menuInfo = item.getMenuInfo();
- if (!(menuInfo instanceof RemoteTabsClientContextMenuInfo)) {
- return false;
- }
-
- final RemoteTabsClientContextMenuInfo info = (RemoteTabsClientContextMenuInfo) menuInfo;
-
- final int itemId = item.getItemId();
- if (itemId == R.id.home_remote_tabs_hide_client) {
- mClientsAdapter.removeItem(info.position);
- return true;
- }
-
- return false;
- }
-
- interface DialogBuilder<E> {
- void createAndShowDialog(List<E> items);
- }
-
- protected class HiddenClientsHelper implements DialogBuilder<RemoteClient> {
- @Override
- public void createAndShowDialog(List<RemoteClient> clientsList) {
- final RemoteClientsDialogFragment dialog = RemoteClientsDialogFragment.newInstance(
- getResources().getString(R.string.home_remote_tabs_hidden_devices_title),
- getResources().getString(R.string.home_remote_tabs_unhide_selected_devices),
- RemoteClientsDialogFragment.ChoiceMode.MULTIPLE, new ArrayList<>(clientsList));
- dialog.setTargetFragment(CombinedHistoryPanel.this, 0);
- dialog.show(getActivity().getSupportFragmentManager(), "show-clients");
- }
- }
-
- @Override
- public void onClients(List<RemoteClient> clients) {
- mClientsAdapter.unhideClients(clients);
- }
-
- /**
- * Stores information regarding the creation of the context menu for a remote client.
- */
- protected static class RemoteTabsClientContextMenuInfo extends HomeContextMenuInfo {
- protected final RemoteClient client;
-
- public RemoteTabsClientContextMenuInfo(View targetView, int position, long id, RemoteClient client) {
- super(targetView, position, id);
- this.client = client;
- }
- }
-
- protected class RemoteTabsRefreshListener implements SwipeRefreshLayout.OnRefreshListener {
- @Override
- public void onRefresh() {
- if (FirefoxAccounts.firefoxAccountsExist(getActivity())) {
- final Account account = FirefoxAccounts.getFirefoxAccount(getActivity());
- FirefoxAccounts.requestImmediateSync(account, STAGES_TO_SYNC_ON_REFRESH, null);
- } else {
- Log.wtf(LOGTAG, "No Firefox Account found; this should never happen. Ignoring.");
- mRefreshLayout.setRefreshing(false);
- }
- }
- }
-
- protected class RemoteTabsSyncListener implements SyncStatusListener {
- @Override
- public Context getContext() {
- return getActivity();
- }
-
- @Override
- public Account getAccount() {
- return FirefoxAccounts.getFirefoxAccount(getContext());
- }
-
- @Override
- public void onSyncStarted() {
- }
-
- @Override
- public void onSyncFinished() {
- mRefreshLayout.setRefreshing(false);
- }
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
-
- mRecentTabsAdapter.stopListeningForClosedTabs();
- mRecentTabsAdapter.stopListeningForHistorySanitize();
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
-
- if (mSyncStatusListener != null) {
- FirefoxAccounts.removeSyncStatusListener(mSyncStatusListener);
- mSyncStatusListener = null;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryRecyclerView.java b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryRecyclerView.java
deleted file mode 100644
index e813e4c44..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryRecyclerView.java
+++ /dev/null
@@ -1,145 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import android.content.Context;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.util.AttributeSet;
-import android.view.KeyEvent;
-import android.view.View;
-import org.mozilla.gecko.db.RemoteClient;
-import org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.widget.RecyclerViewClickSupport;
-
-import java.util.EnumSet;
-
-import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD_RECENT_TABS;
-import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD_SYNC;
-import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.PARENT;
-
-public class CombinedHistoryRecyclerView extends RecyclerView
- implements RecyclerViewClickSupport.OnItemClickListener, RecyclerViewClickSupport.OnItemLongClickListener {
- public static String LOGTAG = "CombinedHistoryRecycView";
-
- protected interface AdapterContextMenuBuilder {
- HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position);
- }
-
- protected HomePager.OnUrlOpenListener mOnUrlOpenListener;
- protected OnPanelLevelChangeListener mOnPanelLevelChangeListener;
- protected CombinedHistoryPanel.DialogBuilder<RemoteClient> mDialogBuilder;
- protected HomeContextMenuInfo mContextMenuInfo;
-
- public CombinedHistoryRecyclerView(Context context) {
- super(context);
- init(context);
- }
-
- public CombinedHistoryRecyclerView(Context context, AttributeSet attributeSet) {
- super(context, attributeSet);
- init(context);
- }
-
- public CombinedHistoryRecyclerView(Context context, AttributeSet attributeSet, int defStyle) {
- super(context, attributeSet, defStyle);
- init(context);
- }
-
- private void init(Context context) {
- LinearLayoutManager layoutManager = new LinearLayoutManager(context);
- layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
- setLayoutManager(layoutManager);
-
- RecyclerViewClickSupport.addTo(this)
- .setOnItemClickListener(this)
- .setOnItemLongClickListener(this);
-
- setOnKeyListener(new View.OnKeyListener() {
- @Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- final int action = event.getAction();
-
- // If the user hit the BACK key, try to move to the parent folder.
- if (action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
- return mOnPanelLevelChangeListener.changeLevel(PARENT);
- }
- return false;
- }
- });
- }
-
- public void setOnHistoryClickedListener(HomePager.OnUrlOpenListener listener) {
- this.mOnUrlOpenListener = listener;
- }
-
- public void setOnPanelLevelChangeListener(OnPanelLevelChangeListener listener) {
- this.mOnPanelLevelChangeListener = listener;
- }
-
- public void setHiddenClientsDialogBuilder(CombinedHistoryPanel.DialogBuilder<RemoteClient> builder) {
- mDialogBuilder = builder;
- }
-
- @Override
- public void onItemClicked(RecyclerView recyclerView, int position, View v) {
- final int viewType = getAdapter().getItemViewType(position);
- final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
- final String telemetryExtra;
-
- switch (itemType) {
- case RECENT_TABS:
- mOnPanelLevelChangeListener.changeLevel(CHILD_RECENT_TABS);
- break;
-
- case SYNCED_DEVICES:
- mOnPanelLevelChangeListener.changeLevel(CHILD_SYNC);
- break;
-
- case CLIENT:
- ((ClientsAdapter) getAdapter()).toggleClient(position);
- break;
-
- case HIDDEN_DEVICES:
- if (mDialogBuilder != null) {
- mDialogBuilder.createAndShowDialog(((ClientsAdapter) getAdapter()).getHiddenClients());
- }
- break;
-
- case NAVIGATION_BACK:
- mOnPanelLevelChangeListener.changeLevel(PARENT);
- break;
-
- case CHILD:
- case HISTORY:
- if (mOnUrlOpenListener != null) {
- final TwoLinePageRow historyItem = (TwoLinePageRow) v;
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, "history");
- mOnUrlOpenListener.onUrlOpen(historyItem.getUrl(), EnumSet.of(HomePager.OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
- }
- break;
-
- case CLOSED_TAB:
- telemetryExtra = ((RecentTabsAdapter) getAdapter()).restoreTabFromPosition(position);
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, telemetryExtra);
- break;
- }
- }
-
- @Override
- public boolean onItemLongClicked(RecyclerView recyclerView, int position, View v) {
- mContextMenuInfo = ((AdapterContextMenuBuilder) getAdapter()).makeContextMenuInfoFromPosition(v, position);
- return showContextMenuForChild(this);
- }
-
- @Override
- public HomeContextMenuInfo getContextMenuInfo() {
- return mContextMenuInfo;
- }
-
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/DynamicPanel.java b/mobile/android/base/java/org/mozilla/gecko/home/DynamicPanel.java
deleted file mode 100644
index d2c136219..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/DynamicPanel.java
+++ /dev/null
@@ -1,393 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.BrowserContract.HomeItems;
-import org.mozilla.gecko.home.HomeConfig.PanelConfig;
-import org.mozilla.gecko.home.PanelLayout.ContextMenuRegistry;
-import org.mozilla.gecko.home.PanelLayout.DatasetHandler;
-import org.mozilla.gecko.home.PanelLayout.DatasetRequest;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.UIAsyncTask;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.support.v4.app.LoaderManager;
-import android.support.v4.content.Loader;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-
-/**
- * Fragment that displays dynamic content specified by a {@code PanelConfig}.
- * The {@code DynamicPanel} UI is built based on the given {@code LayoutType}
- * and its associated list of {@code ViewConfig}.
- *
- * {@code DynamicPanel} manages all necessary Loaders to load panel datasets
- * from their respective content providers. Each panel dataset has its own
- * associated Loader. This is enforced by defining the Loader IDs based on
- * their associated dataset IDs.
- *
- * The {@code PanelLayout} can make load and reset requests on datasets via
- * the provided {@code DatasetHandler}. This way it doesn't need to know the
- * details of how datasets are loaded and reset. Each time a dataset is
- * requested, {@code DynamicPanel} restarts a Loader with the respective ID (see
- * {@code PanelDatasetHandler}).
- *
- * See {@code PanelLayout} for more details on how {@code DynamicPanel}
- * receives dataset requests and delivers them back to the {@code PanelLayout}.
- */
-public class DynamicPanel extends HomeFragment {
- private static final String LOGTAG = "GeckoDynamicPanel";
-
- // Dataset ID to be used by the loader
- private static final String DATASET_REQUEST = "dataset_request";
-
- // Max number of items to display in the panel
- private static final int RESULT_LIMIT = 100;
-
- // The main view for this fragment. This contains the PanelLayout and PanelAuthLayout.
- private FrameLayout mView;
-
- // The panel layout associated with this panel
- private PanelLayout mPanelLayout;
-
- // The layout used to show authentication UI for this panel
- private PanelAuthLayout mPanelAuthLayout;
-
- // Cache used to keep track of whether or not the user has been authenticated.
- private PanelAuthCache mPanelAuthCache;
-
- // Hold a reference to the UiAsyncTask we use to check the state of the
- // PanelAuthCache, so that we can cancel it if necessary.
- private UIAsyncTask.WithoutParams<Boolean> mAuthStateTask;
-
- // The configuration associated with this panel
- private PanelConfig mPanelConfig;
-
- // Callbacks used for the loader
- private PanelLoaderCallbacks mLoaderCallbacks;
-
- // The current UI mode in the fragment
- private UIMode mUIMode;
-
- /*
- * Different UI modes to display depending on the authentication state.
- *
- * PANEL: Layout to display panel data.
- * AUTH: Authentication UI.
- */
- private enum UIMode {
- PANEL,
- AUTH
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- final Bundle args = getArguments();
- if (args != null) {
- mPanelConfig = (PanelConfig) args.getParcelable(HomePager.PANEL_CONFIG_ARG);
- }
-
- if (mPanelConfig == null) {
- throw new IllegalStateException("Can't create a DynamicPanel without a PanelConfig");
- }
-
- mPanelAuthCache = new PanelAuthCache(getActivity());
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- mView = new FrameLayout(getActivity());
- return mView;
- }
-
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
-
- // Restore whatever the UI mode the fragment had before
- // a device rotation.
- if (mUIMode != null) {
- setUIMode(mUIMode);
- }
-
- mPanelAuthCache.setOnChangeListener(new PanelAuthChangeListener());
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- mView = null;
- mPanelLayout = null;
- mPanelAuthLayout = null;
-
- mPanelAuthCache.setOnChangeListener(null);
-
- if (mAuthStateTask != null) {
- mAuthStateTask.cancel();
- mAuthStateTask = null;
- }
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- // Create callbacks before the initial loader is started.
- mLoaderCallbacks = new PanelLoaderCallbacks();
- loadIfVisible();
- }
-
- @Override
- protected void load() {
- Log.d(LOGTAG, "Loading layout");
-
- if (requiresAuth()) {
- mAuthStateTask = new UIAsyncTask.WithoutParams<Boolean>(ThreadUtils.getBackgroundHandler()) {
- @Override
- public synchronized Boolean doInBackground() {
- return mPanelAuthCache.isAuthenticated(mPanelConfig.getId());
- }
-
- @Override
- public void onPostExecute(Boolean isAuthenticated) {
- mAuthStateTask = null;
- setUIMode(isAuthenticated ? UIMode.PANEL : UIMode.AUTH);
- }
- };
- mAuthStateTask.execute();
- } else {
- setUIMode(UIMode.PANEL);
- }
- }
-
- /**
- * @return true if this panel requires authentication.
- */
- private boolean requiresAuth() {
- return mPanelConfig.getAuthConfig() != null;
- }
-
- /**
- * Lazily creates layout for panel data.
- */
- private void createPanelLayout() {
- final ContextMenuRegistry contextMenuRegistry = new ContextMenuRegistry() {
- @Override
- public void register(View view) {
- registerForContextMenu(view);
- }
- };
-
- switch (mPanelConfig.getLayoutType()) {
- case FRAME:
- final PanelDatasetHandler datasetHandler = new PanelDatasetHandler();
- mPanelLayout = new FramePanelLayout(getActivity(), mPanelConfig, datasetHandler,
- mUrlOpenListener, contextMenuRegistry);
- break;
-
- default:
- throw new IllegalStateException("Unrecognized layout type in DynamicPanel");
- }
-
- Log.d(LOGTAG, "Created layout of type: " + mPanelConfig.getLayoutType());
- mView.addView(mPanelLayout);
- }
-
- /**
- * Lazily creates layout for authentication UI.
- */
- private void createPanelAuthLayout() {
- mPanelAuthLayout = new PanelAuthLayout(getActivity(), mPanelConfig);
- mView.addView(mPanelAuthLayout, 0);
- }
-
- private void setUIMode(UIMode mode) {
- switch (mode) {
- case PANEL:
- if (mPanelAuthLayout != null) {
- mPanelAuthLayout.setVisibility(View.GONE);
- }
- if (mPanelLayout == null) {
- createPanelLayout();
- }
- mPanelLayout.setVisibility(View.VISIBLE);
-
- // Only trigger a reload if the UI mode has changed
- // (e.g. auth cache changes) and the fragment is allowed
- // to load its contents. Any loaders associated with the
- // panel layout will be automatically re-bound after a
- // device rotation, no need to explicitly load it again.
- if (mUIMode != mode && canLoad()) {
- mPanelLayout.load();
- }
- break;
-
- case AUTH:
- if (mPanelLayout != null) {
- mPanelLayout.setVisibility(View.GONE);
- }
- if (mPanelAuthLayout == null) {
- createPanelAuthLayout();
- }
- mPanelAuthLayout.setVisibility(View.VISIBLE);
- break;
-
- default:
- throw new IllegalStateException("Unrecognized UIMode in DynamicPanel");
- }
-
- mUIMode = mode;
- }
-
- /**
- * Used by the PanelLayout to make load and reset requests to
- * the holding fragment.
- */
- private class PanelDatasetHandler implements DatasetHandler {
- @Override
- public void requestDataset(DatasetRequest request) {
- Log.d(LOGTAG, "Requesting request: " + request);
-
- final Bundle bundle = new Bundle();
- bundle.putParcelable(DATASET_REQUEST, request);
-
- getLoaderManager().restartLoader(request.getViewIndex(),
- bundle, mLoaderCallbacks);
- }
-
- @Override
- public void resetDataset(int viewIndex) {
- Log.d(LOGTAG, "Resetting dataset: " + viewIndex);
-
- final LoaderManager lm = getLoaderManager();
-
- // Release any resources associated with the dataset if
- // it's currently loaded in memory.
- final Loader<?> datasetLoader = lm.getLoader(viewIndex);
- if (datasetLoader != null) {
- datasetLoader.reset();
- }
- }
- }
-
- /**
- * Cursor loader for the panel datasets.
- */
- private static class PanelDatasetLoader extends SimpleCursorLoader {
- private DatasetRequest mRequest;
-
- public PanelDatasetLoader(Context context, DatasetRequest request) {
- super(context);
- mRequest = request;
- }
-
- public DatasetRequest getRequest() {
- return mRequest;
- }
-
- @Override
- public void onContentChanged() {
- // Ensure the refresh request doesn't affect the view's filter
- // stack (i.e. use DATASET_LOAD type) but keep the current
- // dataset ID and filter.
- final DatasetRequest newRequest =
- new DatasetRequest(mRequest.getViewIndex(),
- DatasetRequest.Type.DATASET_LOAD,
- mRequest.getDatasetId(),
- mRequest.getFilterDetail());
-
- mRequest = newRequest;
- super.onContentChanged();
- }
-
- @Override
- public Cursor loadCursor() {
- final ContentResolver cr = getContext().getContentResolver();
-
- final String selection;
- final String[] selectionArgs;
-
- // Null represents the root filter
- if (mRequest.getFilter() == null) {
- selection = HomeItems.FILTER + " IS NULL";
- selectionArgs = null;
- } else {
- selection = HomeItems.FILTER + " = ?";
- selectionArgs = new String[] { mRequest.getFilter() };
- }
-
- final Uri queryUri = HomeItems.CONTENT_URI.buildUpon()
- .appendQueryParameter(BrowserContract.PARAM_DATASET_ID,
- mRequest.getDatasetId())
- .appendQueryParameter(BrowserContract.PARAM_LIMIT,
- String.valueOf(RESULT_LIMIT))
- .build();
-
- // XXX: You can use HomeItems.CONTENT_FAKE_URI for development
- // to pull items from fake_home_items.json.
- return cr.query(queryUri, null, selection, selectionArgs, null);
- }
- }
-
- /**
- * LoaderCallbacks implementation that interacts with the LoaderManager.
- */
- private class PanelLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
- @Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- final DatasetRequest request = (DatasetRequest) args.getParcelable(DATASET_REQUEST);
-
- Log.d(LOGTAG, "Creating loader for request: " + request);
- return new PanelDatasetLoader(getActivity(), request);
- }
-
- @Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
- final DatasetRequest request = getRequestFromLoader(loader);
- Log.d(LOGTAG, "Finished loader for request: " + request);
-
- if (mPanelLayout != null) {
- mPanelLayout.deliverDataset(request, cursor);
- }
- }
-
- @Override
- public void onLoaderReset(Loader<Cursor> loader) {
- final DatasetRequest request = getRequestFromLoader(loader);
- Log.d(LOGTAG, "Resetting loader for request: " + request);
-
- if (mPanelLayout != null) {
- mPanelLayout.releaseDataset(request.getViewIndex());
- }
- }
-
- private DatasetRequest getRequestFromLoader(Loader<Cursor> loader) {
- final PanelDatasetLoader datasetLoader = (PanelDatasetLoader) loader;
- return datasetLoader.getRequest();
- }
- }
-
- private class PanelAuthChangeListener implements PanelAuthCache.OnChangeListener {
- @Override
- public void onChange(String panelId, boolean isAuthenticated) {
- if (!mPanelConfig.getId().equals(panelId)) {
- return;
- }
-
- setUIMode(isAuthenticated ? UIMode.PANEL : UIMode.AUTH);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/FramePanelLayout.java b/mobile/android/base/java/org/mozilla/gecko/home/FramePanelLayout.java
deleted file mode 100644
index 7168c1576..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/FramePanelLayout.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
-import org.mozilla.gecko.home.HomeConfig.PanelConfig;
-import org.mozilla.gecko.home.HomeConfig.ViewConfig;
-
-import android.content.Context;
-import android.util.Log;
-import android.view.View;
-
-class FramePanelLayout extends PanelLayout {
- private static final String LOGTAG = "GeckoFramePanelLayout";
-
- private final View mChildView;
- private final ViewConfig mChildConfig;
-
- public FramePanelLayout(Context context, PanelConfig panelConfig, DatasetHandler datasetHandler,
- OnUrlOpenListener urlOpenListener, ContextMenuRegistry contextMenuRegistry) {
- super(context, panelConfig, datasetHandler, urlOpenListener, contextMenuRegistry);
-
- // This layout can only hold one view so we simply
- // take the first defined view from PanelConfig.
- mChildConfig = panelConfig.getViewAt(0);
- if (mChildConfig == null) {
- throw new IllegalStateException("FramePanelLayout requires a view in PanelConfig");
- }
-
- mChildView = createPanelView(mChildConfig);
- addView(mChildView);
- }
-
- @Override
- public void load() {
- Log.d(LOGTAG, "Loading");
-
- if (mChildView instanceof DatasetBacked) {
- final FilterDetail filter = new FilterDetail(mChildConfig.getFilter(), null);
-
- final DatasetRequest request = new DatasetRequest(mChildConfig.getIndex(),
- mChildConfig.getDatasetId(),
- filter);
-
- Log.d(LOGTAG, "Requesting child request: " + request);
- requestDataset(request);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HistorySectionsHelper.java b/mobile/android/base/java/org/mozilla/gecko/home/HistorySectionsHelper.java
deleted file mode 100644
index 7a49559f6..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/HistorySectionsHelper.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import android.content.res.Resources;
-
-import org.mozilla.gecko.home.CombinedHistoryAdapter.SectionHeader;
-import org.mozilla.gecko.R;
-
-import java.util.Calendar;
-import java.util.Locale;
-
-
-public class HistorySectionsHelper {
-
- // Constants for different time sections.
- private static final long MS_PER_DAY = 86400000;
- private static final long MS_PER_WEEK = MS_PER_DAY * 7;
-
- public static class SectionDateRange {
- public final long start;
- public final long end;
- public final String displayName;
-
- private SectionDateRange(long start, long end, String displayName) {
- this.start = start;
- this.end = end;
- this.displayName = displayName;
- }
- }
-
- /**
- * Updates the time range in milliseconds covered by each section header and sets the title.
- * @param res Resources for fetching string names
- * @param sectionsArray Array of section bookkeeping objects
- */
- public static void updateRecentSectionOffset(final Resources res, SectionDateRange[] sectionsArray) {
- final long now = System.currentTimeMillis();
- final Calendar cal = Calendar.getInstance();
-
- // Update calendar to this day.
- cal.set(Calendar.HOUR_OF_DAY, 0);
- cal.set(Calendar.MINUTE, 0);
- cal.set(Calendar.SECOND, 0);
- cal.set(Calendar.MILLISECOND, 1);
- final long currentDayMS = cal.getTimeInMillis();
-
- // Calculate the start and end time for each section header and set its display text.
- sectionsArray[SectionHeader.TODAY.ordinal()] =
- new SectionDateRange(currentDayMS, now, res.getString(R.string.history_today_section));
-
- sectionsArray[SectionHeader.YESTERDAY.ordinal()] =
- new SectionDateRange(currentDayMS - MS_PER_DAY, currentDayMS, res.getString(R.string.history_yesterday_section));
-
- sectionsArray[SectionHeader.WEEK.ordinal()] =
- new SectionDateRange(currentDayMS - MS_PER_WEEK, now, res.getString(R.string.history_week_section));
-
- // Update the calendar to beginning of next month to avoid problems calculating the last day of this month.
- cal.add(Calendar.MONTH, 1);
- cal.set(Calendar.DAY_OF_MONTH, cal.getMinimum(Calendar.DAY_OF_MONTH));
-
- // Iterate over the remaining history sections, moving backwards in time.
- for (int i = SectionHeader.THIS_MONTH.ordinal(); i < SectionHeader.OLDER_THAN_SIX_MONTHS.ordinal(); i++) {
- final long end = cal.getTimeInMillis();
-
- cal.add(Calendar.MONTH, -1);
- final long start = cal.getTimeInMillis();
-
- final String displayName = cal.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault());
-
- sectionsArray[i] = new SectionDateRange(start, end, displayName);
- }
-
- sectionsArray[SectionHeader.OLDER_THAN_SIX_MONTHS.ordinal()] =
- new SectionDateRange(0L, cal.getTimeInMillis(), res.getString(R.string.history_older_section));
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeAdapter.java
deleted file mode 100644
index 98d1ae6d8..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeAdapter.java
+++ /dev/null
@@ -1,224 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import org.mozilla.gecko.activitystream.ActivityStream;
-import org.mozilla.gecko.home.HomeConfig.PanelConfig;
-import org.mozilla.gecko.home.HomeConfig.PanelType;
-import org.mozilla.gecko.home.activitystream.ActivityStreamHomeFragment;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentStatePagerAdapter;
-import android.view.ViewGroup;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class HomeAdapter extends FragmentStatePagerAdapter {
-
- private final Context mContext;
- private final ArrayList<PanelInfo> mPanelInfos;
- private final Map<String, HomeFragment> mPanels;
- private final Map<String, Bundle> mRestoreBundles;
-
- private boolean mCanLoadHint;
-
- private OnAddPanelListener mAddPanelListener;
-
- private HomeFragment.PanelStateChangeListener mPanelStateChangeListener = null;
-
- public interface OnAddPanelListener {
- void onAddPanel(String title);
- }
-
- public void setPanelStateChangeListener(HomeFragment.PanelStateChangeListener listener) {
- mPanelStateChangeListener = listener;
-
- for (Fragment fragment : mPanels.values()) {
- ((HomeFragment) fragment).setPanelStateChangeListener(listener);
- }
- }
-
- public HomeAdapter(Context context, FragmentManager fm) {
- super(fm);
-
- mContext = context;
- mCanLoadHint = HomeFragment.DEFAULT_CAN_LOAD_HINT;
-
- mPanelInfos = new ArrayList<>();
- mPanels = new HashMap<>();
- mRestoreBundles = new HashMap<>();
- }
-
- @Override
- public int getCount() {
- return mPanelInfos.size();
- }
-
- @Override
- public Fragment getItem(int position) {
- PanelInfo info = mPanelInfos.get(position);
- return Fragment.instantiate(mContext, info.getClassName(mContext), info.getArgs());
- }
-
- @Override
- public CharSequence getPageTitle(int position) {
- if (mPanelInfos.size() > 0) {
- PanelInfo info = mPanelInfos.get(position);
- return info.getTitle().toUpperCase();
- }
-
- return null;
- }
-
- @Override
- public Object instantiateItem(ViewGroup container, int position) {
- final HomeFragment fragment = (HomeFragment) super.instantiateItem(container, position);
- fragment.setPanelStateChangeListener(mPanelStateChangeListener);
-
- final String id = mPanelInfos.get(position).getId();
- mPanels.put(id, fragment);
-
- if (mRestoreBundles.containsKey(id)) {
- fragment.restoreData(mRestoreBundles.get(id));
- mRestoreBundles.remove(id);
- }
-
- return fragment;
- }
-
- public void setRestoreData(int position, Bundle data) {
- final String id = mPanelInfos.get(position).getId();
- final HomeFragment fragment = mPanels.get(id);
-
- // We have no guarantees as to whether our desired fragment is instantiated yet: therefore
- // we might need to either pass data to the fragment, or store it for later.
- if (fragment != null) {
- fragment.restoreData(data);
- } else {
- mRestoreBundles.put(id, data);
- }
-
- }
-
- @Override
- public void destroyItem(ViewGroup container, int position, Object object) {
- final String id = mPanelInfos.get(position).getId();
-
- super.destroyItem(container, position, object);
- mPanels.remove(id);
- }
-
- public void setOnAddPanelListener(OnAddPanelListener listener) {
- mAddPanelListener = listener;
- }
-
- public int getItemPosition(String panelId) {
- for (int i = 0; i < mPanelInfos.size(); i++) {
- final String id = mPanelInfos.get(i).getId();
- if (id.equals(panelId)) {
- return i;
- }
- }
-
- return -1;
- }
-
- public String getPanelIdAtPosition(int position) {
- // getPanelIdAtPosition() might be called before HomeAdapter
- // has got its initial list of PanelConfigs. Just bail.
- if (mPanelInfos.isEmpty()) {
- return null;
- }
-
- return mPanelInfos.get(position).getId();
- }
-
- private void addPanel(PanelInfo info) {
- mPanelInfos.add(info);
-
- if (mAddPanelListener != null) {
- mAddPanelListener.onAddPanel(info.getTitle());
- }
- }
-
- public void update(List<PanelConfig> panelConfigs) {
- mPanels.clear();
- mPanelInfos.clear();
-
- if (panelConfigs != null) {
- for (PanelConfig panelConfig : panelConfigs) {
- final PanelInfo info = new PanelInfo(panelConfig);
- addPanel(info);
- }
- }
-
- notifyDataSetChanged();
- }
-
- public boolean getCanLoadHint() {
- return mCanLoadHint;
- }
-
- public void setCanLoadHint(boolean canLoadHint) {
- // We cache the last hint value so that we can use it when
- // creating new panels. See PanelInfo.getArgs().
- mCanLoadHint = canLoadHint;
-
- // Enable/disable loading on all existing panels
- for (Fragment panelFragment : mPanels.values()) {
- final HomeFragment panel = (HomeFragment) panelFragment;
- panel.setCanLoadHint(canLoadHint);
- }
- }
-
- private final class PanelInfo {
- private final PanelConfig mPanelConfig;
-
- PanelInfo(PanelConfig panelConfig) {
- mPanelConfig = panelConfig;
- }
-
- public String getId() {
- return mPanelConfig.getId();
- }
-
- public String getTitle() {
- return mPanelConfig.getTitle();
- }
-
- public String getClassName(Context context) {
- final PanelType type = mPanelConfig.getType();
-
- // Override top_sites with ActivityStream panel when enabled
- // PanelType.toString() returns the panel id
- if (type.toString() == "top_sites" &&
- ActivityStream.isEnabled(context) &&
- ActivityStream.isHomePanel()) {
- return ActivityStreamHomeFragment.class.getName();
- }
- return type.getPanelClass().getName();
- }
-
- public Bundle getArgs() {
- final Bundle args = new Bundle();
-
- args.putBoolean(HomePager.CAN_LOAD_ARG, mCanLoadHint);
-
- // Only DynamicPanels need the PanelConfig argument
- if (mPanelConfig.isDynamic()) {
- args.putParcelable(HomePager.PANEL_CONFIG_ARG, mPanelConfig);
- }
-
- return args;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeBanner.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeBanner.java
deleted file mode 100644
index 10f5db39e..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeBanner.java
+++ /dev/null
@@ -1,315 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import org.json.JSONObject;
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.animation.PropertyAnimator;
-import org.mozilla.gecko.animation.PropertyAnimator.Property;
-import org.mozilla.gecko.animation.ViewHelper;
-import org.mozilla.gecko.util.ResourceDrawableUtils;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.widget.EllipsisTextView;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.text.Html;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-
-public class HomeBanner extends LinearLayout
- implements GeckoEventListener {
- private static final String LOGTAG = "GeckoHomeBanner";
-
- // Used for tracking scroll length
- private float mTouchY = -1;
-
- // Used to detect for upwards scroll to push banner all the way up
- private boolean mSnapBannerToTop;
-
- // Tracks whether or not the banner should be shown on the current panel.
- private boolean mActive;
-
- // The user is currently swiping between HomePager pages
- private boolean mScrollingPages;
-
- // Tracks whether the user swiped the banner down, preventing us from autoshowing when the user
- // switches back to the default page.
- private boolean mUserSwipedDown;
-
- // We must use this custom TextView to address an issue on 2.3 and lower where ellipsized text
- // will not wrap more than 2 lines.
- private final EllipsisTextView mTextView;
- private final ImageView mIconView;
-
- // The height of the banner view.
- private final float mHeight;
-
- // Listener that gets called when the banner is dismissed from the close button.
- private OnDismissListener mOnDismissListener;
-
- public interface OnDismissListener {
- public void onDismiss();
- }
-
- public HomeBanner(Context context) {
- this(context, null);
- }
-
- public HomeBanner(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- LayoutInflater.from(context).inflate(R.layout.home_banner_content, this);
-
- mTextView = (EllipsisTextView) findViewById(R.id.text);
- mIconView = (ImageView) findViewById(R.id.icon);
-
- mHeight = getResources().getDimensionPixelSize(R.dimen.home_banner_height);
-
- // Disable the banner until a message is set.
- setEnabled(false);
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- // Tapping on the close button will ensure that the banner is never
- // showed again on this session.
- final ImageButton closeButton = (ImageButton) findViewById(R.id.close);
-
- // The drawable should have 50% opacity.
- closeButton.getDrawable().setAlpha(127);
-
- closeButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- HomeBanner.this.dismiss();
-
- // Send the current message id back to JS.
- GeckoAppShell.notifyObservers("HomeBanner:Dismiss", (String) getTag());
- }
- });
-
- setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- HomeBanner.this.dismiss();
-
- // Send the current message id back to JS.
- GeckoAppShell.notifyObservers("HomeBanner:Click", (String) getTag());
- }
- });
-
- GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, "HomeBanner:Data");
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, "HomeBanner:Data");
- }
-
- public void setScrollingPages(boolean scrollingPages) {
- mScrollingPages = scrollingPages;
- }
-
- public void setOnDismissListener(OnDismissListener listener) {
- mOnDismissListener = listener;
- }
-
- /**
- * Hides and disables the banner.
- */
- private void dismiss() {
- setVisibility(View.GONE);
- setEnabled(false);
-
- if (mOnDismissListener != null) {
- mOnDismissListener.onDismiss();
- }
- }
-
- /**
- * Sends a message to gecko to request a new banner message. UI is updated in handleMessage.
- */
- public void update() {
- GeckoAppShell.notifyObservers("HomeBanner:Get", null);
- }
-
- @Override
- public void handleMessage(String event, JSONObject message) {
- final String id = message.optString("id");
- final String text = message.optString("text");
- final String iconURI = message.optString("iconURI");
-
- // Don't update the banner if the message doesn't have valid id and text.
- if (TextUtils.isEmpty(id) || TextUtils.isEmpty(text)) {
- return;
- }
-
- // Update the banner message on the UI thread.
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- // Store the current message id to pass back to JS in the view's OnClickListener.
- setTag(id);
- mTextView.setOriginalText(Html.fromHtml(text));
-
- ResourceDrawableUtils.getDrawable(getContext(), iconURI, new ResourceDrawableUtils.BitmapLoader() {
- @Override
- public void onBitmapFound(final Drawable d) {
- // Hide the image view if we don't have an icon to show.
- if (d == null) {
- mIconView.setVisibility(View.GONE);
- } else {
- mIconView.setImageDrawable(d);
- mIconView.setVisibility(View.VISIBLE);
- }
- }
- });
-
- GeckoAppShell.notifyObservers("HomeBanner:Shown", id);
-
- // Enable the banner after a message is set.
- setEnabled(true);
-
- // Animate the banner if it is currently active.
- if (mActive) {
- animateUp();
- }
- }
- });
- }
-
- public void setActive(boolean active) {
- // No need to animate if not changing
- if (mActive == active) {
- return;
- }
-
- mActive = active;
-
- // Don't animate if the banner isn't enabled.
- if (!isEnabled()) {
- return;
- }
-
- if (active) {
- animateUp();
- } else {
- animateDown();
- }
- }
-
- private void ensureVisible() {
- // The banner visibility is set to GONE after it is animated off screen,
- // so we need to make it visible again.
- if (getVisibility() == View.GONE) {
- // Translate the banner off screen before setting it to VISIBLE.
- ViewHelper.setTranslationY(this, mHeight);
- setVisibility(View.VISIBLE);
- }
- }
-
- private void animateUp() {
- // Don't try to animate if the user swiped the banner down previously to hide it.
- if (mUserSwipedDown) {
- return;
- }
-
- ensureVisible();
-
- final PropertyAnimator animator = new PropertyAnimator(100);
- animator.attach(this, Property.TRANSLATION_Y, 0);
- animator.start();
- }
-
- private void animateDown() {
- if (ViewHelper.getTranslationY(this) == mHeight) {
- // Hide the banner to avoid intercepting clicks on pre-honeycomb devices.
- setVisibility(View.GONE);
- return;
- }
-
- final PropertyAnimator animator = new PropertyAnimator(100);
- animator.attach(this, Property.TRANSLATION_Y, mHeight);
- animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
- @Override
- public void onPropertyAnimationStart() {
- }
-
- @Override
- public void onPropertyAnimationEnd() {
- // Hide the banner to avoid intercepting clicks on pre-honeycomb devices.
- setVisibility(View.GONE);
- }
- });
- animator.start();
- }
-
- public void handleHomeTouch(MotionEvent event) {
- if (!mActive || !isEnabled() || mScrollingPages) {
- return;
- }
-
- ensureVisible();
-
- switch (event.getActionMasked()) {
- case MotionEvent.ACTION_DOWN: {
- // Track the beginning of the touch
- mTouchY = event.getRawY();
- break;
- }
-
- case MotionEvent.ACTION_MOVE: {
- final float curY = event.getRawY();
- final float delta = mTouchY - curY;
- mSnapBannerToTop = delta <= 0.0f;
-
- float newTranslationY = ViewHelper.getTranslationY(this) + delta;
-
- // Clamp the values to be between 0 and height.
- if (newTranslationY < 0.0f) {
- newTranslationY = 0.0f;
- } else if (newTranslationY > mHeight) {
- newTranslationY = mHeight;
- }
-
- // Don't change this value if it wasn't a significant movement
- if (delta >= 10 || delta <= -10) {
- mUserSwipedDown = (newTranslationY == mHeight);
- }
-
- ViewHelper.setTranslationY(this, newTranslationY);
- mTouchY = curY;
- break;
- }
-
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL: {
- mTouchY = -1;
- if (mSnapBannerToTop) {
- animateUp();
- } else {
- animateDown();
- mUserSwipedDown = true;
- }
- break;
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java
deleted file mode 100644
index 08e79be3a..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java
+++ /dev/null
@@ -1,1694 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.content.Context;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.text.TextUtils;
-import android.util.Pair;
-
-public final class HomeConfig {
- public static final String PREF_KEY_BOOKMARKS_PANEL_ENABLED = "bookmarksPanelEnabled";
- public static final String PREF_KEY_HISTORY_PANEL_ENABLED = "combinedHistoryPanelEnabled";
-
- /**
- * Used to determine what type of HomeFragment subclass to use when creating
- * a given panel. With the exception of DYNAMIC, all of these types correspond
- * to a default set of built-in panels. The DYNAMIC panel type is used by
- * third-party services to create panels with varying types of content.
- */
- @RobocopTarget
- public static enum PanelType implements Parcelable {
- TOP_SITES("top_sites", TopSitesPanel.class),
- BOOKMARKS("bookmarks", BookmarksPanel.class),
- COMBINED_HISTORY("combined_history", CombinedHistoryPanel.class),
- DYNAMIC("dynamic", DynamicPanel.class),
- // Deprecated panels that should no longer exist but are kept around for
- // migration code. Class references have been replaced with new version of the panel.
- DEPRECATED_REMOTE_TABS("remote_tabs", CombinedHistoryPanel.class),
- DEPRECATED_HISTORY("history", CombinedHistoryPanel.class),
- DEPRECATED_READING_LIST("reading_list", BookmarksPanel.class),
- DEPRECATED_RECENT_TABS("recent_tabs", CombinedHistoryPanel.class);
-
- private final String mId;
- private final Class<?> mPanelClass;
-
- PanelType(String id, Class<?> panelClass) {
- mId = id;
- mPanelClass = panelClass;
- }
-
- public static PanelType fromId(String id) {
- if (id == null) {
- throw new IllegalArgumentException("Could not convert null String to PanelType");
- }
-
- for (PanelType panelType : PanelType.values()) {
- if (TextUtils.equals(panelType.mId, id.toLowerCase())) {
- return panelType;
- }
- }
-
- throw new IllegalArgumentException("Could not convert String id to PanelType");
- }
-
- @Override
- public String toString() {
- return mId;
- }
-
- public Class<?> getPanelClass() {
- return mPanelClass;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(ordinal());
- }
-
- public static final Creator<PanelType> CREATOR = new Creator<PanelType>() {
- @Override
- public PanelType createFromParcel(final Parcel source) {
- return PanelType.values()[source.readInt()];
- }
-
- @Override
- public PanelType[] newArray(final int size) {
- return new PanelType[size];
- }
- };
- }
-
- public static class PanelConfig implements Parcelable {
- private final PanelType mType;
- private final String mTitle;
- private final String mId;
- private final LayoutType mLayoutType;
- private final List<ViewConfig> mViews;
- private final AuthConfig mAuthConfig;
- private final EnumSet<Flags> mFlags;
- private final int mPosition;
-
- static final String JSON_KEY_TYPE = "type";
- static final String JSON_KEY_TITLE = "title";
- static final String JSON_KEY_ID = "id";
- static final String JSON_KEY_LAYOUT = "layout";
- static final String JSON_KEY_VIEWS = "views";
- static final String JSON_KEY_AUTH_CONFIG = "authConfig";
- static final String JSON_KEY_DEFAULT = "default";
- static final String JSON_KEY_DISABLED = "disabled";
- static final String JSON_KEY_POSITION = "position";
-
- public enum Flags {
- DEFAULT_PANEL,
- DISABLED_PANEL
- }
-
- public PanelConfig(JSONObject json) throws JSONException, IllegalArgumentException {
- final String panelType = json.optString(JSON_KEY_TYPE, null);
- if (TextUtils.isEmpty(panelType)) {
- mType = PanelType.DYNAMIC;
- } else {
- mType = PanelType.fromId(panelType);
- }
-
- mTitle = json.getString(JSON_KEY_TITLE);
- mId = json.getString(JSON_KEY_ID);
-
- final String layoutTypeId = json.optString(JSON_KEY_LAYOUT, null);
- if (layoutTypeId != null) {
- mLayoutType = LayoutType.fromId(layoutTypeId);
- } else {
- mLayoutType = null;
- }
-
- final JSONArray jsonViews = json.optJSONArray(JSON_KEY_VIEWS);
- if (jsonViews != null) {
- mViews = new ArrayList<ViewConfig>();
-
- final int viewCount = jsonViews.length();
- for (int i = 0; i < viewCount; i++) {
- final JSONObject jsonViewConfig = (JSONObject) jsonViews.get(i);
- final ViewConfig viewConfig = new ViewConfig(i, jsonViewConfig);
- mViews.add(viewConfig);
- }
- } else {
- mViews = null;
- }
-
- final JSONObject jsonAuthConfig = json.optJSONObject(JSON_KEY_AUTH_CONFIG);
- if (jsonAuthConfig != null) {
- mAuthConfig = new AuthConfig(jsonAuthConfig);
- } else {
- mAuthConfig = null;
- }
-
- mFlags = EnumSet.noneOf(Flags.class);
-
- if (json.optBoolean(JSON_KEY_DEFAULT, false)) {
- mFlags.add(Flags.DEFAULT_PANEL);
- }
-
- if (json.optBoolean(JSON_KEY_DISABLED, false)) {
- mFlags.add(Flags.DISABLED_PANEL);
- }
-
- mPosition = json.optInt(JSON_KEY_POSITION, -1);
-
- validate();
- }
-
- @SuppressWarnings("unchecked")
- public PanelConfig(Parcel in) {
- mType = (PanelType) in.readParcelable(getClass().getClassLoader());
- mTitle = in.readString();
- mId = in.readString();
- mLayoutType = (LayoutType) in.readParcelable(getClass().getClassLoader());
-
- mViews = new ArrayList<ViewConfig>();
- in.readTypedList(mViews, ViewConfig.CREATOR);
-
- mAuthConfig = (AuthConfig) in.readParcelable(getClass().getClassLoader());
-
- mFlags = (EnumSet<Flags>) in.readSerializable();
- mPosition = in.readInt();
-
- validate();
- }
-
- public PanelConfig(PanelConfig panelConfig) {
- mType = panelConfig.mType;
- mTitle = panelConfig.mTitle;
- mId = panelConfig.mId;
- mLayoutType = panelConfig.mLayoutType;
-
- mViews = new ArrayList<ViewConfig>();
- List<ViewConfig> viewConfigs = panelConfig.mViews;
- if (viewConfigs != null) {
- for (ViewConfig viewConfig : viewConfigs) {
- mViews.add(new ViewConfig(viewConfig));
- }
- }
-
- mAuthConfig = panelConfig.mAuthConfig;
- mFlags = panelConfig.mFlags.clone();
- mPosition = panelConfig.mPosition;
-
- validate();
- }
-
- public PanelConfig(PanelType type, String title, String id) {
- this(type, title, id, EnumSet.noneOf(Flags.class));
- }
-
- public PanelConfig(PanelType type, String title, String id, EnumSet<Flags> flags) {
- this(type, title, id, null, null, null, flags, -1);
- }
-
- public PanelConfig(PanelType type, String title, String id, LayoutType layoutType,
- List<ViewConfig> views, AuthConfig authConfig, EnumSet<Flags> flags, int position) {
- mType = type;
- mTitle = title;
- mId = id;
- mLayoutType = layoutType;
- mViews = views;
- mAuthConfig = authConfig;
- mFlags = flags;
- mPosition = position;
-
- validate();
- }
-
- private void validate() {
- if (mType == null) {
- throw new IllegalArgumentException("Can't create PanelConfig with null type");
- }
-
- if (TextUtils.isEmpty(mTitle)) {
- throw new IllegalArgumentException("Can't create PanelConfig with empty title");
- }
-
- if (TextUtils.isEmpty(mId)) {
- throw new IllegalArgumentException("Can't create PanelConfig with empty id");
- }
-
- if (mLayoutType == null && mType == PanelType.DYNAMIC) {
- throw new IllegalArgumentException("Can't create a dynamic PanelConfig with null layout type");
- }
-
- if ((mViews == null || mViews.size() == 0) && mType == PanelType.DYNAMIC) {
- throw new IllegalArgumentException("Can't create a dynamic PanelConfig with no views");
- }
-
- if (mFlags == null) {
- throw new IllegalArgumentException("Can't create PanelConfig with null flags");
- }
- }
-
- public PanelType getType() {
- return mType;
- }
-
- public String getTitle() {
- return mTitle;
- }
-
- public String getId() {
- return mId;
- }
-
- public LayoutType getLayoutType() {
- return mLayoutType;
- }
-
- public int getViewCount() {
- return (mViews != null ? mViews.size() : 0);
- }
-
- public ViewConfig getViewAt(int index) {
- return (mViews != null ? mViews.get(index) : null);
- }
-
- public EnumSet<Flags> getFlags() {
- return mFlags.clone();
- }
-
- public boolean isDynamic() {
- return (mType == PanelType.DYNAMIC);
- }
-
- public boolean isDefault() {
- return mFlags.contains(Flags.DEFAULT_PANEL);
- }
-
- private void setIsDefault(boolean isDefault) {
- if (isDefault) {
- mFlags.add(Flags.DEFAULT_PANEL);
- } else {
- mFlags.remove(Flags.DEFAULT_PANEL);
- }
- }
-
- public boolean isDisabled() {
- return mFlags.contains(Flags.DISABLED_PANEL);
- }
-
- private void setIsDisabled(boolean isDisabled) {
- if (isDisabled) {
- mFlags.add(Flags.DISABLED_PANEL);
- } else {
- mFlags.remove(Flags.DISABLED_PANEL);
- }
- }
-
- public AuthConfig getAuthConfig() {
- return mAuthConfig;
- }
-
- public int getPosition() {
- return mPosition;
- }
-
- public JSONObject toJSON() throws JSONException {
- final JSONObject json = new JSONObject();
-
- json.put(JSON_KEY_TYPE, mType.toString());
- json.put(JSON_KEY_TITLE, mTitle);
- json.put(JSON_KEY_ID, mId);
-
- if (mLayoutType != null) {
- json.put(JSON_KEY_LAYOUT, mLayoutType.toString());
- }
-
- if (mViews != null) {
- final JSONArray jsonViews = new JSONArray();
-
- final int viewCount = mViews.size();
- for (int i = 0; i < viewCount; i++) {
- final ViewConfig viewConfig = mViews.get(i);
- final JSONObject jsonViewConfig = viewConfig.toJSON();
- jsonViews.put(jsonViewConfig);
- }
-
- json.put(JSON_KEY_VIEWS, jsonViews);
- }
-
- if (mAuthConfig != null) {
- json.put(JSON_KEY_AUTH_CONFIG, mAuthConfig.toJSON());
- }
-
- if (mFlags.contains(Flags.DEFAULT_PANEL)) {
- json.put(JSON_KEY_DEFAULT, true);
- }
-
- if (mFlags.contains(Flags.DISABLED_PANEL)) {
- json.put(JSON_KEY_DISABLED, true);
- }
-
- json.put(JSON_KEY_POSITION, mPosition);
-
- return json;
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == null) {
- return false;
- }
-
- if (this == o) {
- return true;
- }
-
- if (!(o instanceof PanelConfig)) {
- return false;
- }
-
- final PanelConfig other = (PanelConfig) o;
- return mId.equals(other.mId);
- }
-
- @Override
- public int hashCode() {
- return super.hashCode();
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeParcelable(mType, 0);
- dest.writeString(mTitle);
- dest.writeString(mId);
- dest.writeParcelable(mLayoutType, 0);
- dest.writeTypedList(mViews);
- dest.writeParcelable(mAuthConfig, 0);
- dest.writeSerializable(mFlags);
- dest.writeInt(mPosition);
- }
-
- public static final Creator<PanelConfig> CREATOR = new Creator<PanelConfig>() {
- @Override
- public PanelConfig createFromParcel(final Parcel in) {
- return new PanelConfig(in);
- }
-
- @Override
- public PanelConfig[] newArray(final int size) {
- return new PanelConfig[size];
- }
- };
- }
-
- public static enum LayoutType implements Parcelable {
- FRAME("frame");
-
- private final String mId;
-
- LayoutType(String id) {
- mId = id;
- }
-
- public static LayoutType fromId(String id) {
- if (id == null) {
- throw new IllegalArgumentException("Could not convert null String to LayoutType");
- }
-
- for (LayoutType layoutType : LayoutType.values()) {
- if (TextUtils.equals(layoutType.mId, id.toLowerCase())) {
- return layoutType;
- }
- }
-
- throw new IllegalArgumentException("Could not convert String id to LayoutType");
- }
-
- @Override
- public String toString() {
- return mId;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(ordinal());
- }
-
- public static final Creator<LayoutType> CREATOR = new Creator<LayoutType>() {
- @Override
- public LayoutType createFromParcel(final Parcel source) {
- return LayoutType.values()[source.readInt()];
- }
-
- @Override
- public LayoutType[] newArray(final int size) {
- return new LayoutType[size];
- }
- };
- }
-
- public static enum ViewType implements Parcelable {
- LIST("list"),
- GRID("grid");
-
- private final String mId;
-
- ViewType(String id) {
- mId = id;
- }
-
- public static ViewType fromId(String id) {
- if (id == null) {
- throw new IllegalArgumentException("Could not convert null String to ViewType");
- }
-
- for (ViewType viewType : ViewType.values()) {
- if (TextUtils.equals(viewType.mId, id.toLowerCase())) {
- return viewType;
- }
- }
-
- throw new IllegalArgumentException("Could not convert String id to ViewType");
- }
-
- @Override
- public String toString() {
- return mId;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(ordinal());
- }
-
- public static final Creator<ViewType> CREATOR = new Creator<ViewType>() {
- @Override
- public ViewType createFromParcel(final Parcel source) {
- return ViewType.values()[source.readInt()];
- }
-
- @Override
- public ViewType[] newArray(final int size) {
- return new ViewType[size];
- }
- };
- }
-
- public static enum ItemType implements Parcelable {
- ARTICLE("article"),
- IMAGE("image"),
- ICON("icon");
-
- private final String mId;
-
- ItemType(String id) {
- mId = id;
- }
-
- public static ItemType fromId(String id) {
- if (id == null) {
- throw new IllegalArgumentException("Could not convert null String to ItemType");
- }
-
- for (ItemType itemType : ItemType.values()) {
- if (TextUtils.equals(itemType.mId, id.toLowerCase())) {
- return itemType;
- }
- }
-
- throw new IllegalArgumentException("Could not convert String id to ItemType");
- }
-
- @Override
- public String toString() {
- return mId;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(ordinal());
- }
-
- public static final Creator<ItemType> CREATOR = new Creator<ItemType>() {
- @Override
- public ItemType createFromParcel(final Parcel source) {
- return ItemType.values()[source.readInt()];
- }
-
- @Override
- public ItemType[] newArray(final int size) {
- return new ItemType[size];
- }
- };
- }
-
- public static enum ItemHandler implements Parcelable {
- BROWSER("browser"),
- INTENT("intent");
-
- private final String mId;
-
- ItemHandler(String id) {
- mId = id;
- }
-
- public static ItemHandler fromId(String id) {
- if (id == null) {
- throw new IllegalArgumentException("Could not convert null String to ItemHandler");
- }
-
- for (ItemHandler itemHandler : ItemHandler.values()) {
- if (TextUtils.equals(itemHandler.mId, id.toLowerCase())) {
- return itemHandler;
- }
- }
-
- throw new IllegalArgumentException("Could not convert String id to ItemHandler");
- }
-
- @Override
- public String toString() {
- return mId;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(ordinal());
- }
-
- public static final Creator<ItemHandler> CREATOR = new Creator<ItemHandler>() {
- @Override
- public ItemHandler createFromParcel(final Parcel source) {
- return ItemHandler.values()[source.readInt()];
- }
-
- @Override
- public ItemHandler[] newArray(final int size) {
- return new ItemHandler[size];
- }
- };
- }
-
- public static class ViewConfig implements Parcelable {
- private final int mIndex;
- private final ViewType mType;
- private final String mDatasetId;
- private final ItemType mItemType;
- private final ItemHandler mItemHandler;
- private final String mBackImageUrl;
- private final String mFilter;
- private final EmptyViewConfig mEmptyViewConfig;
- private final HeaderConfig mHeaderConfig;
- private final EnumSet<Flags> mFlags;
-
- static final String JSON_KEY_TYPE = "type";
- static final String JSON_KEY_DATASET = "dataset";
- static final String JSON_KEY_ITEM_TYPE = "itemType";
- static final String JSON_KEY_ITEM_HANDLER = "itemHandler";
- static final String JSON_KEY_BACK_IMAGE_URL = "backImageUrl";
- static final String JSON_KEY_FILTER = "filter";
- static final String JSON_KEY_EMPTY = "empty";
- static final String JSON_KEY_HEADER = "header";
- static final String JSON_KEY_REFRESH_ENABLED = "refreshEnabled";
-
- public enum Flags {
- REFRESH_ENABLED
- }
-
- public ViewConfig(int index, JSONObject json) throws JSONException, IllegalArgumentException {
- mIndex = index;
- mType = ViewType.fromId(json.getString(JSON_KEY_TYPE));
- mDatasetId = json.getString(JSON_KEY_DATASET);
- mItemType = ItemType.fromId(json.getString(JSON_KEY_ITEM_TYPE));
- mItemHandler = ItemHandler.fromId(json.getString(JSON_KEY_ITEM_HANDLER));
- mBackImageUrl = json.optString(JSON_KEY_BACK_IMAGE_URL, null);
- mFilter = json.optString(JSON_KEY_FILTER, null);
-
- final JSONObject jsonEmptyViewConfig = json.optJSONObject(JSON_KEY_EMPTY);
- if (jsonEmptyViewConfig != null) {
- mEmptyViewConfig = new EmptyViewConfig(jsonEmptyViewConfig);
- } else {
- mEmptyViewConfig = null;
- }
-
- final JSONObject jsonHeaderConfig = json.optJSONObject(JSON_KEY_HEADER);
- mHeaderConfig = jsonHeaderConfig != null ? new HeaderConfig(jsonHeaderConfig) : null;
-
- mFlags = EnumSet.noneOf(Flags.class);
- if (json.optBoolean(JSON_KEY_REFRESH_ENABLED, false)) {
- mFlags.add(Flags.REFRESH_ENABLED);
- }
-
- validate();
- }
-
- @SuppressWarnings("unchecked")
- public ViewConfig(Parcel in) {
- mIndex = in.readInt();
- mType = (ViewType) in.readParcelable(getClass().getClassLoader());
- mDatasetId = in.readString();
- mItemType = (ItemType) in.readParcelable(getClass().getClassLoader());
- mItemHandler = (ItemHandler) in.readParcelable(getClass().getClassLoader());
- mBackImageUrl = in.readString();
- mFilter = in.readString();
- mEmptyViewConfig = (EmptyViewConfig) in.readParcelable(getClass().getClassLoader());
- mHeaderConfig = (HeaderConfig) in.readParcelable(getClass().getClassLoader());
- mFlags = (EnumSet<Flags>) in.readSerializable();
-
- validate();
- }
-
- public ViewConfig(ViewConfig viewConfig) {
- mIndex = viewConfig.mIndex;
- mType = viewConfig.mType;
- mDatasetId = viewConfig.mDatasetId;
- mItemType = viewConfig.mItemType;
- mItemHandler = viewConfig.mItemHandler;
- mBackImageUrl = viewConfig.mBackImageUrl;
- mFilter = viewConfig.mFilter;
- mEmptyViewConfig = viewConfig.mEmptyViewConfig;
- mHeaderConfig = viewConfig.mHeaderConfig;
- mFlags = viewConfig.mFlags.clone();
-
- validate();
- }
-
- private void validate() {
- if (mType == null) {
- throw new IllegalArgumentException("Can't create ViewConfig with null type");
- }
-
- if (TextUtils.isEmpty(mDatasetId)) {
- throw new IllegalArgumentException("Can't create ViewConfig with empty dataset ID");
- }
-
- if (mItemType == null) {
- throw new IllegalArgumentException("Can't create ViewConfig with null item type");
- }
-
- if (mItemHandler == null) {
- throw new IllegalArgumentException("Can't create ViewConfig with null item handler");
- }
-
- if (mFlags == null) {
- throw new IllegalArgumentException("Can't create ViewConfig with null flags");
- }
- }
-
- public int getIndex() {
- return mIndex;
- }
-
- public ViewType getType() {
- return mType;
- }
-
- public String getDatasetId() {
- return mDatasetId;
- }
-
- public ItemType getItemType() {
- return mItemType;
- }
-
- public ItemHandler getItemHandler() {
- return mItemHandler;
- }
-
- public String getBackImageUrl() {
- return mBackImageUrl;
- }
-
- public String getFilter() {
- return mFilter;
- }
-
- public EmptyViewConfig getEmptyViewConfig() {
- return mEmptyViewConfig;
- }
-
- public HeaderConfig getHeaderConfig() {
- return mHeaderConfig;
- }
-
- public boolean hasHeaderConfig() {
- return mHeaderConfig != null;
- }
-
- public boolean isRefreshEnabled() {
- return mFlags.contains(Flags.REFRESH_ENABLED);
- }
-
- public JSONObject toJSON() throws JSONException {
- final JSONObject json = new JSONObject();
-
- json.put(JSON_KEY_TYPE, mType.toString());
- json.put(JSON_KEY_DATASET, mDatasetId);
- json.put(JSON_KEY_ITEM_TYPE, mItemType.toString());
- json.put(JSON_KEY_ITEM_HANDLER, mItemHandler.toString());
-
- if (!TextUtils.isEmpty(mBackImageUrl)) {
- json.put(JSON_KEY_BACK_IMAGE_URL, mBackImageUrl);
- }
-
- if (!TextUtils.isEmpty(mFilter)) {
- json.put(JSON_KEY_FILTER, mFilter);
- }
-
- if (mEmptyViewConfig != null) {
- json.put(JSON_KEY_EMPTY, mEmptyViewConfig.toJSON());
- }
-
- if (mHeaderConfig != null) {
- json.put(JSON_KEY_HEADER, mHeaderConfig.toJSON());
- }
-
- if (mFlags.contains(Flags.REFRESH_ENABLED)) {
- json.put(JSON_KEY_REFRESH_ENABLED, true);
- }
-
- return json;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(mIndex);
- dest.writeParcelable(mType, 0);
- dest.writeString(mDatasetId);
- dest.writeParcelable(mItemType, 0);
- dest.writeParcelable(mItemHandler, 0);
- dest.writeString(mBackImageUrl);
- dest.writeString(mFilter);
- dest.writeParcelable(mEmptyViewConfig, 0);
- dest.writeParcelable(mHeaderConfig, 0);
- dest.writeSerializable(mFlags);
- }
-
- public static final Creator<ViewConfig> CREATOR = new Creator<ViewConfig>() {
- @Override
- public ViewConfig createFromParcel(final Parcel in) {
- return new ViewConfig(in);
- }
-
- @Override
- public ViewConfig[] newArray(final int size) {
- return new ViewConfig[size];
- }
- };
- }
-
- public static class EmptyViewConfig implements Parcelable {
- private final String mText;
- private final String mImageUrl;
-
- static final String JSON_KEY_TEXT = "text";
- static final String JSON_KEY_IMAGE_URL = "imageUrl";
-
- public EmptyViewConfig(JSONObject json) throws JSONException, IllegalArgumentException {
- mText = json.optString(JSON_KEY_TEXT, null);
- mImageUrl = json.optString(JSON_KEY_IMAGE_URL, null);
- }
-
- public EmptyViewConfig(Parcel in) {
- mText = in.readString();
- mImageUrl = in.readString();
- }
-
- public EmptyViewConfig(EmptyViewConfig emptyViewConfig) {
- mText = emptyViewConfig.mText;
- mImageUrl = emptyViewConfig.mImageUrl;
- }
-
- public EmptyViewConfig(String text, String imageUrl) {
- mText = text;
- mImageUrl = imageUrl;
- }
-
- public String getText() {
- return mText;
- }
-
- public String getImageUrl() {
- return mImageUrl;
- }
-
- public JSONObject toJSON() throws JSONException {
- final JSONObject json = new JSONObject();
-
- json.put(JSON_KEY_TEXT, mText);
- json.put(JSON_KEY_IMAGE_URL, mImageUrl);
-
- return json;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeString(mText);
- dest.writeString(mImageUrl);
- }
-
- public static final Creator<EmptyViewConfig> CREATOR = new Creator<EmptyViewConfig>() {
- @Override
- public EmptyViewConfig createFromParcel(final Parcel in) {
- return new EmptyViewConfig(in);
- }
-
- @Override
- public EmptyViewConfig[] newArray(final int size) {
- return new EmptyViewConfig[size];
- }
- };
- }
-
- public static class HeaderConfig implements Parcelable {
- static final String JSON_KEY_IMAGE_URL = "image_url";
- static final String JSON_KEY_URL = "url";
-
- private final String mImageUrl;
- private final String mUrl;
-
- public HeaderConfig(JSONObject json) {
- mImageUrl = json.optString(JSON_KEY_IMAGE_URL);
- mUrl = json.optString(JSON_KEY_URL);
- }
-
- public HeaderConfig(Parcel in) {
- mImageUrl = in.readString();
- mUrl = in.readString();
- }
-
- public String getImageUrl() {
- return mImageUrl;
- }
-
- public String getUrl() {
- return mUrl;
- }
-
- public JSONObject toJSON() throws JSONException {
- JSONObject json = new JSONObject();
-
- json.put(JSON_KEY_IMAGE_URL, mImageUrl);
- json.put(JSON_KEY_URL, mUrl);
-
- return json;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeString(mImageUrl);
- dest.writeString(mUrl);
- }
-
- public static final Creator<HeaderConfig> CREATOR = new Creator<HeaderConfig>() {
- @Override
- public HeaderConfig createFromParcel(Parcel source) {
- return new HeaderConfig(source);
- }
-
- @Override
- public HeaderConfig[] newArray(int size) {
- return new HeaderConfig[size];
- }
- };
- }
-
- public static class AuthConfig implements Parcelable {
- private final String mMessageText;
- private final String mButtonText;
- private final String mImageUrl;
-
- static final String JSON_KEY_MESSAGE_TEXT = "messageText";
- static final String JSON_KEY_BUTTON_TEXT = "buttonText";
- static final String JSON_KEY_IMAGE_URL = "imageUrl";
-
- public AuthConfig(JSONObject json) throws JSONException, IllegalArgumentException {
- mMessageText = json.optString(JSON_KEY_MESSAGE_TEXT);
- mButtonText = json.optString(JSON_KEY_BUTTON_TEXT);
- mImageUrl = json.optString(JSON_KEY_IMAGE_URL, null);
- }
-
- public AuthConfig(Parcel in) {
- mMessageText = in.readString();
- mButtonText = in.readString();
- mImageUrl = in.readString();
-
- validate();
- }
-
- public AuthConfig(AuthConfig authConfig) {
- mMessageText = authConfig.mMessageText;
- mButtonText = authConfig.mButtonText;
- mImageUrl = authConfig.mImageUrl;
-
- validate();
- }
-
- public AuthConfig(String messageText, String buttonText, String imageUrl) {
- mMessageText = messageText;
- mButtonText = buttonText;
- mImageUrl = imageUrl;
-
- validate();
- }
-
- private void validate() {
- if (mMessageText == null) {
- throw new IllegalArgumentException("Can't create AuthConfig with null message text");
- }
-
- if (mButtonText == null) {
- throw new IllegalArgumentException("Can't create AuthConfig with null button text");
- }
- }
-
- public String getMessageText() {
- return mMessageText;
- }
-
- public String getButtonText() {
- return mButtonText;
- }
-
- public String getImageUrl() {
- return mImageUrl;
- }
-
- public JSONObject toJSON() throws JSONException {
- final JSONObject json = new JSONObject();
-
- json.put(JSON_KEY_MESSAGE_TEXT, mMessageText);
- json.put(JSON_KEY_BUTTON_TEXT, mButtonText);
- json.put(JSON_KEY_IMAGE_URL, mImageUrl);
-
- return json;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeString(mMessageText);
- dest.writeString(mButtonText);
- dest.writeString(mImageUrl);
- }
-
- public static final Creator<AuthConfig> CREATOR = new Creator<AuthConfig>() {
- @Override
- public AuthConfig createFromParcel(final Parcel in) {
- return new AuthConfig(in);
- }
-
- @Override
- public AuthConfig[] newArray(final int size) {
- return new AuthConfig[size];
- }
- };
- }
- /**
- * Immutable representation of the current state of {@code HomeConfig}.
- * This is what HomeConfig returns from a load() call and takes as
- * input to save a new state.
- *
- * Users of {@code State} should use an {@code Iterator} to iterate
- * through the contained {@code PanelConfig} instances.
- *
- * {@code State} is immutable i.e. you can't add, remove, or update
- * contained elements directly. You have to use an {@code Editor} to
- * change the state, which can be created through the {@code edit()}
- * method.
- */
- public static class State implements Iterable<PanelConfig> {
- private HomeConfig mHomeConfig;
- private final List<PanelConfig> mPanelConfigs;
- private final boolean mIsDefault;
-
- State(List<PanelConfig> panelConfigs, boolean isDefault) {
- this(null, panelConfigs, isDefault);
- }
-
- private State(HomeConfig homeConfig, List<PanelConfig> panelConfigs, boolean isDefault) {
- mHomeConfig = homeConfig;
- mPanelConfigs = Collections.unmodifiableList(panelConfigs);
- mIsDefault = isDefault;
- }
-
- private void setHomeConfig(HomeConfig homeConfig) {
- if (mHomeConfig != null) {
- throw new IllegalStateException("Can't set HomeConfig more than once");
- }
-
- mHomeConfig = homeConfig;
- }
-
- @Override
- public Iterator<PanelConfig> iterator() {
- return mPanelConfigs.iterator();
- }
-
- /**
- * Returns whether this {@code State} instance represents the default
- * {@code HomeConfig} configuration or not.
- */
- public boolean isDefault() {
- return mIsDefault;
- }
-
- /**
- * Creates an {@code Editor} for this state.
- */
- public Editor edit() {
- return new Editor(mHomeConfig, this);
- }
- }
-
- /**
- * {@code Editor} allows you to make changes to a {@code State}. You
- * can create {@code Editor} by calling {@code edit()} on the target
- * {@code State} instance.
- *
- * {@code Editor} works on a copy of the {@code State} that originated
- * it. This means that adding, removing, or updating panels in an
- * {@code Editor} will never change the {@code State} which you
- * created the {@code Editor} from. Calling {@code commit()} or
- * {@code apply()} will cause the new {@code State} instance to be
- * created and saved using the {@code HomeConfig} instance that
- * created the source {@code State}.
- *
- * {@code Editor} is *not* thread-safe. You can only make calls on it
- * from the thread where it was originally created. It will throw an
- * exception if you don't follow this invariant.
- */
- public static class Editor implements Iterable<PanelConfig> {
- private final HomeConfig mHomeConfig;
- private final Map<String, PanelConfig> mConfigMap;
- private final List<String> mConfigOrder;
- private final Thread mOriginalThread;
-
- // Each Pair represents parameters to a GeckoAppShell.notifyObservers call;
- // the first String is the observer topic and the second string is the notification data.
- private List<Pair<String, String>> mNotificationQueue;
- private PanelConfig mDefaultPanel;
- private int mEnabledCount;
-
- private boolean mHasChanged;
- private final boolean mIsFromDefault;
-
- private Editor(HomeConfig homeConfig, State configState) {
- mHomeConfig = homeConfig;
- mOriginalThread = Thread.currentThread();
- mConfigMap = new HashMap<String, PanelConfig>();
- mConfigOrder = new LinkedList<String>();
- mNotificationQueue = new ArrayList<>();
-
- mIsFromDefault = configState.isDefault();
-
- initFromState(configState);
- }
-
- /**
- * Initialize the initial state of the editor from the given
- * {@sode State}. A HashMap is used to represent the list of
- * panels as it provides fast access, and a LinkedList is used to
- * keep track of order. We keep a reference to the default panel
- * and the number of enabled panels to avoid iterating through the
- * map every time we need those.
- *
- * @param configState The source State to load the editor from.
- */
- private void initFromState(State configState) {
- for (PanelConfig panelConfig : configState) {
- final PanelConfig panelCopy = new PanelConfig(panelConfig);
-
- if (!panelCopy.isDisabled()) {
- mEnabledCount++;
- }
-
- if (panelCopy.isDefault()) {
- if (mDefaultPanel == null) {
- mDefaultPanel = panelCopy;
- } else {
- throw new IllegalStateException("Multiple default panels in HomeConfig state");
- }
- }
-
- final String panelId = panelConfig.getId();
- mConfigOrder.add(panelId);
- mConfigMap.put(panelId, panelCopy);
- }
-
- // We should always have a defined default panel if there's
- // at least one enabled panel around.
- if (mEnabledCount > 0 && mDefaultPanel == null) {
- throw new IllegalStateException("Default panel in HomeConfig state is undefined");
- }
- }
-
- private PanelConfig getPanelOrThrow(String panelId) {
- final PanelConfig panelConfig = mConfigMap.get(panelId);
- if (panelConfig == null) {
- throw new IllegalStateException("Tried to access non-existing panel: " + panelId);
- }
-
- return panelConfig;
- }
-
- private boolean isCurrentDefaultPanel(PanelConfig panelConfig) {
- if (mDefaultPanel == null) {
- return false;
- }
-
- return mDefaultPanel.equals(panelConfig);
- }
-
- private void findNewDefault() {
- // Pick the first panel that is neither disabled nor currently
- // set as default.
- for (PanelConfig panelConfig : makeOrderedCopy(false)) {
- if (!panelConfig.isDefault() && !panelConfig.isDisabled()) {
- setDefault(panelConfig.getId());
- return;
- }
- }
-
- mDefaultPanel = null;
- }
-
- /**
- * Makes an ordered list of PanelConfigs that can be references
- * or deep copied objects.
- *
- * @param deepCopy true to make deep-copied objects
- * @return ordered List of PanelConfigs
- */
- private List<PanelConfig> makeOrderedCopy(boolean deepCopy) {
- final List<PanelConfig> copiedList = new ArrayList<PanelConfig>(mConfigOrder.size());
- for (String panelId : mConfigOrder) {
- PanelConfig panelConfig = mConfigMap.get(panelId);
- if (deepCopy) {
- panelConfig = new PanelConfig(panelConfig);
- }
- copiedList.add(panelConfig);
- }
-
- return copiedList;
- }
-
- private void setPanelIsDisabled(PanelConfig panelConfig, boolean disabled) {
- if (panelConfig.isDisabled() == disabled) {
- return;
- }
-
- panelConfig.setIsDisabled(disabled);
- mEnabledCount += (disabled ? -1 : 1);
- }
-
- /**
- * Gets the ID of the current default panel.
- */
- public String getDefaultPanelId() {
- ThreadUtils.assertOnThread(mOriginalThread);
-
- if (mDefaultPanel == null) {
- return null;
- }
-
- return mDefaultPanel.getId();
- }
-
- /**
- * Set a new default panel.
- *
- * @param panelId the ID of the new default panel.
- */
- public void setDefault(String panelId) {
- ThreadUtils.assertOnThread(mOriginalThread);
-
- final PanelConfig panelConfig = getPanelOrThrow(panelId);
- if (isCurrentDefaultPanel(panelConfig)) {
- return;
- }
-
- if (mDefaultPanel != null) {
- mDefaultPanel.setIsDefault(false);
- }
-
- panelConfig.setIsDefault(true);
- setPanelIsDisabled(panelConfig, false);
-
- mDefaultPanel = panelConfig;
- mHasChanged = true;
- }
-
- /**
- * Toggles disabled state for a panel.
- *
- * @param panelId the ID of the target panel.
- * @param disabled true to disable the panel.
- */
- public void setDisabled(String panelId, boolean disabled) {
- ThreadUtils.assertOnThread(mOriginalThread);
-
- final PanelConfig panelConfig = getPanelOrThrow(panelId);
- if (panelConfig.isDisabled() == disabled) {
- return;
- }
-
- setPanelIsDisabled(panelConfig, disabled);
-
- if (disabled) {
- if (isCurrentDefaultPanel(panelConfig)) {
- panelConfig.setIsDefault(false);
- findNewDefault();
- }
- } else if (mEnabledCount == 1) {
- setDefault(panelId);
- }
-
- mHasChanged = true;
- }
-
- /**
- * Adds a new {@code PanelConfig}. It will do nothing if the
- * {@code Editor} already contains a panel with the same ID.
- *
- * @param panelConfig the {@code PanelConfig} instance to be added.
- * @return true if the item has been added.
- */
- public boolean install(PanelConfig panelConfig) {
- ThreadUtils.assertOnThread(mOriginalThread);
-
- if (panelConfig == null) {
- throw new IllegalStateException("Can't install a null panel");
- }
-
- if (!panelConfig.isDynamic()) {
- throw new IllegalStateException("Can't install a built-in panel: " + panelConfig.getId());
- }
-
- if (panelConfig.isDisabled()) {
- throw new IllegalStateException("Can't install a disabled panel: " + panelConfig.getId());
- }
-
- boolean installed = false;
-
- final String id = panelConfig.getId();
- if (!mConfigMap.containsKey(id)) {
- mConfigMap.put(id, panelConfig);
-
- final int position = panelConfig.getPosition();
- if (position < 0 || position >= mConfigOrder.size()) {
- mConfigOrder.add(id);
- } else {
- mConfigOrder.add(position, id);
- }
-
- mEnabledCount++;
- if (mEnabledCount == 1 || panelConfig.isDefault()) {
- setDefault(panelConfig.getId());
- }
-
- installed = true;
-
- // Add an event to the queue if a new panel is successfully installed.
- mNotificationQueue.add(new Pair<String, String>(
- "HomePanels:Installed", panelConfig.getId()));
- }
-
- mHasChanged = true;
- return installed;
- }
-
- /**
- * Removes an existing panel.
- *
- * @return true if the item has been removed.
- */
- public boolean uninstall(String panelId) {
- ThreadUtils.assertOnThread(mOriginalThread);
-
- final PanelConfig panelConfig = mConfigMap.get(panelId);
- if (panelConfig == null) {
- return false;
- }
-
- if (!panelConfig.isDynamic()) {
- throw new IllegalStateException("Can't uninstall a built-in panel: " + panelConfig.getId());
- }
-
- mConfigMap.remove(panelId);
- mConfigOrder.remove(panelId);
-
- if (!panelConfig.isDisabled()) {
- mEnabledCount--;
- }
-
- if (isCurrentDefaultPanel(panelConfig)) {
- findNewDefault();
- }
-
- // Add an event to the queue if a panel is successfully uninstalled.
- mNotificationQueue.add(new Pair<String, String>("HomePanels:Uninstalled", panelId));
-
- mHasChanged = true;
- return true;
- }
-
- /**
- * Moves panel associated with panelId to the specified position.
- *
- * @param panelId Id of panel
- * @param destIndex Destination position
- * @return true if move succeeded
- */
- public boolean moveTo(String panelId, int destIndex) {
- ThreadUtils.assertOnThread(mOriginalThread);
-
- if (!mConfigOrder.contains(panelId)) {
- return false;
- }
-
- mConfigOrder.remove(panelId);
- mConfigOrder.add(destIndex, panelId);
- mHasChanged = true;
-
- return true;
- }
-
- /**
- * Replaces an existing panel with a new {@code PanelConfig} instance.
- *
- * @return true if the item has been updated.
- */
- public boolean update(PanelConfig panelConfig) {
- ThreadUtils.assertOnThread(mOriginalThread);
-
- if (panelConfig == null) {
- throw new IllegalStateException("Can't update a null panel");
- }
-
- boolean updated = false;
-
- final String id = panelConfig.getId();
- if (mConfigMap.containsKey(id)) {
- final PanelConfig oldPanelConfig = mConfigMap.put(id, panelConfig);
-
- // The disabled and default states can't never be
- // changed by an update operation.
- panelConfig.setIsDefault(oldPanelConfig.isDefault());
- panelConfig.setIsDisabled(oldPanelConfig.isDisabled());
-
- updated = true;
- }
-
- mHasChanged = true;
- return updated;
- }
-
- /**
- * Saves the current {@code Editor} state asynchronously in the
- * background thread.
- *
- * @return the resulting {@code State} instance.
- */
- public State apply() {
- ThreadUtils.assertOnThread(mOriginalThread);
-
- // We're about to save the current state in the background thread
- // so we should use a deep copy of the PanelConfig instances to
- // avoid saving corrupted state.
- final State newConfigState =
- new State(mHomeConfig, makeOrderedCopy(true), isDefault());
-
- // Copy the event queue to a new list, so that we only modify mNotificationQueue on
- // the original thread where it was created.
- final List<Pair<String, String>> copiedQueue = mNotificationQueue;
- mNotificationQueue = new ArrayList<>();
-
- ThreadUtils.getBackgroundHandler().post(new Runnable() {
- @Override
- public void run() {
- mHomeConfig.save(newConfigState);
-
- // Send pending events after the new config is saved.
- sendNotificationsToGecko(copiedQueue);
- }
- });
-
- return newConfigState;
- }
-
- /**
- * Saves the current {@code Editor} state synchronously in the
- * current thread.
- *
- * @return the resulting {@code State} instance.
- */
- public State commit() {
- ThreadUtils.assertOnThread(mOriginalThread);
-
- final State newConfigState =
- new State(mHomeConfig, makeOrderedCopy(false), isDefault());
-
- // This is a synchronous blocking operation, hence no
- // need to deep copy the current PanelConfig instances.
- mHomeConfig.save(newConfigState);
-
- // Send pending events after the new config is saved.
- sendNotificationsToGecko(mNotificationQueue);
- mNotificationQueue.clear();
-
- return newConfigState;
- }
-
- /**
- * Returns whether the {@code Editor} represents the default
- * {@code HomeConfig} configuration without any unsaved changes.
- */
- public boolean isDefault() {
- ThreadUtils.assertOnThread(mOriginalThread);
-
- return (!mHasChanged && mIsFromDefault);
- }
-
- public boolean isEmpty() {
- return mConfigMap.isEmpty();
- }
-
- private void sendNotificationsToGecko(List<Pair<String, String>> notifications) {
- for (Pair<String, String> p : notifications) {
- GeckoAppShell.notifyObservers(p.first, p.second);
- }
- }
-
- private class EditorIterator implements Iterator<PanelConfig> {
- private final Iterator<String> mOrderIterator;
-
- public EditorIterator() {
- mOrderIterator = mConfigOrder.iterator();
- }
-
- @Override
- public boolean hasNext() {
- return mOrderIterator.hasNext();
- }
-
- @Override
- public PanelConfig next() {
- final String panelId = mOrderIterator.next();
- return mConfigMap.get(panelId);
- }
-
- @Override
- public void remove() {
- throw new UnsupportedOperationException("Can't 'remove' from on Editor iterator.");
- }
- }
-
- @Override
- public Iterator<PanelConfig> iterator() {
- ThreadUtils.assertOnThread(mOriginalThread);
-
- return new EditorIterator();
- }
- }
-
- public interface OnReloadListener {
- public void onReload();
- }
-
- public interface HomeConfigBackend {
- public State load();
- public void save(State configState);
- public String getLocale();
- public void setOnReloadListener(OnReloadListener listener);
- }
-
- // UUIDs used to create PanelConfigs for default built-in panels. These are
- // public because they can be used in "about:home?panel=UUID" query strings
- // to open specific panels without querying the active Home Panel
- // configuration. Because they don't consider the active configuration, it
- // is only sensible to do this for built-in panels (and not for dynamic
- // panels).
- private static final String TOP_SITES_PANEL_ID = "4becc86b-41eb-429a-a042-88fe8b5a094e";
- private static final String BOOKMARKS_PANEL_ID = "7f6d419a-cd6c-4e34-b26f-f68b1b551907";
- private static final String HISTORY_PANEL_ID = "f134bf20-11f7-4867-ab8b-e8e705d7fbe8";
- private static final String COMBINED_HISTORY_PANEL_ID = "4d716ce2-e063-486d-9e7c-b190d7b04dc6";
- private static final String RECENT_TABS_PANEL_ID = "5c2601a5-eedc-4477-b297-ce4cef52adf8";
- private static final String REMOTE_TABS_PANEL_ID = "72429afd-8d8b-43d8-9189-14b779c563d0";
- private static final String DEPRECATED_READING_LIST_PANEL_ID = "20f4549a-64ad-4c32-93e4-1dcef792733b";
-
- private final HomeConfigBackend mBackend;
-
- public HomeConfig(HomeConfigBackend backend) {
- mBackend = backend;
- }
-
- public State load() {
- final State configState = mBackend.load();
- configState.setHomeConfig(this);
-
- return configState;
- }
-
- public String getLocale() {
- return mBackend.getLocale();
- }
-
- public void save(State configState) {
- mBackend.save(configState);
- }
-
- public void setOnReloadListener(OnReloadListener listener) {
- mBackend.setOnReloadListener(listener);
- }
-
- public static PanelConfig createBuiltinPanelConfig(Context context, PanelType panelType) {
- return createBuiltinPanelConfig(context, panelType, EnumSet.noneOf(PanelConfig.Flags.class));
- }
-
- public static int getTitleResourceIdForBuiltinPanelType(PanelType panelType) {
- switch (panelType) {
- case TOP_SITES:
- return R.string.home_top_sites_title;
-
- case BOOKMARKS:
- case DEPRECATED_READING_LIST:
- return R.string.bookmarks_title;
-
- case DEPRECATED_HISTORY:
- case DEPRECATED_REMOTE_TABS:
- case DEPRECATED_RECENT_TABS:
- case COMBINED_HISTORY:
- return R.string.home_history_title;
-
- default:
- throw new IllegalArgumentException("Only for built-in panel types: " + panelType);
- }
- }
-
- public static String getIdForBuiltinPanelType(PanelType panelType) {
- switch (panelType) {
- case TOP_SITES:
- return TOP_SITES_PANEL_ID;
-
- case BOOKMARKS:
- return BOOKMARKS_PANEL_ID;
-
- case DEPRECATED_HISTORY:
- return HISTORY_PANEL_ID;
-
- case COMBINED_HISTORY:
- return COMBINED_HISTORY_PANEL_ID;
-
- case DEPRECATED_REMOTE_TABS:
- return REMOTE_TABS_PANEL_ID;
-
- case DEPRECATED_READING_LIST:
- return DEPRECATED_READING_LIST_PANEL_ID;
-
- case DEPRECATED_RECENT_TABS:
- return RECENT_TABS_PANEL_ID;
-
- default:
- throw new IllegalArgumentException("Only for built-in panel types: " + panelType);
- }
- }
-
- public static PanelConfig createBuiltinPanelConfig(Context context, PanelType panelType, EnumSet<PanelConfig.Flags> flags) {
- final int titleId = getTitleResourceIdForBuiltinPanelType(panelType);
- final String id = getIdForBuiltinPanelType(panelType);
-
- return new PanelConfig(panelType, context.getString(titleId), id, flags);
- }
-
- public static HomeConfig getDefault(Context context) {
- return new HomeConfig(new HomeConfigPrefsBackend(context));
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigLoader.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigLoader.java
deleted file mode 100644
index 914d0fdd1..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigLoader.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import org.mozilla.gecko.home.HomeConfig.OnReloadListener;
-
-import android.content.Context;
-import android.support.v4.content.AsyncTaskLoader;
-
-public class HomeConfigLoader extends AsyncTaskLoader<HomeConfig.State> {
- private final HomeConfig mConfig;
- private HomeConfig.State mConfigState;
-
- private final Context mContext;
-
- public HomeConfigLoader(Context context, HomeConfig homeConfig) {
- super(context);
- mContext = context;
- mConfig = homeConfig;
- }
-
- @Override
- public HomeConfig.State loadInBackground() {
- return mConfig.load();
- }
-
- @Override
- public void deliverResult(HomeConfig.State configState) {
- if (isReset()) {
- mConfigState = null;
- return;
- }
-
- mConfigState = configState;
- mConfig.setOnReloadListener(new ForceReloadListener());
-
- if (isStarted()) {
- super.deliverResult(configState);
- }
- }
-
- @Override
- protected void onStartLoading() {
- if (mConfigState != null) {
- deliverResult(mConfigState);
- }
-
- if (takeContentChanged() || mConfigState == null) {
- forceLoad();
- }
- }
-
- @Override
- protected void onStopLoading() {
- cancelLoad();
- }
-
- @Override
- public void onCanceled(HomeConfig.State configState) {
- mConfigState = null;
- }
-
- @Override
- protected void onReset() {
- super.onReset();
-
- // Ensure the loader is stopped.
- onStopLoading();
-
- mConfigState = null;
- mConfig.setOnReloadListener(null);
- }
-
- private class ForceReloadListener implements OnReloadListener {
- @Override
- public void onReload() {
- onContentChanged();
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java
deleted file mode 100644
index a2d80788c..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java
+++ /dev/null
@@ -1,663 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import static org.mozilla.gecko.home.HomeConfig.createBuiltinPanelConfig;
-
-import java.util.ArrayList;
-import java.util.EnumSet;
-import java.util.Locale;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.home.HomeConfig.HomeConfigBackend;
-import org.mozilla.gecko.home.HomeConfig.OnReloadListener;
-import org.mozilla.gecko.home.HomeConfig.PanelConfig;
-import org.mozilla.gecko.home.HomeConfig.PanelType;
-import org.mozilla.gecko.home.HomeConfig.State;
-import org.mozilla.gecko.util.HardwareUtils;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.SharedPreferences;
-import android.support.annotation.CheckResult;
-import android.support.annotation.VisibleForTesting;
-import android.support.v4.content.LocalBroadcastManager;
-import android.text.TextUtils;
-import android.util.Log;
-
-public class HomeConfigPrefsBackend implements HomeConfigBackend {
- private static final String LOGTAG = "GeckoHomeConfigBackend";
-
- // Increment this to trigger a migration.
- @VisibleForTesting
- static final int VERSION = 8;
-
- // This key was originally used to store only an array of panel configs.
- public static final String PREFS_CONFIG_KEY_OLD = "home_panels";
-
- // This key is now used to store a version number with the array of panel configs.
- public static final String PREFS_CONFIG_KEY = "home_panels_with_version";
-
- // Keys used with JSON object stored in prefs.
- private static final String JSON_KEY_PANELS = "panels";
- private static final String JSON_KEY_VERSION = "version";
-
- private static final String PREFS_LOCALE_KEY = "home_locale";
-
- private static final String RELOAD_BROADCAST = "HomeConfigPrefsBackend:Reload";
-
- private final Context mContext;
- private ReloadBroadcastReceiver mReloadBroadcastReceiver;
- private OnReloadListener mReloadListener;
-
- private static boolean sMigrationDone;
-
- public HomeConfigPrefsBackend(Context context) {
- mContext = context;
- }
-
- private SharedPreferences getSharedPreferences() {
- return GeckoSharedPrefs.forProfile(mContext);
- }
-
- private State loadDefaultConfig() {
- final ArrayList<PanelConfig> panelConfigs = new ArrayList<PanelConfig>();
-
- panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.TOP_SITES,
- EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)));
-
- panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.BOOKMARKS));
- panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.COMBINED_HISTORY));
-
-
- return new State(panelConfigs, true);
- }
-
- /**
- * Iterate through the panels to check if they are all disabled.
- */
- private static boolean allPanelsAreDisabled(JSONArray jsonPanels) throws JSONException {
- final int count = jsonPanels.length();
- for (int i = 0; i < count; i++) {
- final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i);
-
- if (!jsonPanelConfig.optBoolean(PanelConfig.JSON_KEY_DISABLED, false)) {
- return false;
- }
- }
-
- return true;
- }
-
- protected enum Position {
- NONE, // Not present.
- FRONT, // At the front of the list of panels.
- BACK, // At the back of the list of panels.
- }
-
- /**
- * Create and insert a built-in panel configuration.
- *
- * @param context Android context.
- * @param jsonPanels array of JSON panels to update in place.
- * @param panelType to add.
- * @param positionOnPhones where to place the new panel on phones.
- * @param positionOnTablets where to place the new panel on tablets.
- * @throws JSONException
- */
- protected static void addBuiltinPanelConfig(Context context, JSONArray jsonPanels,
- PanelType panelType, Position positionOnPhones, Position positionOnTablets) throws JSONException {
- // Add the new panel.
- final JSONObject jsonPanelConfig =
- createBuiltinPanelConfig(context, panelType).toJSON();
-
- // If any panel is enabled, then we should make the new panel enabled.
- jsonPanelConfig.put(PanelConfig.JSON_KEY_DISABLED,
- allPanelsAreDisabled(jsonPanels));
-
- final boolean isTablet = HardwareUtils.isTablet();
- final boolean isPhone = !isTablet;
-
- // Maybe add the new panel to the front of the array.
- if ((isPhone && positionOnPhones == Position.FRONT) ||
- (isTablet && positionOnTablets == Position.FRONT)) {
- // This is an inefficient way to stretch [a, b, c] to [a, a, b, c].
- for (int i = jsonPanels.length(); i >= 1; i--) {
- jsonPanels.put(i, jsonPanels.get(i - 1));
- }
- // And this inserts [d, a, b, c].
- jsonPanels.put(0, jsonPanelConfig);
- }
-
- // Maybe add the new panel to the back of the array.
- if ((isPhone && positionOnPhones == Position.BACK) ||
- (isTablet && positionOnTablets == Position.BACK)) {
- jsonPanels.put(jsonPanelConfig);
- }
- }
-
- /**
- * Updates the panels to combine the History and Sync panels into the (Combined) History panel.
- *
- * Tries to replace the History panel with the Combined History panel if visible, or falls back to
- * replacing the Sync panel if it's visible. That way, we minimize panel reordering during a migration.
- * @param context Android context
- * @param jsonPanels array of original JSON panels
- * @return new array of updated JSON panels
- * @throws JSONException
- */
- private static JSONArray combineHistoryAndSyncPanels(Context context, JSONArray jsonPanels) throws JSONException {
- EnumSet<PanelConfig.Flags> historyFlags = null;
- EnumSet<PanelConfig.Flags> syncFlags = null;
-
- int historyIndex = -1;
- int syncIndex = -1;
-
- // Determine state and location of History and Sync panels.
- for (int i = 0; i < jsonPanels.length(); i++) {
- JSONObject panelObj = jsonPanels.getJSONObject(i);
- final PanelConfig panelConfig = new PanelConfig(panelObj);
- final PanelType type = panelConfig.getType();
- if (type == PanelType.DEPRECATED_HISTORY) {
- historyIndex = i;
- historyFlags = panelConfig.getFlags();
- } else if (type == PanelType.DEPRECATED_REMOTE_TABS) {
- syncIndex = i;
- syncFlags = panelConfig.getFlags();
- } else if (type == PanelType.COMBINED_HISTORY) {
- // Partial landing of bug 1220928 combined the History and Sync panels of users who didn't
- // have home panel customizations (including new users), thus they don't this migration.
- return jsonPanels;
- }
- }
-
- if (historyIndex == -1 || syncIndex == -1) {
- throw new IllegalArgumentException("Expected both History and Sync panels to be present prior to Combined History.");
- }
-
- PanelConfig newPanel;
- int replaceIndex;
- int removeIndex;
-
- if (historyFlags.contains(PanelConfig.Flags.DISABLED_PANEL) && !syncFlags.contains(PanelConfig.Flags.DISABLED_PANEL)) {
- // Replace the Sync panel if it's visible and the History panel is disabled.
- replaceIndex = syncIndex;
- removeIndex = historyIndex;
- newPanel = createBuiltinPanelConfig(context, PanelType.COMBINED_HISTORY, syncFlags);
- } else {
- // Otherwise, just replace the History panel.
- replaceIndex = historyIndex;
- removeIndex = syncIndex;
- newPanel = createBuiltinPanelConfig(context, PanelType.COMBINED_HISTORY, historyFlags);
- }
-
- // Copy the array with updated panel and removed panel.
- final JSONArray newArray = new JSONArray();
- for (int i = 0; i < jsonPanels.length(); i++) {
- if (i == replaceIndex) {
- newArray.put(newPanel.toJSON());
- } else if (i == removeIndex) {
- continue;
- } else {
- newArray.put(jsonPanels.get(i));
- }
- }
-
- return newArray;
- }
-
- /**
- * Iterate over all homepanels to verify that there is at least one default panel. If there is
- * no default panel, set History as the default panel. (This is only relevant for two botched
- * migrations where the history panel should have been made the default panel, but wasn't.)
- */
- private static void ensureDefaultPanelForV5orV8(Context context, JSONArray jsonPanels) throws JSONException {
- int historyIndex = -1;
-
- // If all panels are disabled, there is no default panel - this is the only valid state
- // that has no default. We can use this flag to track whether any visible panels have been
- // found.
- boolean enabledPanelsFound = false;
-
- for (int i = 0; i < jsonPanels.length(); i++) {
- final PanelConfig panelConfig = new PanelConfig(jsonPanels.getJSONObject(i));
- if (panelConfig.isDefault()) {
- return;
- }
-
- if (!panelConfig.isDisabled()) {
- enabledPanelsFound = true;
- }
-
- if (panelConfig.getType() == PanelType.COMBINED_HISTORY) {
- historyIndex = i;
- }
- }
-
- if (!enabledPanelsFound) {
- // No panels are enabled, hence there can be no default (see noEnabledPanelsFound declaration
- // for more information).
- return;
- }
-
- // Make the History panel default. We can't modify existing PanelConfigs, so make a new one.
- final PanelConfig historyPanelConfig = createBuiltinPanelConfig(context, PanelType.COMBINED_HISTORY, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL));
- jsonPanels.put(historyIndex, historyPanelConfig.toJSON());
- }
-
- /**
- * Removes a panel from the home panel config.
- * If the removed panel was set as the default home panel, we provide a replacement for it.
- *
- * @param context Android context
- * @param jsonPanels array of original JSON panels
- * @param panelToRemove The home panel to be removed.
- * @param replacementPanel The panel which will replace it if the removed panel
- * was the default home panel.
- * @param alwaysUnhide If true, the replacement panel will always be unhidden,
- * otherwise only if we turn it into the new default panel.
- * @return new array of updated JSON panels
- * @throws JSONException
- */
- private static JSONArray removePanel(Context context, JSONArray jsonPanels,
- PanelType panelToRemove, PanelType replacementPanel, boolean alwaysUnhide) throws JSONException {
- boolean wasDefault = false;
- boolean wasDisabled = false;
- int replacementPanelIndex = -1;
- boolean replacementWasDefault = false;
-
- // JSONArrary doesn't provide remove() for API < 19, therefore we need to manually copy all
- // the items we don't want deleted into a new array.
- final JSONArray newJSONPanels = new JSONArray();
-
- for (int i = 0; i < jsonPanels.length(); i++) {
- final JSONObject panelJSON = jsonPanels.getJSONObject(i);
- final PanelConfig panelConfig = new PanelConfig(panelJSON);
-
- if (panelConfig.getType() == panelToRemove) {
- // If this panel was the default we'll need to assign a new default:
- wasDefault = panelConfig.isDefault();
- wasDisabled = panelConfig.isDisabled();
- } else {
- if (panelConfig.getType() == replacementPanel) {
- replacementPanelIndex = newJSONPanels.length();
- if (panelConfig.isDefault()) {
- replacementWasDefault = true;
- }
- }
-
- newJSONPanels.put(panelJSON);
- }
- }
-
- // Unless alwaysUnhide is true, we make the replacement panel visible only if it is going
- // to be the new default panel, since a hidden default panel doesn't make sense.
- // This is to allow preserving the behaviour of the original reading list migration function.
- if ((wasDefault || alwaysUnhide) && !wasDisabled) {
- final JSONObject replacementPanelConfig;
- if (wasDefault) {
- // If the removed panel was the default, the replacement has to be made the new default
- replacementPanelConfig = createBuiltinPanelConfig(context, replacementPanel, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)).toJSON();
- } else {
- final EnumSet<HomeConfig.PanelConfig.Flags> flags;
- if (replacementWasDefault) {
- // However if the replacement panel was already default, we need to preserve it's default status
- // (By rewriting the PanelConfig, we lose all existing flags, so we need to make sure desired
- // flags are retained - in this case there's only DEFAULT_PANEL, which is mutually
- // exclusive with the DISABLE_PANEL case).
- flags = EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL);
- } else {
- flags = EnumSet.noneOf(PanelConfig.Flags.class);
- }
-
- // The panel is visible since we don't set Flags.DISABLED_PANEL.
- replacementPanelConfig = createBuiltinPanelConfig(context, replacementPanel, flags).toJSON();
- }
-
- if (replacementPanelIndex != -1) {
- newJSONPanels.put(replacementPanelIndex, replacementPanelConfig);
- } else {
- newJSONPanels.put(replacementPanelConfig);
- }
- }
-
- return newJSONPanels;
- }
-
- /**
- * Checks to see if the reading list panel already exists.
- *
- * @param jsonPanels JSONArray array representing the curent set of panel configs.
- *
- * @return boolean Whether or not the reading list panel exists.
- */
- private static boolean readingListPanelExists(JSONArray jsonPanels) {
- final int count = jsonPanels.length();
- for (int i = 0; i < count; i++) {
- try {
- final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i);
- final PanelConfig panelConfig = new PanelConfig(jsonPanelConfig);
- if (panelConfig.getType() == PanelType.DEPRECATED_READING_LIST) {
- return true;
- }
- } catch (Exception e) {
- // It's okay to ignore this exception, since an invalid reading list
- // panel config is equivalent to no reading list panel.
- Log.e(LOGTAG, "Exception loading PanelConfig from JSON", e);
- }
- }
- return false;
- }
-
- @CheckResult
- static synchronized JSONArray migratePrefsFromVersionToVersion(final Context context, final int currentVersion, final int newVersion,
- final JSONArray jsonPanelsIn, final SharedPreferences.Editor prefsEditor) throws JSONException {
-
- JSONArray jsonPanels = jsonPanelsIn;
-
- for (int v = currentVersion + 1; v <= newVersion; v++) {
- Log.d(LOGTAG, "Migrating to version = " + v);
-
- switch (v) {
- case 1:
- // Add "Recent Tabs" panel.
- addBuiltinPanelConfig(context, jsonPanels,
- PanelType.DEPRECATED_RECENT_TABS, Position.FRONT, Position.BACK);
-
- // Remove the old pref key.
- prefsEditor.remove(PREFS_CONFIG_KEY_OLD);
- break;
-
- case 2:
- // Add "Remote Tabs"/"Synced Tabs" panel.
- addBuiltinPanelConfig(context, jsonPanels,
- PanelType.DEPRECATED_REMOTE_TABS, Position.FRONT, Position.BACK);
- break;
-
- case 3:
- // Add the "Reading List" panel if it does not exist. At one time,
- // the Reading List panel was shown only to devices that were not
- // considered "low memory". Now, we expose the panel to all devices.
- // This migration should only occur for "low memory" devices.
- // Note: This will not agree with the default configuration, which
- // has DEPRECATED_REMOTE_TABS after DEPRECATED_READING_LIST on some devices.
- if (!readingListPanelExists(jsonPanels)) {
- addBuiltinPanelConfig(context, jsonPanels,
- PanelType.DEPRECATED_READING_LIST, Position.BACK, Position.BACK);
- }
- break;
-
- case 4:
- // Combine the History and Sync panels. In order to minimize an unexpected reordering
- // of panels, we try to replace the History panel if it's visible, and fall back to
- // the Sync panel if that's visible.
- jsonPanels = combineHistoryAndSyncPanels(context, jsonPanels);
- break;
-
- case 5:
- // This is the fix for bug 1264136 where we lost track of the default panel during some migrations.
- ensureDefaultPanelForV5orV8(context, jsonPanels);
- break;
-
- case 6:
- jsonPanels = removePanel(context, jsonPanels,
- PanelType.DEPRECATED_READING_LIST, PanelType.BOOKMARKS, false);
- break;
-
- case 7:
- jsonPanels = removePanel(context, jsonPanels,
- PanelType.DEPRECATED_RECENT_TABS, PanelType.COMBINED_HISTORY, true);
- break;
-
- case 8:
- // Similar to "case 5" above, this time 1304777 - once again we lost track
- // of the history panel
- ensureDefaultPanelForV5orV8(context, jsonPanels);
- break;
- }
- }
-
- return jsonPanels;
- }
-
- /**
- * Migrates JSON config data storage.
- *
- * @param context Context used to get shared preferences and create built-in panel.
- * @param jsonString String currently stored in preferences.
- *
- * @return JSONArray array representing new set of panel configs.
- */
- private static synchronized JSONArray maybePerformMigration(Context context, String jsonString) throws JSONException {
- // If the migration is already done, we're at the current version.
- if (sMigrationDone) {
- final JSONObject json = new JSONObject(jsonString);
- return json.getJSONArray(JSON_KEY_PANELS);
- }
-
- // Make sure we only do this version check once.
- sMigrationDone = true;
-
- JSONArray jsonPanels;
- final int version;
-
- final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context);
- if (prefs.contains(PREFS_CONFIG_KEY_OLD)) {
- // Our original implementation did not contain versioning, so this is implicitly version 0.
- jsonPanels = new JSONArray(jsonString);
- version = 0;
- } else {
- final JSONObject json = new JSONObject(jsonString);
- jsonPanels = json.getJSONArray(JSON_KEY_PANELS);
- version = json.getInt(JSON_KEY_VERSION);
- }
-
- if (version == VERSION) {
- return jsonPanels;
- }
-
- Log.d(LOGTAG, "Performing migration");
-
- final SharedPreferences.Editor prefsEditor = prefs.edit();
-
- jsonPanels = migratePrefsFromVersionToVersion(context, version, VERSION, jsonPanels, prefsEditor);
-
- // Save the new panel config and the new version number.
- final JSONObject newJson = new JSONObject();
- newJson.put(JSON_KEY_PANELS, jsonPanels);
- newJson.put(JSON_KEY_VERSION, VERSION);
-
- prefsEditor.putString(PREFS_CONFIG_KEY, newJson.toString());
- prefsEditor.apply();
-
- return jsonPanels;
- }
-
- private State loadConfigFromString(String jsonString) {
- final JSONArray jsonPanelConfigs;
- try {
- jsonPanelConfigs = maybePerformMigration(mContext, jsonString);
- updatePrefsFromConfig(jsonPanelConfigs);
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error loading the list of home panels from JSON prefs", e);
-
- // Fallback to default config
- return loadDefaultConfig();
- }
-
- final ArrayList<PanelConfig> panelConfigs = new ArrayList<PanelConfig>();
-
- final int count = jsonPanelConfigs.length();
- for (int i = 0; i < count; i++) {
- try {
- final JSONObject jsonPanelConfig = jsonPanelConfigs.getJSONObject(i);
- final PanelConfig panelConfig = new PanelConfig(jsonPanelConfig);
- panelConfigs.add(panelConfig);
- } catch (Exception e) {
- Log.e(LOGTAG, "Exception loading PanelConfig from JSON", e);
- }
- }
-
- return new State(panelConfigs, false);
- }
-
- @Override
- public State load() {
- final SharedPreferences prefs = getSharedPreferences();
-
- final String key = (prefs.contains(PREFS_CONFIG_KEY_OLD) ? PREFS_CONFIG_KEY_OLD : PREFS_CONFIG_KEY);
- final String jsonString = prefs.getString(key, null);
-
- final State configState;
- if (TextUtils.isEmpty(jsonString)) {
- configState = loadDefaultConfig();
- } else {
- configState = loadConfigFromString(jsonString);
- }
-
- return configState;
- }
-
- @Override
- public void save(State configState) {
- final SharedPreferences prefs = getSharedPreferences();
- final SharedPreferences.Editor editor = prefs.edit();
-
- // No need to save the state to disk if it represents the default
- // HomeConfig configuration. Simply force all existing HomeConfigLoader
- // instances to refresh their contents.
- if (!configState.isDefault()) {
- final JSONArray jsonPanelConfigs = new JSONArray();
-
- for (PanelConfig panelConfig : configState) {
- try {
- final JSONObject jsonPanelConfig = panelConfig.toJSON();
- jsonPanelConfigs.put(jsonPanelConfig);
- } catch (Exception e) {
- Log.e(LOGTAG, "Exception converting PanelConfig to JSON", e);
- }
- }
-
- try {
- final JSONObject json = new JSONObject();
- json.put(JSON_KEY_PANELS, jsonPanelConfigs);
- json.put(JSON_KEY_VERSION, VERSION);
-
- editor.putString(PREFS_CONFIG_KEY, json.toString());
- } catch (JSONException e) {
- Log.e(LOGTAG, "Exception saving PanelConfig state", e);
- }
- }
-
- editor.putString(PREFS_LOCALE_KEY, Locale.getDefault().toString());
- editor.apply();
-
- // Trigger reload listeners on all live backend instances
- sendReloadBroadcast();
- }
-
- @Override
- public String getLocale() {
- final SharedPreferences prefs = getSharedPreferences();
-
- String locale = prefs.getString(PREFS_LOCALE_KEY, null);
- if (locale == null) {
- // Initialize config with the current locale
- final String currentLocale = Locale.getDefault().toString();
-
- final SharedPreferences.Editor editor = prefs.edit();
- editor.putString(PREFS_LOCALE_KEY, currentLocale);
- editor.apply();
-
- // If the user has saved HomeConfig before, return null this
- // one time to trigger a refresh and ensure we use the
- // correct locale for the saved state. For more context,
- // see HomePanelsManager.onLocaleReady().
- if (!prefs.contains(PREFS_CONFIG_KEY)) {
- locale = currentLocale;
- }
- }
-
- return locale;
- }
-
- @Override
- public void setOnReloadListener(OnReloadListener listener) {
- if (mReloadListener != null) {
- unregisterReloadReceiver();
- mReloadBroadcastReceiver = null;
- }
-
- mReloadListener = listener;
-
- if (mReloadListener != null) {
- mReloadBroadcastReceiver = new ReloadBroadcastReceiver();
- registerReloadReceiver();
- }
- }
-
- /**
- * Update prefs that depend on home panels state.
- *
- * This includes the prefs that keep track of whether bookmarks or history are enabled, which are
- * used to control the visibility of the corresponding menu items.
- */
- private void updatePrefsFromConfig(JSONArray panelsArray) {
- final SharedPreferences prefs = GeckoSharedPrefs.forProfile(mContext);
- if (!prefs.contains(HomeConfig.PREF_KEY_BOOKMARKS_PANEL_ENABLED)
- || !prefs.contains(HomeConfig.PREF_KEY_HISTORY_PANEL_ENABLED)) {
-
- final String bookmarkType = PanelType.BOOKMARKS.toString();
- final String historyType = PanelType.COMBINED_HISTORY.toString();
- try {
- for (int i = 0; i < panelsArray.length(); i++) {
- final JSONObject panelObj = panelsArray.getJSONObject(i);
- final String panelType = panelObj.optString(PanelConfig.JSON_KEY_TYPE, null);
- if (panelType == null) {
- break;
- }
- final boolean isDisabled = panelObj.optBoolean(PanelConfig.JSON_KEY_DISABLED, false);
- if (bookmarkType.equals(panelType)) {
- prefs.edit().putBoolean(HomeConfig.PREF_KEY_BOOKMARKS_PANEL_ENABLED, !isDisabled).apply();
- } else if (historyType.equals(panelType)) {
- prefs.edit().putBoolean(HomeConfig.PREF_KEY_HISTORY_PANEL_ENABLED, !isDisabled).apply();
- }
- }
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error fetching panel from config to update prefs");
- }
- }
- }
-
-
- private void sendReloadBroadcast() {
- final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(mContext);
- final Intent reloadIntent = new Intent(RELOAD_BROADCAST);
- lbm.sendBroadcast(reloadIntent);
- }
-
- private void registerReloadReceiver() {
- final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(mContext);
- lbm.registerReceiver(mReloadBroadcastReceiver, new IntentFilter(RELOAD_BROADCAST));
- }
-
- private void unregisterReloadReceiver() {
- final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(mContext);
- lbm.unregisterReceiver(mReloadBroadcastReceiver);
- }
-
- private class ReloadBroadcastReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- mReloadListener.onReload();
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeContextMenuInfo.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeContextMenuInfo.java
deleted file mode 100644
index cefa0329d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeContextMenuInfo.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.util.StringUtils;
-
-import android.database.Cursor;
-import android.text.TextUtils;
-import android.view.View;
-import android.widget.AdapterView.AdapterContextMenuInfo;
-import android.widget.ExpandableListAdapter;
-import android.widget.ListAdapter;
-
-/**
- * A ContextMenuInfo for HomeListView
- */
-public class HomeContextMenuInfo extends AdapterContextMenuInfo {
-
- public String url;
- public String title;
- public boolean isFolder;
- public int historyId = -1;
- public int bookmarkId = -1;
- public RemoveItemType itemType = null;
-
- // Item type to be handled with "Remove" selection.
- public static enum RemoveItemType {
- BOOKMARKS, COMBINED, HISTORY
- }
-
- public HomeContextMenuInfo(View targetView, int position, long id) {
- super(targetView, position, id);
- }
-
- public boolean hasBookmarkId() {
- return bookmarkId > -1;
- }
-
- public boolean hasHistoryId() {
- return historyId > -1;
- }
-
- public boolean hasPartnerBookmarkId() {
- return bookmarkId <= BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START;
- }
-
- public boolean canRemove() {
- return hasBookmarkId() || hasHistoryId() || hasPartnerBookmarkId();
- }
-
- public String getDisplayTitle() {
- if (!TextUtils.isEmpty(title)) {
- return title;
- }
- return StringUtils.stripCommonSubdomains(StringUtils.stripScheme(url, StringUtils.UrlFlags.STRIP_HTTPS));
- }
-
- /**
- * Interface for creating ContextMenuInfo instances from cursors.
- */
- public interface Factory {
- public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor);
- }
-
- /**
- * Interface for creating ContextMenuInfo instances from ListAdapters.
- */
- public interface ListFactory extends Factory {
- public HomeContextMenuInfo makeInfoForAdapter(View view, int position, long id, ListAdapter adapter);
- }
-
- /**
- * Interface for creating ContextMenuInfo instances from ExpandableListAdapters.
- */
- public interface ExpandableFactory {
- public HomeContextMenuInfo makeInfoForAdapter(View view, int position, long id, ExpandableListAdapter adapter);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeExpandableListView.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeExpandableListView.java
deleted file mode 100644
index 7badd6929..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeExpandableListView.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.ContextMenu.ContextMenuInfo;
-import android.view.View;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemLongClickListener;
-import android.widget.ExpandableListView;
-
-/**
- * <code>HomeExpandableListView</code> is a custom extension of
- * <code>ExpandableListView<code>, that packs a <code>HomeContextMenuInfo</code>
- * when any of its rows is long pressed.
- * <p>
- * This is the <code>ExpandableListView</code> equivalent of
- * <code>HomeListView</code>.
- */
-public class HomeExpandableListView extends ExpandableListView
- implements OnItemLongClickListener {
-
- // ContextMenuInfo associated with the currently long pressed list item.
- private HomeContextMenuInfo mContextMenuInfo;
-
- // ContextMenuInfo factory.
- private HomeContextMenuInfo.ExpandableFactory mContextMenuInfoFactory;
-
- public HomeExpandableListView(Context context) {
- this(context, null);
- }
-
- public HomeExpandableListView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public HomeExpandableListView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
-
- setOnItemLongClickListener(this);
- }
-
- @Override
- public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
- if (mContextMenuInfoFactory == null) {
- return false;
- }
-
- // HomeExpandableListView items can correspond to groups and children.
- // The factory can determine whether to add context menu for either,
- // both, or none by unpacking the given position.
- mContextMenuInfo = mContextMenuInfoFactory.makeInfoForAdapter(view, position, id, getExpandableListAdapter());
- return showContextMenuForChild(HomeExpandableListView.this);
- }
-
- @Override
- public ContextMenuInfo getContextMenuInfo() {
- return mContextMenuInfo;
- }
-
- public void setContextMenuInfoFactory(final HomeContextMenuInfo.ExpandableFactory factory) {
- mContextMenuInfoFactory = factory;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java
deleted file mode 100644
index da6e9b703..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java
+++ /dev/null
@@ -1,498 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import java.util.EnumSet;
-
-import org.mozilla.gecko.EditBookmarkDialog;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoApplication;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.IntentHelper;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.SnackbarBuilder;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.db.BrowserContract.SuggestedSites;
-import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
-import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
-import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener;
-import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
-import org.mozilla.gecko.home.TopSitesGridView.TopSitesGridContextMenuInfo;
-import org.mozilla.gecko.reader.SavedReaderViewHelper;
-import org.mozilla.gecko.reader.ReadingListHelper;
-import org.mozilla.gecko.restrictions.Restrictable;
-import org.mozilla.gecko.restrictions.Restrictions;
-import org.mozilla.gecko.util.Clipboard;
-import org.mozilla.gecko.util.StringUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.UIAsyncTask;
-
-import android.app.Activity;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Configuration;
-import android.os.Bundle;
-import android.support.design.widget.Snackbar;
-import android.support.v4.app.Fragment;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.ContextMenu;
-import android.view.ContextMenu.ContextMenuInfo;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-
-/**
- * HomeFragment is an empty fragment that can be added to the HomePager.
- * Subclasses can add their own views.
- * <p>
- * The containing activity <b>must</b> implement {@link OnUrlOpenListener}.
- */
-public abstract class HomeFragment extends Fragment {
- // Log Tag.
- private static final String LOGTAG = "GeckoHomeFragment";
-
- // Share MIME type.
- protected static final String SHARE_MIME_TYPE = "text/plain";
-
- // Default value for "can load" hint
- static final boolean DEFAULT_CAN_LOAD_HINT = false;
-
- // Whether the fragment can load its content or not
- // This is used to defer data loading until the editing
- // mode animation ends.
- private boolean mCanLoadHint;
-
- // Whether the fragment has loaded its content
- private boolean mIsLoaded;
-
- // On URL open listener
- protected OnUrlOpenListener mUrlOpenListener;
-
- // Helper for opening a tab in the background.
- protected OnUrlOpenInBackgroundListener mUrlOpenInBackgroundListener;
-
- protected PanelStateChangeListener mPanelStateChangeListener = null;
-
- /**
- * Listener to notify when a home panels' state has changed in a way that needs to be stored
- * for history/restoration. E.g. when a folder is opened/closed in bookmarks.
- */
- public interface PanelStateChangeListener {
-
- /**
- * @param bundle Data that should be persisted, and passed to this panel if restored at a later
- * stage.
- */
- void onStateChanged(Bundle bundle);
-
- void setCachedRecentTabsCount(int count);
-
- int getCachedRecentTabsCount();
- }
-
- public void restoreData(Bundle data) {
- // Do nothing
- }
-
- public void setPanelStateChangeListener(
- PanelStateChangeListener mPanelStateChangeListener) {
- this.mPanelStateChangeListener = mPanelStateChangeListener;
- }
-
- @Override
- public void onAttach(Activity activity) {
- super.onAttach(activity);
-
- try {
- mUrlOpenListener = (OnUrlOpenListener) activity;
- } catch (ClassCastException e) {
- throw new ClassCastException(activity.toString()
- + " must implement HomePager.OnUrlOpenListener");
- }
-
- try {
- mUrlOpenInBackgroundListener = (OnUrlOpenInBackgroundListener) activity;
- } catch (ClassCastException e) {
- throw new ClassCastException(activity.toString()
- + " must implement HomePager.OnUrlOpenInBackgroundListener");
- }
- }
-
- @Override
- public void onDetach() {
- super.onDetach();
- mUrlOpenListener = null;
- mUrlOpenInBackgroundListener = null;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- final Bundle args = getArguments();
- if (args != null) {
- mCanLoadHint = args.getBoolean(HomePager.CAN_LOAD_ARG, DEFAULT_CAN_LOAD_HINT);
- } else {
- mCanLoadHint = DEFAULT_CAN_LOAD_HINT;
- }
-
- mIsLoaded = false;
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
-
- GeckoApplication.watchReference(getActivity(), this);
- }
-
- @Override
- public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
- if (!(menuInfo instanceof HomeContextMenuInfo)) {
- return;
- }
-
- HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo;
-
- // Don't show the context menu for folders.
- if (info.isFolder) {
- return;
- }
-
- MenuInflater inflater = new MenuInflater(view.getContext());
- inflater.inflate(R.menu.home_contextmenu, menu);
-
- menu.setHeaderTitle(info.getDisplayTitle());
-
- // Hide unused menu items.
- menu.findItem(R.id.top_sites_edit).setVisible(false);
- menu.findItem(R.id.top_sites_pin).setVisible(false);
- menu.findItem(R.id.top_sites_unpin).setVisible(false);
-
- // Hide the "Edit" menuitem if this item isn't a bookmark,
- // or if this is a reading list item.
- if (!info.hasBookmarkId()) {
- menu.findItem(R.id.home_edit_bookmark).setVisible(false);
- }
-
- // Hide the "Remove" menuitem if this item not removable.
- if (!info.canRemove()) {
- menu.findItem(R.id.home_remove).setVisible(false);
- }
-
- if (!StringUtils.isShareableUrl(info.url) || GeckoProfile.get(getActivity()).inGuestMode()) {
- menu.findItem(R.id.home_share).setVisible(false);
- }
-
- if (!Restrictions.isAllowed(view.getContext(), Restrictable.PRIVATE_BROWSING)) {
- menu.findItem(R.id.home_open_private_tab).setVisible(false);
- }
- }
-
- @Override
- public boolean onContextItemSelected(MenuItem item) {
- // onContextItemSelected() is first dispatched to the activity and
- // then dispatched to its fragments. Since fragments cannot "override"
- // menu item selection handling, it's better to avoid menu id collisions
- // between the activity and its fragments.
-
- ContextMenuInfo menuInfo = item.getMenuInfo();
- if (!(menuInfo instanceof HomeContextMenuInfo)) {
- return false;
- }
-
- final HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo;
- final Context context = getActivity();
-
- final int itemId = item.getItemId();
-
- // Track the menu action. We don't know much about the context, but we can use this to determine
- // the frequency of use for various actions.
- String extras = getResources().getResourceEntryName(itemId);
- if (TextUtils.equals(extras, "home_open_private_tab")) {
- // Mask private browsing
- extras = "home_open_new_tab";
- }
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, extras);
-
- if (itemId == R.id.home_copyurl) {
- if (info.url == null) {
- Log.e(LOGTAG, "Can't copy address because URL is null");
- return false;
- }
-
- Clipboard.setText(info.url);
- return true;
- }
-
- if (itemId == R.id.home_share) {
- if (info.url == null) {
- Log.e(LOGTAG, "Can't share because URL is null");
- return false;
- } else {
- IntentHelper.openUriExternal(info.url, SHARE_MIME_TYPE, "", "",
- Intent.ACTION_SEND, info.getDisplayTitle(), false);
-
- // Context: Sharing via chrome homepage contextmenu list (home session should be active)
- Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "home_contextmenu");
- return true;
- }
- }
-
- if (itemId == R.id.home_add_to_launcher) {
- if (info.url == null) {
- Log.e(LOGTAG, "Can't add to home screen because URL is null");
- return false;
- }
-
- // Fetch an icon big enough for use as a home screen icon.
- final String displayTitle = info.getDisplayTitle();
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- GeckoAppShell.createShortcut(displayTitle, info.url);
-
- }
- });
-
- return true;
- }
-
- if (itemId == R.id.home_open_private_tab || itemId == R.id.home_open_new_tab) {
- if (info.url == null) {
- Log.e(LOGTAG, "Can't open in new tab because URL is null");
- return false;
- }
-
- // Some pinned site items have "user-entered" urls. URLs entered in
- // the PinSiteDialog are wrapped in a special URI until we can get a
- // valid URL. If the url is a user-entered url, decode the URL
- // before loading it.
- final String url = StringUtils.decodeUserEnteredUrl(info.url);
-
- final EnumSet<OnUrlOpenInBackgroundListener.Flags> flags = EnumSet.noneOf(OnUrlOpenInBackgroundListener.Flags.class);
- if (item.getItemId() == R.id.home_open_private_tab) {
- flags.add(OnUrlOpenInBackgroundListener.Flags.PRIVATE);
- }
-
- mUrlOpenInBackgroundListener.onUrlOpenInBackground(url, flags);
-
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.CONTEXT_MENU);
-
- return true;
- }
-
- if (itemId == R.id.home_edit_bookmark) {
- // UI Dialog associates to the activity context, not the applications'.
- new EditBookmarkDialog(context).show(info.url);
- return true;
- }
-
- if (itemId == R.id.home_remove) {
- // For Top Sites grid items, position is required in case item is Pinned.
- final int position = info instanceof TopSitesGridContextMenuInfo ? info.position : -1;
-
- if (info.hasPartnerBookmarkId()) {
- new RemovePartnerBookmarkTask(context, info.bookmarkId).execute();
- } else {
- new RemoveItemByUrlTask(context, info.url, info.itemType, position).execute();
- }
- return true;
- }
-
- return false;
- }
-
- @Override
- public void setUserVisibleHint (boolean isVisibleToUser) {
- if (isVisibleToUser == getUserVisibleHint()) {
- return;
- }
-
- super.setUserVisibleHint(isVisibleToUser);
- loadIfVisible();
- }
-
- /**
- * Handle a configuration change by detaching and re-attaching.
- * <p>
- * A HomeFragment only needs to handle onConfiguration change (i.e.,
- * re-attach) if its UI needs to change (i.e., re-inflate layouts, use
- * different styles, etc) for different device orientations. Handling
- * configuration changes in all HomeFragments will simply cause some
- * redundant re-inflations on device rotation. This slight inefficiency
- * avoids potentially not handling a needed onConfigurationChanged in a
- * subclass.
- */
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
-
- // Reattach the fragment, forcing a re-inflation of its view.
- // We use commitAllowingStateLoss() instead of commit() here to avoid
- // an IllegalStateException. If the phone is rotated while Fennec
- // is in the background, onConfigurationChanged() is fired.
- // onConfigurationChanged() is called before onResume(), so
- // using commit() would throw an IllegalStateException since it can't
- // be used between the Activity's onSaveInstanceState() and
- // onResume().
- if (isVisible()) {
- getFragmentManager().beginTransaction()
- .detach(this)
- .attach(this)
- .commitAllowingStateLoss();
- }
- }
-
- void setCanLoadHint(boolean canLoadHint) {
- if (mCanLoadHint == canLoadHint) {
- return;
- }
-
- mCanLoadHint = canLoadHint;
- loadIfVisible();
- }
-
- boolean getCanLoadHint() {
- return mCanLoadHint;
- }
-
- protected abstract void load();
-
- protected boolean canLoad() {
- return (mCanLoadHint && isVisible() && getUserVisibleHint());
- }
-
- protected void loadIfVisible() {
- if (!canLoad() || mIsLoaded) {
- return;
- }
-
- load();
- mIsLoaded = true;
- }
-
- protected static class RemoveItemByUrlTask extends UIAsyncTask.WithoutParams<Void> {
- private final Context mContext;
- private final String mUrl;
- private final RemoveItemType mType;
- private final int mPosition;
- private final BrowserDB mDB;
-
- /**
- * Remove bookmark/history/reading list type item by url, and also unpin the
- * Top Sites grid item at index <code>position</code>.
- */
- public RemoveItemByUrlTask(Context context, String url, RemoveItemType type, int position) {
- super(ThreadUtils.getBackgroundHandler());
-
- mContext = context;
- mUrl = url;
- mType = type;
- mPosition = position;
- mDB = BrowserDB.from(context);
- }
-
- @Override
- public Void doInBackground() {
- ContentResolver cr = mContext.getContentResolver();
-
- if (mPosition > -1) {
- mDB.unpinSite(cr, mPosition);
- if (mDB.hideSuggestedSite(mUrl)) {
- cr.notifyChange(SuggestedSites.CONTENT_URI, null);
- }
- }
-
- switch (mType) {
- case BOOKMARKS:
- removeBookmark(cr);
- break;
-
- case HISTORY:
- removeHistory(cr);
- break;
-
- case COMBINED:
- removeBookmark(cr);
- removeHistory(cr);
- break;
-
- default:
- Log.e(LOGTAG, "Can't remove item type " + mType.toString());
- break;
- }
- return null;
- }
-
- @Override
- public void onPostExecute(Void result) {
- SnackbarBuilder.builder((Activity) mContext)
- .message(R.string.page_removed)
- .duration(Snackbar.LENGTH_LONG)
- .buildAndShow();
- }
-
- private void removeBookmark(ContentResolver cr) {
- SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(mContext);
- final boolean isReaderViewPage = rch.isURLCached(mUrl);
-
- final String extra;
- if (isReaderViewPage) {
- extra = "bookmark_reader";
- } else {
- extra = "bookmark";
- }
-
- Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.CONTEXT_MENU, extra);
- mDB.removeBookmarksWithURL(cr, mUrl);
-
- if (isReaderViewPage) {
- ReadingListHelper.removeCachedReaderItem(mUrl, mContext);
- }
- }
-
- private void removeHistory(ContentResolver cr) {
- mDB.removeHistoryEntry(cr, mUrl);
- }
- }
-
- private static class RemovePartnerBookmarkTask extends UIAsyncTask.WithoutParams<Void> {
- private Context context;
- private long bookmarkId;
-
- public RemovePartnerBookmarkTask(Context context, long bookmarkId) {
- super(ThreadUtils.getBackgroundHandler());
-
- this.context = context;
- this.bookmarkId = bookmarkId;
- }
-
- @Override
- protected Void doInBackground() {
- context.getContentResolver().delete(
- PartnerBookmarksProviderProxy.getUriForBookmark(context, bookmarkId),
- null,
- null
- );
-
- return null;
- }
-
- @Override
- protected void onPostExecute(Void aVoid) {
- SnackbarBuilder.builder((Activity) context)
- .message(R.string.page_removed)
- .duration(Snackbar.LENGTH_LONG)
- .buildAndShow();
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeListView.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeListView.java
deleted file mode 100644
index d179a27ce..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeListView.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.database.Cursor;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.ContextMenu.ContextMenuInfo;
-import android.view.View;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemLongClickListener;
-import android.widget.ListView;
-
-/**
- * HomeListView is a custom extension of ListView, that packs a HomeContextMenuInfo
- * when any of its rows is long pressed.
- */
-public class HomeListView extends ListView
- implements OnItemLongClickListener {
-
- // ContextMenuInfo associated with the currently long pressed list item.
- private HomeContextMenuInfo mContextMenuInfo;
-
- // On URL open listener
- protected OnUrlOpenListener mUrlOpenListener;
-
- // Top divider
- private final boolean mShowTopDivider;
-
- // ContextMenuInfo maker
- private HomeContextMenuInfo.Factory mContextMenuInfoFactory;
-
- public HomeListView(Context context) {
- this(context, null);
- }
-
- public HomeListView(Context context, AttributeSet attrs) {
- this(context, attrs, R.attr.homeListViewStyle);
- }
-
- public HomeListView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
-
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HomeListView, defStyle, 0);
- mShowTopDivider = a.getBoolean(R.styleable.HomeListView_topDivider, false);
- a.recycle();
-
- setOnItemLongClickListener(this);
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- final Drawable divider = getDivider();
- if (mShowTopDivider && divider != null) {
- final int dividerHeight = getDividerHeight();
- final View view = new View(getContext());
- view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, dividerHeight));
- addHeaderView(view);
- }
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- mUrlOpenListener = null;
- }
-
- @Override
- public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
- Object item = parent.getItemAtPosition(position);
-
- // HomeListView could hold headers too. Add a context menu info only for its children.
- if (item instanceof Cursor) {
- Cursor cursor = (Cursor) item;
- if (cursor == null || mContextMenuInfoFactory == null) {
- mContextMenuInfo = null;
- return false;
- }
-
- mContextMenuInfo = mContextMenuInfoFactory.makeInfoForCursor(view, position, id, cursor);
- return showContextMenuForChild(HomeListView.this);
-
- } else if (mContextMenuInfoFactory instanceof HomeContextMenuInfo.ListFactory) {
- mContextMenuInfo = ((HomeContextMenuInfo.ListFactory) mContextMenuInfoFactory).makeInfoForAdapter(view, position, id, getAdapter());
- return showContextMenuForChild(HomeListView.this);
- } else {
- mContextMenuInfo = null;
- return false;
- }
- }
-
- @Override
- public ContextMenuInfo getContextMenuInfo() {
- return mContextMenuInfo;
- }
-
- @Override
- public void setOnItemClickListener(final AdapterView.OnItemClickListener listener) {
- if (listener == null) {
- super.setOnItemClickListener(null);
- return;
- }
-
- super.setOnItemClickListener(new AdapterView.OnItemClickListener() {
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- if (mShowTopDivider) {
- position--;
- }
-
- listener.onItemClick(parent, view, position, id);
- }
- });
- }
-
- public void setContextMenuInfoFactory(final HomeContextMenuInfo.Factory factory) {
- mContextMenuInfoFactory = factory;
- }
-
- public OnUrlOpenListener getOnUrlOpenListener() {
- return mUrlOpenListener;
- }
-
- public void setOnUrlOpenListener(OnUrlOpenListener listener) {
- mUrlOpenListener = listener;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomePager.java b/mobile/android/base/java/org/mozilla/gecko/home/HomePager.java
deleted file mode 100644
index 4915f0c91..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomePager.java
+++ /dev/null
@@ -1,564 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import java.util.ArrayList;
-import java.util.EnumSet;
-import java.util.List;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.animation.PropertyAnimator;
-import org.mozilla.gecko.animation.ViewHelper;
-import org.mozilla.gecko.home.HomeAdapter.OnAddPanelListener;
-import org.mozilla.gecko.home.HomeConfig.PanelConfig;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.LoaderManager;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.Loader;
-import android.support.v4.view.ViewPager;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-
-public class HomePager extends ViewPager implements HomeScreen {
-
- @Override
- public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
- return super.requestFocus(direction, previouslyFocusedRect);
- }
-
- private static final int LOADER_ID_CONFIG = 0;
-
- private final Context mContext;
- private volatile boolean mVisible;
- private Decor mDecor;
- private View mTabStrip;
- private HomeBanner mHomeBanner;
- private int mDefaultPageIndex = -1;
-
- private final OnAddPanelListener mAddPanelListener;
-
- private final HomeConfig mConfig;
- private final ConfigLoaderCallbacks mConfigLoaderCallbacks;
-
- private String mInitialPanelId;
- private Bundle mRestoreData;
-
- // Cached original ViewPager background.
- private final Drawable mOriginalBackground;
-
- // Telemetry session for current panel.
- private TelemetryContract.Session mCurrentPanelSession;
- private String mCurrentPanelSessionSuffix;
-
- // Current load state of HomePager.
- private LoadState mLoadState;
-
- // Listens for when the current panel changes.
- private OnPanelChangeListener mPanelChangedListener;
-
- private HomeFragment.PanelStateChangeListener mPanelStateChangeListener;
-
- // This is mostly used by UI tests to easily fetch
- // specific list views at runtime.
- public static final String LIST_TAG_HISTORY = "history";
- public static final String LIST_TAG_BOOKMARKS = "bookmarks";
- public static final String LIST_TAG_TOP_SITES = "top_sites";
- public static final String LIST_TAG_RECENT_TABS = "recent_tabs";
- public static final String LIST_TAG_BROWSER_SEARCH = "browser_search";
- public static final String LIST_TAG_REMOTE_TABS = "remote_tabs";
-
- public interface OnUrlOpenListener {
- public enum Flags {
- ALLOW_SWITCH_TO_TAB,
- OPEN_WITH_INTENT,
- /**
- * Ensure that the raw URL is opened. If not set, then the reader view version of the page
- * might be opened if the URL is stored as an offline reader-view bookmark.
- */
- NO_READER_VIEW
- }
-
- public void onUrlOpen(String url, EnumSet<Flags> flags);
- }
-
- /**
- * Interface for requesting a new tab be opened in the background.
- * <p>
- * This is the <code>HomeFragment</code> equivalent of opening a new tab by
- * long clicking a link and selecting the "Open new [private] tab" context
- * menu option.
- */
- public interface OnUrlOpenInBackgroundListener {
- public enum Flags {
- PRIVATE,
- }
-
- /**
- * Open a new tab with the given URL
- *
- * @param url to open.
- * @param flags to open new tab with.
- */
- public void onUrlOpenInBackground(String url, EnumSet<Flags> flags);
- }
-
- /**
- * Special type of child views that could be added as pager decorations by default.
- */
- public interface Decor {
- void onAddPagerView(String title);
- void removeAllPagerViews();
- void onPageSelected(int position);
- void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
- void setOnTitleClickListener(TabMenuStrip.OnTitleClickListener onTitleClickListener);
- }
-
- /**
- * State of HomePager with respect to loading its configuration.
- */
- private enum LoadState {
- UNLOADED,
- LOADING,
- LOADED
- }
-
- public static final String CAN_LOAD_ARG = "canLoad";
- public static final String PANEL_CONFIG_ARG = "panelConfig";
-
- public HomePager(Context context) {
- this(context, null);
- }
-
- public HomePager(Context context, AttributeSet attrs) {
- super(context, attrs);
- mContext = context;
-
- mConfig = HomeConfig.getDefault(mContext);
- mConfigLoaderCallbacks = new ConfigLoaderCallbacks();
-
- mAddPanelListener = new OnAddPanelListener() {
- @Override
- public void onAddPanel(String title) {
- if (mDecor != null) {
- mDecor.onAddPagerView(title);
- }
- }
- };
-
- // This is to keep all 4 panels in memory after they are
- // selected in the pager.
- setOffscreenPageLimit(3);
-
- // We can call HomePager.requestFocus to steal focus from the URL bar and drop the soft
- // keyboard. However, if there are no focusable views (e.g. an empty reading list), the
- // URL bar will be refocused. Therefore, we make the HomePager container focusable to
- // ensure there is always a focusable view. This would ordinarily be done via an XML
- // attribute, but it is not working properly.
- setFocusableInTouchMode(true);
-
- mOriginalBackground = getBackground();
- setOnPageChangeListener(new PageChangeListener());
-
- mLoadState = LoadState.UNLOADED;
- }
-
- @Override
- public void addView(View child, int index, ViewGroup.LayoutParams params) {
- if (child instanceof Decor) {
- ((ViewPager.LayoutParams) params).isDecor = true;
- mDecor = (Decor) child;
- mTabStrip = child;
-
- mDecor.setOnTitleClickListener(new TabMenuStrip.OnTitleClickListener() {
- @Override
- public void onTitleClicked(int index) {
- setCurrentItem(index, true);
- }
- });
- }
-
- super.addView(child, index, params);
- }
-
- /**
- * Loads and initializes the pager.
- *
- * @param fm FragmentManager for the adapter
- */
- @Override
- public void load(LoaderManager lm, FragmentManager fm, String panelId, Bundle restoreData, PropertyAnimator animator) {
- mLoadState = LoadState.LOADING;
-
- mVisible = true;
- mInitialPanelId = panelId;
- mRestoreData = restoreData;
-
- // Update the home banner message each time the HomePager is loaded.
- if (mHomeBanner != null) {
- mHomeBanner.update();
- }
-
- // Only animate on post-HC devices, when a non-null animator is given
- final boolean shouldAnimate = animator != null;
-
- final HomeAdapter adapter = new HomeAdapter(mContext, fm);
- adapter.setOnAddPanelListener(mAddPanelListener);
- adapter.setPanelStateChangeListener(mPanelStateChangeListener);
- adapter.setCanLoadHint(true);
- setAdapter(adapter);
-
- // Don't show the tabs strip until we have the
- // list of panels in place.
- mTabStrip.setVisibility(View.INVISIBLE);
-
- // Load list of panels from configuration
- lm.initLoader(LOADER_ID_CONFIG, null, mConfigLoaderCallbacks);
-
- if (shouldAnimate) {
- animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
- @Override
- public void onPropertyAnimationStart() {
- setLayerType(View.LAYER_TYPE_HARDWARE, null);
- }
-
- @Override
- public void onPropertyAnimationEnd() {
- setLayerType(View.LAYER_TYPE_NONE, null);
- }
- });
-
- ViewHelper.setAlpha(this, 0.0f);
-
- animator.attach(this,
- PropertyAnimator.Property.ALPHA,
- 1.0f);
- }
- }
-
- /**
- * Removes all child fragments to free memory.
- */
- @Override
- public void unload() {
- mVisible = false;
- setAdapter(null);
- mLoadState = LoadState.UNLOADED;
-
- // Stop UI Telemetry sessions.
- stopCurrentPanelTelemetrySession();
- }
-
- /**
- * Determines whether the pager is visible.
- *
- * Unlike getVisibility(), this method does not need to be called on the UI
- * thread.
- *
- * @return Whether the pager and its fragments are loaded
- */
- public boolean isVisible() {
- return mVisible;
- }
-
- @Override
- public void setCurrentItem(int item, boolean smoothScroll) {
- super.setCurrentItem(item, smoothScroll);
-
- if (mDecor != null) {
- mDecor.onPageSelected(item);
- }
-
- if (mHomeBanner != null) {
- mHomeBanner.setActive(item == mDefaultPageIndex);
- }
- }
-
- private void restorePanelData(int item, Bundle data) {
- ((HomeAdapter) getAdapter()).setRestoreData(item, data);
- }
-
- /**
- * Shows a home panel. If the given panelId is null,
- * the default panel will be shown. No action will be taken if:
- * * HomePager has not loaded yet
- * * Panel with the given panelId cannot be found
- *
- * If you're trying to open a built-in panel, consider loading the panel url directly with
- * {@link org.mozilla.gecko.AboutPages#getURLForBuiltinPanelType(HomeConfig.PanelType)}.
- *
- * @param panelId of the home panel to be shown.
- */
- @Override
- public void showPanel(String panelId, Bundle restoreData) {
- if (!mVisible) {
- return;
- }
-
- switch (mLoadState) {
- case LOADING:
- mInitialPanelId = panelId;
- mRestoreData = restoreData;
- break;
-
- case LOADED:
- int position = mDefaultPageIndex;
- if (panelId != null) {
- position = ((HomeAdapter) getAdapter()).getItemPosition(panelId);
- }
-
- if (position > -1) {
- setCurrentItem(position);
- if (restoreData != null) {
- restorePanelData(position, restoreData);
- }
- }
- break;
-
- default:
- // Do nothing.
- }
- }
-
- @Override
- public boolean onInterceptTouchEvent(MotionEvent event) {
- if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
- // Drop the soft keyboard by stealing focus from the URL bar.
- requestFocus();
- }
-
- return super.onInterceptTouchEvent(event);
- }
-
- public void setBanner(HomeBanner banner) {
- mHomeBanner = banner;
- }
-
- @Override
- public boolean dispatchTouchEvent(MotionEvent event) {
- if (mHomeBanner != null) {
- mHomeBanner.handleHomeTouch(event);
- }
-
- return super.dispatchTouchEvent(event);
- }
-
- @Override
- public void onToolbarFocusChange(boolean hasFocus) {
- if (mHomeBanner == null) {
- return;
- }
-
- // We should only make the banner active if the toolbar is not focused and we are on the default page
- final boolean active = !hasFocus && getCurrentItem() == mDefaultPageIndex;
- mHomeBanner.setActive(active);
- }
-
- private void updateUiFromConfigState(HomeConfig.State configState) {
- // We only care about the adapter if HomePager is currently
- // loaded, which means it's visible in the activity.
- if (!mVisible) {
- return;
- }
-
- if (mDecor != null) {
- mDecor.removeAllPagerViews();
- }
-
- final HomeAdapter adapter = (HomeAdapter) getAdapter();
-
- // Disable any fragment loading until we have the initial
- // panel selection done.
- adapter.setCanLoadHint(false);
-
- // Destroy any existing panels currently loaded
- // in the pager.
- setAdapter(null);
-
- // Only keep enabled panels.
- final List<PanelConfig> enabledPanels = new ArrayList<PanelConfig>();
-
- for (PanelConfig panelConfig : configState) {
- if (!panelConfig.isDisabled()) {
- enabledPanels.add(panelConfig);
- }
- }
-
- // Update the adapter with the new panel configs
- adapter.update(enabledPanels);
-
- final int count = enabledPanels.size();
- if (count == 0) {
- // Set firefox watermark as background.
- setBackgroundResource(R.drawable.home_pager_empty_state);
- // Hide the tab strip as there are no panels.
- mTabStrip.setVisibility(View.INVISIBLE);
- } else {
- mTabStrip.setVisibility(View.VISIBLE);
- // Restore original background.
- setBackgroundDrawable(mOriginalBackground);
- }
-
- // Re-install the adapter with the final state
- // in the pager.
- setAdapter(adapter);
-
- if (count == 0) {
- mDefaultPageIndex = -1;
-
- // Hide the banner if there are no enabled panels.
- if (mHomeBanner != null) {
- mHomeBanner.setActive(false);
- }
- } else {
- for (int i = 0; i < count; i++) {
- if (enabledPanels.get(i).isDefault()) {
- mDefaultPageIndex = i;
- break;
- }
- }
-
- // Use the default panel if the initial panel wasn't explicitly set by the
- // load() caller, or if the initial panel is not found in the adapter.
- final int itemPosition = (mInitialPanelId == null) ? -1 : adapter.getItemPosition(mInitialPanelId);
- if (itemPosition > -1) {
- setCurrentItem(itemPosition, false);
- if (mRestoreData != null) {
- restorePanelData(itemPosition, mRestoreData);
- mRestoreData = null; // Release data since it's no longer needed
- }
- mInitialPanelId = null;
- } else {
- setCurrentItem(mDefaultPageIndex, false);
- }
- }
-
- // The selection is updated asynchronously so we need to post to
- // UI thread to give the pager time to commit the new page selection
- // internally and load the right initial panel.
- ThreadUtils.getUiHandler().post(new Runnable() {
- @Override
- public void run() {
- adapter.setCanLoadHint(true);
- }
- });
- }
-
- @Override
- public void setOnPanelChangeListener(OnPanelChangeListener listener) {
- mPanelChangedListener = listener;
- }
-
- @Override
- public void setPanelStateChangeListener(HomeFragment.PanelStateChangeListener listener) {
- mPanelStateChangeListener = listener;
-
- HomeAdapter adapter = (HomeAdapter) getAdapter();
- if (adapter != null) {
- adapter.setPanelStateChangeListener(listener);
- }
- }
-
- /**
- * Notify listeners of newly selected panel.
- *
- * @param position of the newly selected panel
- */
- private void notifyPanelSelected(int position) {
- if (mDecor != null) {
- mDecor.onPageSelected(position);
- }
-
- if (mPanelChangedListener != null) {
- final String panelId = ((HomeAdapter) getAdapter()).getPanelIdAtPosition(position);
- mPanelChangedListener.onPanelSelected(panelId);
- }
- }
-
- private class ConfigLoaderCallbacks implements LoaderCallbacks<HomeConfig.State> {
- @Override
- public Loader<HomeConfig.State> onCreateLoader(int id, Bundle args) {
- return new HomeConfigLoader(mContext, mConfig);
- }
-
- @Override
- public void onLoadFinished(Loader<HomeConfig.State> loader, HomeConfig.State configState) {
- mLoadState = LoadState.LOADED;
- updateUiFromConfigState(configState);
- }
-
- @Override
- public void onLoaderReset(Loader<HomeConfig.State> loader) {
- mLoadState = LoadState.UNLOADED;
- }
- }
-
- private class PageChangeListener implements ViewPager.OnPageChangeListener {
- @Override
- public void onPageSelected(int position) {
- notifyPanelSelected(position);
-
- if (mHomeBanner != null) {
- mHomeBanner.setActive(position == mDefaultPageIndex);
- }
-
- // Start a UI telemetry session for the newly selected panel.
- final String newPanelId = ((HomeAdapter) getAdapter()).getPanelIdAtPosition(position);
- startNewPanelTelemetrySession(newPanelId);
- }
-
- @Override
- public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
- if (mDecor != null) {
- mDecor.onPageScrolled(position, positionOffset, positionOffsetPixels);
- }
-
- if (mHomeBanner != null) {
- mHomeBanner.setScrollingPages(positionOffsetPixels != 0);
- }
- }
-
- @Override
- public void onPageScrollStateChanged(int state) { }
- }
-
- /**
- * Start UI telemetry session for the a panel.
- * If there is currently a session open for a panel,
- * it will be stopped before a new one is started.
- *
- * @param panelId of panel to start a session for
- */
- private void startNewPanelTelemetrySession(String panelId) {
- // Stop the current panel's session if we have one.
- stopCurrentPanelTelemetrySession();
-
- mCurrentPanelSession = TelemetryContract.Session.HOME_PANEL;
- mCurrentPanelSessionSuffix = panelId;
- Telemetry.startUISession(mCurrentPanelSession, mCurrentPanelSessionSuffix);
- }
-
- /**
- * Stop the current panel telemetry session if one exists.
- */
- private void stopCurrentPanelTelemetrySession() {
- if (mCurrentPanelSession != null) {
- Telemetry.stopUISession(mCurrentPanelSession, mCurrentPanelSessionSuffix);
- mCurrentPanelSession = null;
- mCurrentPanelSessionSuffix = null;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomePanelsManager.java b/mobile/android/base/java/org/mozilla/gecko/home/HomePanelsManager.java
deleted file mode 100644
index bfd6c5624..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomePanelsManager.java
+++ /dev/null
@@ -1,368 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import static org.mozilla.gecko.home.HomeConfig.createBuiltinPanelConfig;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Queue;
-import java.util.Set;
-import java.util.concurrent.ConcurrentLinkedQueue;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.db.HomeProvider;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.home.HomeConfig.PanelConfig;
-import org.mozilla.gecko.home.PanelInfoManager.PanelInfo;
-import org.mozilla.gecko.home.PanelInfoManager.RequestCallback;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.os.Handler;
-import android.util.Log;
-
-public class HomePanelsManager implements GeckoEventListener {
- public static final String LOGTAG = "HomePanelsManager";
-
- private static final HomePanelsManager sInstance = new HomePanelsManager();
-
- private static final int INVALIDATION_DELAY_MSEC = 500;
- private static final int PANEL_INFO_TIMEOUT_MSEC = 1000;
-
- private static final String EVENT_HOMEPANELS_INSTALL = "HomePanels:Install";
- private static final String EVENT_HOMEPANELS_UNINSTALL = "HomePanels:Uninstall";
- private static final String EVENT_HOMEPANELS_UPDATE = "HomePanels:Update";
- private static final String EVENT_HOMEPANELS_REFRESH = "HomePanels:RefreshDataset";
-
- private static final String JSON_KEY_PANEL = "panel";
- private static final String JSON_KEY_PANEL_ID = "id";
-
- private enum ChangeType {
- UNINSTALL,
- INSTALL,
- UPDATE,
- REFRESH
- }
-
- private enum InvalidationMode {
- DELAYED,
- IMMEDIATE
- }
-
- private static class ConfigChange {
- private final ChangeType type;
- private final Object target;
-
- public ConfigChange(ChangeType type) {
- this(type, null);
- }
-
- public ConfigChange(ChangeType type, Object target) {
- this.type = type;
- this.target = target;
- }
- }
-
- private Context mContext;
- private HomeConfig mHomeConfig;
- private boolean mInitialized;
-
- private final Queue<ConfigChange> mPendingChanges = new ConcurrentLinkedQueue<ConfigChange>();
- private final Runnable mInvalidationRunnable = new InvalidationRunnable();
-
- public static HomePanelsManager getInstance() {
- return sInstance;
- }
-
- public void init(Context context) {
- if (mInitialized) {
- return;
- }
-
- mContext = context;
- mHomeConfig = HomeConfig.getDefault(context);
-
- EventDispatcher.getInstance().registerGeckoThreadListener(this,
- EVENT_HOMEPANELS_INSTALL,
- EVENT_HOMEPANELS_UNINSTALL,
- EVENT_HOMEPANELS_UPDATE,
- EVENT_HOMEPANELS_REFRESH);
-
- mInitialized = true;
- }
-
- public void onLocaleReady(final String locale) {
- ThreadUtils.getBackgroundHandler().post(new Runnable() {
- @Override
- public void run() {
- final String configLocale = mHomeConfig.getLocale();
- if (configLocale == null || !configLocale.equals(locale)) {
- handleLocaleChange();
- }
- }
- });
- }
-
- @Override
- public void handleMessage(String event, JSONObject message) {
- try {
- if (event.equals(EVENT_HOMEPANELS_INSTALL)) {
- Log.d(LOGTAG, EVENT_HOMEPANELS_INSTALL);
- handlePanelInstall(createPanelConfigFromMessage(message), InvalidationMode.DELAYED);
- } else if (event.equals(EVENT_HOMEPANELS_UNINSTALL)) {
- Log.d(LOGTAG, EVENT_HOMEPANELS_UNINSTALL);
- final String panelId = message.getString(JSON_KEY_PANEL_ID);
- handlePanelUninstall(panelId);
- } else if (event.equals(EVENT_HOMEPANELS_UPDATE)) {
- Log.d(LOGTAG, EVENT_HOMEPANELS_UPDATE);
- handlePanelUpdate(createPanelConfigFromMessage(message));
- } else if (event.equals(EVENT_HOMEPANELS_REFRESH)) {
- Log.d(LOGTAG, EVENT_HOMEPANELS_REFRESH);
- handleDatasetRefresh(message);
- }
- } catch (Exception e) {
- Log.e(LOGTAG, "Failed to handle event " + event, e);
- }
- }
-
- private PanelConfig createPanelConfigFromMessage(JSONObject message) throws JSONException {
- final JSONObject json = message.getJSONObject(JSON_KEY_PANEL);
- return new PanelConfig(json);
- }
-
- /**
- * Adds a new PanelConfig to the HomeConfig.
- *
- * This posts the invalidation of HomeConfig immediately.
- *
- * @param panelConfig panel to add
- */
- public void installPanel(PanelConfig panelConfig) {
- Log.d(LOGTAG, "installPanel: " + panelConfig.getTitle());
- handlePanelInstall(panelConfig, InvalidationMode.IMMEDIATE);
- }
-
- /**
- * Runs in the gecko thread.
- */
- private void handlePanelInstall(PanelConfig panelConfig, InvalidationMode mode) {
- mPendingChanges.offer(new ConfigChange(ChangeType.INSTALL, panelConfig));
- Log.d(LOGTAG, "handlePanelInstall: " + mPendingChanges.size());
-
- scheduleInvalidation(mode);
- }
-
- /**
- * Runs in the gecko thread.
- */
- private void handlePanelUninstall(String panelId) {
- mPendingChanges.offer(new ConfigChange(ChangeType.UNINSTALL, panelId));
- Log.d(LOGTAG, "handlePanelUninstall: " + mPendingChanges.size());
-
- scheduleInvalidation(InvalidationMode.DELAYED);
- }
-
- /**
- * Runs in the gecko thread.
- */
- private void handlePanelUpdate(PanelConfig panelConfig) {
- mPendingChanges.offer(new ConfigChange(ChangeType.UPDATE, panelConfig));
- Log.d(LOGTAG, "handlePanelUpdate: " + mPendingChanges.size());
-
- scheduleInvalidation(InvalidationMode.DELAYED);
- }
-
- /**
- * Runs in the background thread.
- */
- private void handleLocaleChange() {
- mPendingChanges.offer(new ConfigChange(ChangeType.REFRESH));
- Log.d(LOGTAG, "handleLocaleChange: " + mPendingChanges.size());
-
- scheduleInvalidation(InvalidationMode.IMMEDIATE);
- }
-
-
- /**
- * Handles a dataset refresh request from Gecko. This is usually
- * triggered by a HomeStorage.save() call in an add-on.
- *
- * Runs in the gecko thread.
- */
- private void handleDatasetRefresh(JSONObject message) {
- final String datasetId;
- try {
- datasetId = message.getString("datasetId");
- } catch (JSONException e) {
- Log.e(LOGTAG, "Failed to handle dataset refresh", e);
- return;
- }
-
- Log.d(LOGTAG, "Refresh request for dataset: " + datasetId);
-
- final ContentResolver cr = mContext.getContentResolver();
- cr.notifyChange(HomeProvider.getDatasetNotificationUri(datasetId), null);
- }
-
- /**
- * Runs in the gecko or main thread.
- */
- private void scheduleInvalidation(InvalidationMode mode) {
- final Handler handler = ThreadUtils.getBackgroundHandler();
-
- handler.removeCallbacks(mInvalidationRunnable);
-
- if (mode == InvalidationMode.IMMEDIATE) {
- handler.post(mInvalidationRunnable);
- } else {
- handler.postDelayed(mInvalidationRunnable, INVALIDATION_DELAY_MSEC);
- }
-
- Log.d(LOGTAG, "scheduleInvalidation: scheduled new invalidation: " + mode);
- }
-
- /**
- * Runs in the background thread.
- */
- private void executePendingChanges(HomeConfig.Editor editor) {
- boolean shouldRefresh = false;
-
- while (!mPendingChanges.isEmpty()) {
- final ConfigChange pendingChange = mPendingChanges.poll();
-
- switch (pendingChange.type) {
- case UNINSTALL: {
- final String panelId = (String) pendingChange.target;
- if (editor.uninstall(panelId)) {
- Log.d(LOGTAG, "executePendingChanges: uninstalled panel " + panelId);
- }
- break;
- }
-
- case INSTALL: {
- final PanelConfig panelConfig = (PanelConfig) pendingChange.target;
- if (editor.install(panelConfig)) {
- Log.d(LOGTAG, "executePendingChanges: added panel " + panelConfig.getId());
- }
- break;
- }
-
- case UPDATE: {
- final PanelConfig panelConfig = (PanelConfig) pendingChange.target;
- if (editor.update(panelConfig)) {
- Log.w(LOGTAG, "executePendingChanges: updated panel " + panelConfig.getId());
- }
- break;
- }
-
- case REFRESH: {
- shouldRefresh = true;
- }
- }
- }
-
- // The editor still represents the default HomeConfig
- // configuration and hasn't been changed by any operation
- // above. No need to refresh as the HomeConfig backend will
- // take of forcing all existing HomeConfigLoader instances to
- // refresh their contents.
- if (shouldRefresh && !editor.isDefault()) {
- executeRefresh(editor);
- }
- }
-
- /**
- * Runs in the background thread.
- */
- private void refreshFromPanelInfos(HomeConfig.Editor editor, List<PanelInfo> panelInfos) {
- Log.d(LOGTAG, "refreshFromPanelInfos");
-
- for (PanelConfig panelConfig : editor) {
- PanelConfig refreshedPanelConfig = null;
-
- if (panelConfig.isDynamic()) {
- for (PanelInfo panelInfo : panelInfos) {
- if (panelInfo.getId().equals(panelConfig.getId())) {
- refreshedPanelConfig = panelInfo.toPanelConfig();
- Log.d(LOGTAG, "refreshFromPanelInfos: refreshing from panel info: " + panelInfo.getId());
- break;
- }
- }
- } else {
- refreshedPanelConfig = createBuiltinPanelConfig(mContext, panelConfig.getType());
- Log.d(LOGTAG, "refreshFromPanelInfos: refreshing built-in panel: " + panelConfig.getId());
- }
-
- if (refreshedPanelConfig == null) {
- Log.d(LOGTAG, "refreshFromPanelInfos: no refreshed panel, falling back: " + panelConfig.getId());
- continue;
- }
-
- Log.d(LOGTAG, "refreshFromPanelInfos: refreshed panel " + refreshedPanelConfig.getId());
- editor.update(refreshedPanelConfig);
- }
- }
-
- /**
- * Runs in the background thread.
- */
- private void executeRefresh(HomeConfig.Editor editor) {
- if (editor.isEmpty()) {
- return;
- }
-
- Log.d(LOGTAG, "executeRefresh");
-
- final Set<String> ids = new HashSet<String>();
- for (PanelConfig panelConfig : editor) {
- ids.add(panelConfig.getId());
- }
-
- final Object panelRequestLock = new Object();
- final List<PanelInfo> latestPanelInfos = new ArrayList<PanelInfo>();
-
- final PanelInfoManager pm = new PanelInfoManager();
- pm.requestPanelsById(ids, new RequestCallback() {
- @Override
- public void onComplete(List<PanelInfo> panelInfos) {
- synchronized (panelRequestLock) {
- latestPanelInfos.addAll(panelInfos);
- Log.d(LOGTAG, "executeRefresh: fetched panel infos: " + panelInfos.size());
-
- panelRequestLock.notifyAll();
- }
- }
- });
-
- try {
- synchronized (panelRequestLock) {
- panelRequestLock.wait(PANEL_INFO_TIMEOUT_MSEC);
-
- Log.d(LOGTAG, "executeRefresh: done fetching panel infos");
- refreshFromPanelInfos(editor, latestPanelInfos);
- }
- } catch (InterruptedException e) {
- Log.e(LOGTAG, "Failed to fetch panels from gecko", e);
- }
- }
-
- /**
- * Runs in the background thread.
- */
- private class InvalidationRunnable implements Runnable {
- @Override
- public void run() {
- final HomeConfig.Editor editor = mHomeConfig.load().edit();
- executePendingChanges(editor);
- editor.apply();
- }
- };
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeScreen.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeScreen.java
deleted file mode 100644
index 1525969a0..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeScreen.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package org.mozilla.gecko.home;
-
-import android.os.Bundle;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.LoaderManager;
-import android.view.View;
-
-import org.mozilla.gecko.animation.PropertyAnimator;
-
-/**
- * Generic interface for any View that can be used as the homescreen.
- *
- * In the past we had the HomePager, which contained the usual homepanels (multiple panels: TopSites,
- * bookmarks, history, etc.), which could be swiped between.
- *
- * This interface allows easily switching between different homepanel implementations. For example
- * the prototype activity-stream panel (which will be a single panel combining the functionality
- * of the previous panels).
- */
-public interface HomeScreen {
- /**
- * Interface for listening into ViewPager panel changes
- */
- public interface OnPanelChangeListener {
- /**
- * Called when a new panel is selected.
- *
- * @param panelId of the newly selected panel
- */
- public void onPanelSelected(String panelId);
- }
-
- // The following two methods are actually methods of View. Since there is no View interface
- // we're forced to do this instead of "extending" View. Any class implementing HomeScreen
- // will have to implement these and pass them through to the underlying View.
- boolean isVisible();
- boolean requestFocus();
-
- void onToolbarFocusChange(boolean hasFocus);
-
- // The following three methods are HomePager specific. The persistence framework might need
- // refactoring/generalising at some point, but it isn't entirely clear what other panels
- // might need so we can leave these as is for now.
- void showPanel(String panelId, Bundle restoreData);
- void setOnPanelChangeListener(OnPanelChangeListener listener);
- void setPanelStateChangeListener(HomeFragment.PanelStateChangeListener listener);
-
- /**
- * Set a banner that may be displayed at the bottom of the HomeScreen. This can be used
- * e.g. to show snippets.
- */
- void setBanner(HomeBanner banner);
-
- void load(LoaderManager lm, FragmentManager fm, String panelId, Bundle restoreData, PropertyAnimator animator);
-
- void unload();
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/ImageLoader.java b/mobile/android/base/java/org/mozilla/gecko/home/ImageLoader.java
deleted file mode 100644
index 2bbd82a8d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/ImageLoader.java
+++ /dev/null
@@ -1,164 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import android.content.Context;
-import android.net.Uri;
-import android.util.DisplayMetrics;
-import android.util.Log;
-
-import com.squareup.picasso.LruCache;
-import com.squareup.picasso.Picasso;
-import com.squareup.picasso.Downloader.Response;
-import com.squareup.picasso.UrlConnectionDownloader;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.util.EnumSet;
-import java.util.Set;
-
-import org.mozilla.gecko.distribution.Distribution;
-
-public class ImageLoader {
- private static final String LOGTAG = "GeckoImageLoader";
-
- private static final String DISTRIBUTION_SCHEME = "gecko.distribution";
- private static final String SUGGESTED_SITES_AUTHORITY = "suggestedsites";
-
- // The order of density factors to try when looking for an image resource
- // in the distribution directory. It looks for an exact match first (1.0) then
- // tries to find images with higher density (2.0 and 1.5). If no image is found,
- // try a lower density (0.5). See loadDistributionImage().
- private static final float[] densityFactors = new float[] { 1.0f, 2.0f, 1.5f, 0.5f };
-
- private static enum Density {
- MDPI,
- HDPI,
- XHDPI,
- XXHDPI;
-
- @Override
- public String toString() {
- return super.toString().toLowerCase();
- }
- }
-
- // Picasso instance and LruCache lrucache are protected by synchronization.
- private static Picasso instance;
- private static LruCache lrucache;
-
- public static synchronized Picasso with(Context context) {
- if (instance == null) {
- lrucache = new LruCache(context);
- Picasso.Builder builder = new Picasso.Builder(context).memoryCache(lrucache);
-
- final Distribution distribution = Distribution.getInstance(context.getApplicationContext());
- builder.downloader(new ImageDownloader(context, distribution));
- instance = builder.build();
- }
-
- return instance;
- }
-
- public static synchronized void clearLruCache() {
- if (lrucache != null) {
- lrucache.evictAll();
- }
- }
-
- /**
- * Custom Downloader built on top of Picasso's UrlConnectionDownloader
- * that supports loading images from custom URIs.
- */
- public static class ImageDownloader extends UrlConnectionDownloader {
- private final Context context;
- private final Distribution distribution;
-
- public ImageDownloader(Context context, Distribution distribution) {
- super(context);
- this.context = context;
- this.distribution = distribution;
- }
-
- private Density getDensity(float factor) {
- final DisplayMetrics dm = context.getResources().getDisplayMetrics();
- final float densityDpi = dm.densityDpi * factor;
-
- if (densityDpi >= DisplayMetrics.DENSITY_XXHIGH) {
- return Density.XXHDPI;
- } else if (densityDpi >= DisplayMetrics.DENSITY_XHIGH) {
- return Density.XHDPI;
- } else if (densityDpi >= DisplayMetrics.DENSITY_HIGH) {
- return Density.HDPI;
- }
-
- // Fallback to mdpi, no need to handle ldpi.
- return Density.MDPI;
- }
-
- @Override
- public Response load(Uri uri, boolean localCacheOnly) throws IOException {
- final String scheme = uri.getScheme();
- if (DISTRIBUTION_SCHEME.equals(scheme)) {
- return loadDistributionImage(uri);
- }
-
- return super.load(uri, localCacheOnly);
- }
-
- private static String getPathForDensity(String basePath, Density density,
- String filename) {
- final File dir = new File(basePath, density.toString());
- return String.format("%s/%s.png", dir.toString(), filename);
- }
-
- /**
- * Handle distribution URIs in Picasso. The expected format is:
- *
- * gecko.distribution://<basepath>/<imagename>
- *
- * Which will look for the following file in the distribution:
- *
- * <distribution-root-dir>/<basepath>/<device-density>/<imagename>.png
- */
- private Response loadDistributionImage(Uri uri) throws IOException {
- // Eliminate the leading '//'
- final String ssp = uri.getSchemeSpecificPart().substring(2);
-
- final String filename;
- final String basePath;
-
- final int slashIndex = ssp.lastIndexOf('/');
- if (slashIndex == -1) {
- filename = ssp;
- basePath = "";
- } else {
- filename = ssp.substring(slashIndex + 1);
- basePath = ssp.substring(0, slashIndex);
- }
-
- Set<Density> triedDensities = EnumSet.noneOf(Density.class);
-
- for (int i = 0; i < densityFactors.length; i++) {
- final Density density = getDensity(densityFactors[i]);
- if (!triedDensities.add(density)) {
- continue;
- }
-
- final String path = getPathForDensity(basePath, density, filename);
- Log.d(LOGTAG, "Trying to load image from distribution " + path);
-
- final File f = distribution.getDistributionFile(path);
- if (f != null) {
- return new Response(new FileInputStream(f), true);
- }
- }
-
- throw new ResponseException("Couldn't find suggested site image in distribution");
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/MultiTypeCursorAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/MultiTypeCursorAdapter.java
deleted file mode 100644
index 26edf13ff..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/MultiTypeCursorAdapter.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.support.v4.widget.CursorAdapter;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * MultiTypeCursorAdapter wraps a cursor and any meta data associated with it.
- * A set of view types (corresponding to the cursor and its meta data)
- * are mapped to a set of layouts.
- */
-abstract class MultiTypeCursorAdapter extends CursorAdapter {
- private final int[] mViewTypes;
- private final int[] mLayouts;
-
- // Bind the view for the given position.
- abstract public void bindView(View view, Context context, int position);
-
- public MultiTypeCursorAdapter(Context context, Cursor cursor, int[] viewTypes, int[] layouts) {
- super(context, cursor, 0);
-
- if (viewTypes.length != layouts.length) {
- throw new IllegalStateException("The view types and the layouts should be of same size");
- }
-
- mViewTypes = viewTypes;
- mLayouts = layouts;
- }
-
- @Override
- public final int getViewTypeCount() {
- return mViewTypes.length;
- }
-
- /**
- * @return Cursor for the given position.
- */
- public final Cursor getCursor(int position) {
- final Cursor cursor = getCursor();
- if (cursor == null || !cursor.moveToPosition(position)) {
- throw new IllegalStateException("Couldn't move cursor to position " + position);
- }
-
- return cursor;
- }
-
- @Override
- public final View getView(int position, View convertView, ViewGroup parent) {
- final Context context = parent.getContext();
- if (convertView == null) {
- convertView = newView(context, position, parent);
- }
-
- bindView(convertView, context, position);
- return convertView;
- }
-
- @Override
- public final void bindView(View view, Context context, Cursor cursor) {
- // Do nothing.
- }
-
- @Override
- public boolean hasStableIds() {
- return false;
- }
-
- @Override
- public final View newView(Context context, Cursor cursor, ViewGroup parent) {
- return null;
- }
-
- /**
- * Inflate a new view from a set of view types and layouts based on the position.
- *
- * @param context Context for inflating the view.
- * @param position Position of the view.
- * @param parent Parent view group that will hold this view.
- */
- private View newView(Context context, int position, ViewGroup parent) {
- final int type = getItemViewType(position);
- final int count = mViewTypes.length;
-
- for (int i = 0; i < count; i++) {
- if (mViewTypes[i] == type) {
- return LayoutInflater.from(context).inflate(mLayouts[i], parent, false);
- }
- }
-
- return null;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthCache.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthCache.java
deleted file mode 100644
index d66919344..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthCache.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
-import android.util.Log;
-
-import org.mozilla.gecko.GeckoSharedPrefs;
-
-/**
- * Cache used to store authentication state of dynamic panels. The values
- * in this cache are set in JS through the Home.panels API.
- *
- * {@code DynamicPanel} uses this cache to determine whether or not to
- * show authentication UI for dynamic panels, including listening for
- * changes in authentication state.
- */
-class PanelAuthCache {
- private static final String LOGTAG = "GeckoPanelAuthCache";
-
- // Keep this in sync with the constant defined in Home.jsm
- private static final String PREFS_PANEL_AUTH_PREFIX = "home_panels_auth_";
-
- private final Context mContext;
- private SharedPrefsListener mSharedPrefsListener;
- private OnChangeListener mChangeListener;
-
- public interface OnChangeListener {
- public void onChange(String panelId, boolean isAuthenticated);
- }
-
- public PanelAuthCache(Context context) {
- mContext = context;
- }
-
- private SharedPreferences getSharedPreferences() {
- return GeckoSharedPrefs.forProfile(mContext);
- }
-
- private String getPanelAuthKey(String panelId) {
- return PREFS_PANEL_AUTH_PREFIX + panelId;
- }
-
- public boolean isAuthenticated(String panelId) {
- final SharedPreferences prefs = getSharedPreferences();
- return prefs.getBoolean(getPanelAuthKey(panelId), false);
- }
-
- public void setOnChangeListener(OnChangeListener listener) {
- final SharedPreferences prefs = getSharedPreferences();
-
- if (mChangeListener != null) {
- prefs.unregisterOnSharedPreferenceChangeListener(mSharedPrefsListener);
- mSharedPrefsListener = null;
- }
-
- mChangeListener = listener;
-
- if (mChangeListener != null) {
- mSharedPrefsListener = new SharedPrefsListener();
- prefs.registerOnSharedPreferenceChangeListener(mSharedPrefsListener);
- }
- }
-
- private class SharedPrefsListener implements OnSharedPreferenceChangeListener {
- @Override
- public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
- if (key.startsWith(PREFS_PANEL_AUTH_PREFIX)) {
- final String panelId = key.substring(PREFS_PANEL_AUTH_PREFIX.length());
- final boolean isAuthenticated = prefs.getBoolean(key, false);
-
- Log.d(LOGTAG, "Auth state changed: panelId=" + panelId + ", isAuthenticated=" + isAuthenticated);
- mChangeListener.onChange(panelId, isAuthenticated);
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthLayout.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthLayout.java
deleted file mode 100644
index 1ad91b7ca..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthLayout.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.home.HomeConfig.AuthConfig;
-import org.mozilla.gecko.home.HomeConfig.PanelConfig;
-
-import android.content.Context;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.squareup.picasso.Picasso;
-
-class PanelAuthLayout extends LinearLayout {
-
- public PanelAuthLayout(Context context, PanelConfig panelConfig) {
- super(context);
-
- final AuthConfig authConfig = panelConfig.getAuthConfig();
- if (authConfig == null) {
- throw new IllegalStateException("Can't create PanelAuthLayout without a valid AuthConfig");
- }
-
- setOrientation(LinearLayout.VERTICAL);
- LayoutInflater.from(context).inflate(R.layout.panel_auth_layout, this);
-
- final TextView messageView = (TextView) findViewById(R.id.message);
- messageView.setText(authConfig.getMessageText());
-
- final Button buttonView = (Button) findViewById(R.id.button);
- buttonView.setText(authConfig.getButtonText());
-
- final String panelId = panelConfig.getId();
- buttonView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- GeckoAppShell.notifyObservers("HomePanels:Authenticate", panelId);
- }
- });
-
- final ImageView imageView = (ImageView) findViewById(R.id.image);
- final String imageUrl = authConfig.getImageUrl();
-
- if (TextUtils.isEmpty(imageUrl)) {
- // Use a default image if an image URL isn't specified.
- imageView.setImageResource(R.drawable.icon_home_empty_firefox);
- } else {
- ImageLoader.with(getContext())
- .load(imageUrl)
- .into(imageView);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelBackItemView.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelBackItemView.java
deleted file mode 100644
index 4772e08ab..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/PanelBackItemView.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.home.PanelLayout.FilterDetail;
-
-import android.content.Context;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.squareup.picasso.Picasso;
-
-class PanelBackItemView extends LinearLayout {
- private final TextView title;
-
- public PanelBackItemView(Context context, String backImageUrl) {
- super(context);
-
- LayoutInflater.from(context).inflate(R.layout.panel_back_item, this);
- setOrientation(HORIZONTAL);
-
- title = (TextView) findViewById(R.id.title);
-
- final ImageView image = (ImageView) findViewById(R.id.image);
-
- if (TextUtils.isEmpty(backImageUrl)) {
- image.setImageResource(R.drawable.arrow_up);
- } else {
- ImageLoader.with(getContext())
- .load(backImageUrl)
- .placeholder(R.drawable.arrow_up)
- .into(image);
- }
- }
-
- public void updateFromFilter(FilterDetail filter) {
- final String backText = getResources()
- .getString(R.string.home_move_back_to_filter, filter.title);
- title.setText(backText);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelHeaderView.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelHeaderView.java
deleted file mode 100644
index 50c4dbc07..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/PanelHeaderView.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.mozilla.gecko.home;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.widget.ImageView;
-
-@SuppressLint("ViewConstructor") // View is only created from code
-public class PanelHeaderView extends ImageView {
- public PanelHeaderView(Context context, HomeConfig.HeaderConfig config) {
- super(context);
-
- setAdjustViewBounds(true);
-
- ImageLoader.with(context)
- .load(config.getImageUrl())
- .into(this);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- int width = MeasureSpec.getSize(widthMeasureSpec);
-
- // Always span the whole width and adjust height as needed.
- widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
-
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelInfoManager.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelInfoManager.java
deleted file mode 100644
index 089e17837..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/PanelInfoManager.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.home.HomeConfig.PanelConfig;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.util.Log;
-import android.util.SparseArray;
-
-public class PanelInfoManager implements GeckoEventListener {
- private static final String LOGTAG = "GeckoPanelInfoManager";
-
- public class PanelInfo {
- private final String mId;
- private final String mTitle;
- private final JSONObject mJSONData;
-
- public PanelInfo(String id, String title, JSONObject jsonData) {
- mId = id;
- mTitle = title;
- mJSONData = jsonData;
- }
-
- public String getId() {
- return mId;
- }
-
- public String getTitle() {
- return mTitle;
- }
-
- public PanelConfig toPanelConfig() {
- try {
- return new PanelConfig(mJSONData);
- } catch (Exception e) {
- Log.e(LOGTAG, "Failed to convert PanelInfo to PanelConfig", e);
- return null;
- }
- }
- }
-
- public interface RequestCallback {
- public void onComplete(List<PanelInfo> panelInfos);
- }
-
- private static final AtomicInteger sRequestId = new AtomicInteger(0);
-
- // Stores set of pending request callbacks.
- private static final SparseArray<RequestCallback> sCallbacks = new SparseArray<RequestCallback>();
-
- /**
- * Asynchronously fetches list of available panels from Gecko
- * for the given IDs.
- *
- * @param ids list of panel ids to be fetched. A null value will fetch all
- * available panels.
- * @param callback onComplete will be called on the UI thread.
- */
- public void requestPanelsById(Set<String> ids, RequestCallback callback) {
- final int requestId = sRequestId.getAndIncrement();
-
- synchronized (sCallbacks) {
- // If there are no pending callbacks, register the event listener.
- if (sCallbacks.size() == 0) {
- GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
- "HomePanels:Data");
- }
- sCallbacks.put(requestId, callback);
- }
-
- final JSONObject message = new JSONObject();
- try {
- message.put("requestId", requestId);
-
- if (ids != null && ids.size() > 0) {
- JSONArray idsArray = new JSONArray();
- for (String id : ids) {
- idsArray.put(id);
- }
-
- message.put("ids", idsArray);
- }
- } catch (JSONException e) {
- Log.e(LOGTAG, "Failed to build event to request panels by id", e);
- return;
- }
-
- GeckoAppShell.notifyObservers("HomePanels:Get", message.toString());
- }
-
- /**
- * Asynchronously fetches list of available panels from Gecko.
- *
- * @param callback onComplete will be called on the UI thread.
- */
- public void requestAvailablePanels(RequestCallback callback) {
- requestPanelsById(null, callback);
- }
-
- /**
- * Handles "HomePanels:Data" events.
- */
- @Override
- public void handleMessage(String event, JSONObject message) {
- final ArrayList<PanelInfo> panelInfos = new ArrayList<PanelInfo>();
-
- try {
- final JSONArray panels = message.getJSONArray("panels");
- final int count = panels.length();
- for (int i = 0; i < count; i++) {
- final PanelInfo panelInfo = getPanelInfoFromJSON(panels.getJSONObject(i));
- panelInfos.add(panelInfo);
- }
-
- final RequestCallback callback;
- final int requestId = message.getInt("requestId");
-
- synchronized (sCallbacks) {
- callback = sCallbacks.get(requestId);
- sCallbacks.delete(requestId);
-
- // Unregister the event listener if there are no more pending callbacks.
- if (sCallbacks.size() == 0) {
- GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
- "HomePanels:Data");
- }
- }
-
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- callback.onComplete(panelInfos);
- }
- });
- } catch (JSONException e) {
- Log.e(LOGTAG, "Exception handling " + event + " message", e);
- }
- }
-
- private PanelInfo getPanelInfoFromJSON(JSONObject jsonPanelInfo) throws JSONException {
- final String id = jsonPanelInfo.getString("id");
- final String title = jsonPanelInfo.getString("title");
-
- return new PanelInfo(id, title, jsonPanelInfo);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelItemView.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelItemView.java
deleted file mode 100644
index 2a97d42bc..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/PanelItemView.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.BrowserContract.HomeItems;
-import org.mozilla.gecko.home.HomeConfig.ItemType;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.graphics.Color;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-class PanelItemView extends LinearLayout {
- private final TextView titleView;
- private final TextView descriptionView;
- private final ImageView imageView;
- private final LinearLayout titleDescContainerView;
- private final ImageView backgroundView;
-
- private PanelItemView(Context context, int layoutId) {
- super(context);
-
- LayoutInflater.from(context).inflate(layoutId, this);
- titleView = (TextView) findViewById(R.id.title);
- descriptionView = (TextView) findViewById(R.id.description);
- imageView = (ImageView) findViewById(R.id.image);
- backgroundView = (ImageView) findViewById(R.id.background);
- titleDescContainerView = (LinearLayout) findViewById(R.id.title_desc_container);
- }
-
- public void updateFromCursor(Cursor cursor) {
- int titleIndex = cursor.getColumnIndexOrThrow(HomeItems.TITLE);
- final String titleText = cursor.getString(titleIndex);
-
- // Only show title if the item has one
- final boolean hasTitle = !TextUtils.isEmpty(titleText);
- titleView.setVisibility(hasTitle ? View.VISIBLE : View.GONE);
- if (hasTitle) {
- titleView.setText(titleText);
- }
-
- int descriptionIndex = cursor.getColumnIndexOrThrow(HomeItems.DESCRIPTION);
- final String descriptionText = cursor.getString(descriptionIndex);
-
- // Only show description if the item has one
- // Descriptions are not supported for IconItemView objects (Bug 1157539)
- final boolean hasDescription = !TextUtils.isEmpty(descriptionText);
- if (descriptionView != null) {
- descriptionView.setVisibility(hasDescription ? View.VISIBLE : View.GONE);
- if (hasDescription) {
- descriptionView.setText(descriptionText);
- }
- }
- if (titleDescContainerView != null) {
- titleDescContainerView.setVisibility(hasTitle || hasDescription ? View.VISIBLE : View.GONE);
- }
-
- int imageIndex = cursor.getColumnIndexOrThrow(HomeItems.IMAGE_URL);
- final String imageUrl = cursor.getString(imageIndex);
-
- // Only try to load the image if the item has define image URL
- final boolean hasImageUrl = !TextUtils.isEmpty(imageUrl);
- imageView.setVisibility(hasImageUrl ? View.VISIBLE : View.GONE);
-
- if (hasImageUrl) {
- ImageLoader.with(getContext())
- .load(imageUrl)
- .into(imageView);
- }
-
- final int columnIndexBackgroundColor = cursor.getColumnIndex(HomeItems.BACKGROUND_COLOR);
- if (columnIndexBackgroundColor != -1) {
- final String color = cursor.getString(columnIndexBackgroundColor);
- if (!TextUtils.isEmpty(color)) {
- setBackgroundColor(Color.parseColor(color));
- }
- }
-
- // Backgrounds are only supported for IconItemView objects (Bug 1157539)
- final int columnIndexBackgroundUrl = cursor.getColumnIndex(HomeItems.BACKGROUND_URL);
- if (columnIndexBackgroundUrl != -1) {
- final String backgroundUrl = cursor.getString(columnIndexBackgroundUrl);
- if (backgroundView != null && !TextUtils.isEmpty(backgroundUrl)) {
- ImageLoader.with(getContext())
- .load(backgroundUrl)
- .fit()
- .into(backgroundView);
- }
- }
- }
-
- private static class ArticleItemView extends PanelItemView {
- private ArticleItemView(Context context) {
- super(context, R.layout.panel_article_item);
- setOrientation(LinearLayout.HORIZONTAL);
- }
- }
-
- private static class ImageItemView extends PanelItemView {
- private ImageItemView(Context context) {
- super(context, R.layout.panel_image_item);
- setOrientation(LinearLayout.VERTICAL);
- }
- }
-
- private static class IconItemView extends PanelItemView {
- private IconItemView(Context context) {
- super(context, R.layout.panel_icon_item);
- }
- }
-
- public static PanelItemView create(Context context, ItemType itemType) {
- switch (itemType) {
- case ARTICLE:
- return new ArticleItemView(context);
-
- case IMAGE:
- return new ImageItemView(context);
-
- case ICON:
- return new IconItemView(context);
-
- default:
- throw new IllegalArgumentException("Could not create panel item view from " + itemType);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelLayout.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelLayout.java
deleted file mode 100644
index 2c2d89ae0..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/PanelLayout.java
+++ /dev/null
@@ -1,747 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.BrowserContract.HomeItems;
-import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
-import org.mozilla.gecko.home.HomeConfig.EmptyViewConfig;
-import org.mozilla.gecko.home.HomeConfig.ItemHandler;
-import org.mozilla.gecko.home.HomeConfig.PanelConfig;
-import org.mozilla.gecko.home.HomeConfig.ViewConfig;
-import org.mozilla.gecko.util.StringUtils;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.text.TextUtils;
-import android.util.Log;
-import android.util.SparseArray;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import java.lang.ref.SoftReference;
-import java.util.EnumSet;
-import java.util.LinkedList;
-import java.util.Map;
-import java.util.WeakHashMap;
-
-import com.squareup.picasso.Picasso;
-
-/**
- * {@code PanelLayout} is the base class for custom layouts to be
- * used in {@code DynamicPanel}. It provides the basic framework
- * that enables custom layouts to request and reset datasets and
- * create panel views. Furthermore, it automates most of the process
- * of binding panel views with their respective datasets.
- *
- * {@code PanelLayout} abstracts the implemention details of how
- * datasets are actually loaded through the {@DatasetHandler} interface.
- * {@code DatasetHandler} provides two operations: request and reset.
- * The results of the dataset requests done via the {@code DatasetHandler}
- * are delivered to the {@code PanelLayout} with the {@code deliverDataset()}
- * method.
- *
- * Subclasses of {@code PanelLayout} should simply use the utilities
- * provided by {@code PanelLayout}. Namely:
- *
- * {@code requestDataset()} - To fetch datasets and auto-bind them to
- * the existing panel views backed by them.
- *
- * {@code resetDataset()} - To release any resources associated with a
- * previously loaded dataset.
- *
- * {@code createPanelView()} - To create a panel view for a ViewConfig
- * associated with the panel.
- *
- * {@code disposePanelView()} - To dispose any dataset references associated
- * with the given view.
- *
- * {@code PanelLayout} subclasses should always use {@code createPanelView()}
- * to create the views dynamically created based on {@code ViewConfig}. This
- * allows {@code PanelLayout} to auto-bind datasets with panel views.
- * {@code PanelLayout} subclasses are free to have any type of views to arrange
- * the panel views in different ways.
- */
-abstract class PanelLayout extends FrameLayout {
- private static final String LOGTAG = "GeckoPanelLayout";
-
- protected final SparseArray<ViewState> mViewStates;
- private final PanelConfig mPanelConfig;
- private final DatasetHandler mDatasetHandler;
- private final OnUrlOpenListener mUrlOpenListener;
- private final ContextMenuRegistry mContextMenuRegistry;
-
- /**
- * To be used by panel views to express that they are
- * backed by datasets.
- */
- public interface DatasetBacked {
- public void setDataset(Cursor cursor);
- public void setFilterManager(FilterManager manager);
- }
-
- /**
- * To be used by requests made to {@code DatasetHandler}s to couple dataset ID with current
- * filter for queries on the database.
- */
- public static class DatasetRequest implements Parcelable {
- public enum Type implements Parcelable {
- DATASET_LOAD,
- FILTER_PUSH,
- FILTER_POP;
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(ordinal());
- }
-
- public static final Creator<Type> CREATOR = new Creator<Type>() {
- @Override
- public Type createFromParcel(final Parcel source) {
- return Type.values()[source.readInt()];
- }
-
- @Override
- public Type[] newArray(final int size) {
- return new Type[size];
- }
- };
- }
-
- private final int mViewIndex;
- private final Type mType;
- private final String mDatasetId;
- private final FilterDetail mFilterDetail;
-
- private DatasetRequest(Parcel in) {
- this.mViewIndex = in.readInt();
- this.mType = (Type) in.readParcelable(getClass().getClassLoader());
- this.mDatasetId = in.readString();
- this.mFilterDetail = (FilterDetail) in.readParcelable(getClass().getClassLoader());
- }
-
- public DatasetRequest(int index, String datasetId, FilterDetail filterDetail) {
- this(index, Type.DATASET_LOAD, datasetId, filterDetail);
- }
-
- public DatasetRequest(int index, Type type, String datasetId, FilterDetail filterDetail) {
- this.mViewIndex = index;
- this.mType = type;
- this.mDatasetId = datasetId;
- this.mFilterDetail = filterDetail;
- }
-
- public int getViewIndex() {
- return mViewIndex;
- }
-
- public Type getType() {
- return mType;
- }
-
- public String getDatasetId() {
- return mDatasetId;
- }
-
- public String getFilter() {
- return (mFilterDetail != null ? mFilterDetail.filter : null);
- }
-
- public FilterDetail getFilterDetail() {
- return mFilterDetail;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(mViewIndex);
- dest.writeParcelable(mType, 0);
- dest.writeString(mDatasetId);
- dest.writeParcelable(mFilterDetail, 0);
- }
-
- public String toString() {
- return "{ index: " + mViewIndex +
- ", type: " + mType +
- ", dataset: " + mDatasetId +
- ", filter: " + mFilterDetail +
- " }";
- }
-
- public static final Creator<DatasetRequest> CREATOR = new Creator<DatasetRequest>() {
- @Override
- public DatasetRequest createFromParcel(Parcel in) {
- return new DatasetRequest(in);
- }
-
- @Override
- public DatasetRequest[] newArray(int size) {
- return new DatasetRequest[size];
- }
- };
- }
-
- /**
- * Defines the contract with the component that is responsible
- * for handling datasets requests.
- */
- public interface DatasetHandler {
- /**
- * Requests a dataset to be fetched and auto-bound to the
- * panel views backed by it.
- */
- public void requestDataset(DatasetRequest request);
-
- /**
- * Releases any resources associated with a panel view. It will
- * do nothing if the view with the given index been created
- * before.
- */
- public void resetDataset(int viewIndex);
- }
-
- public interface PanelView {
- public void setOnItemOpenListener(OnItemOpenListener listener);
- public void setOnKeyListener(OnKeyListener listener);
- public void setContextMenuInfoFactory(HomeContextMenuInfo.Factory factory);
- }
-
- public interface FilterManager {
- public FilterDetail getPreviousFilter();
- public boolean canGoBack();
- public void goBack();
- }
-
- public interface ContextMenuRegistry {
- public void register(View view);
- }
-
- public PanelLayout(Context context, PanelConfig panelConfig, DatasetHandler datasetHandler,
- OnUrlOpenListener urlOpenListener, ContextMenuRegistry contextMenuRegistry) {
- super(context);
- mViewStates = new SparseArray<ViewState>();
- mPanelConfig = panelConfig;
- mDatasetHandler = datasetHandler;
- mUrlOpenListener = urlOpenListener;
- mContextMenuRegistry = contextMenuRegistry;
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- final int count = mViewStates.size();
- for (int i = 0; i < count; i++) {
- final ViewState viewState = mViewStates.valueAt(i);
-
- final View view = viewState.getView();
- if (view != null) {
- maybeSetDataset(view, null);
- }
- }
- mViewStates.clear();
- }
-
- /**
- * Delivers the dataset as a {@code Cursor} to be bound to the
- * panel view backed by it. This is used by the {@code DatasetHandler}
- * in response to a dataset request.
- */
- public final void deliverDataset(DatasetRequest request, Cursor cursor) {
- Log.d(LOGTAG, "Delivering request: " + request);
- final ViewState viewState = mViewStates.get(request.getViewIndex());
- if (viewState == null) {
- return;
- }
-
- switch (request.getType()) {
- case FILTER_PUSH:
- viewState.pushFilter(request.getFilterDetail());
- break;
- case FILTER_POP:
- viewState.popFilter();
- break;
- }
-
- final View activeView = viewState.getActiveView();
- if (activeView == null) {
- throw new IllegalStateException("No active view for view state: " + viewState.getIndex());
- }
-
- final ViewConfig viewConfig = viewState.getViewConfig();
-
- final View newView;
- if (cursor == null || cursor.getCount() == 0) {
- newView = createEmptyView(viewConfig);
- maybeSetDataset(activeView, null);
- } else {
- newView = createPanelView(viewConfig);
- maybeSetDataset(newView, cursor);
- }
-
- if (activeView != newView) {
- replacePanelView(activeView, newView);
- }
- }
-
- /**
- * Releases any references to the given dataset from all
- * existing panel views.
- */
- public final void releaseDataset(int viewIndex) {
- Log.d(LOGTAG, "Releasing dataset: " + viewIndex);
- final ViewState viewState = mViewStates.get(viewIndex);
- if (viewState == null) {
- return;
- }
-
- final View view = viewState.getView();
- if (view != null) {
- maybeSetDataset(view, null);
- }
- }
-
- /**
- * Requests a dataset to be loaded and bound to any existing
- * panel view backed by it.
- */
- protected final void requestDataset(DatasetRequest request) {
- Log.d(LOGTAG, "Requesting request: " + request);
- if (mViewStates.get(request.getViewIndex()) == null) {
- return;
- }
-
- mDatasetHandler.requestDataset(request);
- }
-
- /**
- * Releases any resources associated with a panel view.
- * e.g. close any associated {@code Cursor}.
- */
- protected final void resetDataset(int viewIndex) {
- Log.d(LOGTAG, "Resetting view with index: " + viewIndex);
- if (mViewStates.get(viewIndex) == null) {
- return;
- }
-
- mDatasetHandler.resetDataset(viewIndex);
- }
-
- /**
- * Factory method to create instance of panels from a given
- * {@code ViewConfig}. All panel views defined in {@code PanelConfig}
- * should be created using this method so that {@PanelLayout} can
- * keep track of panel views and their associated datasets.
- */
- protected final View createPanelView(ViewConfig viewConfig) {
- Log.d(LOGTAG, "Creating panel view: " + viewConfig.getType());
-
- ViewState viewState = mViewStates.get(viewConfig.getIndex());
- if (viewState == null) {
- viewState = new ViewState(viewConfig);
- mViewStates.put(viewConfig.getIndex(), viewState);
- }
-
- View view = viewState.getView();
- if (view == null) {
- switch (viewConfig.getType()) {
- case LIST:
- view = new PanelListView(getContext(), viewConfig);
- break;
-
- case GRID:
- view = new PanelRecyclerView(getContext(), viewConfig);
- break;
-
- default:
- throw new IllegalStateException("Unrecognized view type in " + getClass().getSimpleName());
- }
-
- PanelView panelView = (PanelView) view;
- panelView.setOnItemOpenListener(new PanelOnItemOpenListener(viewState));
- panelView.setOnKeyListener(new PanelKeyListener(viewState));
- panelView.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() {
- @Override
- public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
- final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
- info.url = cursor.getString(cursor.getColumnIndexOrThrow(HomeItems.URL));
- info.title = cursor.getString(cursor.getColumnIndexOrThrow(HomeItems.TITLE));
- return info;
- }
- });
-
- mContextMenuRegistry.register(view);
-
- if (view instanceof DatasetBacked) {
- DatasetBacked datasetBacked = (DatasetBacked) view;
- datasetBacked.setFilterManager(new PanelFilterManager(viewState));
-
- if (viewConfig.isRefreshEnabled()) {
- view = new PanelRefreshLayout(getContext(), view,
- mPanelConfig.getId(), viewConfig.getIndex());
- }
- }
-
- viewState.setView(view);
- }
-
- return view;
- }
-
- /**
- * Dispose any dataset references associated with the
- * given view.
- */
- protected final void disposePanelView(View view) {
- Log.d(LOGTAG, "Disposing panel view");
- final int count = mViewStates.size();
- for (int i = 0; i < count; i++) {
- final ViewState viewState = mViewStates.valueAt(i);
-
- if (viewState.getView() == view) {
- maybeSetDataset(view, null);
- mViewStates.remove(viewState.getIndex());
- break;
- }
- }
- }
-
- private void maybeSetDataset(View view, Cursor cursor) {
- if (view instanceof DatasetBacked) {
- final DatasetBacked dsb = (DatasetBacked) view;
- dsb.setDataset(cursor);
- }
- }
-
- private View createEmptyView(ViewConfig viewConfig) {
- Log.d(LOGTAG, "Creating empty view: " + viewConfig.getType());
-
- ViewState viewState = mViewStates.get(viewConfig.getIndex());
- if (viewState == null) {
- throw new IllegalStateException("No view state found for view index: " + viewConfig.getIndex());
- }
-
- View view = viewState.getEmptyView();
- if (view == null) {
- view = LayoutInflater.from(getContext()).inflate(R.layout.home_empty_panel, null);
-
- final EmptyViewConfig emptyViewConfig = viewConfig.getEmptyViewConfig();
-
- // XXX: Refactor this into a custom view (bug 985134)
- final String text = (emptyViewConfig == null) ? null : emptyViewConfig.getText();
- final TextView textView = (TextView) view.findViewById(R.id.home_empty_text);
- if (TextUtils.isEmpty(text)) {
- textView.setText(R.string.home_default_empty);
- } else {
- textView.setText(text);
- }
-
- final String imageUrl = (emptyViewConfig == null) ? null : emptyViewConfig.getImageUrl();
- final ImageView imageView = (ImageView) view.findViewById(R.id.home_empty_image);
-
- if (TextUtils.isEmpty(imageUrl)) {
- imageView.setImageResource(R.drawable.icon_home_empty_firefox);
- } else {
- ImageLoader.with(getContext())
- .load(imageUrl)
- .error(R.drawable.icon_home_empty_firefox)
- .into(imageView);
- }
-
- viewState.setEmptyView(view);
- }
-
- return view;
- }
-
- private void replacePanelView(View currentView, View newView) {
- final ViewGroup parent = (ViewGroup) currentView.getParent();
- parent.addView(newView, parent.indexOfChild(currentView), currentView.getLayoutParams());
- parent.removeView(currentView);
- }
-
- /**
- * Must be implemented by {@code PanelLayout} subclasses to define
- * what happens then the layout is first loaded. Should set initial
- * UI state and request any necessary datasets.
- */
- public abstract void load();
-
- /**
- * Represents a 'live' instance of a panel view associated with
- * the {@code PanelLayout}. Is responsible for tracking the history stack of filters.
- */
- protected class ViewState {
- private final ViewConfig mViewConfig;
- private SoftReference<View> mView;
- private SoftReference<View> mEmptyView;
- private LinkedList<FilterDetail> mFilterStack;
-
- public ViewState(ViewConfig viewConfig) {
- mViewConfig = viewConfig;
- mView = new SoftReference<View>(null);
- mEmptyView = new SoftReference<View>(null);
- }
-
- public ViewConfig getViewConfig() {
- return mViewConfig;
- }
-
- public int getIndex() {
- return mViewConfig.getIndex();
- }
-
- public View getView() {
- return mView.get();
- }
-
- public void setView(View view) {
- mView = new SoftReference<View>(view);
- }
-
- public View getEmptyView() {
- return mEmptyView.get();
- }
-
- public void setEmptyView(View view) {
- mEmptyView = new SoftReference<View>(view);
- }
-
- public View getActiveView() {
- final View view = getView();
- if (view != null && view.getParent() != null) {
- return view;
- }
-
- final View emptyView = getEmptyView();
- if (emptyView != null && emptyView.getParent() != null) {
- return emptyView;
- }
-
- return null;
- }
-
- public String getDatasetId() {
- return mViewConfig.getDatasetId();
- }
-
- public ItemHandler getItemHandler() {
- return mViewConfig.getItemHandler();
- }
-
- /**
- * Get the current filter that this view is displaying, or null if none.
- */
- public FilterDetail getCurrentFilter() {
- if (mFilterStack == null) {
- return null;
- } else {
- return mFilterStack.peek();
- }
- }
-
- /**
- * Get the previous filter that this view was displaying, or null if none.
- */
- public FilterDetail getPreviousFilter() {
- if (!canPopFilter()) {
- return null;
- }
-
- return mFilterStack.get(1);
- }
-
- /**
- * Adds a filter to the history stack for this view.
- */
- public void pushFilter(FilterDetail filter) {
- if (mFilterStack == null) {
- mFilterStack = new LinkedList<FilterDetail>();
-
- // Initialize with the initial filter.
- mFilterStack.push(new FilterDetail(mViewConfig.getFilter(),
- mPanelConfig.getTitle()));
- }
-
- mFilterStack.push(filter);
- }
-
- /**
- * Remove the most recent filter from the stack.
- *
- * @return whether the filter was popped
- */
- public boolean popFilter() {
- if (!canPopFilter()) {
- return false;
- }
-
- mFilterStack.pop();
- return true;
- }
-
- public boolean canPopFilter() {
- return (mFilterStack != null && mFilterStack.size() > 1);
- }
- }
-
- static class FilterDetail implements Parcelable {
- final String filter;
- final String title;
-
- private FilterDetail(Parcel in) {
- this.filter = in.readString();
- this.title = in.readString();
- }
-
- public FilterDetail(String filter, String title) {
- this.filter = filter;
- this.title = title;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeString(filter);
- dest.writeString(title);
- }
-
- public static final Creator<FilterDetail> CREATOR = new Creator<FilterDetail>() {
- @Override
- public FilterDetail createFromParcel(Parcel in) {
- return new FilterDetail(in);
- }
-
- @Override
- public FilterDetail[] newArray(int size) {
- return new FilterDetail[size];
- }
- };
- }
-
- /**
- * Pushes filter to {@code ViewState}'s stack and makes request for new filter value.
- */
- private void pushFilterOnView(ViewState viewState, FilterDetail filterDetail) {
- final int index = viewState.getIndex();
- final String datasetId = viewState.getDatasetId();
-
- mDatasetHandler.requestDataset(new DatasetRequest(index,
- DatasetRequest.Type.FILTER_PUSH,
- datasetId,
- filterDetail));
- }
-
- /**
- * Pops filter from {@code ViewState}'s stack and makes request for previous filter value.
- *
- * @return whether the filter has changed
- */
- private boolean popFilterOnView(ViewState viewState) {
- if (viewState.canPopFilter()) {
- final int index = viewState.getIndex();
- final String datasetId = viewState.getDatasetId();
- final FilterDetail filterDetail = viewState.getPreviousFilter();
-
- mDatasetHandler.requestDataset(new DatasetRequest(index,
- DatasetRequest.Type.FILTER_POP,
- datasetId,
- filterDetail));
-
- return true;
- } else {
- return false;
- }
- }
-
- public interface OnItemOpenListener {
- public void onItemOpen(String url, String title);
- }
-
- private class PanelOnItemOpenListener implements OnItemOpenListener {
- private final ViewState mViewState;
-
- public PanelOnItemOpenListener(ViewState viewState) {
- mViewState = viewState;
- }
-
- @Override
- public void onItemOpen(String url, String title) {
- if (StringUtils.isFilterUrl(url)) {
- FilterDetail filterDetail = new FilterDetail(StringUtils.getFilterFromUrl(url), title);
- pushFilterOnView(mViewState, filterDetail);
- } else {
- EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class);
- if (mViewState.getItemHandler() == ItemHandler.INTENT) {
- flags.add(OnUrlOpenListener.Flags.OPEN_WITH_INTENT);
- }
-
- mUrlOpenListener.onUrlOpen(url, flags);
- }
- }
- }
-
- private class PanelKeyListener implements View.OnKeyListener {
- private final ViewState mViewState;
-
- public PanelKeyListener(ViewState viewState) {
- mViewState = viewState;
- }
-
- @Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
- return popFilterOnView(mViewState);
- }
-
- return false;
- }
- }
-
- private class PanelFilterManager implements FilterManager {
- private final ViewState mViewState;
-
- public PanelFilterManager(ViewState viewState) {
- mViewState = viewState;
- }
-
- @Override
- public FilterDetail getPreviousFilter() {
- return mViewState.getPreviousFilter();
- }
-
- @Override
- public boolean canGoBack() {
- return mViewState.canPopFilter();
- }
-
- @Override
- public void goBack() {
- popFilterOnView(mViewState);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelListView.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelListView.java
deleted file mode 100644
index 505fb9b0d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/PanelListView.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import java.util.EnumSet;
-
-import org.mozilla.gecko.db.BrowserContract.HomeItems;
-import org.mozilla.gecko.home.HomeConfig.ItemHandler;
-import org.mozilla.gecko.home.HomeConfig.ViewConfig;
-import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
-import org.mozilla.gecko.home.PanelLayout.DatasetBacked;
-import org.mozilla.gecko.home.PanelLayout.FilterManager;
-import org.mozilla.gecko.home.PanelLayout.OnItemOpenListener;
-import org.mozilla.gecko.home.PanelLayout.PanelView;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.util.Log;
-import android.view.View;
-import android.widget.AdapterView;
-
-public class PanelListView extends HomeListView
- implements DatasetBacked, PanelView {
-
- private static final String LOGTAG = "GeckoPanelListView";
-
- private final ViewConfig viewConfig;
- private final PanelViewAdapter adapter;
- private final PanelViewItemHandler itemHandler;
- private OnItemOpenListener itemOpenListener;
-
- public PanelListView(Context context, ViewConfig viewConfig) {
- super(context);
-
- this.viewConfig = viewConfig;
- itemHandler = new PanelViewItemHandler();
-
- adapter = new PanelViewAdapter(context, viewConfig);
- setAdapter(adapter);
-
- setOnItemClickListener(new PanelListItemClickListener());
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
- itemHandler.setOnItemOpenListener(itemOpenListener);
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- itemHandler.setOnItemOpenListener(null);
- }
-
- @Override
- public void setDataset(Cursor cursor) {
- Log.d(LOGTAG, "Setting dataset: " + viewConfig.getDatasetId());
- adapter.swapCursor(cursor);
- }
-
- @Override
- public void setOnItemOpenListener(OnItemOpenListener listener) {
- itemHandler.setOnItemOpenListener(listener);
- itemOpenListener = listener;
- }
-
- @Override
- public void setFilterManager(FilterManager filterManager) {
- adapter.setFilterManager(filterManager);
- itemHandler.setFilterManager(filterManager);
- }
-
- private class PanelListItemClickListener implements AdapterView.OnItemClickListener {
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- itemHandler.openItemAtPosition(adapter.getCursor(), position);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerView.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerView.java
deleted file mode 100644
index 9145ab1e1..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerView.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.home.PanelLayout.DatasetBacked;
-import org.mozilla.gecko.home.PanelLayout.PanelView;
-import org.mozilla.gecko.widget.RecyclerViewClickSupport;
-import org.mozilla.gecko.widget.RecyclerViewClickSupport.OnItemClickListener;
-import org.mozilla.gecko.widget.RecyclerViewClickSupport.OnItemLongClickListener;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.support.v7.widget.GridLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.view.View;
-
-/**
- * RecyclerView implementation for grid home panels.
- */
-@SuppressLint("ViewConstructor") // View is only created from code
-public class PanelRecyclerView extends RecyclerView
- implements DatasetBacked, PanelView, OnItemClickListener, OnItemLongClickListener {
- private final PanelRecyclerViewAdapter adapter;
- private final GridLayoutManager layoutManager;
- private final PanelViewItemHandler itemHandler;
- private final float columnWidth;
- private final boolean autoFit;
- private final HomeConfig.ViewConfig viewConfig;
-
- private PanelLayout.OnItemOpenListener itemOpenListener;
- private HomeContextMenuInfo contextMenuInfo;
- private HomeContextMenuInfo.Factory contextMenuInfoFactory;
-
- public PanelRecyclerView(Context context, HomeConfig.ViewConfig viewConfig) {
- super(context);
-
- this.viewConfig = viewConfig;
-
- final Resources resources = context.getResources();
-
- int spanCount;
- if (viewConfig.getItemType() == HomeConfig.ItemType.ICON) {
- autoFit = false;
- spanCount = getResources().getInteger(R.integer.panel_icon_grid_view_columns);
- } else {
- autoFit = true;
- spanCount = 1;
- }
-
- columnWidth = resources.getDimension(R.dimen.panel_grid_view_column_width);
- layoutManager = new GridLayoutManager(context, spanCount);
- adapter = new PanelRecyclerViewAdapter(context, viewConfig);
- itemHandler = new PanelViewItemHandler();
-
- layoutManager.setSpanSizeLookup(new PanelSpanSizeLookup());
-
- setLayoutManager(layoutManager);
- setAdapter(adapter);
-
- int horizontalSpacing = (int) resources.getDimension(R.dimen.panel_grid_view_horizontal_spacing);
- int verticalSpacing = (int) resources.getDimension(R.dimen.panel_grid_view_vertical_spacing);
- int outerSpacing = (int) resources.getDimension(R.dimen.panel_grid_view_outer_spacing);
-
- addItemDecoration(new SpacingDecoration(horizontalSpacing, verticalSpacing));
-
- setPadding(outerSpacing, outerSpacing, outerSpacing, outerSpacing);
- setClipToPadding(false);
-
- RecyclerViewClickSupport.addTo(this)
- .setOnItemClickListener(this)
- .setOnItemLongClickListener(this);
- }
-
- @Override
- protected void onMeasure(int widthSpec, int heightSpec) {
- super.onMeasure(widthSpec, heightSpec);
-
- if (autoFit) {
- // Adjust span based on space available (What GridView does when you say numColumns="auto_fit")
- final int spanCount = (int) Math.max(1, getMeasuredWidth() / columnWidth);
- layoutManager.setSpanCount(spanCount);
- }
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
- itemHandler.setOnItemOpenListener(itemOpenListener);
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- itemHandler.setOnItemOpenListener(null);
- }
-
- @Override
- public void setDataset(Cursor cursor) {
- adapter.swapCursor(cursor);
- }
-
- @Override
- public void setFilterManager(PanelLayout.FilterManager manager) {
- adapter.setFilterManager(manager);
- itemHandler.setFilterManager(manager);
- }
-
- @Override
- public void setOnItemOpenListener(PanelLayout.OnItemOpenListener listener) {
- itemOpenListener = listener;
- itemHandler.setOnItemOpenListener(listener);
- }
-
- @Override
- public HomeContextMenuInfo getContextMenuInfo() {
- return contextMenuInfo;
- }
-
- @Override
- public void setContextMenuInfoFactory(HomeContextMenuInfo.Factory factory) {
- contextMenuInfoFactory = factory;
- }
-
- @Override
- public void onItemClicked(RecyclerView recyclerView, int position, View v) {
- if (viewConfig.hasHeaderConfig()) {
- if (position == 0) {
- itemOpenListener.onItemOpen(viewConfig.getHeaderConfig().getUrl(), null);
- return;
- }
-
- position--;
- }
-
- itemHandler.openItemAtPosition(adapter.getCursor(), position);
- }
-
- @Override
- public boolean onItemLongClicked(RecyclerView recyclerView, int position, View v) {
- if (viewConfig.hasHeaderConfig()) {
- if (position == 0) {
- final HomeConfig.HeaderConfig headerConfig = viewConfig.getHeaderConfig();
-
- final HomeContextMenuInfo info = new HomeContextMenuInfo(v, position, -1);
- info.url = headerConfig.getUrl();
- info.title = headerConfig.getUrl();
-
- contextMenuInfo = info;
- return showContextMenuForChild(this);
- }
-
- position--;
- }
-
- Cursor cursor = adapter.getCursor();
- cursor.moveToPosition(position);
-
- contextMenuInfo = contextMenuInfoFactory.makeInfoForCursor(recyclerView, position, -1, cursor);
- return showContextMenuForChild(this);
- }
-
- private class PanelSpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
- @Override
- public int getSpanSize(int position) {
- if (position == 0 && viewConfig.hasHeaderConfig()) {
- return layoutManager.getSpanCount();
- }
-
- return 1;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerViewAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerViewAdapter.java
deleted file mode 100644
index fa632bccd..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerViewAdapter.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.support.v7.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-
-public class PanelRecyclerViewAdapter extends RecyclerView.Adapter<PanelRecyclerViewAdapter.PanelViewHolder> {
- private static final int VIEW_TYPE_ITEM = 0;
- private static final int VIEW_TYPE_BACK = 1;
- private static final int VIEW_TYPE_HEADER = 2;
-
- public static class PanelViewHolder extends RecyclerView.ViewHolder {
- public static PanelViewHolder create(View itemView) {
-
- // Wrap in a FrameLayout that will handle the highlight on touch
- FrameLayout frameLayout = (FrameLayout) LayoutInflater.from(itemView.getContext())
- .inflate(R.layout.panel_item_container, null);
-
- frameLayout.addView(itemView, 0, new FrameLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
-
- return new PanelViewHolder(frameLayout);
- }
-
- private PanelViewHolder(View itemView) {
- super(itemView);
- }
- }
-
- private final Context context;
- private final HomeConfig.ViewConfig viewConfig;
- private PanelLayout.FilterManager filterManager;
- private Cursor cursor;
-
- public PanelRecyclerViewAdapter(Context context, HomeConfig.ViewConfig viewConfig) {
- this.context = context;
- this.viewConfig = viewConfig;
- }
-
- public void setFilterManager(PanelLayout.FilterManager filterManager) {
- this.filterManager = filterManager;
- }
-
- private boolean isShowingBack() {
- return filterManager != null && filterManager.canGoBack();
- }
-
- public void swapCursor(Cursor cursor) {
- this.cursor = cursor;
-
- notifyDataSetChanged();
- }
-
- public Cursor getCursor() {
- return cursor;
- }
-
- @Override
- public int getItemViewType(int position) {
- if (viewConfig.hasHeaderConfig() && position == 0) {
- return VIEW_TYPE_HEADER;
- } else if (isShowingBack() && position == getBackPosition()) {
- return VIEW_TYPE_BACK;
- } else {
- return VIEW_TYPE_ITEM;
- }
- }
-
- @Override
- public PanelViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
- switch (viewType) {
- case VIEW_TYPE_HEADER:
- return PanelViewHolder.create(new PanelHeaderView(context, viewConfig.getHeaderConfig()));
- case VIEW_TYPE_BACK:
- return PanelViewHolder.create(new PanelBackItemView(context, viewConfig.getBackImageUrl()));
- case VIEW_TYPE_ITEM:
- return PanelViewHolder.create(PanelItemView.create(context, viewConfig.getItemType()));
- default:
- throw new IllegalArgumentException("Unknown view type: " + viewType);
- }
- }
-
- @Override
- public void onBindViewHolder(PanelViewHolder panelViewHolder, int position) {
- final View view = ((FrameLayout) panelViewHolder.itemView).getChildAt(0);
-
- if (viewConfig.hasHeaderConfig()) {
- if (position == 0) {
- // Nothing to do here, the header is static
- return;
- }
- }
-
- if (isShowingBack()) {
- if (position == getBackPosition()) {
- final PanelBackItemView item = (PanelBackItemView) view;
- item.updateFromFilter(filterManager.getPreviousFilter());
- return;
- }
- }
-
- int actualPosition = position
- - (isShowingBack() ? 1 : 0)
- - (viewConfig.hasHeaderConfig() ? 1 : 0);
-
- cursor.moveToPosition(actualPosition);
-
- final PanelItemView panelItemView = (PanelItemView) view;
- panelItemView.updateFromCursor(cursor);
- }
-
- private int getBackPosition() {
- return viewConfig.hasHeaderConfig() ? 1 : 0;
- }
-
- @Override
- public int getItemCount() {
- if (cursor == null) {
- return 0;
- }
-
- return cursor.getCount()
- + (isShowingBack() ? 1 : 0)
- + (viewConfig.hasHeaderConfig() ? 1 : 0);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelRefreshLayout.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelRefreshLayout.java
deleted file mode 100644
index d43a97f31..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/PanelRefreshLayout.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import org.mozilla.gecko.R;
-
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.home.PanelLayout.DatasetBacked;
-import org.mozilla.gecko.home.PanelLayout.FilterManager;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.support.v4.widget.SwipeRefreshLayout;
-import android.util.Log;
-import android.view.View;
-
-/**
- * Used to wrap a {@code DatasetBacked} ListView or GridView to give the child view swipe-to-refresh
- * capabilities.
- *
- * This view acts as a decorator to forward the {@code DatasetBacked} methods to the child view
- * while providing the refresh gesture support on top of it.
- */
-class PanelRefreshLayout extends SwipeRefreshLayout implements DatasetBacked {
- private static final String LOGTAG = "GeckoPanelRefreshLayout";
-
- private static final String JSON_KEY_PANEL_ID = "panelId";
- private static final String JSON_KEY_VIEW_INDEX = "viewIndex";
-
- private final String panelId;
- private final int viewIndex;
- private final DatasetBacked datasetBacked;
-
- /**
- * @param context Android context.
- * @param childView ListView or GridView. Must implement {@code DatasetBacked}.
- * @param panelId The ID from the {@code PanelConfig}.
- * @param viewIndex The index from the {@code ViewConfig}.
- */
- public PanelRefreshLayout(Context context, View childView, String panelId, int viewIndex) {
- super(context);
-
- if (!(childView instanceof DatasetBacked)) {
- throw new IllegalArgumentException("View must implement DatasetBacked to be refreshed");
- }
-
- this.panelId = panelId;
- this.viewIndex = viewIndex;
- this.datasetBacked = (DatasetBacked) childView;
-
- setOnRefreshListener(new RefreshListener());
- addView(childView);
-
- // Must be called after the child view has been added.
- setColorSchemeResources(R.color.fennec_ui_orange, R.color.action_orange);
- }
-
- @Override
- public void setDataset(Cursor cursor) {
- datasetBacked.setDataset(cursor);
- setRefreshing(false);
- }
-
- @Override
- public void setFilterManager(FilterManager manager) {
- datasetBacked.setFilterManager(manager);
- }
-
- private class RefreshListener implements OnRefreshListener {
- @Override
- public void onRefresh() {
- final JSONObject response = new JSONObject();
- try {
- response.put(JSON_KEY_PANEL_ID, panelId);
- response.put(JSON_KEY_VIEW_INDEX, viewIndex);
- } catch (JSONException e) {
- Log.e(LOGTAG, "Could not create refresh message", e);
- return;
- }
-
- GeckoAppShell.notifyObservers("HomePanels:RefreshView", response.toString());
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelViewAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelViewAdapter.java
deleted file mode 100644
index cf03c50c0..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/PanelViewAdapter.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import org.mozilla.gecko.home.HomeConfig.ItemType;
-import org.mozilla.gecko.home.HomeConfig.ViewConfig;
-import org.mozilla.gecko.home.PanelLayout.FilterManager;
-
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.support.v4.widget.CursorAdapter;
-import android.view.View;
-import android.view.ViewGroup;
-
-class PanelViewAdapter extends CursorAdapter {
- private static final int VIEW_TYPE_ITEM = 0;
- private static final int VIEW_TYPE_BACK = 1;
-
- private final ViewConfig viewConfig;
- private FilterManager filterManager;
- private final Context context;
-
- public PanelViewAdapter(Context context, ViewConfig viewConfig) {
- super(context, null, 0);
- this.context = context;
- this.viewConfig = viewConfig;
- }
-
- public void setFilterManager(FilterManager manager) {
- this.filterManager = manager;
- }
-
- @Override
- public final int getViewTypeCount() {
- return 2;
- }
-
- @Override
- public int getCount() {
- return super.getCount() + (isShowingBack() ? 1 : 0);
- }
-
- @Override
- public int getItemViewType(int position) {
- if (isShowingBack() && position == 0) {
- return VIEW_TYPE_BACK;
- } else {
- return VIEW_TYPE_ITEM;
- }
- }
-
- @Override
- public final View getView(int position, View convertView, ViewGroup parent) {
- if (convertView == null) {
- convertView = newView(parent.getContext(), position, parent);
- }
-
- bindView(convertView, position);
- return convertView;
- }
-
- private View newView(Context context, int position, ViewGroup parent) {
- if (getItemViewType(position) == VIEW_TYPE_BACK) {
- return new PanelBackItemView(context, viewConfig.getBackImageUrl());
- } else {
- return PanelItemView.create(context, viewConfig.getItemType());
- }
- }
-
- private void bindView(View view, int position) {
- if (isShowingBack()) {
- if (position == 0) {
- final PanelBackItemView item = (PanelBackItemView) view;
- item.updateFromFilter(filterManager.getPreviousFilter());
- return;
- }
-
- position--;
- }
-
- final Cursor cursor = getCursor(position);
- final PanelItemView item = (PanelItemView) view;
- item.updateFromCursor(cursor);
- }
-
- private boolean isShowingBack() {
- return filterManager != null && filterManager.canGoBack();
- }
-
- private final Cursor getCursor(int position) {
- final Cursor cursor = getCursor();
- if (cursor == null || !cursor.moveToPosition(position)) {
- throw new IllegalStateException("Couldn't move cursor to position " + position);
- }
-
- return cursor;
- }
-
- @Override
- public final void bindView(View view, Context context, Cursor cursor) {
- // Do nothing.
- }
-
- @Override
- public final View newView(Context context, Cursor cursor, ViewGroup parent) {
- return null;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelViewItemHandler.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelViewItemHandler.java
deleted file mode 100644
index a69db0b41..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/PanelViewItemHandler.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import org.mozilla.gecko.db.BrowserContract.HomeItems;
-import org.mozilla.gecko.home.HomeConfig.ViewConfig;
-import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
-import org.mozilla.gecko.home.PanelLayout.FilterManager;
-import org.mozilla.gecko.home.PanelLayout.OnItemOpenListener;
-
-import android.database.Cursor;
-
-import java.util.EnumSet;
-
-class PanelViewItemHandler {
- private OnItemOpenListener mItemOpenListener;
- private FilterManager mFilterManager;
-
- public void setOnItemOpenListener(OnItemOpenListener listener) {
- mItemOpenListener = listener;
- }
-
- public void setFilterManager(FilterManager manager) {
- mFilterManager = manager;
- }
-
- /**
- * If item at this position is a back item, perform the go back action via the
- * {@code FilterManager}. Otherwise, prepare the url to be opened by the
- * {@code OnUrlOpenListener}.
- */
- public void openItemAtPosition(Cursor cursor, int position) {
- if (mFilterManager != null && mFilterManager.canGoBack()) {
- if (position == 0) {
- mFilterManager.goBack();
- return;
- }
-
- position--;
- }
-
- if (cursor == null || !cursor.moveToPosition(position)) {
- throw new IllegalStateException("Couldn't move cursor to position " + position);
- }
-
- int urlIndex = cursor.getColumnIndexOrThrow(HomeItems.URL);
- final String url = cursor.getString(urlIndex);
-
- int titleIndex = cursor.getColumnIndexOrThrow(HomeItems.TITLE);
- final String title = cursor.getString(titleIndex);
-
- if (mItemOpenListener != null) {
- mItemOpenListener.onItemOpen(url, title);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PinSiteDialog.java b/mobile/android/base/java/org/mozilla/gecko/home/PinSiteDialog.java
deleted file mode 100644
index 230b1d329..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/PinSiteDialog.java
+++ /dev/null
@@ -1,256 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import java.util.EnumSet;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.BrowserContract.URLColumns;
-import org.mozilla.gecko.db.BrowserDB.FilterFlags;
-import org.mozilla.gecko.util.StringUtils;
-
-import android.app.Dialog;
-import android.content.Context;
-import android.database.Cursor;
-import android.os.Bundle;
-import android.support.v4.app.DialogFragment;
-import android.support.v4.app.LoaderManager;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.Loader;
-import android.support.v4.widget.CursorAdapter;
-import android.text.Editable;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-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.widget.AdapterView;
-import android.widget.EditText;
-import android.widget.ListView;
-
-/**
- * Dialog fragment that displays frecency search results, for pinning a site, in a GridView.
- */
-class PinSiteDialog extends DialogFragment {
- // Listener for url selection
- public static interface OnSiteSelectedListener {
- public void onSiteSelected(String url, String title);
- }
-
- // Cursor loader ID for search query
- private static final int LOADER_ID_SEARCH = 0;
-
- // Holds the current search term to use in the query
- private String mSearchTerm;
-
- // Adapter for the list of search results
- private SearchAdapter mAdapter;
-
- // Search entry
- private EditText mSearch;
-
- // Search results
- private ListView mList;
-
- // Callbacks used for the search loader
- private CursorLoaderCallbacks mLoaderCallbacks;
-
- // Bookmark selected listener
- private OnSiteSelectedListener mOnSiteSelectedListener;
-
- public static PinSiteDialog newInstance() {
- return new PinSiteDialog();
- }
-
- private PinSiteDialog() {
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setStyle(DialogFragment.STYLE_NO_TITLE, android.R.style.Theme_Holo_Light_Dialog);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- // All list views are styled to look the same with a global activity theme.
- // If the style of the list changes, inflate it from an XML.
- return inflater.inflate(R.layout.pin_site_dialog, container, false);
- }
-
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
-
- mSearch = (EditText) view.findViewById(R.id.search);
- mSearch.addTextChangedListener(new TextWatcher() {
- @Override
- public void afterTextChanged(Editable 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) {
- setSearchTerm(mSearch.getText().toString());
- filter(mSearchTerm);
- }
- });
-
- mSearch.setOnKeyListener(new View.OnKeyListener() {
- @Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- if (keyCode != KeyEvent.KEYCODE_ENTER || mOnSiteSelectedListener == null) {
- return false;
- }
-
- // If the user manually entered a search term or URL, wrap the value in
- // a special URI until we can get a valid URL for this bookmark.
- final String text = mSearch.getText().toString().trim();
- if (!TextUtils.isEmpty(text)) {
- final String url = StringUtils.encodeUserEnteredUrl(text);
- mOnSiteSelectedListener.onSiteSelected(url, text);
- dismiss();
- }
-
- return true;
- }
- });
-
- mSearch.setOnFocusChangeListener(new View.OnFocusChangeListener() {
- @Override
- public void onFocusChange(View v, boolean hasFocus) {
- if (hasFocus) {
- // On rotation, the view gets destroyed and we could be in a race to get the dialog
- // and window (see bug 1072959).
- Dialog dialog = getDialog();
- if (dialog == null) {
- return;
- }
- Window window = dialog.getWindow();
- if (window == null) {
- return;
- }
- window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
- }
- }
- });
-
- mList = (HomeListView) view.findViewById(R.id.list);
- mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- if (mOnSiteSelectedListener != null) {
- final Cursor c = mAdapter.getCursor();
- if (c == null || !c.moveToPosition(position)) {
- return;
- }
-
- final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL));
- final String title = c.getString(c.getColumnIndexOrThrow(URLColumns.TITLE));
- mOnSiteSelectedListener.onSiteSelected(url, title);
- }
-
- // Dismiss the fragment and the dialog.
- dismiss();
- }
- });
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- final LoaderManager manager = getLoaderManager();
-
- // Initialize the search adapter
- mAdapter = new SearchAdapter(getActivity());
- mList.setAdapter(mAdapter);
-
- // Create callbacks before the initial loader is started
- mLoaderCallbacks = new CursorLoaderCallbacks();
-
- // Reconnect to the loader only if present
- manager.initLoader(LOADER_ID_SEARCH, null, mLoaderCallbacks);
-
- // If there is a search term, put it in the text field
- if (!TextUtils.isEmpty(mSearchTerm)) {
- mSearch.setText(mSearchTerm);
- mSearch.selectAll();
- }
-
- // Always start with an empty filter
- filter("");
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
-
- // Discard any additional site selection as the dialog
- // is getting destroyed (see bug 935542).
- setOnSiteSelectedListener(null);
- }
-
- public void setSearchTerm(String searchTerm) {
- mSearchTerm = searchTerm;
- }
-
- private void filter(String searchTerm) {
- // Restart loaders with the new search term
- SearchLoader.restart(getLoaderManager(), LOADER_ID_SEARCH,
- mLoaderCallbacks, searchTerm,
- EnumSet.of(FilterFlags.EXCLUDE_PINNED_SITES));
- }
-
- public void setOnSiteSelectedListener(OnSiteSelectedListener listener) {
- mOnSiteSelectedListener = listener;
- }
-
- private static class SearchAdapter extends CursorAdapter {
- private final LayoutInflater mInflater;
-
- public SearchAdapter(Context context) {
- super(context, null, 0);
- mInflater = LayoutInflater.from(context);
- }
-
- @Override
- public void bindView(View view, Context context, Cursor cursor) {
- TwoLinePageRow row = (TwoLinePageRow) view;
- row.setShowIcons(false);
- row.updateFromCursor(cursor);
- }
-
- @Override
- public View newView(Context context, Cursor cursor, ViewGroup parent) {
- return (TwoLinePageRow) mInflater.inflate(R.layout.home_item_row, parent, false);
- }
- }
-
- private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
- @Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- return SearchLoader.createInstance(getActivity(), args);
- }
-
- @Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
- mAdapter.swapCursor(c);
- }
-
- @Override
- public void onLoaderReset(Loader<Cursor> loader) {
- mAdapter.swapCursor(null);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java
deleted file mode 100755
index 3091f77da..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java
+++ /dev/null
@@ -1,454 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import android.content.Context;
-import android.support.v7.widget.RecyclerView;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.AboutPages;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.SessionParser;
-import org.mozilla.gecko.home.CombinedHistoryAdapter.RecentTabsUpdateHandler;
-import org.mozilla.gecko.home.CombinedHistoryPanel.PanelStateUpdateHandler;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import static org.mozilla.gecko.home.CombinedHistoryItem.ItemType;
-import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD_RECENT_TABS;
-
-public class RecentTabsAdapter extends RecyclerView.Adapter<CombinedHistoryItem>
- implements CombinedHistoryRecyclerView.AdapterContextMenuBuilder, NativeEventListener {
- private static final String LOGTAG = "GeckoRecentTabsAdapter";
-
- private static final int NAVIGATION_BACK_BUTTON_INDEX = 0;
-
- private static final String TELEMETRY_EXTRA_LAST_TIME = "recent_tabs_last_time";
- private static final String TELEMETRY_EXTRA_RECENTLY_CLOSED = "recent_closed_tabs";
- private static final String TELEMETRY_EXTRA_MIXED = "recent_tabs_mixed";
-
- // Recently closed tabs from Gecko.
- private ClosedTab[] recentlyClosedTabs;
- private boolean recentlyClosedTabsReceived = false;
-
- // "Tabs from last time".
- private ClosedTab[] lastSessionTabs;
-
- public static final class ClosedTab {
- public final String url;
- public final String title;
- public final String data;
-
- public ClosedTab(String url, String title, String data) {
- this.url = url;
- this.title = title;
- this.data = data;
- }
- }
-
- private final Context context;
- private final RecentTabsUpdateHandler recentTabsUpdateHandler;
- private final PanelStateUpdateHandler panelStateUpdateHandler;
-
- public RecentTabsAdapter(Context context,
- RecentTabsUpdateHandler recentTabsUpdateHandler,
- PanelStateUpdateHandler panelStateUpdateHandler) {
- this.context = context;
- this.recentTabsUpdateHandler = recentTabsUpdateHandler;
- this.panelStateUpdateHandler = panelStateUpdateHandler;
- recentlyClosedTabs = new ClosedTab[0];
- lastSessionTabs = new ClosedTab[0];
-
- readPreviousSessionData();
- }
-
- public void startListeningForClosedTabs() {
- EventDispatcher.getInstance().registerGeckoThreadListener(this, "ClosedTabs:Data");
- GeckoAppShell.notifyObservers("ClosedTabs:StartNotifications", null);
- }
-
- public void stopListeningForClosedTabs() {
- GeckoAppShell.notifyObservers("ClosedTabs:StopNotifications", null);
- EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "ClosedTabs:Data");
- recentlyClosedTabsReceived = false;
- }
-
- public void startListeningForHistorySanitize() {
- EventDispatcher.getInstance().registerGeckoThreadListener(this, "Sanitize:Finished");
- }
-
- public void stopListeningForHistorySanitize() {
- EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "Sanitize:Finished");
- }
-
- @Override
- public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
- switch (event) {
- case "ClosedTabs:Data":
- updateRecentlyClosedTabs(message);
- break;
- case "Sanitize:Finished":
- clearLastSessionData();
- break;
- }
- }
-
- private void updateRecentlyClosedTabs(NativeJSObject message) {
- final NativeJSObject[] tabs = message.getObjectArray("tabs");
- final int length = tabs.length;
-
- final ClosedTab[] closedTabs = new ClosedTab[length];
- for (int i = 0; i < length; i++) {
- final NativeJSObject tab = tabs[i];
- closedTabs[i] = new ClosedTab(tab.getString("url"), tab.getString("title"), tab.getObject("data").toString());
- }
-
- // Only modify recentlyClosedTabs on the UI thread.
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- // Save some data about the old panel state, so we can be
- // smarter about notifying the recycler view which bits changed.
- int prevClosedTabsCount = recentlyClosedTabs.length;
- boolean prevSectionHeaderVisibility = isSectionHeaderVisible();
- int prevSectionHeaderIndex = getSectionHeaderIndex();
-
- recentlyClosedTabs = closedTabs;
- recentlyClosedTabsReceived = true;
- recentTabsUpdateHandler.onRecentTabsCountUpdated(
- getClosedTabsCount(), recentlyClosedTabsReceived);
- panelStateUpdateHandler.onPanelStateUpdated(CHILD_RECENT_TABS);
-
- // Handle the section header hiding/unhiding.
- updateHeaderVisibility(prevSectionHeaderVisibility, prevSectionHeaderIndex);
-
- // Update the "Recently closed" part of the tab list.
- updateTabsList(prevClosedTabsCount, recentlyClosedTabs.length, getFirstRecentTabIndex(), getLastRecentTabIndex());
- }
- });
- }
-
- private void readPreviousSessionData() {
- // If we happen to initialise before GeckoApp, waiting on either the main or the background
- // thread can lead to a deadlock, so we have to run on a separate thread instead.
- final Thread parseThread = new Thread(new Runnable() {
- @Override
- public void run() {
- // Make sure that the start up code has had a chance to update sessionstore.old as necessary.
- GeckoProfile.get(context).waitForOldSessionDataProcessing();
-
- final String jsonString = GeckoProfile.get(context).readPreviousSessionFile();
- if (jsonString == null) {
- // No previous session data.
- return;
- }
-
- final List<ClosedTab> parsedTabs = new ArrayList<>();
-
- new SessionParser() {
- @Override
- public void onTabRead(SessionTab tab) {
- final String url = tab.getUrl();
-
- // Don't show last tabs for about:home
- if (AboutPages.isAboutHome(url)) {
- return;
- }
-
- parsedTabs.add(new ClosedTab(url, tab.getTitle(), tab.getTabObject().toString()));
- }
- }.parse(jsonString);
-
- final ClosedTab[] closedTabs = parsedTabs.toArray(new ClosedTab[parsedTabs.size()]);
-
- // Only modify lastSessionTabs on the UI thread.
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- // Save some data about the old panel state, so we can be
- // smarter about notifying the recycler view which bits changed.
- int prevClosedTabsCount = lastSessionTabs.length;
- boolean prevSectionHeaderVisibility = isSectionHeaderVisible();
- int prevSectionHeaderIndex = getSectionHeaderIndex();
-
- lastSessionTabs = closedTabs;
- recentTabsUpdateHandler.onRecentTabsCountUpdated(
- getClosedTabsCount(), recentlyClosedTabsReceived);
- panelStateUpdateHandler.onPanelStateUpdated(CHILD_RECENT_TABS);
-
- // Handle the section header hiding/unhiding.
- updateHeaderVisibility(prevSectionHeaderVisibility, prevSectionHeaderIndex);
-
- // Update the "Tabs from last time" part of the tab list.
- updateTabsList(prevClosedTabsCount, lastSessionTabs.length, getFirstLastSessionTabIndex(), getLastLastSessionTabIndex());
- }
- });
- }
- }, "LastSessionTabsThread");
-
- parseThread.start();
- }
-
- private void clearLastSessionData() {
- final ClosedTab[] emptyLastSessionTabs = new ClosedTab[0];
-
- // Only modify mLastSessionTabs on the UI thread.
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- // Save some data about the old panel state, so we can be
- // smarter about notifying the recycler view which bits changed.
- int prevClosedTabsCount = lastSessionTabs.length;
- boolean prevSectionHeaderVisibility = isSectionHeaderVisible();
- int prevSectionHeaderIndex = getSectionHeaderIndex();
-
- lastSessionTabs = emptyLastSessionTabs;
- recentTabsUpdateHandler.onRecentTabsCountUpdated(
- getClosedTabsCount(), recentlyClosedTabsReceived);
- panelStateUpdateHandler.onPanelStateUpdated(CHILD_RECENT_TABS);
-
- // Handle the section header hiding.
- updateHeaderVisibility(prevSectionHeaderVisibility, prevSectionHeaderIndex);
-
- // Handle the "tabs from last time" being cleared.
- if (prevClosedTabsCount > 0) {
- notifyItemRangeRemoved(getFirstLastSessionTabIndex(), prevClosedTabsCount);
- }
- }
- });
- }
-
- private void updateHeaderVisibility(boolean prevSectionHeaderVisibility, int prevSectionHeaderIndex) {
- if (prevSectionHeaderVisibility && !isSectionHeaderVisible()) {
- notifyItemRemoved(prevSectionHeaderIndex);
- } else if (!prevSectionHeaderVisibility && isSectionHeaderVisible()) {
- notifyItemInserted(getSectionHeaderIndex());
- }
- }
-
- /**
- * Updates the tab list as necessary to account for any changes in tab count in a particular data source.
- *
- * Since the session store only sends out full updates, we don't know for sure what has changed compared
- * to the last data set, so we can only animate if the tab count actually changes.
- *
- * @param prevClosedTabsCount The previous number of closed tabs from that data source.
- * @param closedTabsCount The current number of closed tabs contained in that data source.
- * @param firstTabListIndex The current position of that data source's first item in the RecyclerView.
- * @param lastTabListIndex The current position of that data source's last item in the RecyclerView.
- */
- private void updateTabsList(int prevClosedTabsCount, int closedTabsCount, int firstTabListIndex, int lastTabListIndex) {
- final int closedTabsCountChange = closedTabsCount - prevClosedTabsCount;
-
- if (closedTabsCountChange <= 0) {
- notifyItemRangeRemoved(lastTabListIndex + 1, -closedTabsCountChange); // Remove tabs from the bottom of the list.
- notifyItemRangeChanged(firstTabListIndex, closedTabsCount); // Update the contents of the remaining items.
- } else { // closedTabsCountChange > 0
- notifyItemRangeInserted(firstTabListIndex, closedTabsCountChange); // Add additional tabs at the top of the list.
- notifyItemRangeChanged(firstTabListIndex + closedTabsCountChange, prevClosedTabsCount); // Update any previous list items.
- }
- }
-
- public String restoreTabFromPosition(int position) {
- final List<String> dataList = new ArrayList<>(1);
- dataList.add(getClosedTabForPosition(position).data);
-
- final String telemetryExtra =
- position > getLastRecentTabIndex() ? TELEMETRY_EXTRA_LAST_TIME : TELEMETRY_EXTRA_RECENTLY_CLOSED;
-
- restoreSessionWithHistory(dataList);
-
- return telemetryExtra;
- }
-
- public String restoreAllTabs() {
- if (recentlyClosedTabs.length == 0 && lastSessionTabs.length == 0) {
- return null;
- }
-
- final List<String> dataList = new ArrayList<>(getClosedTabsCount());
- addTabDataToList(dataList, recentlyClosedTabs);
- addTabDataToList(dataList, lastSessionTabs);
-
- final String telemetryExtra = recentlyClosedTabs.length > 0 && lastSessionTabs.length > 0 ? TELEMETRY_EXTRA_MIXED :
- recentlyClosedTabs.length > 0 ? TELEMETRY_EXTRA_RECENTLY_CLOSED : TELEMETRY_EXTRA_LAST_TIME;
-
- restoreSessionWithHistory(dataList);
-
- return telemetryExtra;
- }
-
- private void addTabDataToList(List<String> dataList, ClosedTab[] closedTabs) {
- for (ClosedTab closedTab : closedTabs) {
- dataList.add(closedTab.data);
- }
- }
-
- private static void restoreSessionWithHistory(List<String> dataList) {
- final JSONObject json = new JSONObject();
- try {
- json.put("tabs", new JSONArray(dataList));
- } catch (JSONException e) {
- Log.e(LOGTAG, "JSON error", e);
- }
-
- GeckoAppShell.notifyObservers("Session:RestoreRecentTabs", json.toString());
- }
-
- @Override
- public CombinedHistoryItem onCreateViewHolder(ViewGroup parent, int viewType) {
- final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
- final View view;
-
- final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
-
- switch (itemType) {
- case NAVIGATION_BACK:
- view = inflater.inflate(R.layout.home_combined_back_item, parent, false);
- return new CombinedHistoryItem.HistoryItem(view);
-
- case SECTION_HEADER:
- view = inflater.inflate(R.layout.home_header_row, parent, false);
- return new CombinedHistoryItem.BasicItem(view);
-
- case CLOSED_TAB:
- view = inflater.inflate(R.layout.home_item_row, parent, false);
- return new CombinedHistoryItem.HistoryItem(view);
- }
- return null;
- }
-
- @Override
- public void onBindViewHolder(CombinedHistoryItem holder, final int position) {
- final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
-
- switch (itemType) {
- case SECTION_HEADER:
- ((TextView) holder.itemView).setText(context.getString(R.string.home_closed_tabs_title2));
- break;
-
- case CLOSED_TAB:
- final ClosedTab closedTab = getClosedTabForPosition(position);
- ((CombinedHistoryItem.HistoryItem) holder).bind(closedTab);
- break;
- }
- }
-
- @Override
- public int getItemCount() {
- int itemCount = 1; // NAVIGATION_BACK button is always visible.
-
- if (isSectionHeaderVisible()) {
- itemCount += 1;
- }
-
- itemCount += getClosedTabsCount();
-
- return itemCount;
- }
-
- private CombinedHistoryItem.ItemType getItemTypeForPosition(int position) {
- if (position == NAVIGATION_BACK_BUTTON_INDEX) {
- return ItemType.NAVIGATION_BACK;
- }
-
- if (position == getSectionHeaderIndex() && isSectionHeaderVisible()) {
- return ItemType.SECTION_HEADER;
- }
-
- return ItemType.CLOSED_TAB;
- }
-
- @Override
- public int getItemViewType(int position) {
- return CombinedHistoryItem.ItemType.itemTypeToViewType(getItemTypeForPosition(position));
- }
-
- public int getClosedTabsCount() {
- return recentlyClosedTabs.length + lastSessionTabs.length;
- }
-
- private boolean isSectionHeaderVisible() {
- return recentlyClosedTabs.length > 0 || lastSessionTabs.length > 0;
- }
-
- private int getSectionHeaderIndex() {
- return isSectionHeaderVisible() ?
- NAVIGATION_BACK_BUTTON_INDEX + 1 :
- NAVIGATION_BACK_BUTTON_INDEX;
- }
-
- private int getFirstRecentTabIndex() {
- return getSectionHeaderIndex() + 1;
- }
-
- private int getLastRecentTabIndex() {
- return getSectionHeaderIndex() + recentlyClosedTabs.length;
- }
-
- private int getFirstLastSessionTabIndex() {
- return getLastRecentTabIndex() + 1;
- }
-
- private int getLastLastSessionTabIndex() {
- return getLastRecentTabIndex() + lastSessionTabs.length;
- }
-
- /**
- * Get the closed tab corresponding to a RecyclerView list item.
- *
- * The Recent Tab folder combines two data sources, so if we want to get the ClosedTab object
- * behind a certain list item, we need to route this request to the corresponding data source
- * and also transform the global list position into a local array index.
- */
- private ClosedTab getClosedTabForPosition(int position) {
- final ClosedTab closedTab;
- if (position <= getLastRecentTabIndex()) { // Upper part of the list is "Recently closed tabs".
- closedTab = recentlyClosedTabs[position - getFirstRecentTabIndex()];
- } else { // Lower part is "Tabs from last time".
- closedTab = lastSessionTabs[position - getFirstLastSessionTabIndex()];
- }
-
- return closedTab;
- }
-
- @Override
- public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) {
- final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
- final HomeContextMenuInfo info;
-
- switch (itemType) {
- case CLOSED_TAB:
- info = new HomeContextMenuInfo(view, position, -1);
- ClosedTab closedTab = getClosedTabForPosition(position);
- return populateChildInfoFromTab(info, closedTab);
- }
-
- return null;
- }
-
- protected static HomeContextMenuInfo populateChildInfoFromTab(HomeContextMenuInfo info, ClosedTab tab) {
- info.url = tab.url;
- info.title = tab.title;
- return info;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/RemoteTabsExpandableListState.java b/mobile/android/base/java/org/mozilla/gecko/home/RemoteTabsExpandableListState.java
deleted file mode 100644
index 43497ae6c..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/RemoteTabsExpandableListState.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import java.util.HashSet;
-import java.util.Set;
-
-import org.mozilla.gecko.util.PrefUtils;
-
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
-
-/**
- * Encapsulate visual state maintained by the Remote Tabs home panel.
- * <p>
- * This state should persist across database updates by Sync and the like. This
- * state could be stored in a separate "clients_metadata" table and served by
- * the Tabs provider, but that is heavy-weight for what we want to achieve. Such
- * a scheme would require either an expensive table join, or a tricky
- * co-ordination between multiple cursors. In contrast, this is easy and cheap
- * enough to do on the main thread.
- * <p>
- * This state is "per SharedPreferences" object. In practice, there should exist
- * one state object per Gecko Profile; since we can't change profiles without
- * killing our process, this can be a static singleton.
- */
-public class RemoteTabsExpandableListState {
- private static final String PREF_COLLAPSED_CLIENT_GUIDS = "remote_tabs_collapsed_client_guids";
- private static final String PREF_HIDDEN_CLIENT_GUIDS = "remote_tabs_hidden_client_guids";
- private static final String PREF_SELECTED_CLIENT_GUID = "remote_tabs_selected_client_guid";
-
- protected final SharedPreferences sharedPrefs;
-
- // Synchronized by the state instance. The default is to expand a clients
- // tabs, so "not present" means "expanded".
- // Only accessed from the UI thread.
- protected final Set<String> collapsedClients;
-
- // Synchronized by the state instance. The default is to show a client, so
- // "not present" means "shown".
- // Only accessed from the UI thread.
- protected final Set<String> hiddenClients;
-
- // Synchronized by the state instance. The last user selected client guid.
- // The selectedClient may be invalid or null.
- protected String selectedClient;
-
- public RemoteTabsExpandableListState(SharedPreferences sharedPrefs) {
- if (null == sharedPrefs) {
- throw new IllegalArgumentException("sharedPrefs must not be null");
- }
- this.sharedPrefs = sharedPrefs;
-
- this.collapsedClients = getStringSet(PREF_COLLAPSED_CLIENT_GUIDS);
- this.hiddenClients = getStringSet(PREF_HIDDEN_CLIENT_GUIDS);
- this.selectedClient = sharedPrefs.getString(PREF_SELECTED_CLIENT_GUID, null);
- }
-
- /**
- * Extract a string set from shared preferences.
- * <p>
- * Nota bene: it is not OK to modify the set returned by {@link SharedPreferences#getStringSet(String, Set)}.
- *
- * @param pref to read from.
- * @returns string set; never null.
- */
- protected Set<String> getStringSet(String pref) {
- final Set<String> loaded = PrefUtils.getStringSet(sharedPrefs, pref, null);
- if (loaded != null) {
- return new HashSet<String>(loaded);
- } else {
- return new HashSet<String>();
- }
- }
-
- /**
- * Update client membership in a set.
- *
- * @param pref
- * to write updated set to.
- * @param clients
- * set to update membership in.
- * @param clientGuid
- * to update membership of.
- * @param isMember
- * whether the client is a member of the set.
- * @return true if the set of clients was modified.
- */
- protected boolean updateClientMembership(String pref, Set<String> clients, String clientGuid, boolean isMember) {
- final boolean modified;
- if (isMember) {
- modified = clients.add(clientGuid);
- } else {
- modified = clients.remove(clientGuid);
- }
-
- if (modified) {
- // This starts an asynchronous write. We don't care if we drop the
- // write, and we don't really care if we race between writes, since
- // we will return results from our in-memory cache.
- final Editor editor = sharedPrefs.edit();
- PrefUtils.putStringSet(editor, pref, clients);
- editor.apply();
- }
-
- return modified;
- }
-
- /**
- * Mark a client as collapsed.
- *
- * @param clientGuid
- * to update.
- * @param collapsed
- * whether the client is collapsed.
- * @return true if the set of collapsed clients was modified.
- */
- protected synchronized boolean setClientCollapsed(String clientGuid, boolean collapsed) {
- return updateClientMembership(PREF_COLLAPSED_CLIENT_GUIDS, collapsedClients, clientGuid, collapsed);
- }
-
- /**
- * Mark a client as the selected.
- *
- * @param clientGuid
- * to update.
- */
- protected synchronized void setClientAsSelected(String clientGuid) {
- if (hiddenClients.contains(clientGuid)) {
- selectedClient = null;
- } else {
- selectedClient = clientGuid;
- }
-
- final Editor editor = sharedPrefs.edit();
- editor.putString(PREF_SELECTED_CLIENT_GUID, selectedClient);
- editor.apply();
- }
-
- public synchronized boolean isClientCollapsed(String clientGuid) {
- return collapsedClients.contains(clientGuid);
- }
-
- /**
- * Mark a client as hidden.
- *
- * @param clientGuid
- * to update.
- * @param hidden
- * whether the client is hidden.
- * @return true if the set of hidden clients was modified.
- */
- protected synchronized boolean setClientHidden(String clientGuid, boolean hidden) {
- return updateClientMembership(PREF_HIDDEN_CLIENT_GUIDS, hiddenClients, clientGuid, hidden);
- }
-
- public synchronized boolean isClientHidden(String clientGuid) {
- return hiddenClients.contains(clientGuid);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngine.java b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngine.java
deleted file mode 100644
index 9b2d2746a..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngine.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import android.support.annotation.NonNull;
-import org.mozilla.gecko.gfx.BitmapUtils;
-import org.mozilla.gecko.R;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class SearchEngine {
- public static final String LOG_TAG = "GeckoSearchEngine";
-
- public final String name; // Never null.
- public final String identifier; // Can be null.
-
- private final Bitmap icon;
- private volatile List<String> suggestions = new ArrayList<String>(); // Never null.
-
- public SearchEngine(final Context context, final JSONObject engineJSON) throws JSONException {
- if (engineJSON == null) {
- throw new IllegalArgumentException("Can't instantiate SearchEngine from null JSON.");
- }
-
- this.name = getString(engineJSON, "name");
- if (this.name == null) {
- throw new IllegalArgumentException("Cannot have an unnamed search engine.");
- }
-
- this.identifier = getString(engineJSON, "identifier");
-
- final String iconURI = getString(engineJSON, "iconURI");
- if (iconURI == null) {
- Log.w(LOG_TAG, "iconURI is null for search engine " + this.name);
- }
- final Bitmap tempIcon = BitmapUtils.getBitmapFromDataURI(iconURI);
-
- this.icon = (tempIcon != null) ? tempIcon : getDefaultFavicon(context);
- }
-
- private Bitmap getDefaultFavicon(final Context context) {
- return BitmapFactory.decodeResource(context.getResources(), R.drawable.search_icon_inactive);
- }
-
- private static String getString(JSONObject data, String key) throws JSONException {
- if (data.isNull(key)) {
- return null;
- }
- return data.getString(key);
- }
-
- /**
- * @return a non-null string suitable for use by FHR.
- */
- @NonNull
- public String getEngineIdentifier() {
- if (this.identifier != null) {
- return this.identifier;
- }
- if (this.name != null) {
- return "other-" + this.name;
- }
- return "other";
- }
-
- public boolean hasSuggestions() {
- return !this.suggestions.isEmpty();
- }
-
- public int getSuggestionsCount() {
- return this.suggestions.size();
- }
-
- public Iterable<String> getSuggestions() {
- return this.suggestions;
- }
-
- public void setSuggestions(List<String> suggestions) {
- if (suggestions == null) {
- this.suggestions = new ArrayList<String>();
- return;
- }
- this.suggestions = suggestions;
- }
-
- public Bitmap getIcon() {
- return this.icon;
- }
-}
-
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineAdapter.java
deleted file mode 100644
index be5b3b461..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineAdapter.java
+++ /dev/null
@@ -1,122 +0,0 @@
-package org.mozilla.gecko.home;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.graphics.drawable.DrawableCompat;
-import android.support.v7.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-
-import org.mozilla.gecko.R;
-
-import java.util.Collections;
-import java.util.List;
-
-public class SearchEngineAdapter
- extends RecyclerView.Adapter<SearchEngineAdapter.SearchEngineViewHolder> {
-
- private static final String LOGTAG = SearchEngineAdapter.class.getSimpleName();
-
- private static final int VIEW_TYPE_SEARCH_ENGINE = 0;
- private static final int VIEW_TYPE_LABEL = 1;
- private final Context mContext;
-
- private int mContainerWidth;
- private List<SearchEngine> mSearchEngines = Collections.emptyList();
-
- public void setSearchEngines(List<SearchEngine> searchEngines) {
- mSearchEngines = searchEngines;
- notifyDataSetChanged();
- }
-
- /**
- * The container width is used for setting the appropriate calculated amount of width that
- * a search engine icon can have. This varies depending on the space available in the
- * {@link SearchEngineBar}. The setter exists for this attribute, in creating the view in the
- * adapter after said calculation is done when the search bar is created.
- * @param iconContainerWidth Width of each search icon.
- */
- void setIconContainerWidth(int iconContainerWidth) {
- mContainerWidth = iconContainerWidth;
- }
-
- public static class SearchEngineViewHolder extends RecyclerView.ViewHolder {
- final private ImageView faviconView;
-
- public void bindItem(SearchEngine searchEngine) {
- faviconView.setImageBitmap(searchEngine.getIcon());
- final String desc = itemView.getResources().getString(R.string.search_bar_item_desc,
- searchEngine.getEngineIdentifier());
- itemView.setContentDescription(desc);
- }
-
- public SearchEngineViewHolder(View itemView) {
- super(itemView);
- faviconView = (ImageView) itemView.findViewById(R.id.search_engine_icon);
- }
- }
-
- public SearchEngineAdapter(Context context) {
- mContext = context;
- }
-
- @Override
- public int getItemViewType(int position) {
- return position == 0 ? VIEW_TYPE_LABEL : VIEW_TYPE_SEARCH_ENGINE;
- }
-
- public SearchEngine getItem(int position) {
- // We omit the first position which is where the label currently is.
- return position == 0 ? null : mSearchEngines.get(position - 1);
- }
-
- @Override
- public SearchEngineViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- switch (viewType) {
- case VIEW_TYPE_LABEL:
- return new SearchEngineViewHolder(createLabelView(parent));
- case VIEW_TYPE_SEARCH_ENGINE:
- return new SearchEngineViewHolder(createSearchEngineView(parent));
- default:
- throw new IllegalArgumentException("Unknown view type: " + viewType);
- }
- }
-
- @Override
- public void onBindViewHolder(SearchEngineViewHolder holder, int position) {
- if (position != 0) {
- holder.bindItem(getItem(position));
- }
- }
-
- @Override
- public int getItemCount() {
- return mSearchEngines.size() + 1;
- }
-
- private View createLabelView(ViewGroup parent) {
- View view = LayoutInflater.from(mContext)
- .inflate(R.layout.search_engine_bar_label, parent, false);
- final Drawable icon = DrawableCompat.wrap(
- ContextCompat.getDrawable(mContext, R.drawable.search_icon_active).mutate());
- DrawableCompat.setTint(icon, ContextCompat.getColor(mContext, R.color.disabled_grey));
-
- final ImageView iconView = (ImageView) view.findViewById(R.id.search_engine_label);
- iconView.setImageDrawable(icon);
- return view;
- }
-
- private View createSearchEngineView(ViewGroup parent) {
- View view = LayoutInflater.from(mContext)
- .inflate(R.layout.search_engine_bar_item, parent, false);
-
- ViewGroup.LayoutParams params = view.getLayoutParams();
- params.width = mContainerWidth;
- view.setLayoutParams(params);
-
- return view;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineBar.java b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineBar.java
deleted file mode 100644
index 6a6509bcb..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineBar.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.support.v4.content.ContextCompat;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
-import android.util.TypedValue;
-import android.view.View;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-import org.mozilla.gecko.widget.RecyclerViewClickSupport;
-
-import java.util.List;
-
-public class SearchEngineBar extends RecyclerView
- implements RecyclerViewClickSupport.OnItemClickListener {
- private static final String LOGTAG = SearchEngineBar.class.getSimpleName();
-
- private static final float ICON_CONTAINER_MIN_WIDTH_DP = 72;
- private static final float LABEL_CONTAINER_WIDTH_DP = 48;
-
- public interface OnSearchBarClickListener {
- void onSearchBarClickListener(SearchEngine searchEngine);
- }
-
- private final SearchEngineAdapter mAdapter;
- private final LinearLayoutManager mLayoutManager;
- private final Paint mDividerPaint;
- private final float mMinIconContainerWidth;
- private final float mDividerHeight;
- private final int mLabelContainerWidth;
-
- private int mIconContainerWidth;
- private OnSearchBarClickListener mOnSearchBarClickListener;
-
- public SearchEngineBar(final Context context, final AttributeSet attrs) {
- super(context, attrs);
-
- mDividerPaint = new Paint();
- mDividerPaint.setColor(ContextCompat.getColor(context, R.color.toolbar_divider_grey));
- mDividerPaint.setStyle(Paint.Style.FILL_AND_STROKE);
-
- final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
- mMinIconContainerWidth = TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_DIP, ICON_CONTAINER_MIN_WIDTH_DP, displayMetrics);
- mDividerHeight = context.getResources().getDimension(R.dimen.page_row_divider_height);
- mLabelContainerWidth = Math.round(TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_DIP, LABEL_CONTAINER_WIDTH_DP, displayMetrics));
-
- mIconContainerWidth = Math.round(mMinIconContainerWidth);
-
- mAdapter = new SearchEngineAdapter(context);
- mAdapter.setIconContainerWidth(mIconContainerWidth);
- mLayoutManager = new LinearLayoutManager(context);
- mLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
-
- setAdapter(mAdapter);
- setLayoutManager(mLayoutManager);
-
- RecyclerViewClickSupport.addTo(this)
- .setOnItemClickListener(this);
- }
-
- public void setSearchEngines(List<SearchEngine> searchEngines) {
- mAdapter.setSearchEngines(searchEngines);
- }
-
- public void setOnSearchBarClickListener(OnSearchBarClickListener listener) {
- mOnSearchBarClickListener = listener;
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- final int searchEngineCount = mAdapter.getItemCount() - 1;
-
- if (searchEngineCount > 0) {
- final int availableWidth = getMeasuredWidth() - mLabelContainerWidth;
-
- if (searchEngineCount * mMinIconContainerWidth <= availableWidth) {
- // All search engines fit int: So let's just display all.
- mIconContainerWidth = (int) mMinIconContainerWidth;
- } else {
- // If only (n) search engines fit into the available space then display only (x)
- // search engines with (x) picked so that the last search engine will be cut-off
- // (we only display half of it) to show the ability to scroll this view.
-
- final double searchEnginesToDisplay = Math.floor((availableWidth / mMinIconContainerWidth) - 0.5) + 0.5;
- // Use all available width and spread search engine icons
- mIconContainerWidth = (int) (availableWidth / searchEnginesToDisplay);
- }
-
- mAdapter.setIconContainerWidth(mIconContainerWidth);
- }
- }
-
- @Override
- public void onDraw(Canvas canvas) {
- super.onDraw(canvas);
-
- canvas.drawRect(0, 0, getWidth(), mDividerHeight, mDividerPaint);
- }
-
- @Override
- public void onItemClicked(RecyclerView recyclerView, int position, View v) {
- if (mOnSearchBarClickListener == null) {
- throw new IllegalStateException(
- OnSearchBarClickListener.class.getSimpleName() + " is not initializer."
- );
- }
-
- if (position == 0) {
- final Intent settingsIntent = new Intent(getContext(), GeckoPreferences.class);
- GeckoPreferences.setResourceToOpen(settingsIntent, "preferences_search");
- getContext().startActivity(settingsIntent);
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "searchenginebar-settings");
- return;
- }
-
- final SearchEngine searchEngine = mAdapter.getItem(position);
- mOnSearchBarClickListener.onSearchBarClickListener(searchEngine);
- }
-
- /**
- * We manually add the override for getAdapter because we see this method getting stripped
- * out during compile time by aggressive proguard rules.
- */
- @RobocopTarget
- @Override
- public SearchEngineAdapter getAdapter() {
- return mAdapter;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java
deleted file mode 100644
index 5b97a8f5f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java
+++ /dev/null
@@ -1,494 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.home.BrowserSearch.OnEditSuggestionListener;
-import org.mozilla.gecko.home.BrowserSearch.OnSearchListener;
-import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-import org.mozilla.gecko.util.DrawableUtil;
-import org.mozilla.gecko.util.StringUtils;
-import org.mozilla.gecko.util.HardwareUtils;
-import org.mozilla.gecko.widget.AnimatedHeightLayout;
-import org.mozilla.gecko.widget.FaviconView;
-import org.mozilla.gecko.widget.FlowLayout;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.graphics.drawable.Drawable;
-import android.graphics.Typeface;
-import android.support.annotation.Nullable;
-import android.text.TextUtils;
-import android.text.style.StyleSpan;
-import android.text.Spannable;
-import android.text.SpannableStringBuilder;
-import android.util.AttributeSet;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.animation.AlphaAnimation;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-import java.util.EnumSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.regex.Pattern;
-
-class SearchEngineRow extends AnimatedHeightLayout {
- // Duration for fade-in animation
- private static final int ANIMATION_DURATION = 250;
-
- // Inner views
- private final FlowLayout mSuggestionView;
- private final FaviconView mIconView;
- private final LinearLayout mUserEnteredView;
- private final TextView mUserEnteredTextView;
-
- // Inflater used when updating from suggestions
- private final LayoutInflater mInflater;
-
- // Search engine associated with this view
- private SearchEngine mSearchEngine;
-
- // Event listeners for suggestion views
- private final OnClickListener mClickListener;
- private final OnLongClickListener mLongClickListener;
-
- // On URL open listener
- private OnUrlOpenListener mUrlOpenListener;
-
- // On search listener
- private OnSearchListener mSearchListener;
-
- // On edit suggestion listener
- private OnEditSuggestionListener mEditSuggestionListener;
-
- // Selected suggestion view
- private int mSelectedView;
-
- // android:backgroundTint only works in Android 21 and higher so we can't do this statically in the xml
- private Drawable mSearchHistorySuggestionIcon;
-
- // Maximums for suggestions
- private int mMaxSavedSuggestions;
- private int mMaxSearchSuggestions;
-
- private final List<Integer> mOccurrences = new ArrayList<Integer>();
-
- public SearchEngineRow(Context context) {
- this(context, null);
- }
-
- public SearchEngineRow(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public SearchEngineRow(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
-
- mClickListener = new OnClickListener() {
- @Override
- public void onClick(View v) {
- final String suggestion = getSuggestionTextFromView(v);
-
- // If we're not clicking the user-entered view (the first suggestion item)
- // and the search matches a URL pattern, go to that URL. Otherwise, do a
- // search for the term.
- if (v != mUserEnteredView && !StringUtils.isSearchQuery(suggestion, true)) {
- if (mUrlOpenListener != null) {
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "url");
-
- mUrlOpenListener.onUrlOpen(suggestion, EnumSet.noneOf(OnUrlOpenListener.Flags.class));
- }
- } else if (mSearchListener != null) {
- if (v == mUserEnteredView) {
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "user");
- } else {
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, (String) v.getTag());
- }
- mSearchListener.onSearch(mSearchEngine, suggestion, TelemetryContract.Method.SUGGESTION);
- }
- }
- };
-
- mLongClickListener = new OnLongClickListener() {
- @Override
- public boolean onLongClick(View v) {
- if (mEditSuggestionListener != null) {
- final String suggestion = getSuggestionTextFromView(v);
- mEditSuggestionListener.onEditSuggestion(suggestion);
- return true;
- }
-
- return false;
- }
- };
-
- mInflater = LayoutInflater.from(context);
- mInflater.inflate(R.layout.search_engine_row, this);
-
- mSuggestionView = (FlowLayout) findViewById(R.id.suggestion_layout);
- mIconView = (FaviconView) findViewById(R.id.suggestion_icon);
-
- // User-entered search term is first suggestion
- mUserEnteredView = (LinearLayout) findViewById(R.id.suggestion_user_entered);
- mUserEnteredView.setOnClickListener(mClickListener);
-
- mUserEnteredTextView = (TextView) findViewById(R.id.suggestion_text);
- mSearchHistorySuggestionIcon = DrawableUtil.tintDrawableWithColorRes(getContext(), R.drawable.icon_most_recent_empty, R.color.tabs_tray_icon_grey);
-
- // Suggestion limits
- mMaxSavedSuggestions = getResources().getInteger(R.integer.max_saved_suggestions);
- mMaxSearchSuggestions = getResources().getInteger(R.integer.max_search_suggestions);
- }
-
- private void setDescriptionOnSuggestion(View v, String suggestion) {
- v.setContentDescription(getResources().getString(R.string.suggestion_for_engine,
- mSearchEngine.name, suggestion));
- }
-
- private String getSuggestionTextFromView(View v) {
- final TextView suggestionText = (TextView) v.findViewById(R.id.suggestion_text);
- return suggestionText.getText().toString();
- }
-
- /**
- * Finds all occurrences of pattern in string and returns a list of the starting indices
- * of each occurrence.
- *
- * @param pattern The pattern that is searched for
- * @param string The string where we search for the pattern
- */
- private void refreshOccurrencesWith(String pattern, String string) {
- mOccurrences.clear();
-
- // Don't try to search for an empty string - String.indexOf will return 0, which would result
- // in us iterating with lastIndexOfMatch = 0, which eventually results in an OOM.
- if (TextUtils.isEmpty(pattern)) {
- return;
- }
-
- final int patternLength = pattern.length();
-
- int indexOfMatch = 0;
- int lastIndexOfMatch = 0;
- while (indexOfMatch != -1) {
- indexOfMatch = string.indexOf(pattern, lastIndexOfMatch);
- lastIndexOfMatch = indexOfMatch + patternLength;
- if (indexOfMatch != -1) {
- mOccurrences.add(indexOfMatch);
- }
- }
- }
-
- /**
- * Sets the content for the suggestion view.
- *
- * If the suggestion doesn't contain mUserSearchTerm, nothing is made bold.
- * All instances of mUserSearchTerm in the suggestion are not bold.
- *
- * @param v The View that needs to be populated
- * @param suggestion The suggestion text that will be placed in the view
- * @param isUserSavedSearch whether the suggestion is from history or not
- */
- private void setSuggestionOnView(View v, String suggestion, boolean isUserSavedSearch) {
- final ImageView historyIcon = (ImageView) v.findViewById(R.id.suggestion_item_icon);
- if (isUserSavedSearch) {
- historyIcon.setImageDrawable(mSearchHistorySuggestionIcon);
- historyIcon.setVisibility(View.VISIBLE);
- } else {
- historyIcon.setVisibility(View.GONE);
- }
-
- final TextView suggestionText = (TextView) v.findViewById(R.id.suggestion_text);
- final String searchTerm = getSuggestionTextFromView(mUserEnteredView);
- final int searchTermLength = searchTerm.length();
- refreshOccurrencesWith(searchTerm, suggestion);
- if (mOccurrences.size() > 0) {
- final SpannableStringBuilder sb = new SpannableStringBuilder(suggestion);
- int nextStartSpanIndex = 0;
- // Done to make sure that the stretch of text after the last occurrence, till the end of the suggestion, is made bold
- mOccurrences.add(suggestion.length());
- for (int occurrence : mOccurrences) {
- // Even though they're the same style, SpannableStringBuilder will interpret there as being only one Span present if we re-use a StyleSpan
- StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
- sb.setSpan(boldSpan, nextStartSpanIndex, occurrence, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- nextStartSpanIndex = occurrence + searchTermLength;
- }
- mOccurrences.clear();
- suggestionText.setText(sb);
- } else {
- suggestionText.setText(suggestion);
- }
-
- setDescriptionOnSuggestion(suggestionText, suggestion);
- }
-
- /**
- * Perform a search for the user-entered term.
- */
- public void performUserEnteredSearch() {
- String searchTerm = getSuggestionTextFromView(mUserEnteredView);
- if (mSearchListener != null) {
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "user");
- mSearchListener.onSearch(mSearchEngine, searchTerm, TelemetryContract.Method.SUGGESTION);
- }
- }
-
- public void setSearchTerm(String searchTerm) {
- mUserEnteredTextView.setText(searchTerm);
-
- // mSearchEngine is not set in the first call to this method; the content description
- // is instead initially set in updateSuggestions().
- if (mSearchEngine != null) {
- setDescriptionOnSuggestion(mUserEnteredTextView, searchTerm);
- }
- }
-
- public void setOnUrlOpenListener(OnUrlOpenListener listener) {
- mUrlOpenListener = listener;
- }
-
- public void setOnSearchListener(OnSearchListener listener) {
- mSearchListener = listener;
- }
-
- public void setOnEditSuggestionListener(OnEditSuggestionListener listener) {
- mEditSuggestionListener = listener;
- }
-
- private void bindSuggestionView(String suggestion, boolean animate, int recycledSuggestionCount, Integer previousSuggestionChildIndex, boolean isUserSavedSearch, String telemetryTag) {
- final View suggestionItem;
-
- // Reuse suggestion views from recycled view, if possible.
- if (previousSuggestionChildIndex + 1 < recycledSuggestionCount) {
- suggestionItem = mSuggestionView.getChildAt(previousSuggestionChildIndex + 1);
- suggestionItem.setVisibility(View.VISIBLE);
- } else {
- suggestionItem = mInflater.inflate(R.layout.suggestion_item, null);
-
- suggestionItem.setOnClickListener(mClickListener);
- suggestionItem.setOnLongClickListener(mLongClickListener);
-
- suggestionItem.setTag(telemetryTag);
-
- mSuggestionView.addView(suggestionItem);
- }
-
- setSuggestionOnView(suggestionItem, suggestion, isUserSavedSearch);
-
- if (animate) {
- AlphaAnimation anim = new AlphaAnimation(0, 1);
- anim.setDuration(ANIMATION_DURATION);
- anim.setStartOffset(previousSuggestionChildIndex * ANIMATION_DURATION);
- suggestionItem.startAnimation(anim);
- }
- }
-
- private void hideRecycledSuggestions(int lastVisibleChildIndex, int recycledSuggestionCount) {
- // Hide extra suggestions that have been recycled.
- for (int i = lastVisibleChildIndex + 1; i < recycledSuggestionCount; ++i) {
- mSuggestionView.getChildAt(i).setVisibility(View.GONE);
- }
- }
-
- /**
- * Displays search suggestions from previous searches.
- *
- * @param savedSuggestions The List to iterate over for saved search suggestions to display. This function does not
- * enforce a ui maximum or filter. It will show all the suggestions in this list.
- * @param suggestionStartIndex global index of where to start adding suggestion "buttons" in the search engine row. Also
- * acts as a counter for total number of suggestions visible.
- * @param animate whether or not to animate suggestions for visual polish
- * @param recycledSuggestionCount How many suggestion "button" views we could recycle from previous calls
- */
- private void updateFromSavedSearches(List<String> savedSuggestions, boolean animate, int suggestionStartIndex, int recycledSuggestionCount) {
- if (savedSuggestions == null || savedSuggestions.isEmpty()) {
- hideRecycledSuggestions(suggestionStartIndex, recycledSuggestionCount);
- return;
- }
-
- final int numSavedSearches = savedSuggestions.size();
- int indexOfPreviousSuggestion = 0;
- for (int i = 0; i < numSavedSearches; i++) {
- String telemetryTag = "history." + i;
- final String suggestion = savedSuggestions.get(i);
- indexOfPreviousSuggestion = suggestionStartIndex + i;
- bindSuggestionView(suggestion, animate, recycledSuggestionCount, indexOfPreviousSuggestion, true, telemetryTag);
- }
-
- hideRecycledSuggestions(indexOfPreviousSuggestion + 1, recycledSuggestionCount);
- }
-
- /**
- * Displays suggestions supplied by the search engine, relative to number of suggestions from search history.
- *
- * @param animate whether or not to animate suggestions for visual polish
- * @param recycledSuggestionCount How many suggestion "button" views we could recycle from previous calls
- * @param savedSuggestionCount how many saved searches this searchTerm has
- * @return the global count of how many suggestions have been bound/shown in the search engine row
- */
- private int updateFromSearchEngine(boolean animate, List<String> searchEngineSuggestions, int recycledSuggestionCount, int savedSuggestionCount) {
- int maxSuggestions = mMaxSearchSuggestions;
- // If there are less than max saved searches on phones, fill the space with more search engine suggestions
- if (!HardwareUtils.isTablet() && savedSuggestionCount < mMaxSavedSuggestions) {
- maxSuggestions += mMaxSavedSuggestions - savedSuggestionCount;
- }
-
- final int numSearchEngineSuggestions = searchEngineSuggestions.size();
- int relativeIndex;
- for (relativeIndex = 0; relativeIndex < numSearchEngineSuggestions; relativeIndex++) {
- if (relativeIndex == maxSuggestions) {
- break;
- }
-
- // Since the search engine suggestions are listed first, their relative index is their global index
- String telemetryTag = "engine." + relativeIndex;
- final String suggestion = searchEngineSuggestions.get(relativeIndex);
- bindSuggestionView(suggestion, animate, recycledSuggestionCount, relativeIndex, false, telemetryTag);
- }
-
- hideRecycledSuggestions(relativeIndex + 1, recycledSuggestionCount);
-
- // Make sure mSelectedView is still valid.
- if (mSelectedView >= mSuggestionView.getChildCount()) {
- mSelectedView = mSuggestionView.getChildCount() - 1;
- }
-
- return relativeIndex;
- }
-
- /**
- * Updates the whole suggestions UI, the search engine UI, suggestions from the default search engine,
- * and suggestions from search history.
- *
- * This can be called before the opt-in permission prompt is shown or set.
- * Even if both suggestion types are disabled, we need to update the search engine, its image, and the content description.
- *
- * @param searchSuggestionsEnabled whether or not suggestions from the default search engine are enabled
- * @param searchEngine the search engine to use throughout the SearchEngineRow class
- * @param rawSearchHistorySuggestions search history suggestions
- * @param animate whether or not to use animations
- **/
- public void updateSuggestions(boolean searchSuggestionsEnabled, SearchEngine searchEngine, @Nullable List<String> rawSearchHistorySuggestions, boolean animate) {
- mSearchEngine = searchEngine;
- // Set the search engine icon (e.g., Google) for the row.
-
- mIconView.updateAndScaleImage(IconResponse.create(mSearchEngine.getIcon()));
- // Set the initial content description.
- setDescriptionOnSuggestion(mUserEnteredTextView, mUserEnteredTextView.getText().toString());
-
- final int recycledSuggestionCount = mSuggestionView.getChildCount();
- final SharedPreferences prefs = GeckoSharedPrefs.forApp(getContext());
- final boolean savedSearchesEnabled = prefs.getBoolean(GeckoPreferences.PREFS_HISTORY_SAVED_SEARCH, true);
-
- // Remove duplicates of search engine suggestions from saved searches.
- List<String> searchHistorySuggestions = (rawSearchHistorySuggestions != null) ? rawSearchHistorySuggestions : new ArrayList<String>();
-
- // Filter out URLs and long search suggestions
- Iterator<String> searchHistoryIterator = searchHistorySuggestions.iterator();
- while (searchHistoryIterator.hasNext()) {
- final String currentSearchHistory = searchHistoryIterator.next();
-
- if (currentSearchHistory.length() > 50 || Pattern.matches("^(https?|ftp|file)://.*", currentSearchHistory)) {
- searchHistoryIterator.remove();
- }
- }
-
-
- List<String> searchEngineSuggestions = new ArrayList<String>();
- for (String suggestion : searchEngine.getSuggestions()) {
- searchHistorySuggestions.remove(suggestion);
- searchEngineSuggestions.add(suggestion);
- }
- // Make sure the search term itself isn't duplicated. This is more important on phones than tablets where screen
- // space is more precious.
- searchHistorySuggestions.remove(getSuggestionTextFromView(mUserEnteredView));
-
- // Trim the history suggestions down to the maximum allowed.
- if (searchHistorySuggestions.size() >= mMaxSavedSuggestions) {
- // The second index to subList() is exclusive, so this looks like an off by one error but it is not.
- searchHistorySuggestions = searchHistorySuggestions.subList(0, mMaxSavedSuggestions);
- }
- final int searchHistoryCount = searchHistorySuggestions.size();
-
- if (searchSuggestionsEnabled && savedSearchesEnabled) {
- final int suggestionViewCount = updateFromSearchEngine(animate, searchEngineSuggestions, recycledSuggestionCount, searchHistoryCount);
- updateFromSavedSearches(searchHistorySuggestions, animate, suggestionViewCount, recycledSuggestionCount);
- } else if (savedSearchesEnabled) {
- updateFromSavedSearches(searchHistorySuggestions, animate, 0, recycledSuggestionCount);
- } else if (searchSuggestionsEnabled) {
- updateFromSearchEngine(animate, searchEngineSuggestions, recycledSuggestionCount, 0);
- } else {
- // The current search term is treated separately from the suggestions list, hence we can
- // recycle ALL suggestion items here. (We always show the current search term, i.e. 1 item,
- // in front of the search engine suggestions and/or the search history.)
- hideRecycledSuggestions(0, recycledSuggestionCount);
- }
- }
-
- @Override
- public boolean onKeyDown(int keyCode, android.view.KeyEvent event) {
- final View suggestion = mSuggestionView.getChildAt(mSelectedView);
-
- if (event.getAction() != android.view.KeyEvent.ACTION_DOWN) {
- return false;
- }
-
- switch (event.getKeyCode()) {
- case KeyEvent.KEYCODE_DPAD_RIGHT:
- final View nextSuggestion = mSuggestionView.getChildAt(mSelectedView + 1);
- if (nextSuggestion != null) {
- changeSelectedSuggestion(suggestion, nextSuggestion);
- mSelectedView++;
- return true;
- }
- break;
-
- case KeyEvent.KEYCODE_DPAD_LEFT:
- final View prevSuggestion = mSuggestionView.getChildAt(mSelectedView - 1);
- if (prevSuggestion != null) {
- changeSelectedSuggestion(suggestion, prevSuggestion);
- mSelectedView--;
- return true;
- }
- break;
-
- case KeyEvent.KEYCODE_BUTTON_A:
- // TODO: handle long pressing for editing suggestions
- return suggestion.performClick();
- }
-
- return false;
- }
-
- private void changeSelectedSuggestion(View oldSuggestion, View newSuggestion) {
- oldSuggestion.setDuplicateParentStateEnabled(false);
- newSuggestion.setDuplicateParentStateEnabled(true);
- oldSuggestion.refreshDrawableState();
- newSuggestion.refreshDrawableState();
- }
-
- public void onSelected() {
- mSelectedView = 0;
- mUserEnteredView.setDuplicateParentStateEnabled(true);
- mUserEnteredView.refreshDrawableState();
- }
-
- public void onDeselected() {
- final View suggestion = mSuggestionView.getChildAt(mSelectedView);
- suggestion.setDuplicateParentStateEnabled(false);
- suggestion.refreshDrawableState();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SearchLoader.java b/mobile/android/base/java/org/mozilla/gecko/home/SearchLoader.java
deleted file mode 100644
index f7b5b6586..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/SearchLoader.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import java.util.EnumSet;
-
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.db.BrowserDB.FilterFlags;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.os.Bundle;
-import android.os.SystemClock;
-import android.support.v4.app.LoaderManager;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.Loader;
-
-/**
- * Encapsulates the implementation of the search cursor loader.
- */
-class SearchLoader {
- public static final String LOGTAG = "GeckoSearchLoader";
-
- private static final String KEY_SEARCH_TERM = "search_term";
- private static final String KEY_FILTER_FLAGS = "flags";
-
- private SearchLoader() {
- }
-
- @SuppressWarnings("unchecked")
- public static Loader<Cursor> createInstance(Context context, Bundle args) {
- if (args != null) {
- final String searchTerm = args.getString(KEY_SEARCH_TERM);
- final EnumSet<FilterFlags> flags =
- (EnumSet<FilterFlags>) args.getSerializable(KEY_FILTER_FLAGS);
- return new SearchCursorLoader(context, searchTerm, flags);
- } else {
- return new SearchCursorLoader(context, "", EnumSet.noneOf(FilterFlags.class));
- }
- }
-
- private static Bundle createArgs(String searchTerm, EnumSet<FilterFlags> flags) {
- Bundle args = new Bundle();
- args.putString(SearchLoader.KEY_SEARCH_TERM, searchTerm);
- args.putSerializable(SearchLoader.KEY_FILTER_FLAGS, flags);
-
- return args;
- }
-
- public static void init(LoaderManager manager, int loaderId,
- LoaderCallbacks<Cursor> callbacks, String searchTerm) {
- init(manager, loaderId, callbacks, searchTerm, EnumSet.noneOf(FilterFlags.class));
- }
-
- public static void init(LoaderManager manager, int loaderId,
- LoaderCallbacks<Cursor> callbacks, String searchTerm,
- EnumSet<FilterFlags> flags) {
- final Bundle args = createArgs(searchTerm, flags);
- manager.initLoader(loaderId, args, callbacks);
- }
-
- public static void restart(LoaderManager manager, int loaderId,
- LoaderCallbacks<Cursor> callbacks, String searchTerm) {
- restart(manager, loaderId, callbacks, searchTerm, EnumSet.noneOf(FilterFlags.class));
- }
-
- public static void restart(LoaderManager manager, int loaderId,
- LoaderCallbacks<Cursor> callbacks, String searchTerm,
- EnumSet<FilterFlags> flags) {
- final Bundle args = createArgs(searchTerm, flags);
- manager.restartLoader(loaderId, args, callbacks);
- }
-
- public static class SearchCursorLoader extends SimpleCursorLoader {
- private static final String TELEMETRY_HISTOGRAM_LOAD_CURSOR = "FENNEC_SEARCH_LOADER_TIME_MS";
-
- // Max number of search results.
- private static final int SEARCH_LIMIT = 100;
-
- // The target search term associated with the loader.
- private final String mSearchTerm;
-
- // The filter flags associated with the loader.
- private final EnumSet<FilterFlags> mFlags;
- private final GeckoProfile mProfile;
-
- public SearchCursorLoader(Context context, String searchTerm, EnumSet<FilterFlags> flags) {
- super(context);
- mSearchTerm = searchTerm;
- mFlags = flags;
- mProfile = GeckoProfile.get(context);
- }
-
- @Override
- public Cursor loadCursor() {
- final long start = SystemClock.uptimeMillis();
- final Cursor cursor = BrowserDB.from(mProfile).filter(getContext().getContentResolver(), mSearchTerm, SEARCH_LIMIT, mFlags);
- final long end = SystemClock.uptimeMillis();
- final long took = end - start;
- Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_LOAD_CURSOR, (int) Math.min(took, Integer.MAX_VALUE));
- return cursor;
- }
-
- public String getSearchTerm() {
- return mSearchTerm;
- }
- }
-
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SimpleCursorLoader.java b/mobile/android/base/java/org/mozilla/gecko/home/SimpleCursorLoader.java
deleted file mode 100644
index b8889c033..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/SimpleCursorLoader.java
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * This is an adapted version of Android's original CursorLoader
- * without all the ContentProvider-specific bits.
- *
- * 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 org.mozilla.gecko.home;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.support.v4.content.AsyncTaskLoader;
-
-import org.mozilla.gecko.GeckoApplication;
-
-/**
- * A copy of the framework's {@link android.content.CursorLoader} that
- * instead allows the caller to load the Cursor themselves via the abstract
- * {@link #loadCursor()} method, rather than calling out to a ContentProvider via
- * class methods.
- *
- * For new code, prefer {@link android.content.CursorLoader} (see @deprecated).
- *
- * This was originally created to re-use existing code which loaded Cursors manually.
- *
- * @deprecated since the framework provides an implementation, we'd like to eventually remove
- * this class to reduce maintenance burden. Originally planned for bug 1239491, but
- * it'd be more efficient to do this over time, rather than all at once.
- */
-@Deprecated
-public abstract class SimpleCursorLoader extends AsyncTaskLoader<Cursor> {
- final ForceLoadContentObserver mObserver;
- Cursor mCursor;
-
- public SimpleCursorLoader(Context context) {
- super(context);
- mObserver = new ForceLoadContentObserver();
- }
-
- /**
- * Loads the target cursor for this loader. This method is called
- * on a worker thread.
- */
- protected abstract Cursor loadCursor();
-
- /* Runs on a worker thread */
- @Override
- public Cursor loadInBackground() {
- Cursor cursor = loadCursor();
-
- if (cursor != null) {
- // Ensure the cursor window is filled
- cursor.getCount();
- cursor.registerContentObserver(mObserver);
- }
-
- return cursor;
- }
-
- /* Runs on the UI thread */
- @Override
- public void deliverResult(Cursor cursor) {
- if (isReset()) {
- // An async query came in while the loader is stopped
- if (cursor != null) {
- cursor.close();
- }
-
- return;
- }
-
- Cursor oldCursor = mCursor;
- mCursor = cursor;
-
- if (isStarted()) {
- super.deliverResult(cursor);
- }
-
- if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) {
- oldCursor.close();
-
- // Trying to read from the closed cursor will cause crashes, hence we should make
- // sure that no adapters/LoaderCallbacks are holding onto this cursor.
- GeckoApplication.getRefWatcher(getContext()).watch(oldCursor);
- }
- }
-
- /**
- * Starts an asynchronous load of the list data. When the result is ready the callbacks
- * will be called on the UI thread. If a previous load has been completed and is still valid
- * the result may be passed to the callbacks immediately.
- *
- * Must be called from the UI thread
- */
- @Override
- protected void onStartLoading() {
- if (mCursor != null) {
- deliverResult(mCursor);
- }
-
- if (takeContentChanged() || mCursor == null) {
- forceLoad();
- }
- }
-
- /**
- * Must be called from the UI thread
- */
- @Override
- protected void onStopLoading() {
- // Attempt to cancel the current load task if possible.
- cancelLoad();
- }
-
- @Override
- public void onCanceled(Cursor cursor) {
- if (cursor != null && !cursor.isClosed()) {
- cursor.close();
- }
- }
-
- @Override
- protected void onReset() {
- super.onReset();
-
- // Ensure the loader is stopped
- onStopLoading();
-
- if (mCursor != null && !mCursor.isClosed()) {
- mCursor.close();
- }
-
- mCursor = null;
- }
-} \ No newline at end of file
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SpacingDecoration.java b/mobile/android/base/java/org/mozilla/gecko/home/SpacingDecoration.java
deleted file mode 100644
index 039b65e82..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/SpacingDecoration.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package org.mozilla.gecko.home;
-
-import android.graphics.Rect;
-import android.support.v7.widget.RecyclerView;
-import android.view.View;
-
-public class SpacingDecoration extends RecyclerView.ItemDecoration {
- private final int horizontalSpacing;
- private final int verticalSpacing;
-
- public SpacingDecoration(int horizontalSpacing, int verticalSpacing) {
- this.horizontalSpacing = horizontalSpacing;
- this.verticalSpacing = verticalSpacing;
- }
-
- @Override
- public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
- outRect.set(horizontalSpacing, verticalSpacing, horizontalSpacing, verticalSpacing);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/TabMenuStrip.java b/mobile/android/base/java/org/mozilla/gecko/home/TabMenuStrip.java
deleted file mode 100644
index b302d3522..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/TabMenuStrip.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.R;
-
-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.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewTreeObserver;
-import android.view.accessibility.AccessibilityEvent;
-import android.widget.HorizontalScrollView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-/**
- * {@code TabMenuStrip} is the view used to display {@code HomePager} tabs
- * on tablets. See {@code TabMenuStripLayout} for details about how the
- * tabs are created and updated.
- */
-public class TabMenuStrip extends HorizontalScrollView
- implements HomePager.Decor {
-
- // Offset between the selected tab title and the edge of the screen,
- // except for the first and last tab in the tab strip.
- private static final int TITLE_OFFSET_DIPS = 24;
-
- private final int titleOffset;
- private final TabMenuStripLayout layout;
-
- private final Paint shadowPaint;
- private final int shadowSize;
-
- public interface OnTitleClickListener {
- void onTitleClicked(int index);
- }
-
- public TabMenuStrip(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- // Disable the scroll bar.
- setHorizontalScrollBarEnabled(false);
- setFillViewport(true);
-
- final Resources res = getResources();
-
- titleOffset = (int) (TITLE_OFFSET_DIPS * res.getDisplayMetrics().density);
-
- layout = new TabMenuStripLayout(context, attrs);
- addView(layout, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
-
- shadowSize = res.getDimensionPixelSize(R.dimen.tabs_strip_shadow_size);
-
- shadowPaint = new Paint();
- shadowPaint.setColor(ContextCompat.getColor(context, R.color.url_bar_shadow));
- shadowPaint.setStrokeWidth(0.0f);
- }
-
- @Override
- public void draw(Canvas canvas) {
- super.draw(canvas);
-
- final int height = getHeight();
- canvas.drawRect(0, height - shadowSize, layout.getWidth(), height, shadowPaint);
- }
-
- @Override
- public void onAddPagerView(String title) {
- layout.onAddPagerView(title);
- }
-
- @Override
- public void removeAllPagerViews() {
- layout.removeAllViews();
- }
-
- @Override
- public void onPageSelected(final int position) {
- layout.onPageSelected(position);
- }
-
- @Override
- public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
- layout.onPageScrolled(position, positionOffset, positionOffsetPixels);
-
- final View selectedTitle = layout.getChildAt(position);
- if (selectedTitle == null) {
- return;
- }
-
- final int selectedTitleOffset = (int) (positionOffset * selectedTitle.getWidth());
-
- int titleLeft = selectedTitle.getLeft() + selectedTitleOffset;
- if (position > 0) {
- titleLeft -= titleOffset;
- }
-
- int titleRight = selectedTitle.getRight() + selectedTitleOffset;
- if (position < layout.getChildCount() - 1) {
- titleRight += titleOffset;
- }
-
- final int scrollX = getScrollX();
- if (titleLeft < scrollX) {
- // Tab strip overflows to the left.
- scrollTo(titleLeft, 0);
- } else if (titleRight > scrollX + getWidth()) {
- // Tab strip overflows to the right.
- scrollTo(titleRight - getWidth(), 0);
- }
- }
-
- @Override
- public void setOnTitleClickListener(OnTitleClickListener onTitleClickListener) {
- layout.setOnTitleClickListener(onTitleClickListener);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/TabMenuStripLayout.java b/mobile/android/base/java/org/mozilla/gecko/home/TabMenuStripLayout.java
deleted file mode 100644
index a09add80b..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/TabMenuStripLayout.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-
-import android.content.res.ColorStateList;
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewTreeObserver;
-import android.view.accessibility.AccessibilityEvent;
-import android.widget.TextView;
-
-/**
- * {@code TabMenuStripLayout} is the view that draws the {@code HomePager}
- * tabs that are displayed in {@code TabMenuStrip}.
- */
-class TabMenuStripLayout extends LinearLayout
- implements View.OnFocusChangeListener {
-
- private TabMenuStrip.OnTitleClickListener onTitleClickListener;
- private Drawable strip;
- private TextView selectedView;
-
- // Data associated with the scrolling of the strip drawable.
- private View toTab;
- private View fromTab;
- private int fromPosition;
- private int toPosition;
- private float progress;
-
- // This variable is used to predict the direction of scroll.
- private float prevProgress;
- private int tabContentStart;
- private boolean titlebarFill;
- private int activeTextColor;
- private ColorStateList inactiveTextColor;
-
- TabMenuStripLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabMenuStrip);
- final int stripResId = a.getResourceId(R.styleable.TabMenuStrip_strip, -1);
-
- titlebarFill = a.getBoolean(R.styleable.TabMenuStrip_titlebarFill, false);
- tabContentStart = a.getDimensionPixelSize(R.styleable.TabMenuStrip_tabsMarginLeft, 0);
- activeTextColor = a.getColor(R.styleable.TabMenuStrip_activeTextColor, R.color.text_and_tabs_tray_grey);
- inactiveTextColor = a.getColorStateList(R.styleable.TabMenuStrip_inactiveTextColor);
- a.recycle();
-
- if (stripResId != -1) {
- strip = getResources().getDrawable(stripResId);
- }
-
- setWillNotDraw(false);
- }
-
- void onAddPagerView(String title) {
- final TextView button = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.tab_menu_strip, this, false);
- button.setText(title.toUpperCase());
- button.setTextColor(inactiveTextColor);
-
- // Set titles width to weight, or wrap text width.
- if (titlebarFill) {
- button.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 1.0f));
- } else {
- button.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
- }
-
- if (getChildCount() == 0) {
- button.setPadding(button.getPaddingLeft() + tabContentStart,
- button.getPaddingTop(),
- button.getPaddingRight(),
- button.getPaddingBottom());
- }
-
- addView(button);
- button.setOnClickListener(new ViewClickListener(getChildCount() - 1));
- button.setOnFocusChangeListener(this);
- }
-
- void onPageSelected(final int position) {
- if (selectedView != null) {
- selectedView.setTextColor(inactiveTextColor);
- }
-
- selectedView = (TextView) getChildAt(position);
- selectedView.setTextColor(activeTextColor);
-
- // Callback to measure and draw the strip after the view is visible.
- ViewTreeObserver vto = selectedView.getViewTreeObserver();
- if (vto.isAlive()) {
- vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
- @Override
- public void onGlobalLayout() {
- selectedView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
-
- if (strip != null) {
- strip.setBounds(selectedView.getLeft() + (position == 0 ? tabContentStart : 0),
- selectedView.getTop(),
- selectedView.getRight(),
- selectedView.getBottom());
- }
-
- prevProgress = position;
- }
- });
- }
- }
-
- // Page scroll animates the drawable and its bounds from the previous to next child view.
- void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
- if (strip == null) {
- return;
- }
-
- setScrollingData(position, positionOffset);
-
- if (fromTab == null || toTab == null) {
- return;
- }
-
- final int fromTabLeft = fromTab.getLeft();
- final int fromTabRight = fromTab.getRight();
-
- final int toTabLeft = toTab.getLeft();
- final int toTabRight = toTab.getRight();
-
- // The first tab has a padding applied (tabContentStart). We don't want the 'strip' to jump around so we remove
- // this padding slowly (modifier) when scrolling to or from the first tab.
- final int modifier;
-
- if (fromPosition == 0 && toPosition == 1) {
- // Slowly remove extra padding (tabContentStart) based on scroll progress
- modifier = (int) (tabContentStart * (1 - progress));
- } else if (fromPosition == 1 && toPosition == 0) {
- // Slowly add extra padding (tabContentStart) based on scroll progress
- modifier = (int) (tabContentStart * progress);
- } else {
- // We are not scrolling tab 0 in any way, no modifier needed
- modifier = 0;
- }
-
- strip.setBounds((int) (fromTabLeft + ((toTabLeft - fromTabLeft) * progress)) + modifier,
- 0,
- (int) (fromTabRight + ((toTabRight - fromTabRight) * progress)),
- getHeight());
- invalidate();
- }
-
- /*
- * position + positionOffset goes from 0 to 2 as we scroll from page 1 to 3.
- * Normalized progress is relative to the the direction the page is being scrolled towards.
- * For this, we maintain direction of scroll with a state, and the child view we are moving towards and away from.
- */
- void setScrollingData(int position, float positionOffset) {
- if (position >= getChildCount() - 1) {
- return;
- }
-
- final float currProgress = position + positionOffset;
-
- if (prevProgress > currProgress) {
- toPosition = position;
- fromPosition = position + 1;
- progress = 1 - positionOffset;
- } else {
- toPosition = position + 1;
- fromPosition = position;
- progress = positionOffset;
- }
-
- toTab = getChildAt(toPosition);
- fromTab = getChildAt(fromPosition);
-
- prevProgress = currProgress;
- }
-
- @Override
- public void onDraw(Canvas canvas) {
- super.onDraw(canvas);
-
- if (strip != null) {
- strip.draw(canvas);
- }
- }
-
- @Override
- public void onFocusChange(View v, boolean hasFocus) {
- if (v == this && hasFocus && getChildCount() > 0) {
- selectedView.requestFocus();
- return;
- }
-
- if (!hasFocus) {
- return;
- }
-
- int i = 0;
- final int numTabs = getChildCount();
-
- while (i < numTabs) {
- View view = getChildAt(i);
- if (view == v) {
- view.requestFocus();
- if (isShown()) {
- // A view is focused so send an event to announce the menu strip state.
- sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
- }
- break;
- }
-
- i++;
- }
- }
-
- void setOnTitleClickListener(TabMenuStrip.OnTitleClickListener onTitleClickListener) {
- this.onTitleClickListener = onTitleClickListener;
- }
-
- private class ViewClickListener implements OnClickListener {
- private final int mIndex;
-
- public ViewClickListener(int index) {
- mIndex = index;
- }
-
- @Override
- public void onClick(View view) {
- if (onTitleClickListener != null) {
- onTitleClickListener.onTitleClicked(mIndex);
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridItemView.java b/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridItemView.java
deleted file mode 100644
index c17aff209..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridItemView.java
+++ /dev/null
@@ -1,312 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.widget.ImageView.ScaleType;
-import android.widget.RelativeLayout;
-import android.widget.TextView;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.BrowserContract.TopSites;
-import org.mozilla.gecko.icons.IconCallback;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.icons.Icons;
-
-import java.util.concurrent.Future;
-
-/**
- * A view that displays the thumbnail and the title/url for a top/pinned site.
- * If the title/url is longer than the width of the view, they are faded out.
- * If there is no valid url, a default string is shown at 50% opacity.
- * This is denoted by the empty state.
- */
-public class TopSitesGridItemView extends RelativeLayout implements IconCallback {
- private static final String LOGTAG = "GeckoTopSitesGridItemView";
-
- // Empty state, to denote there is no valid url.
- private static final int[] STATE_EMPTY = { android.R.attr.state_empty };
-
- private static final ScaleType SCALE_TYPE_FAVICON = ScaleType.CENTER;
- private static final ScaleType SCALE_TYPE_RESOURCE = ScaleType.CENTER;
- private static final ScaleType SCALE_TYPE_THUMBNAIL = ScaleType.CENTER_CROP;
- private static final ScaleType SCALE_TYPE_URL = ScaleType.CENTER_INSIDE;
-
- // Child views.
- private final TextView mTitleView;
- private final TopSitesThumbnailView mThumbnailView;
-
- // Data backing this view.
- private String mTitle;
- private String mUrl;
-
- private boolean mThumbnailSet;
-
- // Matches BrowserContract.TopSites row types
- private int mType = -1;
-
- // Dirty state.
- private boolean mIsDirty;
-
- private Future<IconResponse> mOngoingIconRequest;
-
- public TopSitesGridItemView(Context context) {
- this(context, null);
- }
-
- public TopSitesGridItemView(Context context, AttributeSet attrs) {
- this(context, attrs, R.attr.topSitesGridItemViewStyle);
- }
-
- public TopSitesGridItemView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
-
- LayoutInflater.from(context).inflate(R.layout.top_sites_grid_item_view, this);
-
- mTitleView = (TextView) findViewById(R.id.title);
- mThumbnailView = (TopSitesThumbnailView) findViewById(R.id.thumbnail);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int[] onCreateDrawableState(int extraSpace) {
- final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
-
- if (mType == TopSites.TYPE_BLANK) {
- mergeDrawableStates(drawableState, STATE_EMPTY);
- }
-
- return drawableState;
- }
-
- /**
- * @return The title shown by this view.
- */
- public String getTitle() {
- return (!TextUtils.isEmpty(mTitle) ? mTitle : mUrl);
- }
-
- /**
- * @return The url shown by this view.
- */
- public String getUrl() {
- return mUrl;
- }
-
- /**
- * @return The site type associated with this view.
- */
- public int getType() {
- return mType;
- }
-
- /**
- * @param title The title for this view.
- */
- public void setTitle(String title) {
- if (mTitle != null && mTitle.equals(title)) {
- return;
- }
-
- mTitle = title;
- updateTitleView();
- }
-
- /**
- * @param url The url for this view.
- */
- public void setUrl(String url) {
- if (mUrl != null && mUrl.equals(url)) {
- return;
- }
-
- mUrl = url;
- updateTitleView();
- }
-
- public void blankOut() {
- mUrl = "";
- mTitle = "";
- updateType(TopSites.TYPE_BLANK);
- updateTitleView();
- cancelIconLoading();
- ImageLoader.with(getContext()).cancelRequest(mThumbnailView);
- displayThumbnail(R.drawable.top_site_add);
-
- }
-
- public void markAsDirty() {
- mIsDirty = true;
- }
-
- /**
- * Updates the title, URL, and pinned state of this view.
- *
- * Also resets our loadId to NOT_LOADING.
- *
- * Returns true if any fields changed.
- */
- public boolean updateState(final String title, final String url, final int type, final TopSitesPanel.ThumbnailInfo thumbnail) {
- boolean changed = false;
- if (mUrl == null || !mUrl.equals(url)) {
- mUrl = url;
- changed = true;
- }
-
- if (mTitle == null || !mTitle.equals(title)) {
- mTitle = title;
- changed = true;
- }
-
- if (thumbnail != null) {
- if (thumbnail.imageUrl != null) {
- displayThumbnail(thumbnail.imageUrl, thumbnail.bgColor);
- } else if (thumbnail.bitmap != null) {
- displayThumbnail(thumbnail.bitmap);
- }
- } else if (changed) {
- // Because we'll have a new favicon or thumbnail arriving shortly, and
- // we need to not reject it because we already had a thumbnail.
- mThumbnailSet = false;
- }
-
- if (changed) {
- updateTitleView();
- cancelIconLoading();
- ImageLoader.with(getContext()).cancelRequest(mThumbnailView);
- }
-
- if (updateType(type)) {
- changed = true;
- }
-
- // The dirty state forces the state update to return true
- // so that the adapter loads favicons once the thumbnails
- // are loaded in TopSitesPanel/TopSitesGridAdapter.
- changed = (changed || mIsDirty);
- mIsDirty = false;
-
- return changed;
- }
-
- /**
- * Try to load an icon for the given page URL.
- */
- public void loadFavicon(String pageUrl) {
- mOngoingIconRequest = Icons.with(getContext())
- .pageUrl(pageUrl)
- .skipNetwork()
- .build()
- .execute(this);
- }
-
- private void cancelIconLoading() {
- if (mOngoingIconRequest != null) {
- mOngoingIconRequest.cancel(true);
- }
- }
-
- /**
- * Display the thumbnail from a resource.
- *
- * @param resId Resource ID of the drawable to show.
- */
- public void displayThumbnail(int resId) {
- mThumbnailView.setScaleType(SCALE_TYPE_RESOURCE);
- mThumbnailView.setImageResource(resId);
- mThumbnailView.setBackgroundColor(0x0);
- mThumbnailSet = false;
- }
-
- /**
- * Display the thumbnail from a bitmap.
- *
- * @param thumbnail The bitmap to show as thumbnail.
- */
- public void displayThumbnail(Bitmap thumbnail) {
- if (thumbnail == null) {
- return;
- }
-
- mThumbnailSet = true;
-
- cancelIconLoading();
- ImageLoader.with(getContext()).cancelRequest(mThumbnailView);
-
- mThumbnailView.setScaleType(SCALE_TYPE_THUMBNAIL);
- mThumbnailView.setImageBitmap(thumbnail, true);
- mThumbnailView.setBackgroundDrawable(null);
- }
-
- /**
- * Display the thumbnail from a URL.
- *
- * @param imageUrl URL of the image to show.
- * @param bgColor background color to use in the view.
- */
- public void displayThumbnail(final String imageUrl, final int bgColor) {
- mThumbnailView.setScaleType(SCALE_TYPE_URL);
- mThumbnailView.setBackgroundColor(bgColor);
- mThumbnailSet = true;
-
- ImageLoader.with(getContext())
- .load(imageUrl)
- .noFade()
- .into(mThumbnailView);
- }
-
- /**
- * Update the item type associated with this view. Returns true if
- * the type has changed, false otherwise.
- */
- private boolean updateType(int type) {
- if (mType == type) {
- return false;
- }
-
- mType = type;
- refreshDrawableState();
-
- int pinResourceId = (type == TopSites.TYPE_PINNED ? R.drawable.pin : 0);
- mTitleView.setCompoundDrawablesWithIntrinsicBounds(pinResourceId, 0, 0, 0);
-
- return true;
- }
-
- /**
- * Update the title shown by this view. If both title and url
- * are empty, mark the state as STATE_EMPTY and show a default text.
- */
- private void updateTitleView() {
- String title = getTitle();
- if (!TextUtils.isEmpty(title)) {
- mTitleView.setText(title);
- } else {
- mTitleView.setText(R.string.home_top_sites_add);
- }
- }
-
- /**
- * Display the loaded icon (if no thumbnail is set).
- */
- @Override
- public void onIconResponse(IconResponse response) {
- if (mThumbnailSet) {
- // Already showing a thumbnail; do nothing.
- return;
- }
-
- mThumbnailView.setScaleType(SCALE_TYPE_FAVICON);
- mThumbnailView.setImageBitmap(response.getBitmap(), false);
- mThumbnailView.setBackgroundColorWithOpacityFilter(response.getColor());
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridView.java b/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridView.java
deleted file mode 100644
index 58a05b198..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridView.java
+++ /dev/null
@@ -1,169 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.ThumbnailHelper;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.view.ContextMenu.ContextMenuInfo;
-import android.view.View;
-import android.widget.AbsListView;
-import android.widget.GridView;
-
-/**
- * A grid view of top and pinned sites.
- * Each cell in the grid is a TopSitesGridItemView.
- */
-public class TopSitesGridView extends GridView {
- private static final String LOGTAG = "GeckoTopSitesGridView";
-
- // Listener for editing pinned sites.
- public static interface OnEditPinnedSiteListener {
- public void onEditPinnedSite(int position, String searchTerm);
- }
-
- // Max number of top sites that needs to be shown.
- private final int mMaxSites;
-
- // Number of columns to show.
- private final int mNumColumns;
-
- // Horizontal spacing in between the rows.
- private final int mHorizontalSpacing;
-
- // Vertical spacing in between the rows.
- private final int mVerticalSpacing;
-
- // Measured width of this view.
- private int mMeasuredWidth;
-
- // Measured height of this view.
- private int mMeasuredHeight;
-
- // A dummy View used to measure the required size of the child Views.
- private final TopSitesGridItemView dummyChildView;
-
- // Context menu info.
- private TopSitesGridContextMenuInfo mContextMenuInfo;
-
- // Whether we're handling focus changes or not. This is used
- // to avoid infinite re-layouts when using this GridView as
- // a ListView header view (see bug 918044).
- private boolean mIsHandlingFocusChange;
-
- public TopSitesGridView(Context context) {
- this(context, null);
- }
-
- public TopSitesGridView(Context context, AttributeSet attrs) {
- this(context, attrs, R.attr.topSitesGridViewStyle);
- }
-
- public TopSitesGridView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- mMaxSites = getResources().getInteger(R.integer.number_of_top_sites);
- mNumColumns = getResources().getInteger(R.integer.number_of_top_sites_cols);
- setNumColumns(mNumColumns);
-
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TopSitesGridView, defStyle, 0);
- mHorizontalSpacing = a.getDimensionPixelOffset(R.styleable.TopSitesGridView_android_horizontalSpacing, 0x00);
- mVerticalSpacing = a.getDimensionPixelOffset(R.styleable.TopSitesGridView_android_verticalSpacing, 0x00);
- a.recycle();
-
- dummyChildView = new TopSitesGridItemView(context);
- // Set a default LayoutParams on the child, if it doesn't have one on its own.
- AbsListView.LayoutParams params = (AbsListView.LayoutParams) dummyChildView.getLayoutParams();
- if (params == null) {
- params = new AbsListView.LayoutParams(AbsListView.LayoutParams.WRAP_CONTENT,
- AbsListView.LayoutParams.WRAP_CONTENT);
- dummyChildView.setLayoutParams(params);
- }
- }
-
- @Override
- protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
- mIsHandlingFocusChange = true;
- super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
- mIsHandlingFocusChange = false;
- }
-
- @Override
- public void requestLayout() {
- if (!mIsHandlingFocusChange) {
- super.requestLayout();
- }
- }
-
- @Override
- public int getColumnWidth() {
- // This method will be called from onMeasure() too.
- // It's better to use getMeasuredWidth(), as it is safe in this case.
- final int totalHorizontalSpacing = mNumColumns > 0 ? (mNumColumns - 1) * mHorizontalSpacing : 0;
- return (getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - totalHorizontalSpacing) / mNumColumns;
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- // Sets the padding for this view.
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- final int measuredWidth = getMeasuredWidth();
- if (measuredWidth == mMeasuredWidth) {
- // Return the cached values as the width is the same.
- setMeasuredDimension(mMeasuredWidth, mMeasuredHeight);
- return;
- }
-
- final int columnWidth = getColumnWidth();
-
- // Measure the exact width of the child, and the height based on the width.
- // Note: the child (and TopSitesThumbnailView) takes care of calculating its height.
- int childWidthSpec = MeasureSpec.makeMeasureSpec(columnWidth, MeasureSpec.EXACTLY);
- int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
- dummyChildView.measure(childWidthSpec, childHeightSpec);
- final int childHeight = dummyChildView.getMeasuredHeight();
-
- // This is the maximum width of the contents of each child in the grid.
- // Use this as the target width for thumbnails.
- final int thumbnailWidth = dummyChildView.getMeasuredWidth() - dummyChildView.getPaddingLeft() - dummyChildView.getPaddingRight();
- ThumbnailHelper.getInstance().setThumbnailWidth(thumbnailWidth);
-
- // Number of rows required to show these top sites.
- final int rows = (int) Math.ceil((double) mMaxSites / mNumColumns);
- final int childrenHeight = childHeight * rows;
- final int totalVerticalSpacing = rows > 0 ? (rows - 1) * mVerticalSpacing : 0;
-
- // Total height of this view.
- final int measuredHeight = childrenHeight + getPaddingTop() + getPaddingBottom() + totalVerticalSpacing;
- setMeasuredDimension(measuredWidth, measuredHeight);
- mMeasuredWidth = measuredWidth;
- mMeasuredHeight = measuredHeight;
- }
-
- @Override
- public ContextMenuInfo getContextMenuInfo() {
- return mContextMenuInfo;
- }
-
- public void setContextMenuInfo(TopSitesGridContextMenuInfo contextMenuInfo) {
- mContextMenuInfo = contextMenuInfo;
- }
-
- /**
- * Stores information regarding the creation of the context menu for a GridView item.
- */
- public static class TopSitesGridContextMenuInfo extends HomeContextMenuInfo {
- public int type = -1;
-
- public TopSitesGridContextMenuInfo(View targetView, int position, long id) {
- super(targetView, position, id);
- this.itemType = RemoveItemType.HISTORY;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesPanel.java b/mobile/android/base/java/org/mozilla/gecko/home/TopSitesPanel.java
deleted file mode 100644
index f39e51ac5..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesPanel.java
+++ /dev/null
@@ -1,968 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import static org.mozilla.gecko.db.URLMetadataTable.TILE_COLOR_COLUMN;
-import static org.mozilla.gecko.db.URLMetadataTable.TILE_IMAGE_URL_COLUMN;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.Future;
-
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.db.BrowserContract.Thumbnails;
-import org.mozilla.gecko.db.BrowserContract.TopSites;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.gfx.BitmapUtils;
-import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
-import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
-import org.mozilla.gecko.home.PinSiteDialog.OnSiteSelectedListener;
-import org.mozilla.gecko.home.TopSitesGridView.OnEditPinnedSiteListener;
-import org.mozilla.gecko.home.TopSitesGridView.TopSitesGridContextMenuInfo;
-import org.mozilla.gecko.icons.IconCallback;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.icons.Icons;
-import org.mozilla.gecko.restrictions.Restrictable;
-import org.mozilla.gecko.restrictions.Restrictions;
-import org.mozilla.gecko.util.StringUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.app.Activity;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.os.Bundle;
-import android.os.SystemClock;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.AsyncTaskLoader;
-import android.support.v4.content.Loader;
-import android.support.v4.widget.CursorAdapter;
-import android.text.TextUtils;
-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.AdapterView;
-import android.widget.ListView;
-
-/**
- * Fragment that displays frecency search results in a ListView.
- */
-public class TopSitesPanel extends HomeFragment {
- // Logging tag name
- private static final String LOGTAG = "GeckoTopSitesPanel";
-
- // Cursor loader ID for the top sites
- private static final int LOADER_ID_TOP_SITES = 0;
-
- // Loader ID for thumbnails
- private static final int LOADER_ID_THUMBNAILS = 1;
-
- // Key for thumbnail urls
- private static final String THUMBNAILS_URLS_KEY = "urls";
-
- // Adapter for the list of top sites
- private VisitedAdapter mListAdapter;
-
- // Adapter for the grid of top sites
- private TopSitesGridAdapter mGridAdapter;
-
- // List of top sites
- private HomeListView mList;
-
- // Grid of top sites
- private TopSitesGridView mGrid;
-
- // Callbacks used for the search and favicon cursor loaders
- private CursorLoaderCallbacks mCursorLoaderCallbacks;
-
- // Callback for thumbnail loader
- private ThumbnailsLoaderCallbacks mThumbnailsLoaderCallbacks;
-
- // Listener for editing pinned sites.
- private EditPinnedSiteListener mEditPinnedSiteListener;
-
- // Max number of entries shown in the grid from the cursor.
- private int mMaxGridEntries;
-
- // Time in ms until the Gecko thread is reset to normal priority.
- private static final long PRIORITY_RESET_TIMEOUT = 10000;
-
- public static TopSitesPanel newInstance() {
- return new TopSitesPanel();
- }
-
- private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
- private static final boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
-
- private static void debug(final String message) {
- if (logDebug) {
- Log.d(LOGTAG, message);
- }
- }
-
- private static void trace(final String message) {
- if (logVerbose) {
- Log.v(LOGTAG, message);
- }
- }
-
- @Override
- public void onAttach(Activity activity) {
- super.onAttach(activity);
-
- mMaxGridEntries = activity.getResources().getInteger(R.integer.number_of_top_sites);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- final View view = inflater.inflate(R.layout.home_top_sites_panel, container, false);
-
- mList = (HomeListView) view.findViewById(R.id.list);
-
- mGrid = new TopSitesGridView(getActivity());
- mList.addHeaderView(mGrid);
-
- return view;
- }
-
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- mEditPinnedSiteListener = new EditPinnedSiteListener();
-
- mList.setTag(HomePager.LIST_TAG_TOP_SITES);
- mList.setHeaderDividersEnabled(false);
-
- mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- final ListView list = (ListView) parent;
- final int headerCount = list.getHeaderViewsCount();
- if (position < headerCount) {
- // The click is on a header, don't do anything.
- return;
- }
-
- // Absolute position for the adapter.
- position += (mGridAdapter.getCount() - headerCount);
-
- final Cursor c = mListAdapter.getCursor();
- if (c == null || !c.moveToPosition(position)) {
- return;
- }
-
- final String url = c.getString(c.getColumnIndexOrThrow(TopSites.URL));
-
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, "top_sites");
-
- // This item is a TwoLinePageRow, so we allow switch-to-tab.
- mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
- }
- });
-
- mList.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() {
- @Override
- public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
- final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
- info.url = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.URL));
- info.title = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.TITLE));
- info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(TopSites.HISTORY_ID));
- info.itemType = RemoveItemType.HISTORY;
- final int bookmarkIdCol = cursor.getColumnIndexOrThrow(TopSites.BOOKMARK_ID);
- if (cursor.isNull(bookmarkIdCol)) {
- // If this is a combined cursor, we may get a history item without a
- // bookmark, in which case the bookmarks ID column value will be null.
- info.bookmarkId = -1;
- } else {
- info.bookmarkId = cursor.getInt(bookmarkIdCol);
- }
- return info;
- }
- });
-
- mGrid.setOnItemClickListener(new AdapterView.OnItemClickListener() {
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- TopSitesGridItemView item = (TopSitesGridItemView) view;
-
- // Decode "user-entered" URLs before loading them.
- String url = StringUtils.decodeUserEnteredUrl(item.getUrl());
- int type = item.getType();
-
- // If the url is empty, the user can pin a site.
- // If not, navigate to the page given by the url.
- if (type != TopSites.TYPE_BLANK) {
- if (mUrlOpenListener != null) {
- final TelemetryContract.Method method;
- if (type == TopSites.TYPE_SUGGESTED) {
- method = TelemetryContract.Method.SUGGESTION;
- } else {
- method = TelemetryContract.Method.GRID_ITEM;
- }
-
- String extra = Integer.toString(position);
- if (type == TopSites.TYPE_PINNED) {
- extra += "-pinned";
- }
-
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, method, extra);
-
- mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.NO_READER_VIEW));
- }
- } else {
- if (mEditPinnedSiteListener != null) {
- mEditPinnedSiteListener.onEditPinnedSite(position, "");
- }
- }
- }
- });
-
- mGrid.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
- @Override
- public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
-
- Cursor cursor = (Cursor) parent.getItemAtPosition(position);
-
- TopSitesGridItemView item = (TopSitesGridItemView) view;
- if (cursor == null || item.getType() == TopSites.TYPE_BLANK) {
- mGrid.setContextMenuInfo(null);
- return false;
- }
-
- TopSitesGridContextMenuInfo contextMenuInfo = new TopSitesGridContextMenuInfo(view, position, id);
- updateContextMenuFromCursor(contextMenuInfo, cursor);
- mGrid.setContextMenuInfo(contextMenuInfo);
- return mGrid.showContextMenuForChild(mGrid);
- }
-
- /*
- * Update the fields of a TopSitesGridContextMenuInfo object
- * from a cursor.
- *
- * @param info context menu info object to be updated
- * @param cursor used to update the context menu info object
- */
- private void updateContextMenuFromCursor(TopSitesGridContextMenuInfo info, Cursor cursor) {
- info.url = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.URL));
- info.title = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.TITLE));
- info.type = cursor.getInt(cursor.getColumnIndexOrThrow(TopSites.TYPE));
- info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(TopSites.HISTORY_ID));
- }
- });
-
- registerForContextMenu(mList);
- registerForContextMenu(mGrid);
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
-
- // Discard any additional item clicks on the list as the
- // panel is getting destroyed (see bugs 930160 & 1096958).
- mList.setOnItemClickListener(null);
- mGrid.setOnItemClickListener(null);
-
- mList = null;
- mGrid = null;
- mListAdapter = null;
- mGridAdapter = null;
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- final Activity activity = getActivity();
-
- // Setup the top sites grid adapter.
- mGridAdapter = new TopSitesGridAdapter(activity, null);
- mGrid.setAdapter(mGridAdapter);
-
- // Setup the top sites list adapter.
- mListAdapter = new VisitedAdapter(activity, null);
- mList.setAdapter(mListAdapter);
-
- // Create callbacks before the initial loader is started
- mCursorLoaderCallbacks = new CursorLoaderCallbacks();
- mThumbnailsLoaderCallbacks = new ThumbnailsLoaderCallbacks();
- loadIfVisible();
- }
-
- @Override
- public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
- if (menuInfo == null) {
- return;
- }
-
- if (!(menuInfo instanceof TopSitesGridContextMenuInfo)) {
- // Long pressed item was not a Top Sites GridView item. Superclass
- // can handle this.
- super.onCreateContextMenu(menu, view, menuInfo);
-
- if (!Restrictions.isAllowed(view.getContext(), Restrictable.CLEAR_HISTORY)) {
- menu.findItem(R.id.home_remove).setVisible(false);
- }
-
- return;
- }
-
- final Context context = view.getContext();
-
- // Long pressed item was a Top Sites GridView item, handle it.
- MenuInflater inflater = new MenuInflater(context);
- inflater.inflate(R.menu.home_contextmenu, menu);
-
- // Hide unused menu items.
- menu.findItem(R.id.home_edit_bookmark).setVisible(false);
-
- menu.findItem(R.id.home_remove).setVisible(Restrictions.isAllowed(context, Restrictable.CLEAR_HISTORY));
-
- TopSitesGridContextMenuInfo info = (TopSitesGridContextMenuInfo) menuInfo;
- menu.setHeaderTitle(info.getDisplayTitle());
-
- if (info.type != TopSites.TYPE_BLANK) {
- if (info.type == TopSites.TYPE_PINNED) {
- menu.findItem(R.id.top_sites_pin).setVisible(false);
- } else {
- menu.findItem(R.id.top_sites_unpin).setVisible(false);
- }
- } else {
- menu.findItem(R.id.home_open_new_tab).setVisible(false);
- menu.findItem(R.id.home_open_private_tab).setVisible(false);
- menu.findItem(R.id.top_sites_pin).setVisible(false);
- menu.findItem(R.id.top_sites_unpin).setVisible(false);
- }
-
- if (!StringUtils.isShareableUrl(info.url) || GeckoProfile.get(getActivity()).inGuestMode()) {
- menu.findItem(R.id.home_share).setVisible(false);
- }
-
- if (!Restrictions.isAllowed(context, Restrictable.PRIVATE_BROWSING)) {
- menu.findItem(R.id.home_open_private_tab).setVisible(false);
- }
- }
-
- @Override
- public boolean onContextItemSelected(MenuItem item) {
- if (super.onContextItemSelected(item)) {
- // HomeFragment was able to handle to selected item.
- return true;
- }
-
- ContextMenuInfo menuInfo = item.getMenuInfo();
-
- if (!(menuInfo instanceof TopSitesGridContextMenuInfo)) {
- return false;
- }
-
- TopSitesGridContextMenuInfo info = (TopSitesGridContextMenuInfo) menuInfo;
-
- final int itemId = item.getItemId();
- final BrowserDB db = BrowserDB.from(getActivity());
-
- if (itemId == R.id.top_sites_pin) {
- final String url = info.url;
- final String title = info.title;
- final int position = info.position;
- final Context context = getActivity().getApplicationContext();
-
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- db.pinSite(context.getContentResolver(), url, title, position);
- }
- });
-
- Telemetry.sendUIEvent(TelemetryContract.Event.PIN);
- return true;
- }
-
- if (itemId == R.id.top_sites_unpin) {
- final int position = info.position;
- final Context context = getActivity().getApplicationContext();
-
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- db.unpinSite(context.getContentResolver(), position);
- }
- });
-
- Telemetry.sendUIEvent(TelemetryContract.Event.UNPIN);
-
- return true;
- }
-
- if (itemId == R.id.top_sites_edit) {
- // Decode "user-entered" URLs before showing them.
- mEditPinnedSiteListener.onEditPinnedSite(info.position,
- StringUtils.decodeUserEnteredUrl(info.url));
-
- Telemetry.sendUIEvent(TelemetryContract.Event.EDIT);
- return true;
- }
-
- return false;
- }
-
- @Override
- protected void load() {
- getLoaderManager().initLoader(LOADER_ID_TOP_SITES, null, mCursorLoaderCallbacks);
-
- // Since this is the primary fragment that loads whenever about:home is
- // visited, we want to load it as quickly as possible. Heavy load on
- // the Gecko thread can slow down the time it takes for thumbnails to
- // appear, especially during startup (bug 897162). By minimizing the
- // Gecko thread priority, we ensure that the UI appears quickly. The
- // priority is reset to normal once thumbnails are loaded.
- ThreadUtils.reduceGeckoPriority(PRIORITY_RESET_TIMEOUT);
- }
-
- /**
- * Listener for editing pinned sites.
- */
- private class EditPinnedSiteListener implements OnEditPinnedSiteListener,
- OnSiteSelectedListener {
- // Tag for the PinSiteDialog fragment.
- private static final String TAG_PIN_SITE = "pin_site";
-
- // Position of the pin.
- private int mPosition;
-
- @Override
- public void onEditPinnedSite(int position, String searchTerm) {
- final FragmentManager manager = getChildFragmentManager();
- PinSiteDialog dialog = (PinSiteDialog) manager.findFragmentByTag(TAG_PIN_SITE);
- if (dialog == null) {
- mPosition = position;
-
- dialog = PinSiteDialog.newInstance();
- dialog.setOnSiteSelectedListener(this);
- dialog.setSearchTerm(searchTerm);
- dialog.show(manager, TAG_PIN_SITE);
- }
- }
-
- @Override
- public void onSiteSelected(final String url, final String title) {
- final int position = mPosition;
- final Context context = getActivity().getApplicationContext();
- final BrowserDB db = BrowserDB.from(getActivity());
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- db.pinSite(context.getContentResolver(), url, title, position);
- }
- });
- }
- }
-
- private void updateUiFromCursor(Cursor c) {
- mList.setHeaderDividersEnabled(c != null && c.getCount() > mMaxGridEntries);
- }
-
- private void updateUiWithThumbnails(Map<String, ThumbnailInfo> thumbnails) {
- if (mGridAdapter != null) {
- mGridAdapter.updateThumbnails(thumbnails);
- }
-
- // Once thumbnails have finished loading, the UI is ready. Reset
- // Gecko to normal priority.
- ThreadUtils.resetGeckoPriority();
- }
-
- private static class TopSitesLoader extends SimpleCursorLoader {
- // Max number of search results.
- private static final int SEARCH_LIMIT = 30;
- private static final String TELEMETRY_HISTOGRAM_LOAD_CURSOR = "FENNEC_TOPSITES_LOADER_TIME_MS";
- private final BrowserDB mDB;
- private final int mMaxGridEntries;
-
- public TopSitesLoader(Context context) {
- super(context);
- mMaxGridEntries = context.getResources().getInteger(R.integer.number_of_top_sites);
- mDB = BrowserDB.from(context);
- }
-
- @Override
- public Cursor loadCursor() {
- final long start = SystemClock.uptimeMillis();
- final Cursor cursor = mDB.getTopSites(getContext().getContentResolver(), mMaxGridEntries, SEARCH_LIMIT);
- final long end = SystemClock.uptimeMillis();
- final long took = end - start;
- Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_LOAD_CURSOR, (int) Math.min(took, Integer.MAX_VALUE));
- return cursor;
- }
- }
-
- private class VisitedAdapter extends CursorAdapter {
- public VisitedAdapter(Context context, Cursor cursor) {
- super(context, cursor, 0);
- }
-
- @Override
- public int getCount() {
- return Math.max(0, super.getCount() - mMaxGridEntries);
- }
-
- @Override
- public Object getItem(int position) {
- return super.getItem(position + mMaxGridEntries);
- }
-
- /**
- * We have to override default getItemId implementation, since for a given position, it returns
- * value of the _id column. In our case _id is always 0 (see Combined view).
- */
- @Override
- public long getItemId(int position) {
- final int adjustedPosition = position + mMaxGridEntries;
- final Cursor cursor = getCursor();
-
- cursor.moveToPosition(adjustedPosition);
- return getItemIdForTopSitesCursor(cursor);
- }
-
- @Override
- public void bindView(View view, Context context, Cursor cursor) {
- final int position = cursor.getPosition();
- cursor.moveToPosition(position + mMaxGridEntries);
-
- final TwoLinePageRow row = (TwoLinePageRow) view;
- row.updateFromCursor(cursor);
- }
-
- @Override
- public View newView(Context context, Cursor cursor, ViewGroup parent) {
- return LayoutInflater.from(context).inflate(R.layout.bookmark_item_row, parent, false);
- }
- }
-
- public class TopSitesGridAdapter extends CursorAdapter {
- private final BrowserDB mDB;
- // Cache to store the thumbnails.
- // Ensure that this is only accessed from the UI thread.
- private Map<String, ThumbnailInfo> mThumbnailInfos;
-
- public TopSitesGridAdapter(Context context, Cursor cursor) {
- super(context, cursor, 0);
- mDB = BrowserDB.from(context);
- }
-
- @Override
- public int getCount() {
- return Math.min(mMaxGridEntries, super.getCount());
- }
-
- @Override
- protected void onContentChanged() {
- // Don't do anything. We don't want to regenerate every time
- // our database is updated.
- return;
- }
-
- /**
- * Update the thumbnails returned by the db.
- *
- * @param thumbnails A map of urls and their thumbnail bitmaps.
- */
- public void updateThumbnails(Map<String, ThumbnailInfo> thumbnails) {
- mThumbnailInfos = thumbnails;
-
- final int count = mGrid.getChildCount();
- for (int i = 0; i < count; i++) {
- TopSitesGridItemView gridItem = (TopSitesGridItemView) mGrid.getChildAt(i);
-
- // All the views have already got their initial state at this point.
- // This will force each view to load favicons for the missing
- // thumbnails if necessary.
- gridItem.markAsDirty();
- }
-
- notifyDataSetChanged();
- }
-
- /**
- * We have to override default getItemId implementation, since for a given position, it returns
- * value of the _id column. In our case _id is always 0 (see Combined view).
- */
- @Override
- public long getItemId(int position) {
- final Cursor cursor = getCursor();
- cursor.moveToPosition(position);
-
- return getItemIdForTopSitesCursor(cursor);
- }
-
- @Override
- public void bindView(View bindView, Context context, Cursor cursor) {
- final String url = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.URL));
- final String title = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.TITLE));
- final int type = cursor.getInt(cursor.getColumnIndexOrThrow(TopSites.TYPE));
-
- final TopSitesGridItemView view = (TopSitesGridItemView) bindView;
-
- // If there is no url, then show "add bookmark".
- if (type == TopSites.TYPE_BLANK) {
- view.blankOut();
- return;
- }
-
- // Show the thumbnail, if any.
- ThumbnailInfo thumbnail = (mThumbnailInfos != null ? mThumbnailInfos.get(url) : null);
-
- // Debounce bindView calls to avoid redundant redraws and favicon
- // fetches.
- final boolean updated = view.updateState(title, url, type, thumbnail);
-
- // Thumbnails are delivered late, so we can't short-circuit any
- // sooner than this. But we can avoid a duplicate favicon
- // fetch...
- if (!updated) {
- debug("bindView called twice for same values; short-circuiting.");
- return;
- }
-
- // Make sure we query suggested images without the user-entered wrapper.
- final String decodedUrl = StringUtils.decodeUserEnteredUrl(url);
-
- // Suggested images have precedence over thumbnails, no need to wait
- // for them to be loaded. See: CursorLoaderCallbacks.onLoadFinished()
- final String imageUrl = mDB.getSuggestedImageUrlForUrl(decodedUrl);
- if (!TextUtils.isEmpty(imageUrl)) {
- final int bgColor = mDB.getSuggestedBackgroundColorForUrl(decodedUrl);
- view.displayThumbnail(imageUrl, bgColor);
- return;
- }
-
- // If thumbnails are still being loaded, don't try to load favicons
- // just yet. If we sent in a thumbnail, we're done now.
- if (mThumbnailInfos == null || thumbnail != null) {
- return;
- }
-
- view.loadFavicon(url);
- }
-
- @Override
- public View newView(Context context, Cursor cursor, ViewGroup parent) {
- return new TopSitesGridItemView(context);
- }
- }
-
- private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
- @Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- trace("Creating TopSitesLoader: " + id);
- return new TopSitesLoader(getActivity());
- }
-
- /**
- * This method is called *twice* in some circumstances.
- *
- * If you try to avoid that through some kind of boolean flag,
- * sometimes (e.g., returning to the activity) you'll *not* be called
- * twice, and thus you'll never draw thumbnails.
- *
- * The root cause is TopSitesLoader.loadCursor being called twice.
- * Why that is... dunno.
- */
- public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
- debug("onLoadFinished: " + c.getCount() + " rows.");
-
- mListAdapter.swapCursor(c);
- mGridAdapter.swapCursor(c);
- updateUiFromCursor(c);
-
- final int col = c.getColumnIndexOrThrow(TopSites.URL);
-
- // Load the thumbnails.
- // Even though the cursor we're given is supposed to be fresh,
- // we getIcon a bad first value unless we reset its position.
- // Using move(-1) and moveToNext() doesn't work correctly under
- // rotation, so we use moveToFirst.
- if (!c.moveToFirst()) {
- return;
- }
-
- final ArrayList<String> urls = new ArrayList<String>();
- int i = 1;
- do {
- final String url = c.getString(col);
-
- // Only try to fetch thumbnails for non-empty URLs that
- // don't have an associated suggested image URL.
- final GeckoProfile profile = GeckoProfile.get(getActivity());
- if (TextUtils.isEmpty(url) || BrowserDB.from(profile).hasSuggestedImageUrl(url)) {
- continue;
- }
-
- urls.add(url);
- } while (i++ < mMaxGridEntries && c.moveToNext());
-
- if (urls.isEmpty()) {
- // Short-circuit empty results to the UI.
- updateUiWithThumbnails(new HashMap<String, ThumbnailInfo>());
- return;
- }
-
- Bundle bundle = new Bundle();
- bundle.putStringArrayList(THUMBNAILS_URLS_KEY, urls);
- getLoaderManager().restartLoader(LOADER_ID_THUMBNAILS, bundle, mThumbnailsLoaderCallbacks);
- }
-
- @Override
- public void onLoaderReset(Loader<Cursor> loader) {
- if (mListAdapter != null) {
- mListAdapter.swapCursor(null);
- }
-
- if (mGridAdapter != null) {
- mGridAdapter.swapCursor(null);
- }
- }
- }
-
- static class ThumbnailInfo {
- public final Bitmap bitmap;
- public final String imageUrl;
- public final int bgColor;
-
- public ThumbnailInfo(final Bitmap bitmap) {
- this.bitmap = bitmap;
- this.imageUrl = null;
- this.bgColor = Color.TRANSPARENT;
- }
-
- public ThumbnailInfo(final String imageUrl, final int bgColor) {
- this.bitmap = null;
- this.imageUrl = imageUrl;
- this.bgColor = bgColor;
- }
-
- public static ThumbnailInfo fromMetadata(final Map<String, Object> data) {
- if (data == null) {
- return null;
- }
-
- final String imageUrl = (String) data.get(TILE_IMAGE_URL_COLUMN);
- if (imageUrl == null) {
- return null;
- }
-
- int bgColor = Color.WHITE;
- final String colorString = (String) data.get(TILE_COLOR_COLUMN);
- try {
- bgColor = Color.parseColor(colorString);
- } catch (Exception ex) {
- }
-
- return new ThumbnailInfo(imageUrl, bgColor);
- }
- }
-
- /**
- * An AsyncTaskLoader to load the thumbnails from a cursor.
- */
- static class ThumbnailsLoader extends AsyncTaskLoader<Map<String, ThumbnailInfo>> {
- private final BrowserDB mDB;
- private Map<String, ThumbnailInfo> mThumbnailInfos;
- private final ArrayList<String> mUrls;
-
- private static final List<String> COLUMNS;
- static {
- final ArrayList<String> tempColumns = new ArrayList<>(2);
- tempColumns.add(TILE_IMAGE_URL_COLUMN);
- tempColumns.add(TILE_COLOR_COLUMN);
- COLUMNS = Collections.unmodifiableList(tempColumns);
- }
-
- public ThumbnailsLoader(Context context, ArrayList<String> urls) {
- super(context);
- mUrls = urls;
- mDB = BrowserDB.from(context);
- }
-
- @Override
- public Map<String, ThumbnailInfo> loadInBackground() {
- final Map<String, ThumbnailInfo> thumbnails = new HashMap<String, ThumbnailInfo>();
- if (mUrls == null || mUrls.size() == 0) {
- return thumbnails;
- }
-
- // We need to query metadata based on the URL without any refs, hence we create a new
- // mapping and list of these URLs (we need to preserve the original URL for display purposes)
- final Map<String, String> queryURLs = new HashMap<>();
- for (final String pageURL : mUrls) {
- queryURLs.put(pageURL, StringUtils.stripRef(pageURL));
- }
-
- // Query the DB for tile images.
- final ContentResolver cr = getContext().getContentResolver();
- // Use the stripped URLs for querying the DB
- final Map<String, Map<String, Object>> metadata = mDB.getURLMetadata().getForURLs(cr, queryURLs.values(), COLUMNS);
-
- // Keep a list of urls that don't have tiles images. We'll use thumbnails for them instead.
- final List<String> thumbnailUrls = new ArrayList<String>();
- for (final String pageURL : mUrls) {
- final String queryURL = queryURLs.get(pageURL);
-
- ThumbnailInfo info = ThumbnailInfo.fromMetadata(metadata.get(queryURL));
- if (info == null) {
- // If we didn't find metadata, we'll look for a thumbnail for this url.
- thumbnailUrls.add(pageURL);
- continue;
- }
-
- thumbnails.put(pageURL, info);
- }
-
- if (thumbnailUrls.size() == 0) {
- return thumbnails;
- }
-
- // Query the DB for tile thumbnails.
- final Cursor cursor = mDB.getThumbnailsForUrls(cr, thumbnailUrls);
- if (cursor == null) {
- return thumbnails;
- }
-
- try {
- final int urlIndex = cursor.getColumnIndexOrThrow(Thumbnails.URL);
- final int dataIndex = cursor.getColumnIndexOrThrow(Thumbnails.DATA);
-
- while (cursor.moveToNext()) {
- String url = cursor.getString(urlIndex);
-
- // This should never be null, but if it is...
- final byte[] b = cursor.getBlob(dataIndex);
- if (b == null) {
- continue;
- }
-
- final Bitmap bitmap = BitmapUtils.decodeByteArray(b);
-
- // Our thumbnails are never null, so if we getIcon a null decoded
- // bitmap, it's because we hit an OOM or some other disaster.
- // Give up immediately rather than hammering on.
- if (bitmap == null) {
- Log.w(LOGTAG, "Aborting thumbnail load; decode failed.");
- break;
- }
-
- thumbnails.put(url, new ThumbnailInfo(bitmap));
- }
- } finally {
- cursor.close();
- }
-
- return thumbnails;
- }
-
- @Override
- public void deliverResult(Map<String, ThumbnailInfo> thumbnails) {
- if (isReset()) {
- mThumbnailInfos = null;
- return;
- }
-
- mThumbnailInfos = thumbnails;
-
- if (isStarted()) {
- super.deliverResult(thumbnails);
- }
- }
-
- @Override
- protected void onStartLoading() {
- if (mThumbnailInfos != null) {
- deliverResult(mThumbnailInfos);
- }
-
- if (takeContentChanged() || mThumbnailInfos == null) {
- forceLoad();
- }
- }
-
- @Override
- protected void onStopLoading() {
- cancelLoad();
- }
-
- @Override
- public void onCanceled(Map<String, ThumbnailInfo> thumbnails) {
- mThumbnailInfos = null;
- }
-
- @Override
- protected void onReset() {
- super.onReset();
-
- // Ensure the loader is stopped.
- onStopLoading();
-
- mThumbnailInfos = null;
- }
- }
-
- /**
- * Loader callbacks for the thumbnails on TopSitesGridView.
- */
- private class ThumbnailsLoaderCallbacks implements LoaderCallbacks<Map<String, ThumbnailInfo>> {
- @Override
- public Loader<Map<String, ThumbnailInfo>> onCreateLoader(int id, Bundle args) {
- return new ThumbnailsLoader(getActivity(), args.getStringArrayList(THUMBNAILS_URLS_KEY));
- }
-
- @Override
- public void onLoadFinished(Loader<Map<String, ThumbnailInfo>> loader, Map<String, ThumbnailInfo> thumbnails) {
- updateUiWithThumbnails(thumbnails);
- }
-
- @Override
- public void onLoaderReset(Loader<Map<String, ThumbnailInfo>> loader) {
- if (mGridAdapter != null) {
- mGridAdapter.updateThumbnails(null);
- }
- }
- }
-
- /**
- * We are trying to return stable IDs so that Android can recycle views appropriately:
- * - If we have a history ID then we return it
- * - If we only have a bookmark ID then we negate it and return it. We negate it in order
- * to avoid clashing/conflicting with history IDs.
- *
- * @param cursorInPosition Cursor already moved to position for which we're getting a stable ID
- * @return Stable ID for a given cursor
- */
- private static long getItemIdForTopSitesCursor(final Cursor cursorInPosition) {
- final int historyIdCol = cursorInPosition.getColumnIndexOrThrow(TopSites.HISTORY_ID);
- final long historyId = cursorInPosition.getLong(historyIdCol);
- if (historyId != 0) {
- return historyId;
- }
-
- final int bookmarkIdCol = cursorInPosition.getColumnIndexOrThrow(TopSites.BOOKMARK_ID);
- final long bookmarkId = cursorInPosition.getLong(bookmarkIdCol);
- return -1 * bookmarkId;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesThumbnailView.java b/mobile/android/base/java/org/mozilla/gecko/home/TopSitesThumbnailView.java
deleted file mode 100644
index dd45014b0..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesThumbnailView.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.ThumbnailHelper;
-import org.mozilla.gecko.widget.CropImageView;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.PorterDuff.Mode;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-
-/**
- * A width constrained ImageView to show thumbnails of top and pinned sites.
- */
-public class TopSitesThumbnailView extends CropImageView {
- private static final String LOGTAG = "GeckoTopSitesThumbnailView";
-
- // 27.34% opacity filter for the dominant color.
- private static final int COLOR_FILTER = 0x46FFFFFF;
-
- // Default filter color for "Add a bookmark" views.
- private final int mDefaultColor = ContextCompat.getColor(getContext(), R.color.top_site_default);
-
- // Stroke width for the border.
- private final float mStrokeWidth = getResources().getDisplayMetrics().density * 2;
-
- // Paint for drawing the border.
- private final Paint mBorderPaint;
-
- public TopSitesThumbnailView(Context context) {
- this(context, null);
-
- // A border will be drawn if needed.
- setWillNotDraw(false);
- }
-
- public TopSitesThumbnailView(Context context, AttributeSet attrs) {
- this(context, attrs, R.attr.topSitesThumbnailViewStyle);
- }
-
- public TopSitesThumbnailView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
-
- // Initialize the border paint.
- final Resources res = getResources();
- mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- mBorderPaint.setColor(ContextCompat.getColor(context, R.color.top_site_border));
- mBorderPaint.setStyle(Paint.Style.STROKE);
- }
-
- @Override
- protected float getAspectRatio() {
- return ThumbnailHelper.TOP_SITES_THUMBNAIL_ASPECT_RATIO;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void onDraw(Canvas canvas) {
- super.onDraw(canvas);
-
- if (getBackground() == null) {
- mBorderPaint.setStrokeWidth(mStrokeWidth);
- canvas.drawRect(0, 0, getWidth(), getHeight(), mBorderPaint);
- }
- }
-
- /**
- * Sets the background color with a filter to reduce the color opacity.
- *
- * @param color the color filter to apply over the drawable.
- */
- public void setBackgroundColorWithOpacityFilter(int color) {
- setBackgroundColor(color & COLOR_FILTER);
- }
-
- /**
- * Sets the background to a Drawable by applying the specified color as a filter.
- *
- * @param color the color filter to apply over the drawable.
- */
- @Override
- public void setBackgroundColor(int color) {
- if (color == 0) {
- color = mDefaultColor;
- }
-
- Drawable drawable = getResources().getDrawable(R.drawable.top_sites_thumbnail_bg);
- drawable.setColorFilter(color, Mode.SRC_ATOP);
- setBackgroundDrawable(drawable);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java b/mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java
deleted file mode 100644
index 68eb8daa5..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java
+++ /dev/null
@@ -1,324 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import java.lang.ref.WeakReference;
-import java.util.concurrent.Future;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import org.mozilla.gecko.AboutPages;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.BrowserContract.Combined;
-import org.mozilla.gecko.db.BrowserContract.URLColumns;
-import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
-import org.mozilla.gecko.icons.IconDescriptor;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.icons.Icons;
-import org.mozilla.gecko.reader.ReaderModeUtils;
-import org.mozilla.gecko.reader.SavedReaderViewHelper;
-import org.mozilla.gecko.widget.FaviconView;
-
-public class TwoLinePageRow extends LinearLayout
- implements Tabs.OnTabsChangedListener {
-
- protected static final int NO_ICON = 0;
-
- private final TextView mTitle;
- private final TextView mUrl;
- private final ImageView mStatusIcon;
-
- private int mSwitchToTabIconId;
-
- private final FaviconView mFavicon;
- private Future<IconResponse> mOngoingIconLoad;
-
- private boolean mShowIcons;
-
- // The URL for the page corresponding to this view.
- private String mPageUrl;
-
- private boolean mHasReaderCacheItem;
-
- public TwoLinePageRow(Context context) {
- this(context, null);
- }
-
- public TwoLinePageRow(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- setGravity(Gravity.CENTER_VERTICAL);
-
- LayoutInflater.from(context).inflate(R.layout.two_line_page_row, this);
- // Merge layouts lose their padding, so set it dynamically.
- setPadding(0, 0, (int) getResources().getDimension(R.dimen.page_row_edge_padding), 0);
-
- mTitle = (TextView) findViewById(R.id.title);
- mUrl = (TextView) findViewById(R.id.url);
- mStatusIcon = (ImageView) findViewById(R.id.status_icon_bookmark);
-
- mSwitchToTabIconId = NO_ICON;
- mShowIcons = true;
-
- mFavicon = (FaviconView) findViewById(R.id.icon);
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- Tabs.registerOnTabsChangedListener(this);
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- // Tabs' listener array is safe to modify during use: its
- // iteration pattern is based on snapshots.
- Tabs.unregisterOnTabsChangedListener(this);
- }
-
- /**
- * Update the row in response to a tab change event.
- * <p>
- * This method is always invoked on the UI thread.
- */
- @Override
- public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final String data) {
- // Carefully check if this tab event is relevant to this row.
- final String pageUrl = mPageUrl;
- if (pageUrl == null) {
- return;
- }
- if (tab == null) {
- return;
- }
-
- // Return early if the page URL doesn't match the current tab URL,
- // or the old tab URL.
- // data is an empty String for ADDED/CLOSED, and contains the previous/old URL during
- // LOCATION_CHANGE (the new URL is retrieved using tab.getURL()).
- // tabURL and data may be about:reader URLs if the current or old tab page was a reader view
- // page, however pageUrl will always be a plain URL (i.e. we only add about:reader when opening
- // a reader view bookmark, at all other times it's a normal bookmark with normal URL).
- final String tabUrl = tab.getURL();
- if (!pageUrl.equals(ReaderModeUtils.stripAboutReaderUrl(tabUrl)) &&
- !pageUrl.equals(ReaderModeUtils.stripAboutReaderUrl(data))) {
- return;
- }
-
- // Note: we *might* need to update the display status (i.e. switch-to-tab icon/label) if
- // a matching tab has been opened/closed/switched to a different page. updateDisplayedUrl() will
- // determine the changes (if any) that actually need to be made. A tab change with a matching URL
- // does not imply that any changes are needed - e.g. if a given URL is already open in one tab, and
- // is also opened in a second tab, the switch-to-tab status doesn't change, closing 1 of 2 tabs with a URL
- // similarly doesn't change the switch-to-tab display, etc. (However closing the last tab for
- // a given URL does require a status change, as does opening the first tab with that URL.)
- switch (msg) {
- case ADDED:
- case CLOSED:
- case LOCATION_CHANGE:
- updateDisplayedUrl();
- break;
- default:
- break;
- }
- }
-
- private void setTitle(String text) {
- mTitle.setText(text);
- }
-
- protected void setUrl(String text) {
- mUrl.setText(text);
- }
-
- protected void setUrl(int stringId) {
- mUrl.setText(stringId);
- }
-
- protected String getUrl() {
- return mPageUrl;
- }
-
- protected void setSwitchToTabIcon(int iconId) {
- if (mSwitchToTabIconId == iconId) {
- return;
- }
-
- mSwitchToTabIconId = iconId;
- mUrl.setCompoundDrawablesWithIntrinsicBounds(mSwitchToTabIconId, 0, 0, 0);
- }
-
- private void updateStatusIcon(boolean isBookmark, boolean isReaderItem) {
- if (isReaderItem) {
- mStatusIcon.setImageResource(R.drawable.status_icon_readercache);
- } else if (isBookmark) {
- mStatusIcon.setImageResource(R.drawable.star_blue);
- }
-
- if (mShowIcons && (isBookmark || isReaderItem)) {
- mStatusIcon.setVisibility(View.VISIBLE);
- } else if (mShowIcons) {
- // We use INVISIBLE to have consistent padding for our items. This means text/URLs
- // fade consistently in the same location, regardless of them being bookmarked.
- mStatusIcon.setVisibility(View.INVISIBLE);
- } else {
- mStatusIcon.setVisibility(View.GONE);
- }
-
- }
-
- /**
- * Stores the page URL, so that we can use it to replace "Switch to tab" if the open
- * tab changes or is closed.
- */
- private void updateDisplayedUrl(String url, boolean hasReaderCacheItem) {
- mPageUrl = url;
- mHasReaderCacheItem = hasReaderCacheItem;
- updateDisplayedUrl();
- }
-
- /**
- * Replaces the page URL with "Switch to tab" if there is already a tab open with that URL.
- * Only looks for tabs that are either private or non-private, depending on the current
- * selected tab.
- */
- protected void updateDisplayedUrl() {
- final Tab selectedTab = Tabs.getInstance().getSelectedTab();
- final boolean isPrivate = (selectedTab != null) && (selectedTab.isPrivate());
-
- // We always want to display the underlying page url, however for readermode pages
- // we navigate to the about:reader equivalent, hence we need to use that url when finding
- // existing tabs
- final String navigationUrl = mHasReaderCacheItem ? ReaderModeUtils.getAboutReaderForUrl(mPageUrl) : mPageUrl;
- Tab tab = Tabs.getInstance().getFirstTabForUrl(navigationUrl, isPrivate);
-
-
- if (!mShowIcons || tab == null) {
- setUrl(mPageUrl);
- setSwitchToTabIcon(NO_ICON);
- } else {
- setUrl(R.string.switch_to_tab);
- setSwitchToTabIcon(R.drawable.ic_url_bar_tab);
- }
- }
-
- public void setShowIcons(boolean showIcons) {
- mShowIcons = showIcons;
- }
-
- /**
- * Update the data displayed by this row.
- * <p>
- * This method must be invoked on the UI thread.
- *
- * @param title to display.
- * @param url to display.
- */
- public void update(String title, String url) {
- update(title, url, 0, false);
- }
-
- protected void update(String title, String url, long bookmarkId, boolean hasReaderCacheItem) {
- if (mShowIcons) {
- // The bookmark id will be 0 (null in database) when the url
- // is not a bookmark and negative for 'fake' bookmarks.
- final boolean isBookmark = bookmarkId > 0;
-
- updateStatusIcon(isBookmark, hasReaderCacheItem);
- } else {
- updateStatusIcon(false, false);
- }
-
- // Use the URL instead of an empty title for consistency with the normal URL
- // bar view - this is the equivalent of getDisplayTitle() in Tab.java
- setTitle(TextUtils.isEmpty(title) ? url : title);
-
- // No point updating the below things if URL has not changed. Prevents evil Favicon flicker.
- if (url.equals(mPageUrl)) {
- return;
- }
-
- // Blank the Favicon, so we don't show the wrong Favicon if we scroll and miss DB.
- mFavicon.clearImage();
-
- if (mOngoingIconLoad != null) {
- mOngoingIconLoad.cancel(true);
- }
-
- // Displayed RecentTabsPanel URLs may refer to pages opened in reader mode, so we
- // remove the about:reader prefix to ensure the Favicon loads properly.
- final String pageURL = ReaderModeUtils.stripAboutReaderUrl(url);
-
- if (TextUtils.isEmpty(pageURL)) {
- // If url is empty, display the item as-is but do not load an icon if we do not have a page URL (bug 1310622)
- } else if (bookmarkId < BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START) {
- mOngoingIconLoad = Icons.with(getContext())
- .pageUrl(pageURL)
- .skipNetwork()
- .privileged(true)
- .icon(IconDescriptor.createGenericIcon(
- PartnerBookmarksProviderProxy.getUriForIcon(getContext(), bookmarkId).toString()))
- .build()
- .execute(mFavicon.createIconCallback());
- } else {
- mOngoingIconLoad = Icons.with(getContext())
- .pageUrl(pageURL)
- .skipNetwork()
- .build()
- .execute(mFavicon.createIconCallback());
-
- }
-
- updateDisplayedUrl(url, hasReaderCacheItem);
- }
-
- /**
- * Update the data displayed by this row.
- * <p>
- * This method must be invoked on the UI thread.
- *
- * @param cursor to extract data from.
- */
- public void updateFromCursor(Cursor cursor) {
- if (cursor == null) {
- return;
- }
-
- int titleIndex = cursor.getColumnIndexOrThrow(URLColumns.TITLE);
- final String title = cursor.getString(titleIndex);
-
- int urlIndex = cursor.getColumnIndexOrThrow(URLColumns.URL);
- final String url = cursor.getString(urlIndex);
-
- final long bookmarkId;
- final int bookmarkIdIndex = cursor.getColumnIndex(Combined.BOOKMARK_ID);
- if (bookmarkIdIndex != -1) {
- bookmarkId = cursor.getLong(bookmarkIdIndex);
- } else {
- bookmarkId = 0;
- }
-
- SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(getContext());
- final boolean hasReaderCacheItem = rch.isURLCached(url);
-
- update(title, url, bookmarkId, hasReaderCacheItem);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStream.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStream.java
deleted file mode 100644
index ef0c105d3..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStream.java
+++ /dev/null
@@ -1,145 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
- package org.mozilla.gecko.home.activitystream;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.os.Bundle;
-import android.support.v4.app.LoaderManager;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.content.Loader;
-import android.support.v4.graphics.ColorUtils;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.util.TypedValue;
-import android.widget.FrameLayout;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.home.HomePager;
-import org.mozilla.gecko.home.activitystream.topsites.TopSitesPagerAdapter;
-import org.mozilla.gecko.util.ContextUtils;
-import org.mozilla.gecko.widget.RecyclerViewClickSupport;
-
-public class ActivityStream extends FrameLayout {
- private final StreamRecyclerAdapter adapter;
-
- private static final int LOADER_ID_HIGHLIGHTS = 0;
- private static final int LOADER_ID_TOPSITES = 1;
-
- private static final int MINIMUM_TILES = 4;
- private static final int MAXIMUM_TILES = 6;
-
- private int desiredTileWidth;
- private int desiredTilesHeight;
- private int tileMargin;
-
- public ActivityStream(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- setBackgroundColor(ContextCompat.getColor(context, R.color.about_page_header_grey));
-
- inflate(context, R.layout.as_content, this);
-
- adapter = new StreamRecyclerAdapter();
-
- RecyclerView rv = (RecyclerView) findViewById(R.id.activity_stream_main_recyclerview);
-
- rv.setAdapter(adapter);
- rv.setLayoutManager(new LinearLayoutManager(getContext()));
- rv.setHasFixedSize(true);
-
- RecyclerViewClickSupport.addTo(rv)
- .setOnItemClickListener(adapter);
-
- final Resources resources = getResources();
- desiredTileWidth = resources.getDimensionPixelSize(R.dimen.activity_stream_desired_tile_width);
- desiredTilesHeight = resources.getDimensionPixelSize(R.dimen.activity_stream_desired_tile_height);
- tileMargin = resources.getDimensionPixelSize(R.dimen.activity_stream_base_margin);
- }
-
- void setOnUrlOpenListeners(HomePager.OnUrlOpenListener onUrlOpenListener, HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
- adapter.setOnUrlOpenListeners(onUrlOpenListener, onUrlOpenInBackgroundListener);
- }
-
- public void load(LoaderManager lm) {
- CursorLoaderCallbacks callbacks = new CursorLoaderCallbacks();
-
- lm.initLoader(LOADER_ID_HIGHLIGHTS, null, callbacks);
- lm.initLoader(LOADER_ID_TOPSITES, null, callbacks);
- }
-
- public void unload() {
- adapter.swapHighlightsCursor(null);
- adapter.swapTopSitesCursor(null);
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
-
- int tiles = (w - tileMargin) / (desiredTileWidth + tileMargin);
-
- if (tiles < MINIMUM_TILES) {
- tiles = MINIMUM_TILES;
-
- setPadding(0, 0, 0, 0);
- } else if (tiles > MAXIMUM_TILES) {
- tiles = MAXIMUM_TILES;
-
- // Use the remaining space as padding
- int needed = tiles * (desiredTileWidth + tileMargin) + tileMargin;
- int padding = (w - needed) / 2;
- w = needed;
-
- setPadding(padding, 0, padding, 0);
- } else {
- setPadding(0, 0, 0, 0);
- }
-
- final float ratio = (float) desiredTilesHeight / (float) desiredTileWidth;
- final int tilesWidth = (w - (tiles * tileMargin) - tileMargin) / tiles;
- final int tilesHeight = (int) (ratio * tilesWidth);
-
- adapter.setTileSize(tiles, tilesWidth, tilesHeight);
- }
-
- private class CursorLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
- @Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- final Context context = getContext();
- if (id == LOADER_ID_HIGHLIGHTS) {
- return BrowserDB.from(context).getHighlights(context, 10);
- } else if (id == LOADER_ID_TOPSITES) {
- return BrowserDB.from(context).getActivityStreamTopSites(
- context, TopSitesPagerAdapter.PAGES * MAXIMUM_TILES);
- } else {
- throw new IllegalArgumentException("Can't handle loader id " + id);
- }
- }
-
- @Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
- if (loader.getId() == LOADER_ID_HIGHLIGHTS) {
- adapter.swapHighlightsCursor(data);
- } else if (loader.getId() == LOADER_ID_TOPSITES) {
- adapter.swapTopSitesCursor(data);
- }
- }
-
- @Override
- public void onLoaderReset(Loader<Cursor> loader) {
- if (loader.getId() == LOADER_ID_HIGHLIGHTS) {
- adapter.swapHighlightsCursor(null);
- } else if (loader.getId() == LOADER_ID_TOPSITES) {
- adapter.swapTopSitesCursor(null);
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeFragment.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeFragment.java
deleted file mode 100644
index 09f6705d7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeFragment.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-package org.mozilla.gecko.home.activitystream;
-
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.home.HomeFragment;
-
-/**
- * Simple wrapper around the ActivityStream view that allows embedding as a HomePager panel.
- */
-public class ActivityStreamHomeFragment
- extends HomeFragment {
- private ActivityStream activityStream;
-
- @Override
- protected void load() {
- activityStream.load(getLoaderManager());
- }
-
- @Nullable
- @Override
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- if (activityStream == null) {
- activityStream = (ActivityStream) inflater.inflate(R.layout.activity_stream, container, false);
- activityStream.setOnUrlOpenListeners(mUrlOpenListener, mUrlOpenInBackgroundListener);
- }
-
- return activityStream;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeScreen.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeScreen.java
deleted file mode 100644
index 4decc8218..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeScreen.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-package org.mozilla.gecko.home.activitystream;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.LoaderManager;
-import android.util.AttributeSet;
-
-import org.mozilla.gecko.animation.PropertyAnimator;
-import org.mozilla.gecko.home.HomeBanner;
-import org.mozilla.gecko.home.HomeFragment;
-import org.mozilla.gecko.home.HomeScreen;
-
-/**
- * HomeScreen implementation that displays ActivityStream.
- */
-public class ActivityStreamHomeScreen
- extends ActivityStream
- implements HomeScreen {
-
- private boolean visible = false;
-
- public ActivityStreamHomeScreen(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public boolean isVisible() {
- return visible;
- }
-
- @Override
- public void onToolbarFocusChange(boolean hasFocus) {
-
- }
-
- @Override
- public void showPanel(String panelId, Bundle restoreData) {
-
- }
-
- @Override
- public void setOnPanelChangeListener(OnPanelChangeListener listener) {
-
- }
-
- @Override
- public void setPanelStateChangeListener(HomeFragment.PanelStateChangeListener listener) {
-
- }
-
- @Override
- public void setBanner(HomeBanner banner) {
-
- }
-
- @Override
- public void load(LoaderManager lm, FragmentManager fm, String panelId, Bundle restoreData,
- PropertyAnimator animator) {
- super.load(lm);
- visible = true;
- }
-
- @Override
- public void unload() {
- super.unload();
- visible = false;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamItem.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamItem.java
deleted file mode 100644
index 24348dfe0..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamItem.java
+++ /dev/null
@@ -1,196 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-package org.mozilla.gecko.home.activitystream;
-
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.graphics.Color;
-import android.support.v4.view.ViewPager;
-import android.support.v7.widget.RecyclerView;
-import android.text.TextUtils;
-import android.text.format.DateUtils;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.activitystream.ActivityStream.LabelCallback;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.home.HomePager;
-import org.mozilla.gecko.home.activitystream.menu.ActivityStreamContextMenu;
-import org.mozilla.gecko.home.activitystream.topsites.CirclePageIndicator;
-import org.mozilla.gecko.home.activitystream.topsites.TopSitesPagerAdapter;
-import org.mozilla.gecko.icons.IconCallback;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.icons.Icons;
-import org.mozilla.gecko.util.DrawableUtil;
-import org.mozilla.gecko.util.ViewUtil;
-import org.mozilla.gecko.util.TouchTargetUtil;
-import org.mozilla.gecko.widget.FaviconView;
-
-import java.util.concurrent.Future;
-
-import static org.mozilla.gecko.activitystream.ActivityStream.extractLabel;
-
-public abstract class StreamItem extends RecyclerView.ViewHolder {
- public StreamItem(View itemView) {
- super(itemView);
- }
-
- public static class HighlightsTitle extends StreamItem {
- public static final int LAYOUT_ID = R.layout.activity_stream_main_highlightstitle;
-
- public HighlightsTitle(View itemView) {
- super(itemView);
- }
- }
-
- public static class TopPanel extends StreamItem {
- public static final int LAYOUT_ID = R.layout.activity_stream_main_toppanel;
-
- private final ViewPager topSitesPager;
-
- public TopPanel(View itemView, HomePager.OnUrlOpenListener onUrlOpenListener, HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
- super(itemView);
-
- topSitesPager = (ViewPager) itemView.findViewById(R.id.topsites_pager);
- topSitesPager.setAdapter(new TopSitesPagerAdapter(itemView.getContext(), onUrlOpenListener, onUrlOpenInBackgroundListener));
-
- CirclePageIndicator indicator = (CirclePageIndicator) itemView.findViewById(R.id.topsites_indicator);
- indicator.setViewPager(topSitesPager);
- }
-
- public void bind(Cursor cursor, int tiles, int tilesWidth, int tilesHeight) {
- final TopSitesPagerAdapter adapter = (TopSitesPagerAdapter) topSitesPager.getAdapter();
- adapter.setTilesSize(tiles, tilesWidth, tilesHeight);
- adapter.swapCursor(cursor);
-
- final Resources resources = itemView.getResources();
- final int tilesMargin = resources.getDimensionPixelSize(R.dimen.activity_stream_base_margin);
- final int textHeight = resources.getDimensionPixelSize(R.dimen.activity_stream_top_sites_text_height);
-
- ViewGroup.LayoutParams layoutParams = topSitesPager.getLayoutParams();
- layoutParams.height = tilesHeight + tilesMargin + textHeight;
- topSitesPager.setLayoutParams(layoutParams);
- }
- }
-
- public static class HighlightItem extends StreamItem implements IconCallback {
- public static final int LAYOUT_ID = R.layout.activity_stream_card_history_item;
-
- String title;
- String url;
-
- final FaviconView vIconView;
- final TextView vLabel;
- final TextView vTimeSince;
- final TextView vSourceView;
- final TextView vPageView;
- final ImageView vSourceIconView;
-
- private Future<IconResponse> ongoingIconLoad;
- private int tilesMargin;
-
- public HighlightItem(final View itemView,
- final HomePager.OnUrlOpenListener onUrlOpenListener,
- final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
- super(itemView);
-
- tilesMargin = itemView.getResources().getDimensionPixelSize(R.dimen.activity_stream_base_margin);
-
- vLabel = (TextView) itemView.findViewById(R.id.card_history_label);
- vTimeSince = (TextView) itemView.findViewById(R.id.card_history_time_since);
- vIconView = (FaviconView) itemView.findViewById(R.id.icon);
- vSourceView = (TextView) itemView.findViewById(R.id.card_history_source);
- vPageView = (TextView) itemView.findViewById(R.id.page);
- vSourceIconView = (ImageView) itemView.findViewById(R.id.source_icon);
-
- final ImageView menuButton = (ImageView) itemView.findViewById(R.id.menu);
-
- menuButton.setImageDrawable(
- DrawableUtil.tintDrawable(menuButton.getContext(), R.drawable.menu, Color.LTGRAY));
-
- TouchTargetUtil.ensureTargetHitArea(menuButton, itemView);
-
- menuButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- ActivityStreamContextMenu.show(v.getContext(),
- menuButton,
- ActivityStreamContextMenu.MenuMode.HIGHLIGHT,
- title, url, onUrlOpenListener, onUrlOpenInBackgroundListener,
- vIconView.getWidth(), vIconView.getHeight());
- }
- });
-
- ViewUtil.enableTouchRipple(menuButton);
- }
-
- public void bind(Cursor cursor, int tilesWidth, int tilesHeight) {
-
- final long time = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Highlights.DATE));
- final String ago = DateUtils.getRelativeTimeSpanString(time, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, 0).toString();
-
- title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.History.TITLE));
- url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
-
- vLabel.setText(title);
- vTimeSince.setText(ago);
-
- ViewGroup.LayoutParams layoutParams = vIconView.getLayoutParams();
- layoutParams.width = tilesWidth - tilesMargin;
- layoutParams.height = tilesHeight;
- vIconView.setLayoutParams(layoutParams);
-
- updateSource(cursor);
- updatePage(url);
-
- if (ongoingIconLoad != null) {
- ongoingIconLoad.cancel(true);
- }
-
- ongoingIconLoad = Icons.with(itemView.getContext())
- .pageUrl(url)
- .skipNetwork()
- .build()
- .execute(this);
- }
-
- private void updateSource(final Cursor cursor) {
- final boolean isBookmark = -1 != cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID));
- final boolean isHistory = -1 != cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID));
-
- if (isBookmark) {
- vSourceView.setText(R.string.activity_stream_highlight_label_bookmarked);
- vSourceView.setVisibility(View.VISIBLE);
- vSourceIconView.setImageResource(R.drawable.ic_as_bookmarked);
- } else if (isHistory) {
- vSourceView.setText(R.string.activity_stream_highlight_label_visited);
- vSourceView.setVisibility(View.VISIBLE);
- vSourceIconView.setImageResource(R.drawable.ic_as_visited);
- } else {
- vSourceView.setVisibility(View.INVISIBLE);
- vSourceIconView.setImageResource(0);
- }
-
- vSourceView.setText(vSourceView.getText());
- }
-
- private void updatePage(final String url) {
- extractLabel(itemView.getContext(), url, false, new LabelCallback() {
- @Override
- public void onLabelExtracted(String label) {
- vPageView.setText(TextUtils.isEmpty(label) ? url : label);
- }
- });
- }
-
- @Override
- public void onIconResponse(IconResponse response) {
- vIconView.updateImage(response);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamRecyclerAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamRecyclerAdapter.java
deleted file mode 100644
index f7cda2e7f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamRecyclerAdapter.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home.activitystream;
-
-import android.database.Cursor;
-import android.support.v7.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.home.HomePager;
-import org.mozilla.gecko.home.activitystream.StreamItem.HighlightItem;
-import org.mozilla.gecko.home.activitystream.StreamItem.TopPanel;
-import org.mozilla.gecko.widget.RecyclerViewClickSupport;
-
-import java.util.EnumSet;
-
-public class StreamRecyclerAdapter extends RecyclerView.Adapter<StreamItem> implements RecyclerViewClickSupport.OnItemClickListener {
- private Cursor highlightsCursor;
- private Cursor topSitesCursor;
-
- private HomePager.OnUrlOpenListener onUrlOpenListener;
- private HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener;
-
- private int tiles;
- private int tilesWidth;
- private int tilesHeight;
-
- void setOnUrlOpenListeners(HomePager.OnUrlOpenListener onUrlOpenListener, HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
- this.onUrlOpenListener = onUrlOpenListener;
- this.onUrlOpenInBackgroundListener = onUrlOpenInBackgroundListener;
- }
-
- public void setTileSize(int tiles, int tilesWidth, int tilesHeight) {
- this.tilesWidth = tilesWidth;
- this.tilesHeight = tilesHeight;
- this.tiles = tiles;
-
- notifyDataSetChanged();
- }
-
- @Override
- public int getItemViewType(int position) {
- if (position == 0) {
- return TopPanel.LAYOUT_ID;
- } else if (position == 1) {
- return StreamItem.HighlightsTitle.LAYOUT_ID;
- } else {
- return HighlightItem.LAYOUT_ID;
- }
- }
-
- @Override
- public StreamItem onCreateViewHolder(ViewGroup parent, final int type) {
- final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
-
- if (type == TopPanel.LAYOUT_ID) {
- return new TopPanel(inflater.inflate(type, parent, false), onUrlOpenListener, onUrlOpenInBackgroundListener);
- } else if (type == StreamItem.HighlightsTitle.LAYOUT_ID) {
- return new StreamItem.HighlightsTitle(inflater.inflate(type, parent, false));
- } else if (type == HighlightItem.LAYOUT_ID) {
- return new HighlightItem(inflater.inflate(type, parent, false), onUrlOpenListener, onUrlOpenInBackgroundListener);
- } else {
- throw new IllegalStateException("Missing inflation for ViewType " + type);
- }
- }
-
- private int translatePositionToCursor(int position) {
- if (position == 0) {
- throw new IllegalArgumentException("Requested cursor position for invalid item");
- }
-
- // We have two blank panels at the top, hence remove that to obtain the cursor position
- return position - 2;
- }
-
- @Override
- public void onBindViewHolder(StreamItem holder, int position) {
- int type = getItemViewType(position);
-
- if (type == HighlightItem.LAYOUT_ID) {
- final int cursorPosition = translatePositionToCursor(position);
-
- highlightsCursor.moveToPosition(cursorPosition);
- ((HighlightItem) holder).bind(highlightsCursor, tilesWidth, tilesHeight);
- } else if (type == TopPanel.LAYOUT_ID) {
- ((TopPanel) holder).bind(topSitesCursor, tiles, tilesWidth, tilesHeight);
- }
- }
-
- @Override
- public void onItemClicked(RecyclerView recyclerView, int position, View v) {
- if (position < 1) {
- // The header contains top sites and has its own click handling.
- return;
- }
-
- highlightsCursor.moveToPosition(
- translatePositionToCursor(position));
-
- final String url = highlightsCursor.getString(
- highlightsCursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
-
- onUrlOpenListener.onUrlOpen(url, EnumSet.of(HomePager.OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
- }
-
- @Override
- public int getItemCount() {
- final int highlightsCount;
-
- if (highlightsCursor != null) {
- highlightsCount = highlightsCursor.getCount();
- } else {
- highlightsCount = 0;
- }
-
- return highlightsCount + 2;
- }
-
- public void swapHighlightsCursor(Cursor cursor) {
- highlightsCursor = cursor;
-
- notifyDataSetChanged();
- }
-
- public void swapTopSitesCursor(Cursor cursor) {
- this.topSitesCursor = cursor;
-
- notifyItemChanged(0);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/ActivityStreamContextMenu.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/ActivityStreamContextMenu.java
deleted file mode 100644
index 525d3b426..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/ActivityStreamContextMenu.java
+++ /dev/null
@@ -1,239 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-package org.mozilla.gecko.home.activitystream.menu;
-
-import android.content.Context;
-import android.content.Intent;
-import android.database.Cursor;
-import android.support.annotation.NonNull;
-import android.support.design.widget.NavigationView;
-import android.view.MenuItem;
-import android.view.View;
-
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.IntentHelper;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.home.HomePager;
-import org.mozilla.gecko.util.Clipboard;
-import org.mozilla.gecko.util.HardwareUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.UIAsyncTask;
-
-import java.util.EnumSet;
-
-@RobocopTarget
-public abstract class ActivityStreamContextMenu
- implements NavigationView.OnNavigationItemSelectedListener {
-
- public enum MenuMode {
- HIGHLIGHT,
- TOPSITE
- }
-
- final Context context;
-
- final String title;
- final String url;
-
- final HomePager.OnUrlOpenListener onUrlOpenListener;
- final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener;
-
- boolean isAlreadyBookmarked; // default false;
-
- public abstract MenuItem getItemByID(int id);
-
- public abstract void show();
-
- public abstract void dismiss();
-
- final MenuMode mode;
-
- /* package-private */ ActivityStreamContextMenu(final Context context,
- final MenuMode mode,
- final String title, @NonNull final String url,
- HomePager.OnUrlOpenListener onUrlOpenListener,
- HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
- this.context = context;
-
- this.mode = mode;
-
- this.title = title;
- this.url = url;
- this.onUrlOpenListener = onUrlOpenListener;
- this.onUrlOpenInBackgroundListener = onUrlOpenInBackgroundListener;
- }
-
- /**
- * Must be called before the menu is shown.
- * <p/>
- * Your implementation must be ready to return items from getItemByID() before postInit() is
- * called, i.e. you should probably inflate your menu items before this call.
- */
- protected void postInit() {
- // Disable "dismiss" for topsites until we have decided on its behaviour for topsites
- // (currently "dismiss" adds the URL to a highlights-specific blocklist, which the topsites
- // query has no knowledge of).
- if (mode == MenuMode.TOPSITE) {
- final MenuItem dismissItem = getItemByID(R.id.dismiss);
- dismissItem.setVisible(false);
- }
-
- // Disable the bookmark item until we know its bookmark state
- final MenuItem bookmarkItem = getItemByID(R.id.bookmark);
- bookmarkItem.setEnabled(false);
-
- (new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
- @Override
- protected Void doInBackground() {
- isAlreadyBookmarked = BrowserDB.from(context).isBookmark(context.getContentResolver(), url);
- return null;
- }
-
- @Override
- protected void onPostExecute(Void aVoid) {
- if (isAlreadyBookmarked) {
- bookmarkItem.setTitle(R.string.bookmark_remove);
- }
-
- bookmarkItem.setEnabled(true);
- }
- }).execute();
-
- // Only show the "remove from history" item if a page actually has history
- final MenuItem deleteHistoryItem = getItemByID(R.id.delete);
- deleteHistoryItem.setVisible(false);
-
- (new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
- boolean hasHistory;
-
- @Override
- protected Void doInBackground() {
- final Cursor cursor = BrowserDB.from(context).getHistoryForURL(context.getContentResolver(), url);
- try {
- if (cursor != null &&
- cursor.getCount() == 1) {
- hasHistory = true;
- } else {
- hasHistory = false;
- }
- } finally {
- cursor.close();
- }
- return null;
- }
-
- @Override
- protected void onPostExecute(Void aVoid) {
- if (hasHistory) {
- deleteHistoryItem.setVisible(true);
- }
- }
- }).execute();
- }
-
-
- @Override
- public boolean onNavigationItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.share:
- Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "menu");
- IntentHelper.openUriExternal(url, "text/plain", "", "", Intent.ACTION_SEND, title, false);
- break;
-
- case R.id.bookmark:
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- final BrowserDB db = BrowserDB.from(context);
-
- if (isAlreadyBookmarked) {
- db.removeBookmarksWithURL(context.getContentResolver(), url);
- } else {
- db.addBookmark(context.getContentResolver(), title, url);
- }
-
- }
- });
- break;
-
- case R.id.copy_url:
- Clipboard.setText(url);
- break;
-
- case R.id.add_homescreen:
- GeckoAppShell.createShortcut(title, url);
- break;
-
- case R.id.open_new_tab:
- onUrlOpenInBackgroundListener.onUrlOpenInBackground(url, EnumSet.noneOf(HomePager.OnUrlOpenInBackgroundListener.Flags.class));
- break;
-
- case R.id.open_new_private_tab:
- onUrlOpenInBackgroundListener.onUrlOpenInBackground(url, EnumSet.of(HomePager.OnUrlOpenInBackgroundListener.Flags.PRIVATE));
- break;
-
- case R.id.dismiss:
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- BrowserDB.from(context)
- .blockActivityStreamSite(context.getContentResolver(),
- url);
- }
- });
- break;
-
- case R.id.delete:
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- BrowserDB.from(context)
- .removeHistoryEntry(context.getContentResolver(),
- url);
- }
- });
- break;
-
- default:
- throw new IllegalArgumentException("Menu item with ID=" + item.getItemId() + " not handled");
- }
-
- dismiss();
- return true;
- }
-
-
- @RobocopTarget
- public static ActivityStreamContextMenu show(Context context,
- View anchor,
- final MenuMode menuMode,
- final String title, @NonNull final String url,
- HomePager.OnUrlOpenListener onUrlOpenListener,
- HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener,
- final int tilesWidth, final int tilesHeight) {
- final ActivityStreamContextMenu menu;
-
- if (!HardwareUtils.isTablet()) {
- menu = new BottomSheetContextMenu(context,
- menuMode,
- title, url,
- onUrlOpenListener, onUrlOpenInBackgroundListener,
- tilesWidth, tilesHeight);
- } else {
- menu = new PopupContextMenu(context,
- anchor,
- menuMode,
- title, url,
- onUrlOpenListener, onUrlOpenInBackgroundListener);
- }
-
- menu.show();
- return menu;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/BottomSheetContextMenu.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/BottomSheetContextMenu.java
deleted file mode 100644
index e95867c36..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/BottomSheetContextMenu.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-package org.mozilla.gecko.home.activitystream.menu;
-
-import android.content.Context;
-import android.support.annotation.NonNull;
-import android.support.design.widget.BottomSheetBehavior;
-import android.support.design.widget.BottomSheetDialog;
-import android.support.design.widget.NavigationView;
-import android.view.LayoutInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.activitystream.ActivityStream;
-import org.mozilla.gecko.home.HomePager;
-import org.mozilla.gecko.icons.IconCallback;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.icons.Icons;
-import org.mozilla.gecko.widget.FaviconView;
-
-import static org.mozilla.gecko.activitystream.ActivityStream.extractLabel;
-
-/* package-private */ class BottomSheetContextMenu
- extends ActivityStreamContextMenu {
-
-
- private final BottomSheetDialog bottomSheetDialog;
-
- private final NavigationView navigationView;
-
- public BottomSheetContextMenu(final Context context,
- final MenuMode mode,
- final String title, @NonNull final String url,
- HomePager.OnUrlOpenListener onUrlOpenListener,
- HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener,
- final int tilesWidth, final int tilesHeight) {
-
- super(context,
- mode,
- title,
- url,
- onUrlOpenListener,
- onUrlOpenInBackgroundListener);
-
- final LayoutInflater inflater = LayoutInflater.from(context);
- final View content = inflater.inflate(R.layout.activity_stream_contextmenu_bottomsheet, null);
-
- bottomSheetDialog = new BottomSheetDialog(context);
- bottomSheetDialog.setContentView(content);
-
- ((TextView) content.findViewById(R.id.title)).setText(title);
-
- extractLabel(context, url, false, new ActivityStream.LabelCallback() {
- public void onLabelExtracted(String label) {
- ((TextView) content.findViewById(R.id.url)).setText(label);
- }
- });
-
- // Copy layouted parameters from the Highlights / TopSites items to ensure consistency
- final FaviconView faviconView = (FaviconView) content.findViewById(R.id.icon);
- ViewGroup.LayoutParams layoutParams = faviconView.getLayoutParams();
- layoutParams.width = tilesWidth;
- layoutParams.height = tilesHeight;
- faviconView.setLayoutParams(layoutParams);
-
- Icons.with(context)
- .pageUrl(url)
- .skipNetwork()
- .build()
- .execute(new IconCallback() {
- @Override
- public void onIconResponse(IconResponse response) {
- faviconView.updateImage(response);
- }
- });
-
- navigationView = (NavigationView) content.findViewById(R.id.menu);
- navigationView.setNavigationItemSelectedListener(this);
-
- super.postInit();
- }
-
- @Override
- public MenuItem getItemByID(int id) {
- return navigationView.getMenu().findItem(id);
- }
-
- @Override
- public void show() {
- bottomSheetDialog.show();
- }
-
- public void dismiss() {
- bottomSheetDialog.dismiss();
- }
-
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/PopupContextMenu.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/PopupContextMenu.java
deleted file mode 100644
index 56615937b..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/PopupContextMenu.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-package org.mozilla.gecko.home.activitystream.menu;
-
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Color;
-import android.graphics.drawable.ColorDrawable;
-import android.support.annotation.NonNull;
-import android.support.design.widget.NavigationView;
-import android.view.LayoutInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.widget.PopupWindow;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.home.HomePager;
-
-/* package-private */ class PopupContextMenu
- extends ActivityStreamContextMenu {
-
- private final PopupWindow popupWindow;
- private final NavigationView navigationView;
-
- private final View anchor;
-
- public PopupContextMenu(final Context context,
- View anchor,
- final MenuMode mode,
- final String title,
- @NonNull final String url,
- HomePager.OnUrlOpenListener onUrlOpenListener,
- HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
- super(context,
- mode,
- title,
- url,
- onUrlOpenListener,
- onUrlOpenInBackgroundListener);
-
- this.anchor = anchor;
-
- final LayoutInflater inflater = LayoutInflater.from(context);
-
- View card = inflater.inflate(R.layout.activity_stream_contextmenu_popupmenu, null);
- navigationView = (NavigationView) card.findViewById(R.id.menu);
- navigationView.setNavigationItemSelectedListener(this);
-
- popupWindow = new PopupWindow(context);
- popupWindow.setContentView(card);
- popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
- popupWindow.setFocusable(true);
-
- super.postInit();
- }
-
- @Override
- public MenuItem getItemByID(int id) {
- return navigationView.getMenu().findItem(id);
- }
-
- @Override
- public void show() {
- // By default popupWindow follows the pre-material convention of displaying the popup
- // below a View. We need to shift it over the view:
- popupWindow.showAsDropDown(anchor,
- 0,
- -(anchor.getHeight() + anchor.getPaddingBottom()));
- }
-
- public void dismiss() {
- popupWindow.dismiss();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/CirclePageIndicator.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/CirclePageIndicator.java
deleted file mode 100644
index 096f0c597..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/CirclePageIndicator.java
+++ /dev/null
@@ -1,568 +0,0 @@
-/*
- * Copyright (C) 2011 Patrik Akerfeldt
- * Copyright (C) 2011 Jake Wharton
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.mozilla.gecko.home.activitystream.topsites;
-
-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.Paint.Style;
-import android.graphics.drawable.Drawable;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.v4.view.MotionEventCompat;
-import android.support.v4.view.ViewConfigurationCompat;
-import android.support.v4.view.ViewPager;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewConfiguration;
-
-import org.mozilla.gecko.R;
-
-import static android.graphics.Paint.ANTI_ALIAS_FLAG;
-import static android.widget.LinearLayout.HORIZONTAL;
-import static android.widget.LinearLayout.VERTICAL;
-
-/**
- * Draws circles (one for each view). The current view position is filled and
- * others are only stroked.
- *
- * This file was imported from Jake Wharton's ViewPagerIndicator library:
- * https://github.com/JakeWharton/ViewPagerIndicator
- * It was modified to not extend the PageIndicator interface (as we only use one single Indicator)
- * implementation, and has had some minor appearance related modifications added alter.
- */
-public class CirclePageIndicator
- extends View
- implements ViewPager.OnPageChangeListener {
-
- /**
- * Separation between circles, as a factor of the circle radius. By default CirclePageIndicator
- * shipped with a separation factor of 3, however we want to be able to tweak this for
- * ActivityStream.
- *
- * If/when we reuse this indicator elsewhere, this should probably become a configurable property.
- */
- private static final int SEPARATION_FACTOR = 7;
-
- private static final int INVALID_POINTER = -1;
-
- private float mRadius;
- private final Paint mPaintPageFill = new Paint(ANTI_ALIAS_FLAG);
- private final Paint mPaintStroke = new Paint(ANTI_ALIAS_FLAG);
- private final Paint mPaintFill = new Paint(ANTI_ALIAS_FLAG);
- private ViewPager mViewPager;
- private ViewPager.OnPageChangeListener mListener;
- private int mCurrentPage;
- private int mSnapPage;
- private float mPageOffset;
- private int mScrollState;
- private int mOrientation;
- private boolean mCentered;
- private boolean mSnap;
-
- private int mTouchSlop;
- private float mLastMotionX = -1;
- private int mActivePointerId = INVALID_POINTER;
- private boolean mIsDragging;
-
-
- public CirclePageIndicator(Context context) {
- this(context, null);
- }
-
- public CirclePageIndicator(Context context, AttributeSet attrs) {
- this(context, attrs, R.attr.vpiCirclePageIndicatorStyle);
- }
-
- public CirclePageIndicator(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- if (isInEditMode()) return;
-
- //Load defaults from resources
- final Resources res = getResources();
- final int defaultPageColor = res.getColor(R.color.default_circle_indicator_page_color);
- final int defaultFillColor = res.getColor(R.color.default_circle_indicator_fill_color);
- final int defaultOrientation = res.getInteger(R.integer.default_circle_indicator_orientation);
- final int defaultStrokeColor = res.getColor(R.color.default_circle_indicator_stroke_color);
- final float defaultStrokeWidth = res.getDimension(R.dimen.default_circle_indicator_stroke_width);
- final float defaultRadius = res.getDimension(R.dimen.default_circle_indicator_radius);
- final boolean defaultCentered = res.getBoolean(R.bool.default_circle_indicator_centered);
- final boolean defaultSnap = res.getBoolean(R.bool.default_circle_indicator_snap);
-
- //Retrieve styles attributes
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CirclePageIndicator, defStyle, 0);
-
- mCentered = a.getBoolean(R.styleable.CirclePageIndicator_centered, defaultCentered);
- mOrientation = a.getInt(R.styleable.CirclePageIndicator_android_orientation, defaultOrientation);
- mPaintPageFill.setStyle(Style.FILL);
- mPaintPageFill.setColor(a.getColor(R.styleable.CirclePageIndicator_pageColor, defaultPageColor));
- mPaintStroke.setStyle(Style.STROKE);
- mPaintStroke.setColor(a.getColor(R.styleable.CirclePageIndicator_strokeColor, defaultStrokeColor));
- mPaintStroke.setStrokeWidth(a.getDimension(R.styleable.CirclePageIndicator_strokeWidth, defaultStrokeWidth));
- mPaintFill.setStyle(Style.FILL);
- mPaintFill.setColor(a.getColor(R.styleable.CirclePageIndicator_fillColor, defaultFillColor));
- mRadius = a.getDimension(R.styleable.CirclePageIndicator_radius, defaultRadius);
- mSnap = a.getBoolean(R.styleable.CirclePageIndicator_snap, defaultSnap);
-
- Drawable background = a.getDrawable(R.styleable.CirclePageIndicator_android_background);
- if (background != null) {
- setBackgroundDrawable(background);
- }
-
- a.recycle();
-
- final ViewConfiguration configuration = ViewConfiguration.get(context);
- mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
- }
-
-
- public void setCentered(boolean centered) {
- mCentered = centered;
- invalidate();
- }
-
- public boolean isCentered() {
- return mCentered;
- }
-
- public void setPageColor(int pageColor) {
- mPaintPageFill.setColor(pageColor);
- invalidate();
- }
-
- public int getPageColor() {
- return mPaintPageFill.getColor();
- }
-
- public void setFillColor(int fillColor) {
- mPaintFill.setColor(fillColor);
- invalidate();
- }
-
- public int getFillColor() {
- return mPaintFill.getColor();
- }
-
- public void setOrientation(int orientation) {
- switch (orientation) {
- case HORIZONTAL:
- case VERTICAL:
- mOrientation = orientation;
- requestLayout();
- break;
-
- default:
- throw new IllegalArgumentException("Orientation must be either HORIZONTAL or VERTICAL.");
- }
- }
-
- public int getOrientation() {
- return mOrientation;
- }
-
- public void setStrokeColor(int strokeColor) {
- mPaintStroke.setColor(strokeColor);
- invalidate();
- }
-
- public int getStrokeColor() {
- return mPaintStroke.getColor();
- }
-
- public void setStrokeWidth(float strokeWidth) {
- mPaintStroke.setStrokeWidth(strokeWidth);
- invalidate();
- }
-
- public float getStrokeWidth() {
- return mPaintStroke.getStrokeWidth();
- }
-
- public void setRadius(float radius) {
- mRadius = radius;
- invalidate();
- }
-
- public float getRadius() {
- return mRadius;
- }
-
- public void setSnap(boolean snap) {
- mSnap = snap;
- invalidate();
- }
-
- public boolean isSnap() {
- return mSnap;
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
-
- if (mViewPager == null) {
- return;
- }
- final int count = mViewPager.getAdapter().getCount();
- if (count == 0) {
- return;
- }
-
- if (mCurrentPage >= count) {
- setCurrentItem(count - 1);
- return;
- }
-
- int longSize;
- int longPaddingBefore;
- int longPaddingAfter;
- int shortPaddingBefore;
- if (mOrientation == HORIZONTAL) {
- longSize = getWidth();
- longPaddingBefore = getPaddingLeft();
- longPaddingAfter = getPaddingRight();
- shortPaddingBefore = getPaddingTop();
- } else {
- longSize = getHeight();
- longPaddingBefore = getPaddingTop();
- longPaddingAfter = getPaddingBottom();
- shortPaddingBefore = getPaddingLeft();
- }
-
- final float threeRadius = mRadius * SEPARATION_FACTOR;
- final float shortOffset = shortPaddingBefore + mRadius;
- float longOffset = longPaddingBefore + mRadius;
- if (mCentered) {
- longOffset += ((longSize - longPaddingBefore - longPaddingAfter) / 2.0f) - ((count * threeRadius) / 2.0f);
- }
-
- float dX;
- float dY;
-
- float pageFillRadius = mRadius;
- if (mPaintStroke.getStrokeWidth() > 0) {
- pageFillRadius -= mPaintStroke.getStrokeWidth() / 2.0f;
- }
-
- //Draw stroked circles
- for (int iLoop = 0; iLoop < count; iLoop++) {
- float drawLong = longOffset + (iLoop * threeRadius);
- if (mOrientation == HORIZONTAL) {
- dX = drawLong;
- dY = shortOffset;
- } else {
- dX = shortOffset;
- dY = drawLong;
- }
- // Only paint fill if not completely transparent
- if (mPaintPageFill.getAlpha() > 0) {
- canvas.drawCircle(dX, dY, pageFillRadius, mPaintPageFill);
- }
-
- // Only paint stroke if a stroke width was non-zero
- if (pageFillRadius != mRadius) {
- canvas.drawCircle(dX, dY, mRadius, mPaintStroke);
- }
- }
-
- //Draw the filled circle according to the current scroll
- float cx = (mSnap ? mSnapPage : mCurrentPage) * threeRadius;
- if (!mSnap) {
- cx += mPageOffset * threeRadius;
- }
- if (mOrientation == HORIZONTAL) {
- dX = longOffset + cx;
- dY = shortOffset;
- } else {
- dX = shortOffset;
- dY = longOffset + cx;
- }
- canvas.drawCircle(dX, dY, mRadius, mPaintFill);
- }
-
- public boolean onTouchEvent(android.view.MotionEvent ev) {
- if (super.onTouchEvent(ev)) {
- return true;
- }
- if ((mViewPager == null) || (mViewPager.getAdapter().getCount() == 0)) {
- return false;
- }
-
- final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
- mLastMotionX = ev.getX();
- break;
-
- case MotionEvent.ACTION_MOVE: {
- final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
- final float x = MotionEventCompat.getX(ev, activePointerIndex);
- final float deltaX = x - mLastMotionX;
-
- if (!mIsDragging) {
- if (Math.abs(deltaX) > mTouchSlop) {
- mIsDragging = true;
- }
- }
-
- if (mIsDragging) {
- mLastMotionX = x;
- if (mViewPager.isFakeDragging() || mViewPager.beginFakeDrag()) {
- mViewPager.fakeDragBy(deltaX);
- }
- }
-
- break;
- }
-
- case MotionEvent.ACTION_CANCEL:
- case MotionEvent.ACTION_UP:
- if (!mIsDragging) {
- final int count = mViewPager.getAdapter().getCount();
- final int width = getWidth();
- final float halfWidth = width / 2f;
- final float sixthWidth = width / 6f;
-
- if ((mCurrentPage > 0) && (ev.getX() < halfWidth - sixthWidth)) {
- if (action != MotionEvent.ACTION_CANCEL) {
- mViewPager.setCurrentItem(mCurrentPage - 1);
- }
- return true;
- } else if ((mCurrentPage < count - 1) && (ev.getX() > halfWidth + sixthWidth)) {
- if (action != MotionEvent.ACTION_CANCEL) {
- mViewPager.setCurrentItem(mCurrentPage + 1);
- }
- return true;
- }
- }
-
- mIsDragging = false;
- mActivePointerId = INVALID_POINTER;
- if (mViewPager.isFakeDragging()) mViewPager.endFakeDrag();
- break;
-
- case MotionEventCompat.ACTION_POINTER_DOWN: {
- final int index = MotionEventCompat.getActionIndex(ev);
- mLastMotionX = MotionEventCompat.getX(ev, index);
- mActivePointerId = MotionEventCompat.getPointerId(ev, index);
- break;
- }
-
- case MotionEventCompat.ACTION_POINTER_UP:
- final int pointerIndex = MotionEventCompat.getActionIndex(ev);
- final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
- if (pointerId == mActivePointerId) {
- final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
- mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
- }
- mLastMotionX = MotionEventCompat.getX(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId));
- break;
- }
-
- return true;
- }
-
- public void setViewPager(ViewPager view) {
- if (mViewPager == view) {
- return;
- }
- if (mViewPager != null) {
- mViewPager.setOnPageChangeListener(null);
- }
- if (view.getAdapter() == null) {
- throw new IllegalStateException("ViewPager does not have adapter instance.");
- }
- mViewPager = view;
- mViewPager.setOnPageChangeListener(this);
- invalidate();
- }
-
- public void setViewPager(ViewPager view, int initialPosition) {
- setViewPager(view);
- setCurrentItem(initialPosition);
- }
-
- public void setCurrentItem(int item) {
- if (mViewPager == null) {
- throw new IllegalStateException("ViewPager has not been bound.");
- }
- mViewPager.setCurrentItem(item);
- mCurrentPage = item;
- invalidate();
- }
-
- public void notifyDataSetChanged() {
- invalidate();
- }
-
- @Override
- public void onPageScrollStateChanged(int state) {
- mScrollState = state;
-
- if (mListener != null) {
- mListener.onPageScrollStateChanged(state);
- }
- }
-
- public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
- mCurrentPage = position;
- mPageOffset = positionOffset;
- invalidate();
-
- if (mListener != null) {
- mListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
- }
- }
-
- @Override
- public void onPageSelected(int position) {
- if (mSnap || mScrollState == ViewPager.SCROLL_STATE_IDLE) {
- mCurrentPage = position;
- mSnapPage = position;
- invalidate();
- }
-
- if (mListener != null) {
- mListener.onPageSelected(position);
- }
- }
-
- public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) {
- mListener = listener;
- }
-
- /*
- * (non-Javadoc)
- *
- * @see android.view.View#onMeasure(int, int)
- */
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- if (mOrientation == HORIZONTAL) {
- setMeasuredDimension(measureLong(widthMeasureSpec), measureShort(heightMeasureSpec));
- } else {
- setMeasuredDimension(measureShort(widthMeasureSpec), measureLong(heightMeasureSpec));
- }
- }
-
- /**
- * Determines the width of this view
- *
- * @param measureSpec
- * A measureSpec packed into an int
- * @return The width of the view, honoring constraints from measureSpec
- */
- private int measureLong(int measureSpec) {
- int result;
- int specMode = MeasureSpec.getMode(measureSpec);
- int specSize = MeasureSpec.getSize(measureSpec);
-
- if ((specMode == MeasureSpec.EXACTLY) || (mViewPager == null)) {
- //We were told how big to be
- result = specSize;
- } else {
- //Calculate the width according the views count
- final int count = mViewPager.getAdapter().getCount();
- result = (int)(getPaddingLeft() + getPaddingRight()
- + (count * 2 * mRadius) + (count - 1) * mRadius + 1);
- //Respect AT_MOST value if that was what is called for by measureSpec
- if (specMode == MeasureSpec.AT_MOST) {
- result = Math.min(result, specSize);
- }
- }
- return result;
- }
-
- /**
- * Determines the height of this view
- *
- * @param measureSpec
- * A measureSpec packed into an int
- * @return The height of the view, honoring constraints from measureSpec
- */
- private int measureShort(int measureSpec) {
- int result;
- int specMode = MeasureSpec.getMode(measureSpec);
- int specSize = MeasureSpec.getSize(measureSpec);
-
- if (specMode == MeasureSpec.EXACTLY) {
- //We were told how big to be
- result = specSize;
- } else {
- //Measure the height
- result = (int)(2 * mRadius + getPaddingTop() + getPaddingBottom() + 1);
- //Respect AT_MOST value if that was what is called for by measureSpec
- if (specMode == MeasureSpec.AT_MOST) {
- result = Math.min(result, specSize);
- }
- }
- return result;
- }
-
- @Override
- public void onRestoreInstanceState(Parcelable state) {
- SavedState savedState = (SavedState)state;
- super.onRestoreInstanceState(savedState.getSuperState());
- mCurrentPage = savedState.currentPage;
- mSnapPage = savedState.currentPage;
- requestLayout();
- }
-
- @Override
- public Parcelable onSaveInstanceState() {
- Parcelable superState = super.onSaveInstanceState();
- SavedState savedState = new SavedState(superState);
- savedState.currentPage = mCurrentPage;
- return savedState;
- }
-
- static class SavedState extends BaseSavedState {
- int currentPage;
-
- public SavedState(Parcelable superState) {
- super(superState);
- }
-
- private SavedState(Parcel in) {
- super(in);
- currentPage = in.readInt();
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- super.writeToParcel(dest, flags);
- dest.writeInt(currentPage);
- }
-
- @SuppressWarnings("UnusedDeclaration")
- public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
- @Override
- public SavedState createFromParcel(Parcel in) {
- return new SavedState(in);
- }
-
- @Override
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesCard.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesCard.java
deleted file mode 100644
index b436a466f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesCard.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-package org.mozilla.gecko.home.activitystream.topsites;
-
-import android.graphics.Color;
-import android.support.v7.widget.RecyclerView;
-import android.view.View;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.activitystream.ActivityStream;
-import org.mozilla.gecko.home.HomePager;
-import org.mozilla.gecko.home.activitystream.menu.ActivityStreamContextMenu;
-import org.mozilla.gecko.icons.IconCallback;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.icons.Icons;
-import org.mozilla.gecko.util.DrawableUtil;
-import org.mozilla.gecko.util.ViewUtil;
-import org.mozilla.gecko.util.TouchTargetUtil;
-import org.mozilla.gecko.widget.FaviconView;
-
-import java.util.EnumSet;
-import java.util.concurrent.Future;
-
-class TopSitesCard extends RecyclerView.ViewHolder
- implements IconCallback, View.OnClickListener {
- private final FaviconView faviconView;
-
- private final TextView title;
- private final ImageView menuButton;
- private Future<IconResponse> ongoingIconLoad;
-
- private String url;
-
- private final HomePager.OnUrlOpenListener onUrlOpenListener;
- private final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener;
-
- public TopSitesCard(FrameLayout card, final HomePager.OnUrlOpenListener onUrlOpenListener, final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
- super(card);
-
- faviconView = (FaviconView) card.findViewById(R.id.favicon);
-
- title = (TextView) card.findViewById(R.id.title);
- menuButton = (ImageView) card.findViewById(R.id.menu);
-
- this.onUrlOpenListener = onUrlOpenListener;
- this.onUrlOpenInBackgroundListener = onUrlOpenInBackgroundListener;
-
- card.setOnClickListener(this);
-
- TouchTargetUtil.ensureTargetHitArea(menuButton, card);
- menuButton.setOnClickListener(this);
-
- ViewUtil.enableTouchRipple(menuButton);
- }
-
- void bind(final TopSitesPageAdapter.TopSite topSite) {
- ActivityStream.extractLabel(itemView.getContext(), topSite.url, true, new ActivityStream.LabelCallback() {
- @Override
- public void onLabelExtracted(String label) {
- title.setText(label);
- }
- });
-
- this.url = topSite.url;
-
- if (ongoingIconLoad != null) {
- ongoingIconLoad.cancel(true);
- }
-
- ongoingIconLoad = Icons.with(itemView.getContext())
- .pageUrl(topSite.url)
- .skipNetwork()
- .build()
- .execute(this);
- }
-
- @Override
- public void onIconResponse(IconResponse response) {
- faviconView.updateImage(response);
-
- final int tintColor = !response.hasColor() || response.getColor() == Color.WHITE ? Color.LTGRAY : Color.WHITE;
-
- menuButton.setImageDrawable(
- DrawableUtil.tintDrawable(menuButton.getContext(), R.drawable.menu, tintColor));
- }
-
- @Override
- public void onClick(View clickedView) {
- if (clickedView == itemView) {
- onUrlOpenListener.onUrlOpen(url, EnumSet.noneOf(HomePager.OnUrlOpenListener.Flags.class));
- } else if (clickedView == menuButton) {
- ActivityStreamContextMenu.show(clickedView.getContext(),
- menuButton,
- ActivityStreamContextMenu.MenuMode.TOPSITE,
- title.getText().toString(), url,
- onUrlOpenListener, onUrlOpenInBackgroundListener,
- faviconView.getWidth(), faviconView.getHeight());
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPage.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPage.java
deleted file mode 100644
index 45fdc0d1a..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPage.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-package org.mozilla.gecko.home.activitystream.topsites;
-
-import android.content.Context;
-import android.support.annotation.Nullable;
-import android.support.v7.widget.GridLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.util.AttributeSet;
-import android.view.View;
-
-import org.mozilla.gecko.home.HomePager;
-import org.mozilla.gecko.widget.RecyclerViewClickSupport;
-
-import java.util.EnumSet;
-
-public class TopSitesPage
- extends RecyclerView {
- public TopSitesPage(Context context,
- @Nullable AttributeSet attrs) {
- super(context, attrs);
-
- setLayoutManager(new GridLayoutManager(context, 1));
- }
-
- public void setTiles(int tiles) {
- setLayoutManager(new GridLayoutManager(getContext(), tiles));
- }
-
- private HomePager.OnUrlOpenListener onUrlOpenListener;
- private HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener;
-
- public TopSitesPageAdapter getAdapter() {
- return (TopSitesPageAdapter) super.getAdapter();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPageAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPageAdapter.java
deleted file mode 100644
index 29e6aca3d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPageAdapter.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-package org.mozilla.gecko.home.activitystream.topsites;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.support.annotation.UiThread;
-import android.support.v7.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.home.HomePager;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class TopSitesPageAdapter extends RecyclerView.Adapter<TopSitesCard> {
- static final class TopSite {
- public final long id;
- public final String url;
- public final String title;
-
- TopSite(long id, String url, String title) {
- this.id = id;
- this.url = url;
- this.title = title;
- }
- }
-
- private List<TopSite> topSites;
- private int tiles;
- private int tilesWidth;
- private int tilesHeight;
- private int textHeight;
-
- private final HomePager.OnUrlOpenListener onUrlOpenListener;
- private final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener;
-
- public TopSitesPageAdapter(Context context, int tiles, int tilesWidth, int tilesHeight,
- HomePager.OnUrlOpenListener onUrlOpenListener, HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
- setHasStableIds(true);
-
- this.topSites = new ArrayList<>();
- this.tiles = tiles;
- this.tilesWidth = tilesWidth;
- this.tilesHeight = tilesHeight;
- this.textHeight = context.getResources().getDimensionPixelSize(R.dimen.activity_stream_top_sites_text_height);
-
- this.onUrlOpenListener = onUrlOpenListener;
- this.onUrlOpenInBackgroundListener = onUrlOpenInBackgroundListener;
- }
-
- /**
- *
- * @param cursor
- * @param startIndex The first item that this topsites group should show. This item, and the following
- * 3 items will be displayed by this adapter.
- */
- public void swapCursor(Cursor cursor, int startIndex) {
- topSites.clear();
-
- if (cursor == null) {
- return;
- }
-
- for (int i = 0; i < tiles && startIndex + i < cursor.getCount(); i++) {
- cursor.moveToPosition(startIndex + i);
-
- // The Combined View only contains pages that have been visited at least once, i.e. any
- // page in the TopSites query will contain a HISTORY_ID. _ID however will be 0 for all rows.
- final long id = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID));
- final String url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
- final String title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.TITLE));
-
- topSites.add(new TopSite(id, url, title));
- }
-
- notifyDataSetChanged();
- }
-
- @Override
- public void onBindViewHolder(TopSitesCard holder, int position) {
- holder.bind(topSites.get(position));
- }
-
- @Override
- public TopSitesCard onCreateViewHolder(ViewGroup parent, int viewType) {
- final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
-
- final FrameLayout card = (FrameLayout) inflater.inflate(R.layout.activity_stream_topsites_card, parent, false);
- final View content = card.findViewById(R.id.content);
-
- ViewGroup.LayoutParams layoutParams = content.getLayoutParams();
- layoutParams.width = tilesWidth;
- layoutParams.height = tilesHeight + textHeight;
- content.setLayoutParams(layoutParams);
-
- return new TopSitesCard(card, onUrlOpenListener, onUrlOpenInBackgroundListener);
- }
-
- @Override
- public int getItemCount() {
- return topSites.size();
- }
-
- @Override
- @UiThread
- public long getItemId(int position) {
- return topSites.get(position).id;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPagerAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPagerAdapter.java
deleted file mode 100644
index dc824d902..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPagerAdapter.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-package org.mozilla.gecko.home.activitystream.topsites;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.support.v4.view.PagerAdapter;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.home.HomePager;
-
-import java.util.LinkedList;
-
-/**
- * The primary / top-level TopSites adapter: it handles the ViewPager, and also handles
- * all lower-level Adapters that populate the individual topsite items.
- */
-public class TopSitesPagerAdapter extends PagerAdapter {
- public static final int PAGES = 4;
-
- private int tiles;
- private int tilesWidth;
- private int tilesHeight;
-
- private LinkedList<TopSitesPage> pages = new LinkedList<>();
-
- private final Context context;
- private final HomePager.OnUrlOpenListener onUrlOpenListener;
- private final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener;
-
- private int count = 0;
-
- public TopSitesPagerAdapter(Context context,
- HomePager.OnUrlOpenListener onUrlOpenListener,
- HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
- this.context = context;
- this.onUrlOpenListener = onUrlOpenListener;
- this.onUrlOpenInBackgroundListener = onUrlOpenInBackgroundListener;
- }
-
- public void setTilesSize(int tiles, int tilesWidth, int tilesHeight) {
- this.tilesWidth = tilesWidth;
- this.tilesHeight = tilesHeight;
- this.tiles = tiles;
- }
-
- @Override
- public int getCount() {
- return Math.min(count, 4);
- }
-
- @Override
- public boolean isViewFromObject(View view, Object object) {
- return view == object;
- }
-
- @Override
- public Object instantiateItem(ViewGroup container, int position) {
- TopSitesPage page = pages.get(position);
-
- container.addView(page);
-
- return page;
- }
-
- @Override
- public int getItemPosition(Object object) {
- return PagerAdapter.POSITION_NONE;
- }
-
- @Override
- public void destroyItem(ViewGroup container, int position, Object object) {
- container.removeView((View) object);
- }
-
- public void swapCursor(Cursor cursor) {
- // Divide while rounding up: 0 items = 0 pages, 1-ITEMS_PER_PAGE items = 1 page, etc.
- if (cursor != null) {
- count = (cursor.getCount() - 1) / tiles + 1;
- } else {
- count = 0;
- }
-
- pages.clear();
- final int pageDelta = count;
-
- if (pageDelta > 0) {
- final LayoutInflater inflater = LayoutInflater.from(context);
- for (int i = 0; i < pageDelta; i++) {
- final TopSitesPage page = (TopSitesPage) inflater.inflate(R.layout.activity_stream_topsites_page, null, false);
-
- page.setTiles(tiles);
- final TopSitesPageAdapter adapter = new TopSitesPageAdapter(context, tiles, tilesWidth, tilesHeight,
- onUrlOpenListener, onUrlOpenInBackgroundListener);
- page.setAdapter(adapter);
- pages.add(page);
- }
- } else if (pageDelta < 0) {
- for (int i = 0; i > pageDelta; i--) {
- final TopSitesPage page = pages.getLast();
-
- // Ensure the page doesn't use the old/invalid cursor anymore
- page.getAdapter().swapCursor(null, 0);
-
- pages.removeLast();
- }
- } else {
- // do nothing: we will be updating all the pages below
- }
-
- int startIndex = 0;
- for (TopSitesPage page : pages) {
- page.getAdapter().swapCursor(cursor, startIndex);
- startIndex += tiles;
- }
-
- notifyDataSetChanged();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/IconCallback.java b/mobile/android/base/java/org/mozilla/gecko/icons/IconCallback.java
deleted file mode 100644
index 0232a4ea6..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/IconCallback.java
+++ /dev/null
@@ -1,13 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons;
-
-/**
- * Interface for a callback that will be executed once an icon has been loaded successfully.
- */
-public interface IconCallback {
- void onIconResponse(IconResponse response);
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/IconDescriptor.java b/mobile/android/base/java/org/mozilla/gecko/icons/IconDescriptor.java
deleted file mode 100644
index 359c47e53..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/IconDescriptor.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons;
-
-import android.support.annotation.IntDef;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-
-/**
- * A class describing the location and properties of an icon that can be loaded.
- */
-public class IconDescriptor {
- @IntDef({ TYPE_GENERIC, TYPE_FAVICON, TYPE_TOUCHICON, TYPE_LOOKUP })
- @interface IconType {}
-
- // The type values are used for ranking icons (higher values = try to load first).
- @VisibleForTesting static final int TYPE_GENERIC = 0;
- @VisibleForTesting static final int TYPE_LOOKUP = 1;
- @VisibleForTesting static final int TYPE_FAVICON = 5;
- @VisibleForTesting static final int TYPE_TOUCHICON = 10;
-
- private final String url;
- private final int size;
- private final String mimeType;
- private final int type;
-
- /**
- * Create a generic icon located at the given URL. No MIME type or size is known.
- */
- public static IconDescriptor createGenericIcon(String url) {
- return new IconDescriptor(TYPE_GENERIC, url, 0, null);
- }
-
- /**
- * Create a favicon located at the given URL and with a known size and MIME type.
- */
- public static IconDescriptor createFavicon(String url, int size, String mimeType) {
- return new IconDescriptor(TYPE_FAVICON, url, size, mimeType);
- }
-
- /**
- * Create a touch icon located at the given URL and with a known MIME type and size.
- */
- public static IconDescriptor createTouchicon(String url, int size, String mimeType) {
- return new IconDescriptor(TYPE_TOUCHICON, url, size, mimeType);
- }
-
- /**
- * Create an icon located at an URL that has been returned from a disk or memory storage. This
- * is an icon with an URL we loaded an icon from previously. Therefore we give it a little higher
- * ranking than a generic icon - even though we do not know the MIME type or size of the icon.
- */
- public static IconDescriptor createLookupIcon(String url) {
- return new IconDescriptor(TYPE_LOOKUP, url, 0, null);
- }
-
- private IconDescriptor(@IconType int type, String url, int size, String mimeType) {
- this.type = type;
- this.url = url;
- this.size = size;
- this.mimeType = mimeType;
- }
-
- /**
- * Get the URL of the icon.
- */
- public String getUrl() {
- return url;
- }
-
- /**
- * Get the (assumed) size of the icon. Returns 0 if no size is known.
- */
- public int getSize() {
- return size;
- }
-
- /**
- * Get the type of the icon (favicon, touch icon, generic, lookup).
- */
- @IconType
- public int getType() {
- return type;
- }
-
- /**
- * Get the (assumed) MIME type of the icon. Returns null if no MIME type is known.
- */
- @Nullable
- public String getMimeType() {
- return mimeType;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/IconDescriptorComparator.java b/mobile/android/base/java/org/mozilla/gecko/icons/IconDescriptorComparator.java
deleted file mode 100644
index 3c6064825..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/IconDescriptorComparator.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons;
-
-import java.util.Comparator;
-
-/**
- * This comparator implementation compares IconDescriptor objects in order to determine which icon
- * to load first.
- *
- * In general this comparator will try touch icons before favicons (they usually have a higher resolution)
- * and prefers larger icons over smaller ones.
- */
-/* package-private */ class IconDescriptorComparator implements Comparator<IconDescriptor> {
- @Override
- public int compare(final IconDescriptor lhs, final IconDescriptor rhs) {
- if (lhs.getUrl().equals(rhs.getUrl())) {
- // Two descriptors pointing to the same URL are always referencing the same icon. So treat
- // them as equal.
- return 0;
- }
-
- // First compare the types. We prefer touch icons because they tend to have a higher resolution
- // than ordinary favicons.
- if (lhs.getType() != rhs.getType()) {
- return compareType(lhs, rhs);
- }
-
- // If one of them is larger than pick the larger icon.
- if (lhs.getSize() != rhs.getSize()) {
- return compareSizes(lhs, rhs);
- }
-
- // If there's no other way to choose, we prefer container types. They *might* contain
- // an image larger than the size given in the <link> tag.
- final boolean lhsContainer = IconsHelper.isContainerType(lhs.getMimeType());
- final boolean rhsContainer = IconsHelper.isContainerType(rhs.getMimeType());
-
- if (lhsContainer != rhsContainer) {
- return lhsContainer ? -1 : 1;
- }
-
- // There's no way to know which icon might be better. However we need to pick a consistent
- // one to avoid breaking the TreeSet implementation (See Bug 1331808). Therefore we are
- // picking one by just comparing the URLs.
- return lhs.getUrl().compareTo(rhs.getUrl());
- }
-
- private int compareType(IconDescriptor lhs, IconDescriptor rhs) {
- if (lhs.getType() > rhs.getType()) {
- return -1;
- } else {
- return 1;
- }
- }
-
- private int compareSizes(IconDescriptor lhs, IconDescriptor rhs) {
- if (lhs.getSize() > rhs.getSize()) {
- return -1;
- } else {
- return 1;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/IconRequest.java b/mobile/android/base/java/org/mozilla/gecko/icons/IconRequest.java
deleted file mode 100644
index be000642e..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/IconRequest.java
+++ /dev/null
@@ -1,181 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons;
-
-import android.content.Context;
-import android.support.annotation.VisibleForTesting;
-
-import org.mozilla.gecko.R;
-
-import java.util.Iterator;
-import java.util.TreeSet;
-import java.util.concurrent.Future;
-
-/**
- * A class describing a request to load an icon for a website.
- */
-public class IconRequest {
- private Context context;
-
- // Those values are written by the IconRequestBuilder class.
- /* package-private */ String pageUrl;
- /* package-private */ boolean privileged;
- /* package-private */ TreeSet<IconDescriptor> icons;
- /* package-private */ boolean skipNetwork;
- /* package-private */ boolean backgroundThread;
- /* package-private */ boolean skipDisk;
- /* package-private */ boolean skipMemory;
- /* package-private */ int targetSize;
- /* package-private */ boolean prepareOnly;
- private IconCallback callback;
-
- /* package-private */ IconRequest(Context context) {
- this.context = context.getApplicationContext();
- this.icons = new TreeSet<>(new IconDescriptorComparator());
-
- // Setting some sensible defaults.
- this.privileged = false;
- this.skipMemory = false;
- this.skipDisk = false;
- this.skipNetwork = false;
- this.targetSize = context.getResources().getDimensionPixelSize(R.dimen.favicon_bg);
- this.prepareOnly = false;
- }
-
- /**
- * Execute this request and try to load an icon. Once an icon has been loaded successfully the
- * callback will be executed.
- *
- * The returned Future can be used to cancel the job.
- */
- public Future<IconResponse> execute(IconCallback callback) {
- setCallback(callback);
-
- return IconRequestExecutor.submit(this);
- }
-
- @VisibleForTesting void setCallback(IconCallback callback) {
- this.callback = callback;
- }
-
- /**
- * Get the (application) context associated with this request.
- */
- public Context getContext() {
- return context;
- }
-
- /**
- * Get the descriptor for the potentially best icon. This is the icon that should be loaded if
- * possible.
- */
- public IconDescriptor getBestIcon() {
- return icons.first();
- }
-
- /**
- * Get the URL of the page for which an icon should be loaded.
- */
- public String getPageUrl() {
- return pageUrl;
- }
-
- /**
- * Is this request allowed to load icons from internal data sources like the omni.ja?
- */
- public boolean isPrivileged() {
- return privileged;
- }
-
- /**
- * Get the number of icon descriptors associated with this request.
- */
- public int getIconCount() {
- return icons.size();
- }
-
- /**
- * Get the required target size of the icon.
- */
- public int getTargetSize() {
- return targetSize;
- }
-
- /**
- * Should a loader access the network to load this icon?
- */
- public boolean shouldSkipNetwork() {
- return skipNetwork;
- }
-
- /**
- * Should a loader access the disk to load this icon?
- */
- public boolean shouldSkipDisk() {
- return skipDisk;
- }
-
- /**
- * Should a loader access the memory cache to load this icon?
- */
- public boolean shouldSkipMemory() {
- return skipMemory;
- }
-
- /**
- * Get an iterator to iterate over all icon descriptors associated with this request.
- */
- public Iterator<IconDescriptor> getIconIterator() {
- return icons.iterator();
- }
-
- /**
- * Create a builder to modify this request.
- *
- * Calling methods on the builder will modify this object and not create a copy.
- */
- public IconRequestBuilder modify() {
- return new IconRequestBuilder(this);
- }
-
- /**
- * Should the callback be executed on a background thread? By default a callback is always
- * executed on the UI thread because an icon is usually loaded in order to display it somewhere
- * in the UI.
- */
- /* package-private */ boolean shouldRunOnBackgroundThread() {
- return backgroundThread;
- }
-
- /* package-private */ IconCallback getCallback() {
- return callback;
- }
-
- /* package-private */ boolean hasIconDescriptors() {
- return !icons.isEmpty();
- }
-
- /**
- * Move to the next icon. This method is called after all loaders for the current best icon
- * have failed. After calling this method getBestIcon() will return the next icon to try.
- * hasIconDescriptors() should be called before requesting the next icon.
- */
- /* package-private */ void moveToNextIcon() {
- if (!icons.remove(getBestIcon())) {
- // Calling this method when there's no next icon is an error (use hasIconDescriptors()).
- // Theoretically this method can fail even if there's a next icon (like it did in bug 1331808).
- // In this case crashing to see and fix the issue is desired.
- throw new IllegalStateException("Moving to next icon failed. Could not remove first icon from set.");
- }
- }
-
- /**
- * Should this request be prepared but not actually load an icon?
- */
- /* package-private */ boolean shouldPrepareOnly() {
- return prepareOnly;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/IconRequestBuilder.java b/mobile/android/base/java/org/mozilla/gecko/icons/IconRequestBuilder.java
deleted file mode 100644
index d9fd9ec5a..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/IconRequestBuilder.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons;
-
-import android.content.Context;
-import android.support.annotation.CheckResult;
-
-import org.mozilla.gecko.GeckoAppShell;
-
-import ch.boye.httpclientandroidlib.util.TextUtils;
-
-/**
- * Builder for creating a request to load an icon.
- */
-public class IconRequestBuilder {
- private final IconRequest request;
-
- /* package-private */ IconRequestBuilder(Context context) {
- this(new IconRequest(context));
- }
-
- /* package-private */ IconRequestBuilder(IconRequest request) {
- this.request = request;
- }
-
- /**
- * Set the URL of the page for which the icon should be loaded.
- */
- @CheckResult
- public IconRequestBuilder pageUrl(String pageUrl) {
- request.pageUrl = pageUrl;
- return this;
- }
-
- /**
- * Set whether this request is allowed to load icons from non http(s) URLs (e.g. the omni.ja).
- *
- * For example web content referencing internal URLs should not lead to us loading icons from
- * internal data structures like the omni.ja.
- */
- @CheckResult
- public IconRequestBuilder privileged(boolean privileged) {
- request.privileged = privileged;
- return this;
- }
-
- /**
- * Add an icon descriptor describing the location and properties of an icon. All descriptors
- * will be ranked and tried in order of their rank. Executing the request will modify the list
- * of icons (filter or add additional descriptors).
- */
- @CheckResult
- public IconRequestBuilder icon(IconDescriptor descriptor) {
- request.icons.add(descriptor);
- return this;
- }
-
- /**
- * Skip the network and do not load an icon from a network connection.
- */
- @CheckResult
- public IconRequestBuilder skipNetwork() {
- request.skipNetwork = true;
- return this;
- }
-
- /**
- * Skip the disk cache and do not load an icon from disk.
- */
- @CheckResult
- public IconRequestBuilder skipDisk() {
- request.skipDisk = true;
- return this;
- }
-
- /**
- * Skip the memory cache and do not return a previously loaded icon.
- */
- @CheckResult
- public IconRequestBuilder skipMemory() {
- request.skipMemory = true;
- return this;
- }
-
- /**
- * The icon will be used as (Android) launcher icon. The loaded icon will be scaled to the
- * preferred Android launcher icon size.
- */
- public IconRequestBuilder forLauncherIcon() {
- request.targetSize = GeckoAppShell.getPreferredIconSize();
- return this;
- }
-
- /**
- * Execute the callback on the background thread. By default the callback is always executed on
- * the UI thread in order to add the loaded icon to a view easily.
- */
- @CheckResult
- public IconRequestBuilder executeCallbackOnBackgroundThread() {
- request.backgroundThread = true;
- return this;
- }
-
- /**
- * When executing the request then only prepare executing it but do not actually load an icon.
- * This mode is only used for some legacy code that uses the icon URL and therefore needs to
- * perform a lookup of the URL but doesn't want to load the icon yet.
- */
- public IconRequestBuilder prepareOnly() {
- request.prepareOnly = true;
- return this;
- }
-
- /**
- * Return the request built with this builder.
- */
- @CheckResult
- public IconRequest build() {
- if (TextUtils.isEmpty(request.pageUrl)) {
- throw new IllegalStateException("Page URL is required");
- }
-
- return request;
- }
-
- /**
- * This is a no-op method.
- *
- * All builder methods are annotated with @CheckResult to denote that the
- * methods return the builder object and that it is typically an error to not call another method
- * on it until eventually calling build().
- *
- * However in some situations code can keep a reference
- * to the builder object and call methods only when a specific event occurs. To make this explicit
- * and avoid lint errors this method can be called.
- */
- public void deferBuild() {
- // No op
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/IconRequestExecutor.java b/mobile/android/base/java/org/mozilla/gecko/icons/IconRequestExecutor.java
deleted file mode 100644
index aad784980..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/IconRequestExecutor.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons;
-
-import android.support.annotation.NonNull;
-
-import org.mozilla.gecko.icons.loader.ContentProviderLoader;
-import org.mozilla.gecko.icons.loader.DataUriLoader;
-import org.mozilla.gecko.icons.loader.DiskLoader;
-import org.mozilla.gecko.icons.loader.IconDownloader;
-import org.mozilla.gecko.icons.loader.IconGenerator;
-import org.mozilla.gecko.icons.loader.IconLoader;
-import org.mozilla.gecko.icons.loader.JarLoader;
-import org.mozilla.gecko.icons.loader.LegacyLoader;
-import org.mozilla.gecko.icons.loader.MemoryLoader;
-import org.mozilla.gecko.icons.preparation.AboutPagesPreparer;
-import org.mozilla.gecko.icons.preparation.AddDefaultIconUrl;
-import org.mozilla.gecko.icons.preparation.FilterKnownFailureUrls;
-import org.mozilla.gecko.icons.preparation.FilterMimeTypes;
-import org.mozilla.gecko.icons.preparation.FilterPrivilegedUrls;
-import org.mozilla.gecko.icons.preparation.LookupIconUrl;
-import org.mozilla.gecko.icons.preparation.Preparer;
-import org.mozilla.gecko.icons.processing.ColorProcessor;
-import org.mozilla.gecko.icons.processing.DiskProcessor;
-import org.mozilla.gecko.icons.processing.MemoryProcessor;
-import org.mozilla.gecko.icons.processing.Processor;
-import org.mozilla.gecko.icons.processing.ResizingProcessor;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Executor for icon requests.
- */
-/* package-private */ class IconRequestExecutor {
- /**
- * Loader implementation that generates an icon if none could be loaded.
- */
- private static final IconLoader GENERATOR = new IconGenerator();
-
- /**
- * Ordered list of prepares that run before any icon is loaded.
- */
- private static final List<Preparer> PREPARERS = Arrays.asList(
- // First we look into our memory and disk caches if there are some known icon URLs for
- // the page URL of the request.
- new LookupIconUrl(),
-
- // For all icons with MIME type we filter entries with unknown MIME type that we probably
- // cannot decode anyways.
- new FilterMimeTypes(),
-
- // If this is not a request that is allowed to load icons from privileged locations (omni.jar)
- // then filter such icon URLs.
- new FilterPrivilegedUrls(),
-
- // This preparer adds an icon URL for about pages. It's added after the filter for privileged
- // URLs. We always want to be able to load those specific icons.
- new AboutPagesPreparer(),
-
- // Add the default favicon URL (*/favicon.ico) to the list of icon URLs; with a low priority,
- // this icon URL should be tried last.
- new AddDefaultIconUrl(),
-
- // Finally we filter all URLs that failed to load recently (4xx / 5xx errors).
- new FilterKnownFailureUrls()
- );
-
- /**
- * Ordered list of loaders. If a loader returns a response object then subsequent loaders are not run.
- */
- private static final List<IconLoader> LOADERS = Arrays.asList(
- // First we try to load an icon that is already in the memory. That's cheap.
- new MemoryLoader(),
-
- // Try to decode the icon if it is a data: URI.
- new DataUriLoader(),
-
- // Try to load the icon from the omni.ha if it's a jar:jar URI.
- new JarLoader(),
-
- // Try to load the icon from a content provider (if applicable).
- new ContentProviderLoader(),
-
- // Try to load the icon from the disk cache.
- new DiskLoader(),
-
- // If the icon is not in any of our cashes and can't be decoded then look into the
- // database (legacy). Maybe this icon was loaded before the new code was deployed.
- new LegacyLoader(),
-
- // Download the icon from the web.
- new IconDownloader()
- );
-
- /**
- * Ordered list of processors that run after an icon has been loaded.
- */
- private static final List<Processor> PROCESSORS = Arrays.asList(
- // Store the icon (and mapping) in the disk cache if needed
- new DiskProcessor(),
-
- // Resize the icon to match the target size (if possible)
- new ResizingProcessor(),
-
- // Extract the dominant color from the icon
- new ColorProcessor(),
-
- // Store the icon in the memory cache
- new MemoryProcessor()
- );
-
- private static final ExecutorService EXECUTOR;
- static {
- final ThreadFactory factory = new ThreadFactory() {
- @Override
- public Thread newThread(@NonNull Runnable runnable) {
- Thread thread = new Thread(runnable, "GeckoIconTask");
- thread.setDaemon(false);
- thread.setPriority(Thread.NORM_PRIORITY);
- return thread;
- }
- };
-
- // Single thread executor
- EXECUTOR = new ThreadPoolExecutor(
- 1, /* corePoolSize */
- 1, /* maximumPoolSize */
- 0L, /* keepAliveTime */
- TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<Runnable>(),
- factory);
- }
-
- /**
- * Submit the request for execution.
- */
- /* package-private */ static Future<IconResponse> submit(IconRequest request) {
- return EXECUTOR.submit(
- new IconTask(request, PREPARERS, LOADERS, PROCESSORS, GENERATOR)
- );
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/IconResponse.java b/mobile/android/base/java/org/mozilla/gecko/icons/IconResponse.java
deleted file mode 100644
index 726619eb9..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/IconResponse.java
+++ /dev/null
@@ -1,167 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons;
-
-import android.graphics.Bitmap;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.text.TextUtils;
-
-/**
- * Response object containing a successful loaded icon and meta data.
- */
-public class IconResponse {
- /**
- * Create a response for a plain bitmap.
- */
- public static IconResponse create(@NonNull Bitmap bitmap) {
- return new IconResponse(bitmap);
- }
-
- /**
- * Create a response for a bitmap that has been loaded from the network by requesting a specific URL.
- */
- public static IconResponse createFromNetwork(@NonNull Bitmap bitmap, @NonNull String url) {
- final IconResponse response = new IconResponse(bitmap);
- response.url = url;
- response.fromNetwork = true;
- return response;
- }
-
- /**
- * Create a response for a generated bitmap with a dominant color.
- */
- public static IconResponse createGenerated(@NonNull Bitmap bitmap, int color) {
- final IconResponse response = new IconResponse(bitmap);
- response.color = color;
- response.generated = true;
- return response;
- }
-
- /**
- * Create a response for a bitmap that has been loaded from the memory cache.
- */
- public static IconResponse createFromMemory(@NonNull Bitmap bitmap, @NonNull String url, int color) {
- final IconResponse response = new IconResponse(bitmap);
- response.url = url;
- response.color = color;
- response.fromMemory = true;
- return response;
- }
-
- /**
- * Create a response for a bitmap that has been loaded from the disk cache.
- */
- public static IconResponse createFromDisk(@NonNull Bitmap bitmap, @NonNull String url) {
- final IconResponse response = new IconResponse(bitmap);
- response.url = url;
- response.fromDisk = true;
- return response;
- }
-
- private Bitmap bitmap;
- private int color;
- private boolean fromNetwork;
- private boolean fromMemory;
- private boolean fromDisk;
- private boolean generated;
- private String url;
-
- private IconResponse(Bitmap bitmap) {
- if (bitmap == null) {
- throw new NullPointerException("Bitmap is null");
- }
-
- this.bitmap = bitmap;
- this.color = 0;
- this.url = null;
- this.fromNetwork = false;
- this.fromMemory = false;
- this.fromDisk = false;
- this.generated = false;
- }
-
- /**
- * Get the icon bitmap. This method will always return a bitmap.
- */
- @NonNull
- public Bitmap getBitmap() {
- return bitmap;
- }
-
- /**
- * Get the dominant color of the icon. Will return 0 if no color could be extracted.
- */
- public int getColor() {
- return color;
- }
-
- /**
- * Does this response contain a dominant color?
- */
- public boolean hasColor() {
- return color != 0;
- }
-
- /**
- * Has this icon been loaded from the network?
- */
- public boolean isFromNetwork() {
- return fromNetwork;
- }
-
- /**
- * Has this icon been generated?
- */
- public boolean isGenerated() {
- return generated;
- }
-
- /**
- * Has this icon been loaded from memory (cache)?
- */
- public boolean isFromMemory() {
- return fromMemory;
- }
-
- /**
- * Has this icon been loaded from disk (cache)?
- */
- public boolean isFromDisk() {
- return fromDisk;
- }
-
- /**
- * Get the URL this icon has been loaded from.
- */
- @Nullable
- public String getUrl() {
- return url;
- }
-
- /**
- * Does this response contain an URL from which the icon has been loaded?
- */
- public boolean hasUrl() {
- return !TextUtils.isEmpty(url);
- }
-
- /**
- * Update the color of this response. This method is called by processors updating meta data
- * after the icon has been loaded.
- */
- public void updateColor(int color) {
- this.color = color;
- }
-
- /**
- * Update the bitmap of this response. This method is called by processors that modify the
- * loaded icon.
- */
- public void updateBitmap(Bitmap bitmap) {
- this.bitmap = bitmap;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/IconTask.java b/mobile/android/base/java/org/mozilla/gecko/icons/IconTask.java
deleted file mode 100644
index 411a31980..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/IconTask.java
+++ /dev/null
@@ -1,222 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons;
-
-import android.graphics.Bitmap;
-import android.support.annotation.NonNull;
-import android.util.Log;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.icons.loader.IconLoader;
-import org.mozilla.gecko.icons.preparation.Preparer;
-import org.mozilla.gecko.icons.processing.Processor;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import java.util.List;
-import java.util.concurrent.Callable;
-
-/**
- * Task that will be run by the IconRequestExecutor for every icon request.
- */
-/* package-private */ class IconTask implements Callable<IconResponse> {
- private static final String LOGTAG = "Gecko/IconTask";
- private static final boolean DEBUG = false;
-
- private final List<Preparer> preparers;
- private final List<IconLoader> loaders;
- private final List<Processor> processors;
- private final IconLoader generator;
- private final IconRequest request;
-
- /* package-private */ IconTask(
- @NonNull IconRequest request,
- @NonNull List<Preparer> preparers,
- @NonNull List<IconLoader> loaders,
- @NonNull List<Processor> processors,
- @NonNull IconLoader generator) {
- this.request = request;
- this.preparers = preparers;
- this.loaders = loaders;
- this.processors = processors;
- this.generator = generator;
- }
-
- @Override
- public IconResponse call() {
- try {
- logRequest(request);
-
- prepareRequest(request);
-
- if (request.shouldPrepareOnly()) {
- // This request should only be prepared but not load an actual icon.
- return null;
- }
-
- final IconResponse response = loadIcon(request);
-
- if (response != null) {
- processIcon(request, response);
- executeCallback(request, response);
-
- logResponse(response);
-
- return response;
- }
- } catch (InterruptedException e) {
- Log.d(LOGTAG, "IconTask was interrupted", e);
-
- // Clear interrupt thread.
- Thread.interrupted();
- } catch (Throwable e) {
- handleException(e);
- }
-
- return null;
- }
-
- /**
- * Check if this thread was interrupted (e.g. this task was cancelled). Throws an InterruptedException
- * to stop executing the task in this case.
- */
- private void ensureNotInterrupted() throws InterruptedException {
- if (Thread.currentThread().isInterrupted()) {
- throw new InterruptedException("Task has been cancelled");
- }
- }
-
- private void executeCallback(IconRequest request, final IconResponse response) {
- final IconCallback callback = request.getCallback();
-
- if (callback != null) {
- if (request.shouldRunOnBackgroundThread()) {
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- callback.onIconResponse(response);
- }
- });
- } else {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- callback.onIconResponse(response);
- }
- });
- }
- }
- }
-
- private void prepareRequest(IconRequest request) throws InterruptedException {
- for (Preparer preparer : preparers) {
- ensureNotInterrupted();
-
- preparer.prepare(request);
-
- logPreparer(request, preparer);
- }
- }
-
- private IconResponse loadIcon(IconRequest request) throws InterruptedException {
- while (request.hasIconDescriptors()) {
- for (IconLoader loader : loaders) {
- ensureNotInterrupted();
-
- IconResponse response = loader.load(request);
-
- logLoader(request, loader, response);
-
- if (response != null) {
- return response;
- }
- }
-
- request.moveToNextIcon();
- }
-
- return generator.load(request);
- }
-
- private void processIcon(IconRequest request, IconResponse response) throws InterruptedException {
- for (Processor processor : processors) {
- ensureNotInterrupted();
-
- processor.process(request, response);
-
- logProcessor(processor);
- }
- }
-
- private void handleException(final Throwable t) {
- if (AppConstants.NIGHTLY_BUILD) {
- // We want to be aware of problems: Let's re-throw the exception on the main thread to
- // force an app crash. However we only do this in Nightly builds. Every other build
- // (especially release builds) should just carry on and log the error.
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- throw new RuntimeException("Icon task thread crashed", t);
- }
- });
- } else {
- Log.e(LOGTAG, "Icon task crashed", t);
- }
- }
-
- private boolean shouldLog() {
- // Do not log anything if debugging is disabled and never log anything in a non-nightly build.
- return DEBUG && AppConstants.NIGHTLY_BUILD;
- }
-
- private void logPreparer(IconRequest request, Preparer preparer) {
- if (!shouldLog()) {
- return;
- }
-
- Log.d(LOGTAG, String.format(" PREPARE %s" + " (%s)",
- preparer.getClass().getSimpleName(),
- request.getIconCount()));
- }
-
- private void logLoader(IconRequest request, IconLoader loader, IconResponse response) {
- if (!shouldLog()) {
- return;
- }
-
- Log.d(LOGTAG, String.format(" LOAD [%s] %s : %s",
- response != null ? "X" : " ",
- loader.getClass().getSimpleName(),
- request.getBestIcon().getUrl()));
- }
-
- private void logProcessor(Processor processor) {
- if (!shouldLog()) {
- return;
- }
-
- Log.d(LOGTAG, " PROCESS " + processor.getClass().getSimpleName());
- }
-
- private void logResponse(IconResponse response) {
- if (!shouldLog()) {
- return;
- }
-
- final Bitmap bitmap = response.getBitmap();
-
- Log.d(LOGTAG, String.format("=> ICON: %sx%s", bitmap.getWidth(), bitmap.getHeight()));
- }
-
- private void logRequest(IconRequest request) {
- if (!shouldLog()) {
- return;
- }
-
- Log.d(LOGTAG, String.format("REQUEST (%s) %s",
- request.getIconCount(),
- request.getPageUrl()));
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/Icons.java b/mobile/android/base/java/org/mozilla/gecko/icons/Icons.java
deleted file mode 100644
index a5505a694..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/Icons.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons;
-
-import android.content.Context;
-import android.support.annotation.CheckResult;
-
-/**
- * Entry point for loading icons for websites (just high quality icons, can be favicons or
- * touch icons).
- *
- * The API is loosely inspired by Picasso's builder.
- *
- * Example:
- *
- * Icons.with(context)
- * .pageUrl(pageURL)
- * .skipNetwork()
- * .privileged(true)
- * .icon(IconDescriptor.createGenericIcon(url))
- * .build()
- * .execute(callback);
- */
-public abstract class Icons {
- /**
- * Create a new request for loading a website icon.
- */
- @CheckResult
- public static IconRequestBuilder with(Context context) {
- return new IconRequestBuilder(context);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/IconsHelper.java b/mobile/android/base/java/org/mozilla/gecko/icons/IconsHelper.java
deleted file mode 100644
index d351eb4b7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/IconsHelper.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons;
-
-import android.net.Uri;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.text.TextUtils;
-import android.util.Log;
-
-import org.mozilla.gecko.AboutPages;
-import org.mozilla.gecko.util.StringUtils;
-
-import java.util.HashSet;
-
-/**
- * Helper methods for icon related tasks.
- */
-public class IconsHelper {
- private static final String LOGTAG = "Gecko/IconsHelper";
-
- // Mime types of things we are capable of decoding.
- private static final HashSet<String> sDecodableMimeTypes = new HashSet<>();
-
- // Mime types of things we are both capable of decoding and are container formats (May contain
- // multiple different sizes of image)
- private static final HashSet<String> sContainerMimeTypes = new HashSet<>();
-
- static {
- // MIME types extracted from http://filext.com - ostensibly all in-use mime types for the
- // corresponding formats.
- // ICO
- sContainerMimeTypes.add("image/vnd.microsoft.icon");
- sContainerMimeTypes.add("image/ico");
- sContainerMimeTypes.add("image/icon");
- sContainerMimeTypes.add("image/x-icon");
- sContainerMimeTypes.add("text/ico");
- sContainerMimeTypes.add("application/ico");
-
- // Add supported container types to the set of supported types.
- sDecodableMimeTypes.addAll(sContainerMimeTypes);
-
- // PNG
- sDecodableMimeTypes.add("image/png");
- sDecodableMimeTypes.add("application/png");
- sDecodableMimeTypes.add("application/x-png");
-
- // GIF
- sDecodableMimeTypes.add("image/gif");
-
- // JPEG
- sDecodableMimeTypes.add("image/jpeg");
- sDecodableMimeTypes.add("image/jpg");
- sDecodableMimeTypes.add("image/pipeg");
- sDecodableMimeTypes.add("image/vnd.swiftview-jpeg");
- sDecodableMimeTypes.add("application/jpg");
- sDecodableMimeTypes.add("application/x-jpg");
-
- // BMP
- sDecodableMimeTypes.add("application/bmp");
- sDecodableMimeTypes.add("application/x-bmp");
- sDecodableMimeTypes.add("application/x-win-bitmap");
- sDecodableMimeTypes.add("image/bmp");
- sDecodableMimeTypes.add("image/x-bmp");
- sDecodableMimeTypes.add("image/x-bitmap");
- sDecodableMimeTypes.add("image/x-xbitmap");
- sDecodableMimeTypes.add("image/x-win-bitmap");
- sDecodableMimeTypes.add("image/x-windows-bitmap");
- sDecodableMimeTypes.add("image/x-ms-bitmap");
- sDecodableMimeTypes.add("image/x-ms-bmp");
- sDecodableMimeTypes.add("image/ms-bmp");
- }
-
- /**
- * Helper method to getIcon the default Favicon URL for a given pageURL. Generally: somewhere.com/favicon.ico
- *
- * @param pageURL Page URL for which a default Favicon URL is requested
- * @return The default Favicon URL or null if no default URL could be guessed.
- */
- @Nullable
- public static String guessDefaultFaviconURL(String pageURL) {
- if (TextUtils.isEmpty(pageURL)) {
- return null;
- }
-
- // Special-casing for about: pages. The favicon for about:pages which don't provide a link tag
- // is bundled in the database, keyed only by page URL, hence the need to return the page URL
- // here. If the database ever migrates to stop being silly in this way, this can plausibly
- // be removed.
- if (AboutPages.isAboutPage(pageURL) || pageURL.startsWith("jar:")) {
- return pageURL;
- }
-
- if (!StringUtils.isHttpOrHttps(pageURL)) {
- // Guessing a default URL only makes sense for http(s) URLs.
- return null;
- }
-
- try {
- // Fall back to trying "someScheme:someDomain.someExtension/favicon.ico".
- Uri uri = Uri.parse(pageURL);
- if (uri.getAuthority().isEmpty()) {
- return null;
- }
-
- return uri.buildUpon()
- .path("favicon.ico")
- .clearQuery()
- .fragment("")
- .build()
- .toString();
- } catch (Exception e) {
- Log.d(LOGTAG, "Exception getting default favicon URL");
- return null;
- }
- }
-
- /**
- * Helper function to determine if the provided mime type is that of a format that can contain
- * multiple image types. At time of writing, the only such type is ICO.
- * @param mimeType Mime type to check.
- * @return true if the given mime type is a container type, false otherwise.
- */
- public static boolean isContainerType(@NonNull String mimeType) {
- return sContainerMimeTypes.contains(mimeType);
- }
-
- /**
- * Helper function to determine if we can decode a particular mime type.
- *
- * @param imgType Mime type to check for decodability.
- * @return false if the given mime type is certainly not decodable, true if it might be.
- */
- public static boolean canDecodeType(@NonNull String imgType) {
- return sDecodableMimeTypes.contains(imgType);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/decoders/FaviconDecoder.java b/mobile/android/base/java/org/mozilla/gecko/icons/decoders/FaviconDecoder.java
deleted file mode 100644
index 43f5d0ac6..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/decoders/FaviconDecoder.java
+++ /dev/null
@@ -1,197 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.decoders;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.util.Base64;
-import android.util.Log;
-
-import org.mozilla.gecko.gfx.BitmapUtils;
-
-import java.util.Iterator;
-import java.util.NoSuchElementException;
-
-/**
- * Class providing static utility methods for decoding favicons.
- */
-public class FaviconDecoder {
- private static final String LOG_TAG = "GeckoFaviconDecoder";
-
- static enum ImageMagicNumbers {
- // It is irritating that Java bytes are signed...
- PNG(new byte[] {(byte) (0x89 & 0xFF), 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}),
- GIF(new byte[] {0x47, 0x49, 0x46, 0x38}),
- JPEG(new byte[] {-0x1, -0x28, -0x1, -0x20}),
- BMP(new byte[] {0x42, 0x4d}),
- WEB(new byte[] {0x57, 0x45, 0x42, 0x50, 0x0a});
-
- public byte[] value;
-
- private ImageMagicNumbers(byte[] value) {
- this.value = value;
- }
- }
-
- /**
- * Check for image format magic numbers of formats supported by Android.
- * @param buffer Byte buffer to check for magic numbers
- * @param offset Offset at which to look for magic numbers.
- * @return true if the buffer contains a bitmap decodable by Android (Or at least, a sequence
- * starting with the magic numbers thereof). false otherwise.
- */
- private static boolean isDecodableByAndroid(byte[] buffer, int offset) {
- for (ImageMagicNumbers m : ImageMagicNumbers.values()) {
- if (bufferStartsWith(buffer, m.value, offset)) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Utility function to check for the existence of a test byte sequence at a given offset in a
- * buffer.
- *
- * @param buffer Byte buffer to search.
- * @param test Byte sequence to search for.
- * @param bufferOffset Index in input buffer to expect test sequence.
- * @return true if buffer contains the byte sequence given in test at offset bufferOffset, false
- * otherwise.
- */
- static boolean bufferStartsWith(byte[] buffer, byte[] test, int bufferOffset) {
- if (buffer.length < test.length) {
- return false;
- }
-
- for (int i = 0; i < test.length; ++i) {
- if (buffer[bufferOffset + i] != test[i]) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Decode the favicon present in the region of the provided byte[] starting at offset and
- * proceeding for length bytes, if any. Returns either the resulting LoadFaviconResult or null if the
- * given range does not contain a bitmap we know how to decode.
- *
- * @param buffer Byte array containing the favicon to decode.
- * @param offset The index of the first byte in the array of the region of interest.
- * @param length The length of the region in the array to decode.
- * @return The decoded version of the bitmap in the described region, or null if none can be
- * decoded.
- */
- public static LoadFaviconResult decodeFavicon(Context context, byte[] buffer, int offset, int length) {
- LoadFaviconResult result;
- if (isDecodableByAndroid(buffer, offset)) {
- result = new LoadFaviconResult();
- result.offset = offset;
- result.length = length;
- result.isICO = false;
-
- Bitmap decodedImage = BitmapUtils.decodeByteArray(buffer, offset, length);
- if (decodedImage == null) {
- // What we got wasn't decodable after all. Probably corrupted image, or we got a muffled OOM.
- return null;
- }
-
- // We assume here that decodeByteArray doesn't hold on to the entire supplied
- // buffer -- worst case, each of our buffers will be twice the necessary size.
- result.bitmapsDecoded = new SingleBitmapIterator(decodedImage);
- result.faviconBytes = buffer;
-
- return result;
- }
-
- // If it's not decodable by Android, it might be an ICO. Let's try.
- ICODecoder decoder = new ICODecoder(context, buffer, offset, length);
-
- result = decoder.decode();
-
- if (result == null) {
- return null;
- }
-
- return result;
- }
-
- public static LoadFaviconResult decodeDataURI(Context context, String uri) {
- if (uri == null) {
- Log.w(LOG_TAG, "Can't decode null data: URI.");
- return null;
- }
-
- if (!uri.startsWith("data:image/")) {
- // Can't decode non-image data: URI.
- return null;
- }
-
- // Otherwise, let's attack this blindly. Strictly we should be parsing.
- int offset = uri.indexOf(',') + 1;
- if (offset == 0) {
- Log.w(LOG_TAG, "No ',' in data: URI; malformed?");
- return null;
- }
-
- try {
- String base64 = uri.substring(offset);
- byte[] raw = Base64.decode(base64, Base64.DEFAULT);
- return decodeFavicon(context, raw);
- } catch (Exception e) {
- Log.w(LOG_TAG, "Couldn't decode data: URI.", e);
- return null;
- }
- }
-
- public static LoadFaviconResult decodeFavicon(Context context, byte[] buffer) {
- return decodeFavicon(context, buffer, 0, buffer.length);
- }
-
- /**
- * Iterator to hold a single bitmap.
- */
- static class SingleBitmapIterator implements Iterator<Bitmap> {
- private Bitmap bitmap;
-
- public SingleBitmapIterator(Bitmap b) {
- bitmap = b;
- }
-
- /**
- * Slightly cheating here - this iterator supports peeking (Handy in a couple of obscure
- * places where the runtime type of the Iterator under consideration is known and
- * destruction of it is discouraged.
- *
- * @return The bitmap carried by this SingleBitmapIterator.
- */
- public Bitmap peek() {
- return bitmap;
- }
-
- @Override
- public boolean hasNext() {
- return bitmap != null;
- }
-
- @Override
- public Bitmap next() {
- if (bitmap == null) {
- throw new NoSuchElementException("Element already returned from SingleBitmapIterator.");
- }
-
- Bitmap ret = bitmap;
- bitmap = null;
- return ret;
- }
-
- @Override
- public void remove() {
- throw new UnsupportedOperationException("remove() not supported on SingleBitmapIterator.");
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/decoders/ICODecoder.java b/mobile/android/base/java/org/mozilla/gecko/icons/decoders/ICODecoder.java
deleted file mode 100644
index 44e3f1252..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/decoders/ICODecoder.java
+++ /dev/null
@@ -1,396 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.decoders;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.support.annotation.VisibleForTesting;
-import android.util.SparseArray;
-
-import java.util.Iterator;
-import java.util.NoSuchElementException;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.gfx.BitmapUtils;
-import org.mozilla.gecko.R;
-
-/**
- * Utility class for determining the region of a provided array which contains the largest bitmap,
- * assuming the provided array is a valid ICO and the bitmap desired is square, and for pruning
- * unwanted entries from ICO files, if desired.
- *
- * An ICO file is a container format that may hold up to 255 images in either BMP or PNG format.
- * A mixture of image types may not exist.
- *
- * The format consists of a header specifying the number, n, of images, followed by the Icon Directory.
- *
- * The Icon Directory consists of n Icon Directory Entries, each 16 bytes in length, specifying, for
- * the corresponding image, the dimensions, colour information, payload size, and location in the file.
- *
- * All numerical fields follow a little-endian byte ordering.
- *
- * Header format:
- *
- * 0 1 2 3
- * 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
- * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- * | Reserved field. Must be zero | Type (1 for ICO, 2 for CUR) |
- * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- * | Image count (n) |
- * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- *
- * The type field is expected to always be 1. CUR format images should not be used for Favicons.
- *
- *
- * Icon Directory Entry format:
- *
- * 0 1 2 3
- * 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
- * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- * | Image width | Image height | Palette size | Reserved (0) |
- * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- * | Colour plane count | Bits per pixel |
- * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- * | Size of image data, in bytes |
- * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- * | Start of image data, as an offset from start of file |
- * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- *
- * Image dimensions of zero are to be interpreted as image dimensions of 256.
- *
- * The palette size field records the number of colours in the stored BMP, if a palette is used. Zero
- * if the payload is a PNG or no palette is in use.
- *
- * The number of colour planes is, usually, 0 (Not in use) or 1. Values greater than 1 are to be
- * interpreted not as a colour plane count, but as a multiplying factor on the bits per pixel field.
- * (Apparently 65535 was not deemed a sufficiently large maximum value of bits per pixel.)
- *
- *
- * The Icon Directory consists of n-many Icon Directory Entries in sequence, with no gaps.
- *
- * This class is not thread safe.
- */
-public class ICODecoder implements Iterable<Bitmap> {
- // The number of bytes that compacting will save for us to bother doing it.
- public static final int COMPACT_THRESHOLD = 4000;
-
- // Some geometry of an ICO file.
- public static final int ICO_HEADER_LENGTH_BYTES = 6;
- public static final int ICO_ICONDIRENTRY_LENGTH_BYTES = 16;
-
- // The buffer containing bytes to attempt to decode.
- private byte[] decodand;
-
- // The region of the decodand to decode.
- private int offset;
- private int len;
-
- IconDirectoryEntry[] iconDirectory;
- private boolean isValid;
- private boolean hasDecoded;
- private int largestFaviconSize;
-
- @RobocopTarget
- public ICODecoder(Context context, byte[] decodand, int offset, int len) {
- this.decodand = decodand;
- this.offset = offset;
- this.len = len;
- this.largestFaviconSize = context.getResources()
- .getDimensionPixelSize(R.dimen.favicon_largest_interesting_size);
- }
-
- /**
- * Decode the Icon Directory for this ICO and store the result in iconDirectory.
- *
- * @return true if ICO decoding was considered to probably be a success, false if it certainly
- * was a failure.
- */
- private boolean decodeIconDirectoryAndPossiblyPrune() {
- hasDecoded = true;
-
- // Fail if the end of the described range is out of bounds.
- if (offset + len > decodand.length) {
- return false;
- }
-
- // Fail if we don't have enough space for the header.
- if (len < ICO_HEADER_LENGTH_BYTES) {
- return false;
- }
-
- // Check that the reserved fields in the header are indeed zero, and that the type field
- // specifies ICO. If not, we've probably been given something that isn't really an ICO.
- if (decodand[offset] != 0 ||
- decodand[offset + 1] != 0 ||
- decodand[offset + 2] != 1 ||
- decodand[offset + 3] != 0) {
- return false;
- }
-
- // Here, and in many other places, byte values are ANDed with 0xFF. This is because Java
- // bytes are signed - to obtain a numerical value of a longer type which holds the unsigned
- // interpretation of the byte of interest, we do this.
- int numEncodedImages = (decodand[offset + 4] & 0xFF) |
- (decodand[offset + 5] & 0xFF) << 8;
-
-
- // Fail if there are no images or the field is corrupt.
- if (numEncodedImages <= 0) {
- return false;
- }
-
- final int headerAndDirectorySize = ICO_HEADER_LENGTH_BYTES + (numEncodedImages * ICO_ICONDIRENTRY_LENGTH_BYTES);
-
- // Fail if there is not enough space in the buffer for the stated number of icondir entries,
- // let alone the data.
- if (len < headerAndDirectorySize) {
- return false;
- }
-
- // Put the pointer on the first byte of the first Icon Directory Entry.
- int bufferIndex = offset + ICO_HEADER_LENGTH_BYTES;
-
- // We now iterate over the Icon Directory, decoding each entry as we go. We also need to
- // discard all entries except one >= the maximum interesting size.
-
- // Size of the smallest image larger than the limit encountered.
- int minimumMaximum = Integer.MAX_VALUE;
-
- // Used to track the best entry for each size. The entries we want to keep.
- SparseArray<IconDirectoryEntry> preferenceArray = new SparseArray<IconDirectoryEntry>();
-
- for (int i = 0; i < numEncodedImages; i++, bufferIndex += ICO_ICONDIRENTRY_LENGTH_BYTES) {
- // Decode the Icon Directory Entry at this offset.
- IconDirectoryEntry newEntry = IconDirectoryEntry.createFromBuffer(decodand, offset, len, bufferIndex);
- newEntry.index = i;
-
- if (newEntry.isErroneous) {
- continue;
- }
-
- if (newEntry.width > largestFaviconSize) {
- // If we already have a smaller image larger than the maximum size of interest, we
- // don't care about the new one which is larger than the smallest image larger than
- // the maximum size.
- if (newEntry.width >= minimumMaximum) {
- continue;
- }
-
- // Remove the previous minimum-maximum.
- preferenceArray.delete(minimumMaximum);
-
- minimumMaximum = newEntry.width;
- }
-
- IconDirectoryEntry oldEntry = preferenceArray.get(newEntry.width);
- if (oldEntry == null) {
- preferenceArray.put(newEntry.width, newEntry);
- continue;
- }
-
- if (oldEntry.compareTo(newEntry) < 0) {
- preferenceArray.put(newEntry.width, newEntry);
- }
- }
-
- final int count = preferenceArray.size();
-
- // Abort if no entries are desired (Perhaps all are corrupt?)
- if (count == 0) {
- return false;
- }
-
- // Allocate space for the icon directory entries in the decoded directory.
- iconDirectory = new IconDirectoryEntry[count];
-
- // The size of the data in the buffer that we find useful.
- int retainedSpace = ICO_HEADER_LENGTH_BYTES;
-
- for (int i = 0; i < count; i++) {
- IconDirectoryEntry e = preferenceArray.valueAt(i);
- retainedSpace += ICO_ICONDIRENTRY_LENGTH_BYTES + e.payloadSize;
- iconDirectory[i] = e;
- }
-
- isValid = true;
-
- // Set the number of images field in the buffer to reflect the number of retained entries.
- decodand[offset + 4] = (byte) iconDirectory.length;
- decodand[offset + 5] = (byte) (iconDirectory.length >>> 8);
-
- if ((len - retainedSpace) > COMPACT_THRESHOLD) {
- compactingCopy(retainedSpace);
- }
-
- return true;
- }
-
- /**
- * Copy the buffer into a new array of exactly the required size, omitting any unwanted data.
- */
- private void compactingCopy(int spaceRetained) {
- byte[] buf = new byte[spaceRetained];
-
- // Copy the header.
- System.arraycopy(decodand, offset, buf, 0, ICO_HEADER_LENGTH_BYTES);
-
- int headerPtr = ICO_HEADER_LENGTH_BYTES;
-
- int payloadPtr = ICO_HEADER_LENGTH_BYTES + (iconDirectory.length * ICO_ICONDIRENTRY_LENGTH_BYTES);
-
- int ind = 0;
- for (IconDirectoryEntry entry : iconDirectory) {
- // Copy this entry.
- System.arraycopy(decodand, offset + entry.getOffset(), buf, headerPtr, ICO_ICONDIRENTRY_LENGTH_BYTES);
-
- // Copy its payload.
- System.arraycopy(decodand, offset + entry.payloadOffset, buf, payloadPtr, entry.payloadSize);
-
- // Update the offset field.
- buf[headerPtr + 12] = (byte) payloadPtr;
- buf[headerPtr + 13] = (byte) (payloadPtr >>> 8);
- buf[headerPtr + 14] = (byte) (payloadPtr >>> 16);
- buf[headerPtr + 15] = (byte) (payloadPtr >>> 24);
-
- entry.payloadOffset = payloadPtr;
- entry.index = ind;
-
- payloadPtr += entry.payloadSize;
- headerPtr += ICO_ICONDIRENTRY_LENGTH_BYTES;
- ind++;
- }
-
- decodand = buf;
- offset = 0;
- len = spaceRetained;
- }
-
- /**
- * Decode and return the bitmap represented by the given index in the Icon Directory, if valid.
- *
- * @param index The index into the Icon Directory of the image of interest.
- * @return The decoded Bitmap object for this image, or null if the entry is invalid or decoding
- * fails.
- */
- public Bitmap decodeBitmapAtIndex(int index) {
- final IconDirectoryEntry iconDirEntry = iconDirectory[index];
-
- if (iconDirEntry.payloadIsPNG) {
- // PNG payload. Simply extract it and decode it.
- return BitmapUtils.decodeByteArray(decodand, offset + iconDirEntry.payloadOffset, iconDirEntry.payloadSize);
- }
-
- // The payload is a BMP, so we need to do some magic to get the decoder to do what we want.
- // We construct an ICO containing just the image we want, and let Android do the rest.
- byte[] decodeTarget = new byte[ICO_HEADER_LENGTH_BYTES + ICO_ICONDIRENTRY_LENGTH_BYTES + iconDirEntry.payloadSize];
-
- // Set the type field in the ICO header.
- decodeTarget[2] = 1;
-
- // Set the num-images field in the header to 1.
- decodeTarget[4] = 1;
-
- // Copy the ICONDIRENTRY we need into the new buffer.
- System.arraycopy(decodand, offset + iconDirEntry.getOffset(), decodeTarget, ICO_HEADER_LENGTH_BYTES, ICO_ICONDIRENTRY_LENGTH_BYTES);
-
- // Copy the payload into the new buffer.
- final int singlePayloadOffset = ICO_HEADER_LENGTH_BYTES + ICO_ICONDIRENTRY_LENGTH_BYTES;
- System.arraycopy(decodand, offset + iconDirEntry.payloadOffset, decodeTarget, singlePayloadOffset, iconDirEntry.payloadSize);
-
- // Update the offset field of the ICONDIRENTRY to make the new ICO valid.
- decodeTarget[ICO_HEADER_LENGTH_BYTES + 12] = singlePayloadOffset;
- decodeTarget[ICO_HEADER_LENGTH_BYTES + 13] = (singlePayloadOffset >>> 8);
- decodeTarget[ICO_HEADER_LENGTH_BYTES + 14] = (singlePayloadOffset >>> 16);
- decodeTarget[ICO_HEADER_LENGTH_BYTES + 15] = (singlePayloadOffset >>> 24);
-
- // Decode the newly-constructed singleton-ICO.
- return BitmapUtils.decodeByteArray(decodeTarget);
- }
-
- /**
- * Fetch an iterator over the images in this ICO, or null if this ICO seems to be invalid.
- *
- * @return An iterator over the Bitmaps stored in this ICO, or null if decoding fails.
- */
- @Override
- public ICOIterator iterator() {
- // If a previous call to decode concluded this ICO is invalid, abort.
- if (hasDecoded && !isValid) {
- return null;
- }
-
- // If we've not been decoded before, but now fail to make any sense of the ICO, abort.
- if (!hasDecoded) {
- if (!decodeIconDirectoryAndPossiblyPrune()) {
- return null;
- }
- }
-
- // If decoding was a success, return an iterator over the images in this ICO.
- return new ICOIterator();
- }
-
- /**
- * Decode this ICO and return the result as a LoadFaviconResult.
- * @return A LoadFaviconResult representing the decoded ICO.
- */
- public LoadFaviconResult decode() {
- // The call to iterator returns null if decoding fails.
- Iterator<Bitmap> bitmaps = iterator();
- if (bitmaps == null) {
- return null;
- }
-
- LoadFaviconResult result = new LoadFaviconResult();
-
- result.bitmapsDecoded = bitmaps;
- result.faviconBytes = decodand;
- result.offset = offset;
- result.length = len;
- result.isICO = true;
-
- return result;
- }
-
- @VisibleForTesting
- @RobocopTarget
- public IconDirectoryEntry[] getIconDirectory() {
- return iconDirectory;
- }
-
- @VisibleForTesting
- @RobocopTarget
- public int getLargestFaviconSize() {
- return largestFaviconSize;
- }
-
- /**
- * Inner class to iterate over the elements in the ICO represented by the enclosing instance.
- */
- private class ICOIterator implements Iterator<Bitmap> {
- private int mIndex;
-
- @Override
- public boolean hasNext() {
- return mIndex < iconDirectory.length;
- }
-
- @Override
- public Bitmap next() {
- if (mIndex > iconDirectory.length) {
- throw new NoSuchElementException("No more elements in this ICO.");
- }
- return decodeBitmapAtIndex(mIndex++);
- }
-
- @Override
- public void remove() {
- if (iconDirectory[mIndex] == null) {
- throw new IllegalStateException("Remove already called for element " + mIndex);
- }
- iconDirectory[mIndex] = null;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/decoders/IconDirectoryEntry.java b/mobile/android/base/java/org/mozilla/gecko/icons/decoders/IconDirectoryEntry.java
deleted file mode 100644
index 82ff91a55..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/decoders/IconDirectoryEntry.java
+++ /dev/null
@@ -1,212 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.decoders;
-
-import android.support.annotation.VisibleForTesting;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-
-/**
- * Representation of an ICO file ICONDIRENTRY structure.
- */
-public class IconDirectoryEntry implements Comparable<IconDirectoryEntry> {
-
- public static int maxBPP;
-
- int width;
- int height;
- int paletteSize;
- int bitsPerPixel;
- int payloadSize;
- int payloadOffset;
- boolean payloadIsPNG;
-
- // Tracks the index in the Icon Directory of this entry. Useful only for pruning.
- int index;
- boolean isErroneous;
-
- @RobocopTarget
- public IconDirectoryEntry(int width, int height, int paletteSize, int bitsPerPixel, int payloadSize, int payloadOffset, boolean payloadIsPNG) {
- this.width = width;
- this.height = height;
- this.paletteSize = paletteSize;
- this.bitsPerPixel = bitsPerPixel;
- this.payloadSize = payloadSize;
- this.payloadOffset = payloadOffset;
- this.payloadIsPNG = payloadIsPNG;
- }
-
- /**
- * Method to get a dummy Icon Directory Entry with the Erroneous bit set.
- *
- * @return An erroneous placeholder Icon Directory Entry.
- */
- public static IconDirectoryEntry getErroneousEntry() {
- IconDirectoryEntry ret = new IconDirectoryEntry(-1, -1, -1, -1, -1, -1, false);
- ret.isErroneous = true;
-
- return ret;
- }
-
- /**
- * Create an IconDirectoryEntry object from a byte[]. Interprets the buffer starting at the given
- * offset as an IconDirectoryEntry and returns the result.
- *
- * @param buffer Byte array containing the icon directory entry to decode.
- * @param regionOffset Offset into the byte array of the valid region of the buffer.
- * @param regionLength Length of the valid region in the buffer.
- * @param entryOffset Offset of the icon directory entry to decode within the buffer.
- * @return An IconDirectoryEntry object representing the entry specified, or null if the entry
- * is obviously invalid.
- */
- public static IconDirectoryEntry createFromBuffer(byte[] buffer, int regionOffset, int regionLength, int entryOffset) {
- // Verify that the reserved field is really zero.
- if (buffer[entryOffset + 3] != 0) {
- return getErroneousEntry();
- }
-
- // Verify that the entry points to a region that actually exists in the buffer, else bin it.
- int fieldPtr = entryOffset + 8;
- int entryLength = (buffer[fieldPtr] & 0xFF) |
- (buffer[fieldPtr + 1] & 0xFF) << 8 |
- (buffer[fieldPtr + 2] & 0xFF) << 16 |
- (buffer[fieldPtr + 3] & 0xFF) << 24;
-
- // Advance to the offset field.
- fieldPtr += 4;
-
- int payloadOffset = (buffer[fieldPtr] & 0xFF) |
- (buffer[fieldPtr + 1] & 0xFF) << 8 |
- (buffer[fieldPtr + 2] & 0xFF) << 16 |
- (buffer[fieldPtr + 3] & 0xFF) << 24;
-
- // Fail if the entry describes a region outside the buffer.
- if (payloadOffset < 0 || entryLength < 0 || payloadOffset + entryLength > regionOffset + regionLength) {
- return getErroneousEntry();
- }
-
- // Extract the image dimensions.
- int imageWidth = buffer[entryOffset] & 0xFF;
- int imageHeight = buffer[entryOffset + 1] & 0xFF;
-
- // Because Microsoft, a size value of zero represents an image size of 256.
- if (imageWidth == 0) {
- imageWidth = 256;
- }
-
- if (imageHeight == 0) {
- imageHeight = 256;
- }
-
- // If the image uses a colour palette, this is the number of colours, otherwise this is zero.
- int paletteSize = buffer[entryOffset + 2] & 0xFF;
-
- // The plane count - usually 0 or 1. When > 1, taken as multiplier on bitsPerPixel.
- int colorPlanes = buffer[entryOffset + 4] & 0xFF;
-
- int bitsPerPixel = (buffer[entryOffset + 6] & 0xFF) |
- (buffer[entryOffset + 7] & 0xFF) << 8;
-
- if (colorPlanes > 1) {
- bitsPerPixel *= colorPlanes;
- }
-
- // Look for PNG magic numbers at the start of the payload.
- boolean payloadIsPNG = FaviconDecoder.bufferStartsWith(buffer, FaviconDecoder.ImageMagicNumbers.PNG.value, regionOffset + payloadOffset);
-
- return new IconDirectoryEntry(imageWidth, imageHeight, paletteSize, bitsPerPixel, entryLength, payloadOffset, payloadIsPNG);
- }
-
- /**
- * Get the number of bytes from the start of the ICO file to the beginning of this entry.
- */
- public int getOffset() {
- return ICODecoder.ICO_HEADER_LENGTH_BYTES + (index * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES);
- }
-
- @Override
- public int compareTo(IconDirectoryEntry another) {
- if (width > another.width) {
- return 1;
- }
-
- if (width < another.width) {
- return -1;
- }
-
- // Where both images exceed the max BPP, take the smaller of the two BPP values.
- if (bitsPerPixel >= maxBPP && another.bitsPerPixel >= maxBPP) {
- if (bitsPerPixel < another.bitsPerPixel) {
- return 1;
- }
-
- if (bitsPerPixel > another.bitsPerPixel) {
- return -1;
- }
- }
-
- // Otherwise, take the larger of the BPP values.
- if (bitsPerPixel > another.bitsPerPixel) {
- return 1;
- }
-
- if (bitsPerPixel < another.bitsPerPixel) {
- return -1;
- }
-
- // Prefer large palettes.
- if (paletteSize > another.paletteSize) {
- return 1;
- }
-
- if (paletteSize < another.paletteSize) {
- return -1;
- }
-
- // Prefer smaller payloads.
- if (payloadSize < another.payloadSize) {
- return 1;
- }
-
- if (payloadSize > another.payloadSize) {
- return -1;
- }
-
- // If all else fails, prefer PNGs over BMPs. They tend to be smaller.
- if (payloadIsPNG && !another.payloadIsPNG) {
- return 1;
- }
-
- if (!payloadIsPNG && another.payloadIsPNG) {
- return -1;
- }
-
- return 0;
- }
-
- public static void setMaxBPP(int maxBPP) {
- IconDirectoryEntry.maxBPP = maxBPP;
- }
-
- @VisibleForTesting
- @RobocopTarget
- public int getWidth() {
- return width;
- }
-
- @Override
- public String toString() {
- return "IconDirectoryEntry{" +
- "\nwidth=" + width +
- ", \nheight=" + height +
- ", \npaletteSize=" + paletteSize +
- ", \nbitsPerPixel=" + bitsPerPixel +
- ", \npayloadSize=" + payloadSize +
- ", \npayloadOffset=" + payloadOffset +
- ", \npayloadIsPNG=" + payloadIsPNG +
- ", \nindex=" + index +
- '}';
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/decoders/LoadFaviconResult.java b/mobile/android/base/java/org/mozilla/gecko/icons/decoders/LoadFaviconResult.java
deleted file mode 100644
index cc196b91e..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/decoders/LoadFaviconResult.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.decoders;
-
-import android.graphics.Bitmap;
-import android.support.annotation.Nullable;
-import android.util.Log;
-import android.util.SparseArray;
-
-import java.io.ByteArrayOutputStream;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-
-/**
- * Class representing the result of loading a favicon.
- * This operation will produce either a collection of favicons, a single favicon, or no favicon.
- * It is necessary to model single favicons differently to a collection of one favicon (An entity
- * that may not exist with this scheme) since the in-database representation of these things differ.
- * (In particular, collections of favicons are stored in encoded ICO format, whereas single icons are
- * stored as decoded bitmap blobs.)
- */
-public class LoadFaviconResult {
- private static final String LOGTAG = "LoadFaviconResult";
-
- byte[] faviconBytes;
- int offset;
- int length;
-
- boolean isICO;
- Iterator<Bitmap> bitmapsDecoded;
-
- public Iterator<Bitmap> getBitmaps() {
- return bitmapsDecoded;
- }
-
- /**
- * Return a representation of this result suitable for storing in the database.
- *
- * @return A byte array containing the bytes from which this result was decoded,
- * or null if re-encoding failed.
- */
- public byte[] getBytesForDatabaseStorage() {
- // Begin by normalising the buffer.
- if (offset != 0 || length != faviconBytes.length) {
- final byte[] normalised = new byte[length];
- System.arraycopy(faviconBytes, offset, normalised, 0, length);
- offset = 0;
- faviconBytes = normalised;
- }
-
- // For results containing multiple images, we store the result verbatim. (But cutting the
- // buffer to size first).
- // We may instead want to consider re-encoding the entire ICO as a collection of efficiently
- // encoded PNGs. This may not be worth the CPU time (Indeed, the encoding of single-image
- // favicons may also not be worth the time/space tradeoff.).
- if (isICO) {
- return faviconBytes;
- }
-
- // For results containing a single image, we re-encode the
- // result as a PNG in an effort to save space.
- final Bitmap favicon = ((FaviconDecoder.SingleBitmapIterator) bitmapsDecoded).peek();
- final ByteArrayOutputStream stream = new ByteArrayOutputStream();
-
- try {
- if (favicon.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
- return stream.toByteArray();
- }
- } catch (OutOfMemoryError e) {
- Log.w(LOGTAG, "Out of memory re-compressing favicon.");
- }
-
- Log.w(LOGTAG, "Favicon re-compression failed.");
- return null;
- }
-
- @Nullable
- public Bitmap getBestBitmap(int targetWidthAndHeight) {
- final SparseArray<Bitmap> iconMap = new SparseArray<>();
- final List<Integer> sizes = new ArrayList<>();
-
- while (bitmapsDecoded.hasNext()) {
- final Bitmap b = bitmapsDecoded.next();
-
- // It's possible to receive null, most likely due to OOM or a zero-sized image,
- // from BitmapUtils.decodeByteArray(byte[], int, int, BitmapFactory.Options)
- if (b != null) {
- iconMap.put(b.getWidth(), b);
- sizes.add(b.getWidth());
- }
- }
-
- int bestSize = selectBestSizeFromList(sizes, targetWidthAndHeight);
-
- if (bestSize == -1) {
- // No icons found: this could occur if we weren't able to process any of the
- // supplied icons.
- return null;
- }
-
- return iconMap.get(bestSize);
- }
-
- /**
- * Select the closest icon size from a list of icon sizes.
- * We just find the first icon that is larger than the preferred size if available, or otherwise select the
- * largest icon (if all icons are smaller than the preferred size).
- *
- * @return The closest icon size, or -1 if no sizes are supplied.
- */
- public static int selectBestSizeFromList(final List<Integer> sizes, final int preferredSize) {
- if (sizes.isEmpty()) {
- // This isn't ideal, however current code assumes this as an error value for now.
- return -1;
- }
-
- Collections.sort(sizes);
-
- for (int size : sizes) {
- if (size >= preferredSize) {
- return size;
- }
- }
-
- // If all icons are smaller than the preferred size then we don't have an icon
- // selected yet, therefore just take the largest (last) icon.
- return sizes.get(sizes.size() - 1);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/loader/ContentProviderLoader.java b/mobile/android/base/java/org/mozilla/gecko/icons/loader/ContentProviderLoader.java
deleted file mode 100644
index be8e6d7de..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/loader/ContentProviderLoader.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.loader;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.text.TextUtils;
-
-import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
-import org.mozilla.gecko.icons.decoders.FaviconDecoder;
-import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.icons.IconResponse;
-
-/**
- * Loader for loading icons from a content provider. This loader was primarily written to load icons
- * from the partner bookmarks provider. However it can load icons from arbitrary content providers
- * as long as they return a cursor with a "favicon" or "touchicon" column (blob).
- */
-public class ContentProviderLoader implements IconLoader {
- @Override
- public IconResponse load(IconRequest request) {
- if (request.shouldSkipDisk()) {
- // If we should not load data from disk then we do not load from content providers either.
- return null;
- }
-
- final String iconUrl = request.getBestIcon().getUrl();
- final Context context = request.getContext();
- final int targetSize = request.getTargetSize();
-
- if (TextUtils.isEmpty(iconUrl) || !iconUrl.startsWith("content://")) {
- return null;
- }
-
- Cursor cursor = context.getContentResolver().query(
- Uri.parse(iconUrl),
- new String[] {
- PartnerBookmarksProviderProxy.PartnerContract.TOUCHICON,
- PartnerBookmarksProviderProxy.PartnerContract.FAVICON,
- },
- null,
- null,
- null
- );
-
- if (cursor == null) {
- return null;
- }
-
- try {
- if (!cursor.moveToFirst()) {
- return null;
- }
-
- // Try the touch icon first. It has a higher resolution usually.
- Bitmap icon = decodeFromCursor(request.getContext(), cursor, PartnerBookmarksProviderProxy.PartnerContract.TOUCHICON, targetSize);
- if (icon != null) {
- return IconResponse.create(icon);
- }
-
- icon = decodeFromCursor(request.getContext(), cursor, PartnerBookmarksProviderProxy.PartnerContract.FAVICON, targetSize);
- if (icon != null) {
- return IconResponse.create(icon);
- }
- } finally {
- cursor.close();
- }
-
- return null;
- }
-
- private Bitmap decodeFromCursor(Context context, Cursor cursor, String column, int targetWidthAndHeight) {
- final int index = cursor.getColumnIndex(column);
- if (index == -1) {
- return null;
- }
-
- if (cursor.isNull(index)) {
- return null;
- }
-
- final byte[] data = cursor.getBlob(index);
- LoadFaviconResult result = FaviconDecoder.decodeFavicon(context, data, 0, data.length);
- if (result == null) {
- return null;
- }
-
- return result.getBestBitmap(targetWidthAndHeight);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/loader/DataUriLoader.java b/mobile/android/base/java/org/mozilla/gecko/icons/loader/DataUriLoader.java
deleted file mode 100644
index 9ddc138ec..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/loader/DataUriLoader.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.loader;
-
-import org.mozilla.gecko.icons.decoders.FaviconDecoder;
-import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.icons.IconResponse;
-
-/**
- * Loader for loading icons from a data URI. This loader will try to decode any data with an
- * "image/*" MIME type.
- *
- * https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
- */
-public class DataUriLoader implements IconLoader {
- @Override
- public IconResponse load(IconRequest request) {
- final String iconUrl = request.getBestIcon().getUrl();
-
- if (!iconUrl.startsWith("data:image/")) {
- return null;
- }
-
- LoadFaviconResult loadFaviconResult = FaviconDecoder.decodeDataURI(request.getContext(), iconUrl);
- if (loadFaviconResult == null) {
- return null;
- }
-
- return IconResponse.create(
- loadFaviconResult.getBestBitmap(request.getTargetSize()));
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/loader/DiskLoader.java b/mobile/android/base/java/org/mozilla/gecko/icons/loader/DiskLoader.java
deleted file mode 100644
index 18a38e32b..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/loader/DiskLoader.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.loader;
-
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.icons.storage.DiskStorage;
-
-/**
- * Loader implementation for loading icons from the disk cache (Implemented by DiskStorage).
- */
-public class DiskLoader implements IconLoader {
- @Override
- public IconResponse load(IconRequest request) {
- if (request.shouldSkipDisk()) {
- return null;
- }
-
- final DiskStorage storage = DiskStorage.get(request.getContext());
- final String iconUrl = request.getBestIcon().getUrl();
-
- return storage.getIcon(iconUrl);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/loader/IconDownloader.java b/mobile/android/base/java/org/mozilla/gecko/icons/loader/IconDownloader.java
deleted file mode 100644
index 3ae9d15d0..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/loader/IconDownloader.java
+++ /dev/null
@@ -1,219 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.loader;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.support.annotation.VisibleForTesting;
-import android.util.Log;
-
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.icons.decoders.FaviconDecoder;
-import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.icons.storage.FailureCache;
-import org.mozilla.gecko.util.IOUtils;
-import org.mozilla.gecko.util.ProxySelector;
-import org.mozilla.gecko.util.StringUtils;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.HashSet;
-
-/**
- * This loader implementation downloads icons from http(s) URLs.
- */
-public class IconDownloader implements IconLoader {
- private static final String LOGTAG = "Gecko/Downloader";
-
- /**
- * The maximum number of http redirects (3xx) until we give up.
- */
- private static final int MAX_REDIRECTS_TO_FOLLOW = 5;
-
- /**
- * The default size of the buffer to use for downloading Favicons in the event no size is given
- * by the server. */
- private static final int DEFAULT_FAVICON_BUFFER_SIZE_BYTES = 25000;
-
- @Override
- public IconResponse load(IconRequest request) {
- if (request.shouldSkipNetwork()) {
- return null;
- }
-
- final String iconUrl = request.getBestIcon().getUrl();
-
- if (!StringUtils.isHttpOrHttps(iconUrl)) {
- return null;
- }
-
- try {
- final LoadFaviconResult result = downloadAndDecodeImage(request.getContext(), iconUrl);
- if (result == null) {
- return null;
- }
-
- final Bitmap bitmap = result.getBestBitmap(request.getTargetSize());
- if (bitmap == null) {
- return null;
- }
-
- return IconResponse.createFromNetwork(bitmap, iconUrl);
- } catch (Exception e) {
- Log.e(LOGTAG, "Error reading favicon", e);
- } catch (OutOfMemoryError e) {
- Log.e(LOGTAG, "Insufficient memory to process favicon");
- }
-
- return null;
- }
-
- /**
- * Download the Favicon from the given URL and pass it to the decoder function.
- *
- * @param targetFaviconURL URL of the favicon to download.
- * @return A LoadFaviconResult containing the bitmap(s) extracted from the downloaded file, or
- * null if no or corrupt data was received.
- * @throws IOException If attempts to fully read the stream result in such an exception, such as
- * in the event of a transient connection failure.
- * @throws URISyntaxException If the underlying call to tryDownload retries and raises such an
- * exception trying a fallback URL.
- */
- @VisibleForTesting
- LoadFaviconResult downloadAndDecodeImage(Context context, String targetFaviconURL) throws IOException, URISyntaxException {
- // Try the URL we were given.
- final HttpURLConnection connection = tryDownload(targetFaviconURL);
- if (connection == null) {
- return null;
- }
-
- InputStream stream = null;
-
- // Decode the image from the fetched response.
- try {
- stream = connection.getInputStream();
- return decodeImageFromResponse(context, stream, connection.getHeaderFieldInt("Content-Length", -1));
- } finally {
- // Close the stream and free related resources.
- IOUtils.safeStreamClose(stream);
- connection.disconnect();
- }
- }
-
- /**
- * Helper method for trying the download request to grab a Favicon.
- *
- * @param faviconURI URL of Favicon to try and download
- * @return The HttpResponse containing the downloaded Favicon if successful, null otherwise.
- */
- private HttpURLConnection tryDownload(String faviconURI) throws URISyntaxException, IOException {
- final HashSet<String> visitedLinkSet = new HashSet<>();
- visitedLinkSet.add(faviconURI);
- return tryDownloadRecurse(faviconURI, visitedLinkSet);
- }
-
- /**
- * Try to download from the favicon URL and recursively follow redirects.
- */
- private HttpURLConnection tryDownloadRecurse(String faviconURI, HashSet<String> visited) throws URISyntaxException, IOException {
- if (visited.size() == MAX_REDIRECTS_TO_FOLLOW) {
- return null;
- }
-
- final HttpURLConnection connection = connectTo(faviconURI);
-
- // Was the response a failure?
- final int status = connection.getResponseCode();
-
- // Handle HTTP status codes requesting a redirect.
- if (status >= 300 && status < 400) {
- final String newURI = connection.getHeaderField("Location");
-
- // Handle mad web servers.
- try {
- if (newURI == null || newURI.equals(faviconURI)) {
- return null;
- }
-
- if (visited.contains(newURI)) {
- // Already been redirected here - abort.
- return null;
- }
-
- visited.add(newURI);
- } finally {
- connection.disconnect();
- }
-
- return tryDownloadRecurse(newURI, visited);
- }
-
- if (status >= 400) {
- // Client or Server error. Let's not retry loading from this URL again for some time.
- FailureCache.get().rememberFailure(faviconURI);
-
- connection.disconnect();
- return null;
- }
-
- return connection;
- }
-
- @VisibleForTesting
- HttpURLConnection connectTo(String faviconURI) throws URISyntaxException, IOException {
- final HttpURLConnection connection = (HttpURLConnection) ProxySelector.openConnectionWithProxy(
- new URI(faviconURI));
-
- connection.setRequestProperty("User-Agent", GeckoAppShell.getGeckoInterface().getDefaultUAString());
-
- // We implemented or own way of following redirects back when this code was using HttpClient.
- // Nowadays we should let HttpUrlConnection do the work - assuming that it doesn't follow
- // redirects in loops forever.
- connection.setInstanceFollowRedirects(false);
-
- connection.connect();
-
- return connection;
- }
-
- /**
- * Copies the favicon stream to a buffer and decodes downloaded content into bitmaps using the
- * FaviconDecoder.
- *
- * @param stream to decode
- * @param contentLength as reported by the server (or -1)
- * @return A LoadFaviconResult containing the bitmap(s) extracted from the downloaded file, or
- * null if no or corrupt data were received.
- * @throws IOException If attempts to fully read the stream result in such an exception, such as
- * in the event of a transient connection failure.
- */
- private LoadFaviconResult decodeImageFromResponse(Context context, InputStream stream, int contentLength) throws IOException {
- // This may not be provided, but if it is, it's useful.
- final int bufferSize;
- if (contentLength > 0) {
- // The size was reported and sane, so let's use that.
- // Integer overflow should not be a problem for Favicon sizes...
- bufferSize = contentLength + 1;
- } else {
- // No declared size, so guess and reallocate later if it turns out to be too small.
- bufferSize = DEFAULT_FAVICON_BUFFER_SIZE_BYTES;
- }
-
- // Read the InputStream into a byte[].
- final IOUtils.ConsumedInputStream result = IOUtils.readFully(stream, bufferSize);
- if (result == null) {
- return null;
- }
-
- // Having downloaded the image, decode it.
- return FaviconDecoder.decodeFavicon(context, result.getData(), 0, result.consumedLength);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/loader/IconGenerator.java b/mobile/android/base/java/org/mozilla/gecko/icons/loader/IconGenerator.java
deleted file mode 100644
index e0139345d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/loader/IconGenerator.java
+++ /dev/null
@@ -1,168 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.loader;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.RectF;
-import android.net.Uri;
-import android.support.annotation.NonNull;
-import android.support.annotation.VisibleForTesting;
-import android.text.TextUtils;
-import android.util.TypedValue;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.icons.IconResponse;
-
-/**
- * This loader will generate an icon in case no icon could be loaded. In order to do so this needs
- * to be the last loader that will be tried.
- */
-public class IconGenerator implements IconLoader {
- // Mozilla's Visual Design Colour Palette
- // http://firefoxux.github.io/StyleGuide/#/visualDesign/colours
- private static final int[] COLORS = {
- 0xFFc33c32,
- 0xFFf25820,
- 0xFFff9216,
- 0xFFffcb00,
- 0xFF57bd35,
- 0xFF01bdad,
- 0xFF0996f8,
- 0xFF02538b,
- 0xFF1f386e,
- 0xFF7a2f7a,
- 0xFFea385e,
- };
-
- // List of common prefixes of host names. Those prefixes will be striped before a prepresentative
- // character for an URL is determined.
- private static final String[] COMMON_PREFIXES = {
- "www.",
- "m.",
- "mobile.",
- };
-
- private static final int TEXT_SIZE_DP = 12;
- @Override
- public IconResponse load(IconRequest request) {
- if (request.getIconCount() > 1) {
- // There are still other icons to try. We will only generate an icon if there's only one
- // icon left and all previous loaders have failed (assuming this is the last one).
- return null;
- }
-
- return generate(request.getContext(), request.getPageUrl());
- }
-
- /**
- * Generate default favicon for the given page URL.
- */
- @VisibleForTesting static IconResponse generate(Context context, String pageURL) {
- final Resources resources = context.getResources();
- final int widthAndHeight = resources.getDimensionPixelSize(R.dimen.favicon_bg);
- final int roundedCorners = resources.getDimensionPixelOffset(R.dimen.favicon_corner_radius);
-
- final Bitmap favicon = Bitmap.createBitmap(widthAndHeight, widthAndHeight, Bitmap.Config.ARGB_8888);
- final Canvas canvas = new Canvas(favicon);
-
- final int color = pickColor(pageURL);
-
- final Paint paint = new Paint();
- paint.setColor(color);
-
- canvas.drawRoundRect(new RectF(0, 0, widthAndHeight, widthAndHeight), roundedCorners, roundedCorners, paint);
-
- paint.setColor(Color.WHITE);
-
- final String character = getRepresentativeCharacter(pageURL);
-
- final float textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, TEXT_SIZE_DP, context.getResources().getDisplayMetrics());
-
- paint.setTextAlign(Paint.Align.CENTER);
- paint.setTextSize(textSize);
- paint.setAntiAlias(true);
-
- canvas.drawText(character,
- canvas.getWidth() / 2,
- (int) ((canvas.getHeight() / 2) - ((paint.descent() + paint.ascent()) / 2)),
- paint);
-
- return IconResponse.createGenerated(favicon, color);
- }
-
- /**
- * Get a representative character for the given URL.
- *
- * For example this method will return "f" for "http://m.facebook.com/foobar".
- */
- @VisibleForTesting static String getRepresentativeCharacter(String url) {
- if (TextUtils.isEmpty(url)) {
- return "?";
- }
-
- final String snippet = getRepresentativeSnippet(url);
- for (int i = 0; i < snippet.length(); i++) {
- char c = snippet.charAt(i);
-
- if (Character.isLetterOrDigit(c)) {
- return String.valueOf(Character.toUpperCase(c));
- }
- }
-
- // Nothing found..
- return "?";
- }
-
- /**
- * Return a color for this URL. Colors will be based on the host. URLs with the same host will
- * return the same color.
- */
- @VisibleForTesting static int pickColor(String url) {
- if (TextUtils.isEmpty(url)) {
- return COLORS[0];
- }
-
- final String snippet = getRepresentativeSnippet(url);
- final int color = Math.abs(snippet.hashCode() % COLORS.length);
-
- return COLORS[color];
- }
-
- /**
- * Get the representative part of the URL. Usually this is the host (without common prefixes).
- */
- private static String getRepresentativeSnippet(@NonNull String url) {
- Uri uri = Uri.parse(url);
-
- // Use the host if available
- String snippet = uri.getHost();
-
- if (TextUtils.isEmpty(snippet)) {
- // If the uri does not have a host (e.g. file:// uris) then use the path
- snippet = uri.getPath();
- }
-
- if (TextUtils.isEmpty(snippet)) {
- // If we still have no snippet then just return the question mark
- return "?";
- }
-
- // Strip common prefixes that we do not want to use to determine the representative character
- for (String prefix : COMMON_PREFIXES) {
- if (snippet.startsWith(prefix)) {
- snippet = snippet.substring(prefix.length());
- }
- }
-
- return snippet;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/loader/IconLoader.java b/mobile/android/base/java/org/mozilla/gecko/icons/loader/IconLoader.java
deleted file mode 100644
index 8158babc3..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/loader/IconLoader.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.loader;
-
-import android.support.annotation.Nullable;
-
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.icons.IconResponse;
-
-/**
- * Generic interface for classes that can load icons.
- */
-public interface IconLoader {
- /**
- * Loads the icon for this request or returns null if this loader can't load an icon for this
- * request or just failed this time.
- */
- @Nullable
- IconResponse load(IconRequest request);
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/loader/JarLoader.java b/mobile/android/base/java/org/mozilla/gecko/icons/loader/JarLoader.java
deleted file mode 100644
index 882d32da5..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/loader/JarLoader.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.loader;
-
-import android.content.Context;
-import android.util.Log;
-
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.util.GeckoJarReader;
-
-/**
- * Loader implementation for loading icons from the omni.ja (jar:jar: URLs).
- *
- * https://developer.mozilla.org/en-US/docs/Mozilla/About_omni.ja_(formerly_omni.jar)
- */
-public class JarLoader implements IconLoader {
- private static final String LOGTAG = "Gecko/JarLoader";
-
- @Override
- public IconResponse load(IconRequest request) {
- if (request.shouldSkipDisk()) {
- return null;
- }
-
- final String iconUrl = request.getBestIcon().getUrl();
-
- if (!iconUrl.startsWith("jar:jar:")) {
- return null;
- }
-
- try {
- final Context context = request.getContext();
- return IconResponse.create(
- GeckoJarReader.getBitmap(context, context.getResources(), iconUrl));
- } catch (Exception e) {
- // Just about anything could happen here.
- Log.w(LOGTAG, "Error fetching favicon from JAR.", e);
- return null;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/loader/LegacyLoader.java b/mobile/android/base/java/org/mozilla/gecko/icons/loader/LegacyLoader.java
deleted file mode 100644
index d1efc3ad9..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/loader/LegacyLoader.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.loader;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.graphics.Bitmap;
-
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.icons.IconResponse;
-
-/**
- * This legacy loader loads icons from the abandoned database storage. This loader should only exist
- * for a couple of releases and be removed afterwards.
- *
- * When updating to an app version with the new loaders our initial storage won't have any data so
- * we need to continue loading from the database storage until the new storage has a good set of data.
- */
-public class LegacyLoader implements IconLoader {
- @Override
- public IconResponse load(IconRequest request) {
- if (!request.shouldSkipNetwork()) {
- // If we are allowed to load from the network for this request then just ommit the legacy
- // loader and fetch a fresh new icon.
- return null;
- }
-
- if (request.shouldSkipDisk()) {
- return null;
- }
-
- if (request.getIconCount() > 1) {
- // There are still other icon URLs to try. Let's try to load from the legacy loader only
- // if there's one icon left and the other loads have failed. We will ignore the icon URL
- // anyways and try to receive the legacy icon URL from the database.
- return null;
- }
-
- final Bitmap bitmap = loadBitmapFromDatabase(request);
-
- if (bitmap == null) {
- return null;
- }
-
- return IconResponse.create(bitmap);
- }
-
- /* package-private */ Bitmap loadBitmapFromDatabase(IconRequest request) {
- final Context context = request.getContext();
- final ContentResolver contentResolver = context.getContentResolver();
- final BrowserDB db = BrowserDB.from(context);
-
- // We ask the database for the favicon URL and ignore the icon URL in the request object:
- // As we are not updating the database anymore the icon might be stored under a different URL.
- final String legacyFaviconUrl = db.getFaviconURLFromPageURL(contentResolver, request.getPageUrl());
- if (legacyFaviconUrl == null) {
- // No URL -> Nothing to load.
- return null;
- }
-
- final LoadFaviconResult result = db.getFaviconForUrl(context, context.getContentResolver(), legacyFaviconUrl);
- if (result == null) {
- return null;
- }
-
- return result.getBestBitmap(request.getTargetSize());
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/loader/MemoryLoader.java b/mobile/android/base/java/org/mozilla/gecko/icons/loader/MemoryLoader.java
deleted file mode 100644
index 98b651fc7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/loader/MemoryLoader.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.loader;
-
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.icons.storage.MemoryStorage;
-
-/**
- * Loader implementation for loading icons from an in-memory cached (Implemented by MemoryStorage).
- */
-public class MemoryLoader implements IconLoader {
- private final MemoryStorage storage;
-
- public MemoryLoader() {
- storage = MemoryStorage.get();
- }
-
- @Override
- public IconResponse load(IconRequest request) {
- if (request.shouldSkipMemory()) {
- return null;
- }
-
- final String iconUrl = request.getBestIcon().getUrl();
- return storage.getIcon(iconUrl);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/AboutPagesPreparer.java b/mobile/android/base/java/org/mozilla/gecko/icons/preparation/AboutPagesPreparer.java
deleted file mode 100644
index d335cbf51..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/AboutPagesPreparer.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.preparation;
-
-import org.mozilla.gecko.AboutPages;
-import org.mozilla.gecko.icons.IconDescriptor;
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.util.GeckoJarReader;
-
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * Preparer implementation for adding the omni.ja URL for internal about: pages.
- */
-public class AboutPagesPreparer implements Preparer {
- private Set<String> aboutUrls;
-
- public AboutPagesPreparer() {
- aboutUrls = new HashSet<>();
-
- Collections.addAll(aboutUrls, AboutPages.DEFAULT_ICON_PAGES);
- }
-
- @Override
- public void prepare(IconRequest request) {
- if (aboutUrls.contains(request.getPageUrl())) {
- final String iconUrl = GeckoJarReader.getJarURL(request.getContext(), "chrome/chrome/content/branding/favicon64.png");
-
- request.modify()
- .icon(IconDescriptor.createLookupIcon(iconUrl))
- .deferBuild();
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/AddDefaultIconUrl.java b/mobile/android/base/java/org/mozilla/gecko/icons/preparation/AddDefaultIconUrl.java
deleted file mode 100644
index 5bc7d1c1f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/AddDefaultIconUrl.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.preparation;
-
-import android.text.TextUtils;
-
-import org.mozilla.gecko.icons.IconDescriptor;
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.icons.IconsHelper;
-import org.mozilla.gecko.util.StringUtils;
-
-/**
- * Preparer to add the "default/guessed" favicon URL (domain/favicon.ico) to the list of URLs to
- * try loading the favicon from.
- *
- * The default URL will be added with a very low priority so that we will only try to load from this
- * URL if all other options failed.
- */
-public class AddDefaultIconUrl implements Preparer {
- @Override
- public void prepare(IconRequest request) {
- if (!StringUtils.isHttpOrHttps(request.getPageUrl())) {
- return;
- }
-
- final String defaultFaviconUrl = IconsHelper.guessDefaultFaviconURL(request.getPageUrl());
- if (TextUtils.isEmpty(defaultFaviconUrl)) {
- // We couldn't generate a default favicon URL for this URL. Nothing to do here.
- return;
- }
-
- request.modify()
- .icon(IconDescriptor.createGenericIcon(defaultFaviconUrl))
- .deferBuild();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterKnownFailureUrls.java b/mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterKnownFailureUrls.java
deleted file mode 100644
index effd31a03..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterKnownFailureUrls.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.preparation;
-
-import org.mozilla.gecko.icons.IconDescriptor;
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.icons.storage.FailureCache;
-
-import java.util.Iterator;
-
-public class FilterKnownFailureUrls implements Preparer {
- @Override
- public void prepare(IconRequest request) {
- final FailureCache failureCache = FailureCache.get();
- final Iterator<IconDescriptor> iterator = request.getIconIterator();
-
- while (iterator.hasNext()) {
- final IconDescriptor descriptor = iterator.next();
-
- if (failureCache.isKnownFailure(descriptor.getUrl())) {
- // Loading from this URL has failed in the past. Do not try again.
- iterator.remove();
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterMimeTypes.java b/mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterMimeTypes.java
deleted file mode 100644
index a12dad2ad..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterMimeTypes.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.preparation;
-
-import android.text.TextUtils;
-
-import org.mozilla.gecko.icons.IconDescriptor;
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.icons.IconsHelper;
-
-import java.util.Iterator;
-
-/**
- * Preparer implementation to filter unknown MIME types to avoid loading images that we cannot decode.
- */
-public class FilterMimeTypes implements Preparer {
- @Override
- public void prepare(IconRequest request) {
- final Iterator<IconDescriptor> iterator = request.getIconIterator();
-
- while (iterator.hasNext()) {
- final IconDescriptor descriptor = iterator.next();
- final String mimeType = descriptor.getMimeType();
-
- if (TextUtils.isEmpty(mimeType)) {
- // We do not have a MIME type for this icon, so we cannot know in advance if we are able
- // to decode it. Let's just continue.
- continue;
- }
-
- if (!IconsHelper.canDecodeType(mimeType)) {
- iterator.remove();
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterPrivilegedUrls.java b/mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterPrivilegedUrls.java
deleted file mode 100644
index abf34c038..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterPrivilegedUrls.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package org.mozilla.gecko.icons.preparation;
-
-import org.mozilla.gecko.icons.IconDescriptor;
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.util.StringUtils;
-
-import java.util.Iterator;
-
-/**
- * Filter non http/https URLs if the request is not from privileged code.
- */
-public class FilterPrivilegedUrls implements Preparer {
- @Override
- public void prepare(IconRequest request) {
- if (request.isPrivileged()) {
- // This request is privileged. No need to filter anything.
- return;
- }
-
- final Iterator<IconDescriptor> iterator = request.getIconIterator();
-
- while (iterator.hasNext()) {
- IconDescriptor descriptor = iterator.next();
-
- if (!StringUtils.isHttpOrHttps(descriptor.getUrl())) {
- iterator.remove();
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/LookupIconUrl.java b/mobile/android/base/java/org/mozilla/gecko/icons/preparation/LookupIconUrl.java
deleted file mode 100644
index 0c7641112..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/LookupIconUrl.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.preparation;
-
-import org.mozilla.gecko.icons.IconDescriptor;
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.icons.storage.DiskStorage;
-import org.mozilla.gecko.icons.storage.MemoryStorage;
-
-/**
- * Preparer implementation to lookup the icon URL for the page URL in the request. This class tries
- * to locate the icon URL by looking through previously stored mappings on disk and in memory.
- */
-public class LookupIconUrl implements Preparer {
- @Override
- public void prepare(IconRequest request) {
- if (lookupFromMemory(request)) {
- return;
- }
-
- lookupFromDisk(request);
- }
-
- private boolean lookupFromMemory(IconRequest request) {
- final String iconUrl = MemoryStorage.get()
- .getMapping(request.getPageUrl());
-
- if (iconUrl != null) {
- request.modify()
- .icon(IconDescriptor.createLookupIcon(iconUrl))
- .deferBuild();
-
- return true;
- }
-
- return false;
- }
-
- private boolean lookupFromDisk(IconRequest request) {
- final String iconUrl = DiskStorage.get(request.getContext())
- .getMapping(request.getPageUrl());
-
- if (iconUrl != null) {
- request.modify()
- .icon(IconDescriptor.createLookupIcon(iconUrl))
- .deferBuild();
-
- return true;
- }
-
- return false;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/Preparer.java b/mobile/android/base/java/org/mozilla/gecko/icons/preparation/Preparer.java
deleted file mode 100644
index 466307ead..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/preparation/Preparer.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.preparation;
-
-import org.mozilla.gecko.icons.IconRequest;
-
-/**
- * Generic interface for a class "preparing" a request before we try to load icons. A class
- * implementing this interface can modify the request (e.g. filter or add icon URLs).
- */
-public interface Preparer {
- /**
- * Inspects or modifies the request before any icon is loaded.
- */
- void prepare(IconRequest request);
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/processing/ColorProcessor.java b/mobile/android/base/java/org/mozilla/gecko/icons/processing/ColorProcessor.java
deleted file mode 100644
index 3f7110034..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/processing/ColorProcessor.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.processing;
-
-import android.support.v7.graphics.Palette;
-import android.util.Log;
-
-import org.mozilla.gecko.gfx.BitmapUtils;
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.util.HardwareUtils;
-
-/**
- * Processor implementation to extract the dominant color from the icon and attach it to the icon
- * response object.
- */
-public class ColorProcessor implements Processor {
- private static final String LOGTAG = "GeckoColorProcessor";
- private static final int DEFAULT_COLOR = 0; // 0 == No color
-
- @Override
- public void process(IconRequest request, IconResponse response) {
- if (response.hasColor()) {
- return;
- }
-
- if (HardwareUtils.isX86System()) {
- // (Bug 1318667) We are running into crashes when using the palette library with
- // specific icons on x86 devices. They take down the whole VM and are not recoverable.
- // Unfortunately our release icon is triggering this crash. Until we can switch to a
- // newer version of the support library where this does not happen, we are using our
- // own slower implementation.
- extractColorUsingCustomImplementation(response);
- } else {
- extractColorUsingPaletteSupportLibrary(response);
- }
- }
-
- private void extractColorUsingPaletteSupportLibrary(final IconResponse response) {
- try {
- final Palette palette = Palette.from(response.getBitmap()).generate();
- response.updateColor(palette.getVibrantColor(DEFAULT_COLOR));
- } catch (ArrayIndexOutOfBoundsException e) {
- // We saw the palette library fail with an ArrayIndexOutOfBoundsException intermittently
- // in automation. In this case lets just swallow the exception and move on without a
- // color. This is a valid condition and callers should handle this gracefully (Bug 1318560).
- Log.e(LOGTAG, "Palette generation failed with ArrayIndexOutOfBoundsException", e);
-
- response.updateColor(DEFAULT_COLOR);
- }
- }
-
- private void extractColorUsingCustomImplementation(final IconResponse response) {
- final int dominantColor = BitmapUtils.getDominantColor(response.getBitmap());
-
- response.updateColor(dominantColor);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/processing/DiskProcessor.java b/mobile/android/base/java/org/mozilla/gecko/icons/processing/DiskProcessor.java
deleted file mode 100644
index 150aa503b..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/processing/DiskProcessor.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package org.mozilla.gecko.icons.processing;
-
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.icons.storage.DiskStorage;
-import org.mozilla.gecko.util.StringUtils;
-
-public class DiskProcessor implements Processor {
- @Override
- public void process(IconRequest request, IconResponse response) {
- if (request.shouldSkipDisk()) {
- return;
- }
-
- if (!response.hasUrl() || !StringUtils.isHttpOrHttps(response.getUrl())) {
- // If the response does not contain an URL from which the icon was loaded or if this is
- // not a http(s) URL then we cannot store this or do not need to (because it's already
- // stored somewhere else, like for URLs pointing inside the omni.ja).
- return;
- }
-
- final DiskStorage storage = DiskStorage.get(request.getContext());
-
- if (response.isFromNetwork()) {
- // The icon has been loaded from the network. Store it on the disk now.
- storage.putIcon(response);
- }
-
- if (response.isFromMemory() || response.isFromDisk() || response.isFromNetwork()) {
- // Remember mapping between page URL and storage URL. Even when this icon has been loaded
- // from memory or disk this does not mean that we stored this mapping already: We could
- // have loaded this icon for a different page URL previously.
- storage.putMapping(request, response.getUrl());
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/processing/MemoryProcessor.java b/mobile/android/base/java/org/mozilla/gecko/icons/processing/MemoryProcessor.java
deleted file mode 100644
index 245faded5..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/processing/MemoryProcessor.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.processing;
-
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.icons.storage.MemoryStorage;
-
-public class MemoryProcessor implements Processor {
- private final MemoryStorage storage;
-
- public MemoryProcessor() {
- storage = MemoryStorage.get();
- }
-
- @Override
- public void process(IconRequest request, IconResponse response) {
- if (request.shouldSkipMemory() || request.getIconCount() == 0 || response.isGenerated()) {
- // Do not cache this icon in memory if we should skip the memory cache or if this icon
- // has been generated. We can re-generate it if needed.
- return;
- }
-
- final String iconUrl = request.getBestIcon().getUrl();
-
- if (iconUrl.startsWith("data:image/")) {
- // The image data is encoded in the URL. It doesn't make sense to store the URL and the
- // bitmap in cache.
- return;
- }
-
- storage.putMapping(request, iconUrl);
- storage.putIcon(iconUrl, response);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/processing/Processor.java b/mobile/android/base/java/org/mozilla/gecko/icons/processing/Processor.java
deleted file mode 100644
index df7d63c6c..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/processing/Processor.java
+++ /dev/null
@@ -1,21 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.processing;
-
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.icons.IconResponse;
-
-/**
- * Generic interface for a class that processes a response object after an icon has been loaded and
- * decoded. A class implementing this interface can attach additional data to the response or modify
- * the bitmap (e.g. resizing).
- */
-public interface Processor {
- /**
- * Process a response object containing an icon loaded for this request.
- */
- void process(IconRequest request, IconResponse response);
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/processing/ResizingProcessor.java b/mobile/android/base/java/org/mozilla/gecko/icons/processing/ResizingProcessor.java
deleted file mode 100644
index 63b479021..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/processing/ResizingProcessor.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.processing;
-
-import android.graphics.Bitmap;
-import android.support.annotation.VisibleForTesting;
-
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.icons.IconResponse;
-
-/**
- * Processor implementation for resizing the loaded icon based on the target size.
- */
-public class ResizingProcessor implements Processor {
- @Override
- public void process(IconRequest request, IconResponse response) {
- if (response.isFromMemory()) {
- // This bitmap has been loaded from memory, so it has already gone through the resizing
- // process. We do not want to resize the image every time we hit the memory cache.
- return;
- }
-
- final Bitmap originalBitmap = response.getBitmap();
- final int size = originalBitmap.getWidth();
-
- final int targetSize = request.getTargetSize();
-
- if (size == targetSize) {
- // The bitmap has exactly the size we are looking for.
- return;
- }
-
- final Bitmap resizedBitmap;
-
- if (size > targetSize) {
- resizedBitmap = resize(originalBitmap, targetSize);
- } else {
- // Our largest primary is smaller than the desired size. Upscale by a maximum of 2x.
- // 'largestSize' now reflects the maximum size we can upscale to.
- final int largestSize = size * 2;
-
- if (largestSize > targetSize) {
- // Perfect! We can upscale by less than 2x and reach the needed size. Do it.
- resizedBitmap = resize(originalBitmap, targetSize);
- } else {
- // We don't have enough information to make the target size look non terrible. Best effort:
- resizedBitmap = resize(originalBitmap, largestSize);
- }
- }
-
- response.updateBitmap(resizedBitmap);
-
- originalBitmap.recycle();
- }
-
- @VisibleForTesting Bitmap resize(Bitmap bitmap, int targetSize) {
- try {
- return Bitmap.createScaledBitmap(bitmap, targetSize, targetSize, true);
- } catch (OutOfMemoryError error) {
- // There's not enough memory to create a resized copy of the bitmap in memory. Let's just
- // use what we have.
- return bitmap;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/storage/DiskStorage.java b/mobile/android/base/java/org/mozilla/gecko/icons/storage/DiskStorage.java
deleted file mode 100644
index 3c0e2a554..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/storage/DiskStorage.java
+++ /dev/null
@@ -1,293 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.storage;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.support.annotation.CheckResult;
-import android.support.annotation.Nullable;
-import android.util.Log;
-
-import com.jakewharton.disklrucache.DiskLruCache;
-
-import org.mozilla.gecko.background.nativecode.NativeCrypto;
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.sync.Utils;
-import org.mozilla.gecko.util.IOUtils;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
-import java.security.MessageDigest;
-
-/**
- * Least Recently Used (LRU) disk cache for icons and the mappings from page URLs to icon URLs.
- */
-public class DiskStorage {
- private static final String LOGTAG = "Gecko/DiskStorage";
-
- /**
- * Maximum size (in bytes) of the cache. This cache is located in the cache directory of the
- * application and can be cleared by the user.
- */
- private static final int DISK_CACHE_SIZE = 50 * 1024 * 1024;
-
- /**
- * Version of the cache. Updating the version will invalidate all existing items.
- */
- private static final int CACHE_VERSION = 1;
-
- private static final String KEY_PREFIX_ICON = "icon:";
- private static final String KEY_PREFIX_MAPPING = "mapping:";
-
- private static DiskStorage instance;
-
- public static DiskStorage get(Context context) {
- if (instance == null) {
- instance = new DiskStorage(context);
- }
-
- return instance;
- }
-
- private Context context;
- private DiskLruCache cache;
-
- private DiskStorage(Context context) {
- this.context = context.getApplicationContext();
- }
-
- @CheckResult
- private synchronized DiskLruCache ensureCacheIsReady() throws IOException {
- if (cache == null || cache.isClosed()) {
- cache = DiskLruCache.open(
- new File(context.getCacheDir(), "icons"),
- CACHE_VERSION,
- 1,
- DISK_CACHE_SIZE);
- }
-
- return cache;
- }
-
- /**
- * Store a mapping from page URL to icon URL in the cache.
- */
- public void putMapping(IconRequest request, String iconUrl) {
- putMapping(request.getPageUrl(), iconUrl);
- }
-
- /**
- * Store a mapping from page URL to icon URL in the cache.
- */
- public void putMapping(String pageUrl, String iconUrl) {
- DiskLruCache.Editor editor = null;
-
- try {
- final DiskLruCache cache = ensureCacheIsReady();
-
- final String key = createKey(KEY_PREFIX_MAPPING, pageUrl);
- if (key == null) {
- return;
- }
-
- editor = cache.edit(key);
- if (editor == null) {
- return;
- }
-
- editor.set(0, iconUrl);
- editor.commit();
- } catch (IOException e) {
- Log.w(LOGTAG, "IOException while accessing disk cache", e);
-
- abortSilently(editor);
- }
- }
-
- /**
- * Store an icon in the cache (uses the icon URL as key).
- */
- public void putIcon(IconResponse response) {
- putIcon(response.getUrl(), response.getBitmap());
- }
-
- /**
- * Store an icon in the cache (uses the icon URL as key).
- */
- public void putIcon(String iconUrl, Bitmap bitmap) {
- OutputStream outputStream = null;
- DiskLruCache.Editor editor = null;
-
- try {
- final DiskLruCache cache = ensureCacheIsReady();
-
- final String key = createKey(KEY_PREFIX_ICON, iconUrl);
- if (key == null) {
- return;
- }
-
- editor = cache.edit(key);
- if (editor == null) {
- return;
- }
-
- outputStream = editor.newOutputStream(0);
- boolean success = bitmap.compress(Bitmap.CompressFormat.PNG, 100 /* quality; ignored. PNG is lossless */, outputStream);
-
- outputStream.close();
-
- if (success) {
- editor.commit();
- } else {
- editor.abort();
- }
- } catch (IOException e) {
- Log.w(LOGTAG, "IOException while accessing disk cache", e);
-
- abortSilently(editor);
- } finally {
- IOUtils.safeStreamClose(outputStream);
- }
- }
-
-
-
- /**
- * Get an icon for the icon URL from the cache. Returns null if no icon is cached for this URL.
- */
- @Nullable
- public IconResponse getIcon(String iconUrl) {
- InputStream inputStream = null;
-
- try {
- final DiskLruCache cache = ensureCacheIsReady();
-
- final String key = createKey(KEY_PREFIX_ICON, iconUrl);
- if (key == null) {
- return null;
- }
-
- if (cache.isClosed()) {
- throw new RuntimeException("CLOSED");
- }
-
- final DiskLruCache.Snapshot snapshot = cache.get(key);
- if (snapshot == null) {
- return null;
- }
-
- inputStream = snapshot.getInputStream(0);
-
- final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
- if (bitmap == null) {
- return null;
- }
-
- return IconResponse.createFromDisk(bitmap, iconUrl);
- } catch (IOException e) {
- Log.w(LOGTAG, "IOException while accessing disk cache", e);
- } finally {
- IOUtils.safeStreamClose(inputStream);
- }
-
- return null;
- }
-
- /**
- * Get the icon URL for this page URL. Returns null if no mapping is in the cache.
- */
- @Nullable
- public String getMapping(String pageUrl) {
- try {
- final DiskLruCache cache = ensureCacheIsReady();
-
- final String key = createKey(KEY_PREFIX_MAPPING, pageUrl);
- if (key == null) {
- return null;
- }
-
- DiskLruCache.Snapshot snapshot = cache.get(key);
- if (snapshot == null) {
- return null;
- }
-
- return snapshot.getString(0);
- } catch (IOException e) {
- Log.w(LOGTAG, "IOException while accessing disk cache", e);
- }
-
- return null;
- }
-
- /**
- * Remove all entries from this cache.
- */
- public void evictAll() {
- try {
- final DiskLruCache cache = ensureCacheIsReady();
-
- cache.delete();
-
- } catch (IOException e) {
- Log.w(LOGTAG, "IOException while accessing disk cache", e);
- }
- }
-
- /**
- * Create a key for this URL using the given prefix.
- *
- * The disk cache requires valid file names to be used as key. Therefore we hash the created key
- * (SHA-256).
- */
- @Nullable
- private String createKey(String prefix, String url) {
- try {
- // We use our own crypto implementation to avoid the penalty of loading the java crypto
- // framework.
- byte[] ctx = NativeCrypto.sha256init();
- if (ctx == null) {
- return null;
- }
-
- byte[] data = prefix.getBytes("UTF-8");
- NativeCrypto.sha256update(ctx, data, data.length);
-
- data = url.getBytes("UTF-8");
- NativeCrypto.sha256update(ctx, data, data.length);
- return Utils.byte2Hex(NativeCrypto.sha256finalize(ctx));
- } catch (NoClassDefFoundError | ExceptionInInitializerError error) {
- // We could not load libmozglue.so. Let's use Java's MessageDigest as fallback. We do
- // this primarily for our unit tests that can't load native libraries. On an device
- // we will have a lot of other problems if we can't load libmozglue.so
- try {
- MessageDigest md = MessageDigest.getInstance("SHA-256");
- md.update(prefix.getBytes("UTF-8"));
- md.update(url.getBytes("UTF-8"));
- return Utils.byte2Hex(md.digest());
- } catch (Exception e) {
- // Just give up. And let everyone know.
- throw new RuntimeException(e);
- }
- } catch (UnsupportedEncodingException e) {
- throw new AssertionError("Should not happen: Device does not understand UTF-8");
- }
- }
-
- private void abortSilently(DiskLruCache.Editor editor) {
- if (editor != null) {
- try {
- editor.abort();
- } catch (IOException e) {
- // Ignore
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/storage/FailureCache.java b/mobile/android/base/java/org/mozilla/gecko/icons/storage/FailureCache.java
deleted file mode 100644
index b45cb0fce..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/storage/FailureCache.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.storage;
-
-import android.os.SystemClock;
-import android.support.annotation.VisibleForTesting;
-import android.util.LruCache;
-
-/**
- * In-memory cache to remember URLs from which loading icons has failed recently.
- */
-public class FailureCache {
- /**
- * Retry loading failed icons after 4 hours.
- */
- private static final long FAILURE_RETRY_MILLISECONDS = 1000 * 60 * 60 * 4;
-
- private static final int MAX_ENTRIES = 25;
-
- private static FailureCache instance;
-
- public static synchronized FailureCache get() {
- if (instance == null) {
- instance = new FailureCache();
- }
-
- return instance;
- }
-
- private final LruCache<String, Long> cache;
-
- private FailureCache() {
- cache = new LruCache<>(MAX_ENTRIES);
- }
-
- /**
- * Remember this icon URL after loading from it (over the network) has failed.
- */
- public void rememberFailure(String iconUrl) {
- cache.put(iconUrl, SystemClock.elapsedRealtime());
- }
-
- /**
- * Has loading from this URL failed previously and recently?
- */
- public boolean isKnownFailure(String iconUrl) {
- synchronized (cache) {
- final Long failedAt = cache.get(iconUrl);
- if (failedAt == null) {
- return false;
- }
-
- if (failedAt + FAILURE_RETRY_MILLISECONDS < SystemClock.elapsedRealtime()) {
- // The wait time has passed and we can retry loading from this URL.
- cache.remove(iconUrl);
- return false;
- }
- }
-
- return true;
- }
-
- @VisibleForTesting
- public void evictAll() {
- cache.evictAll();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/icons/storage/MemoryStorage.java b/mobile/android/base/java/org/mozilla/gecko/icons/storage/MemoryStorage.java
deleted file mode 100644
index e0a96f7c7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/icons/storage/MemoryStorage.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.icons.storage;
-
-import android.graphics.Bitmap;
-import android.support.annotation.Nullable;
-import android.util.Log;
-import android.util.LruCache;
-
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.icons.IconResponse;
-
-/**
- * Least Recently Used (LRU) memory cache for icons and the mappings from page URLs to icon URLs.
- */
-public class MemoryStorage {
- /**
- * Maximum number of items in the cache for mapping page URLs to icon URLs.
- */
- private static final int MAPPING_CACHE_SIZE = 500;
-
- private static MemoryStorage instance;
-
- public static synchronized MemoryStorage get() {
- if (instance == null) {
- instance = new MemoryStorage();
- }
-
- return instance;
- }
-
- /**
- * Class representing an cached icon. We store the original bitmap and the color in cache only.
- */
- private static class CacheEntry {
- private final Bitmap bitmap;
- private final int color;
-
- private CacheEntry(Bitmap bitmap, int color) {
- this.bitmap = bitmap;
- this.color = color;
- }
- }
-
- private final LruCache<String, CacheEntry> iconCache; // Guarded by 'this'
- private final LruCache<String, String> mappingCache; // Guarded by 'this'
-
- private MemoryStorage() {
- iconCache = new LruCache<String, CacheEntry>(calculateCacheSize()) {
- @Override
- protected int sizeOf(String key, CacheEntry value) {
- return value.bitmap.getByteCount() / 1024;
- }
- };
-
- mappingCache = new LruCache<>(MAPPING_CACHE_SIZE);
- }
-
- private int calculateCacheSize() {
- // Use a maximum of 1/8 of the available memory for storing cached icons.
- int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
- return maxMemory / 8;
- }
-
- /**
- * Store a mapping from page URL to icon URL in the cache.
- */
- public synchronized void putMapping(IconRequest request, String iconUrl) {
- mappingCache.put(request.getPageUrl(), iconUrl);
- }
-
- /**
- * Get the icon URL for this page URL. Returns null if no mapping is in the cache.
- */
- @Nullable
- public synchronized String getMapping(String pageUrl) {
- return mappingCache.get(pageUrl);
- }
-
- /**
- * Store an icon in the cache (uses the icon URL as key).
- */
- public synchronized void putIcon(String url, IconResponse response) {
- final CacheEntry entry = new CacheEntry(response.getBitmap(), response.getColor());
-
- iconCache.put(url, entry);
- }
-
- /**
- * Get an icon for the icon URL from the cache. Returns null if no icon is cached for this URL.
- */
- @Nullable
- public synchronized IconResponse getIcon(String iconUrl) {
- final CacheEntry entry = iconCache.get(iconUrl);
- if (entry == null) {
- return null;
- }
-
- return IconResponse.createFromMemory(entry.bitmap, iconUrl, entry.color);
- }
-
- /**
- * Remove all entries from this cache.
- */
- public synchronized void evictAll() {
- iconCache.evictAll();
- mappingCache.evictAll();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManager.java b/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManager.java
deleted file mode 100644
index 33a97955f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManager.java
+++ /dev/null
@@ -1,195 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.javaaddons;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Message;
-import android.util.Log;
-import dalvik.system.DexClassLoader;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.util.GeckoEventListener;
-
-import java.io.File;
-import java.lang.reflect.Constructor;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-
-/**
- * The manager for addon-provided Java code.
- *
- * Java code in addons can be loaded using the Dex:Load message, and unloaded
- * via the Dex:Unload message. Addon classes loaded are checked for a constructor
- * that takes a Map&lt;String, Handler.Callback&gt;. If such a constructor
- * exists, it is called and the objects populated into the map by the constructor
- * are registered as event listeners. If no such constructor exists, the default
- * constructor is invoked instead.
- *
- * Note: The Map and Handler.Callback classes were used in this API definition
- * rather than defining a custom class. This was done explicitly so that the
- * addon code can be compiled against the android.jar provided in the Android
- * SDK, rather than having to be compiled against Fennec source code.
- *
- * The Handler.Callback instances provided (as described above) are invoked with
- * Message objects when the corresponding events are dispatched. The Bundle
- * object attached to the Message will contain the "primitive" values from the
- * JSON of the event. ("primitive" includes bool/int/long/double/String). If
- * the addon callback wishes to synchronously return a value back to the event
- * dispatcher, they can do so by inserting the response string into the bundle
- * under the key "response".
- */
-public class JavaAddonManager implements GeckoEventListener {
- private static final String LOGTAG = "GeckoJavaAddonManager";
-
- private static JavaAddonManager sInstance;
-
- private final EventDispatcher mDispatcher;
- private final Map<String, Map<String, GeckoEventListener>> mAddonCallbacks;
-
- private Context mApplicationContext;
-
- public static JavaAddonManager getInstance() {
- if (sInstance == null) {
- sInstance = new JavaAddonManager();
- }
- return sInstance;
- }
-
- private JavaAddonManager() {
- mDispatcher = EventDispatcher.getInstance();
- mAddonCallbacks = new HashMap<String, Map<String, GeckoEventListener>>();
- }
-
- public void init(Context applicationContext) {
- if (mApplicationContext != null) {
- // we've already done this registration. don't do it again
- return;
- }
- mApplicationContext = applicationContext;
- mDispatcher.registerGeckoThreadListener(this,
- "Dex:Load",
- "Dex:Unload");
- JavaAddonManagerV1.getInstance().init(applicationContext);
- }
-
- @Override
- public void handleMessage(String event, JSONObject message) {
- try {
- if (event.equals("Dex:Load")) {
- String zipFile = message.getString("zipfile");
- String implClass = message.getString("impl");
- Log.d(LOGTAG, "Attempting to load classes.dex file from " + zipFile + " and instantiate " + implClass);
- try {
- File tmpDir = mApplicationContext.getDir("dex", 0);
- DexClassLoader loader = new DexClassLoader(zipFile, tmpDir.getAbsolutePath(), null, mApplicationContext.getClassLoader());
- Class<?> c = loader.loadClass(implClass);
- try {
- Constructor<?> constructor = c.getDeclaredConstructor(Map.class);
- Map<String, Handler.Callback> callbacks = new HashMap<String, Handler.Callback>();
- constructor.newInstance(callbacks);
- registerCallbacks(zipFile, callbacks);
- } catch (NoSuchMethodException nsme) {
- Log.d(LOGTAG, "Did not find constructor with parameters Map<String, Handler.Callback>. Falling back to default constructor...");
- // fallback for instances with no constructor that takes a Map<String, Handler.Callback>
- c.newInstance();
- }
- } catch (Exception e) {
- Log.e(LOGTAG, "Unable to load dex successfully", e);
- }
- } else if (event.equals("Dex:Unload")) {
- String zipFile = message.getString("zipfile");
- unregisterCallbacks(zipFile);
- }
- } catch (JSONException e) {
- Log.e(LOGTAG, "Exception handling message [" + event + "]:", e);
- }
- }
-
- private void registerCallbacks(String zipFile, Map<String, Handler.Callback> callbacks) {
- Map<String, GeckoEventListener> addonCallbacks = mAddonCallbacks.get(zipFile);
- if (addonCallbacks != null) {
- Log.w(LOGTAG, "Found pre-existing callbacks for zipfile [" + zipFile + "]; aborting re-registration!");
- return;
- }
- addonCallbacks = new HashMap<String, GeckoEventListener>();
- for (String event : callbacks.keySet()) {
- CallbackWrapper wrapper = new CallbackWrapper(callbacks.get(event));
- mDispatcher.registerGeckoThreadListener(wrapper, event);
- addonCallbacks.put(event, wrapper);
- }
- mAddonCallbacks.put(zipFile, addonCallbacks);
- }
-
- private void unregisterCallbacks(String zipFile) {
- Map<String, GeckoEventListener> callbacks = mAddonCallbacks.remove(zipFile);
- if (callbacks == null) {
- Log.w(LOGTAG, "Attempting to unregister callbacks from zipfile [" + zipFile + "] which has no callbacks registered.");
- return;
- }
- for (String event : callbacks.keySet()) {
- mDispatcher.unregisterGeckoThreadListener(callbacks.get(event), event);
- }
- }
-
- private static class CallbackWrapper implements GeckoEventListener {
- private final Handler.Callback mDelegate;
- private Bundle mBundle;
-
- CallbackWrapper(Handler.Callback delegate) {
- mDelegate = delegate;
- }
-
- private Bundle jsonToBundle(JSONObject json) {
- // XXX right now we only support primitive types;
- // we don't recurse down into JSONArray or JSONObject instances
- Bundle b = new Bundle();
- for (Iterator<?> keys = json.keys(); keys.hasNext(); ) {
- try {
- String key = (String)keys.next();
- Object value = json.get(key);
- if (value instanceof Integer) {
- b.putInt(key, (Integer)value);
- } else if (value instanceof String) {
- b.putString(key, (String)value);
- } else if (value instanceof Boolean) {
- b.putBoolean(key, (Boolean)value);
- } else if (value instanceof Long) {
- b.putLong(key, (Long)value);
- } else if (value instanceof Double) {
- b.putDouble(key, (Double)value);
- }
- } catch (JSONException e) {
- Log.d(LOGTAG, "Error during JSON->bundle conversion", e);
- }
- }
- return b;
- }
-
- @Override
- public void handleMessage(String event, JSONObject json) {
- try {
- if (mBundle != null) {
- Log.w(LOGTAG, "Event [" + event + "] handler is re-entrant; response messages may be lost");
- }
- mBundle = jsonToBundle(json);
- Message msg = new Message();
- msg.setData(mBundle);
- mDelegate.handleMessage(msg);
-
- JSONObject obj = new JSONObject();
- obj.put("response", mBundle.getString("response"));
- EventDispatcher.sendResponse(json, obj);
- mBundle = null;
- } catch (Exception e) {
- Log.e(LOGTAG, "Caught exception thrown from wrapped addon message handler", e);
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManagerV1.java b/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManagerV1.java
deleted file mode 100644
index f361773ca..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManagerV1.java
+++ /dev/null
@@ -1,260 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.javaaddons;
-
-import android.content.Context;
-import android.util.Log;
-import android.util.Pair;
-import dalvik.system.DexClassLoader;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.sync.Utils;
-import org.mozilla.gecko.util.GeckoJarReader;
-import org.mozilla.gecko.util.GeckoRequest;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.javaaddons.JavaAddonInterfaceV1;
-
-import java.io.File;
-import java.io.IOException;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
-import java.util.HashMap;
-import java.util.IdentityHashMap;
-import java.util.Map;
-
-public class JavaAddonManagerV1 implements NativeEventListener {
- private static final String LOGTAG = "GeckoJavaAddonMgrV1";
- public static final String MESSAGE_LOAD = "JavaAddonManagerV1:Load";
- public static final String MESSAGE_UNLOAD = "JavaAddonManagerV1:Unload";
-
- private static JavaAddonManagerV1 sInstance;
-
- // Protected by static synchronized.
- private Context mApplicationContext;
-
- private final org.mozilla.gecko.EventDispatcher mDispatcher;
-
- // Protected by synchronized (this).
- private final Map<String, EventDispatcherImpl> mGUIDToDispatcherMap = new HashMap<>();
-
- public static synchronized JavaAddonManagerV1 getInstance() {
- if (sInstance == null) {
- sInstance = new JavaAddonManagerV1();
- }
- return sInstance;
- }
-
- private JavaAddonManagerV1() {
- mDispatcher = org.mozilla.gecko.EventDispatcher.getInstance();
- }
-
- public synchronized void init(Context applicationContext) {
- if (mApplicationContext != null) {
- // We've already registered; don't register again.
- return;
- }
- mApplicationContext = applicationContext;
- mDispatcher.registerGeckoThreadListener(this,
- MESSAGE_LOAD,
- MESSAGE_UNLOAD);
- }
-
- protected String getExtension(String filename) {
- if (filename == null) {
- return "";
- }
- final int last = filename.lastIndexOf(".");
- if (last < 0) {
- return "";
- }
- return filename.substring(last);
- }
-
- protected synchronized EventDispatcherImpl registerNewInstance(String classname, String filename)
- throws ClassNotFoundException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, IOException {
- Log.d(LOGTAG, "Attempting to instantiate " + classname + "from filename " + filename);
-
- // It's important to maintain the extension, either .dex, .apk, .jar.
- final String extension = getExtension(filename);
- final File dexFile = GeckoJarReader.extractStream(mApplicationContext, filename, mApplicationContext.getCacheDir(), "." + extension);
- try {
- if (dexFile == null) {
- throw new IOException("Could not find file " + filename);
- }
- final File tmpDir = mApplicationContext.getDir("dex", 0); // We'd prefer getCodeCacheDir but it's API 21+.
- final DexClassLoader loader = new DexClassLoader(dexFile.getAbsolutePath(), tmpDir.getAbsolutePath(), null, mApplicationContext.getClassLoader());
- final Class<?> c = loader.loadClass(classname);
- final Constructor<?> constructor = c.getDeclaredConstructor(Context.class, JavaAddonInterfaceV1.EventDispatcher.class);
- final String guid = Utils.generateGuid();
- final EventDispatcherImpl dispatcher = new EventDispatcherImpl(guid, filename);
- final Object instance = constructor.newInstance(mApplicationContext, dispatcher);
- mGUIDToDispatcherMap.put(guid, dispatcher);
- return dispatcher;
- } finally {
- // DexClassLoader writes an optimized version, so we can get rid of our temporary extracted version.
- if (dexFile != null) {
- dexFile.delete();
- }
- }
- }
-
- @Override
- public synchronized void handleMessage(String event, NativeJSObject message, org.mozilla.gecko.util.EventCallback callback) {
- try {
- switch (event) {
- case MESSAGE_LOAD: {
- if (callback == null) {
- throw new IllegalArgumentException("callback must not be null");
- }
- final String classname = message.getString("classname");
- final String filename = message.getString("filename");
- final EventDispatcherImpl dispatcher = registerNewInstance(classname, filename);
- callback.sendSuccess(dispatcher.guid);
- }
- break;
- case MESSAGE_UNLOAD: {
- if (callback == null) {
- throw new IllegalArgumentException("callback must not be null");
- }
- final String guid = message.getString("guid");
- final EventDispatcherImpl dispatcher = mGUIDToDispatcherMap.remove(guid);
- if (dispatcher == null) {
- Log.w(LOGTAG, "Attempting to unload addon with unknown associated dispatcher; ignoring.");
- callback.sendSuccess(false);
- }
- dispatcher.unregisterAllEventListeners();
- callback.sendSuccess(true);
- }
- break;
- }
- } catch (Exception e) {
- Log.e(LOGTAG, "Exception handling message [" + event + "]", e);
- if (callback != null) {
- callback.sendError("Exception handling message [" + event + "]: " + e.toString());
- }
- }
- }
-
- /**
- * An event dispatcher is tied to a single Java Addon instance. It serves to prefix all
- * messages with its unique GUID.
- * <p/>
- * Curiously, the dispatcher does not hold a direct reference to its add-on instance. It will
- * likely hold indirect instances through its wrapping map, since the instance will probably
- * register event listeners that hold a reference to itself. When these listeners are
- * unregistered, any link will be broken, allowing the instances to be garbage collected.
- */
- private class EventDispatcherImpl implements JavaAddonInterfaceV1.EventDispatcher {
- private final String guid;
- private final String dexFileName;
-
- // Protected by synchronized (this).
- private final Map<JavaAddonInterfaceV1.EventListener, Pair<NativeEventListener, String[]>> mListenerToWrapperMap = new IdentityHashMap<>();
-
- public EventDispatcherImpl(String guid, String dexFileName) {
- this.guid = guid;
- this.dexFileName = dexFileName;
- }
-
- protected class ListenerWrapper implements NativeEventListener {
- private final JavaAddonInterfaceV1.EventListener listener;
-
- public ListenerWrapper(JavaAddonInterfaceV1.EventListener listener) {
- this.listener = listener;
- }
-
- @Override
- public void handleMessage(String prefixedEvent, NativeJSObject message, final org.mozilla.gecko.util.EventCallback callback) {
- if (!prefixedEvent.startsWith(guid + ":")) {
- return;
- }
- final String event = prefixedEvent.substring(guid.length() + 1); // Skip "guid:".
- try {
- JavaAddonInterfaceV1.EventCallback callbackAdapter = null;
- if (callback != null) {
- callbackAdapter = new JavaAddonInterfaceV1.EventCallback() {
- @Override
- public void sendSuccess(Object response) {
- callback.sendSuccess(response);
- }
-
- @Override
- public void sendError(Object response) {
- callback.sendError(response);
- }
- };
- }
- final JSONObject json = new JSONObject(message.toString());
- listener.handleMessage(mApplicationContext, event, json, callbackAdapter);
- } catch (Exception e) {
- Log.e(LOGTAG, "Exception handling message [" + prefixedEvent + "]", e);
- if (callback != null) {
- callback.sendError("Got exception handling message [" + prefixedEvent + "]: " + e.toString());
- }
- }
- }
- }
-
- @Override
- public synchronized void registerEventListener(final JavaAddonInterfaceV1.EventListener listener, String... events) {
- if (mListenerToWrapperMap.containsKey(listener)) {
- Log.e(LOGTAG, "Attempting to register listener which is already registered; ignoring.");
- return;
- }
-
- final NativeEventListener listenerWrapper = new ListenerWrapper(listener);
-
- final String[] prefixedEvents = new String[events.length];
- for (int i = 0; i < events.length; i++) {
- prefixedEvents[i] = this.guid + ":" + events[i];
- }
- mDispatcher.registerGeckoThreadListener(listenerWrapper, prefixedEvents);
- mListenerToWrapperMap.put(listener, new Pair<>(listenerWrapper, prefixedEvents));
- }
-
- @Override
- public synchronized void unregisterEventListener(final JavaAddonInterfaceV1.EventListener listener) {
- final Pair<NativeEventListener, String[]> pair = mListenerToWrapperMap.remove(listener);
- if (pair == null) {
- Log.e(LOGTAG, "Attempting to unregister listener which is not registered; ignoring.");
- return;
- }
- mDispatcher.unregisterGeckoThreadListener(pair.first, pair.second);
- }
-
-
- protected synchronized void unregisterAllEventListeners() {
- // Unregister everything, then forget everything.
- for (Pair<NativeEventListener, String[]> pair : mListenerToWrapperMap.values()) {
- mDispatcher.unregisterGeckoThreadListener(pair.first, pair.second);
- }
- mListenerToWrapperMap.clear();
- }
-
- @Override
- public void sendRequestToGecko(final String event, final JSONObject message, final JavaAddonInterfaceV1.RequestCallback callback) {
- final String prefixedEvent = guid + ":" + event;
- GeckoAppShell.sendRequestToGecko(new GeckoRequest(prefixedEvent, message) {
- @Override
- public void onResponse(NativeJSObject nativeJSObject) {
- if (callback == null) {
- // Nothing to do.
- return;
- }
- try {
- final JSONObject json = new JSONObject(nativeJSObject.toString());
- callback.onResponse(GeckoAppShell.getContext(), json);
- } catch (JSONException e) {
- // No way to report failure.
- Log.e(LOGTAG, "Exception handling response to request [" + event + "]:", e);
- }
- }
- });
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/lwt/LightweightTheme.java b/mobile/android/base/java/org/mozilla/gecko/lwt/LightweightTheme.java
deleted file mode 100644
index 0f27c1feb..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/lwt/LightweightTheme.java
+++ /dev/null
@@ -1,455 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.lwt;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.List;
-
-import org.json.JSONObject;
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.gfx.BitmapUtils;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.WindowUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
-
-import android.app.Application;
-import android.content.SharedPreferences;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.Shader;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.Gravity;
-import android.view.View;
-import android.view.ViewParent;
-
-public class LightweightTheme implements GeckoEventListener {
- private static final String LOGTAG = "GeckoLightweightTheme";
-
- private static final String PREFS_URL = "lightweightTheme.headerURL";
- private static final String PREFS_COLOR = "lightweightTheme.color";
-
- private static final String ASSETS_PREFIX = "resource://android/assets/";
-
- private final Application mApplication;
-
- private Bitmap mBitmap;
- private int mColor;
- private boolean mIsLight;
-
- public static interface OnChangeListener {
- // The View should change its background/text color.
- public void onLightweightThemeChanged();
-
- // The View should reset to its default background/text color.
- public void onLightweightThemeReset();
- }
-
- private final List<OnChangeListener> mListeners;
-
- class LightweightThemeRunnable implements Runnable {
- private String mHeaderURL;
- private String mColor;
-
- private String mSavedURL;
- private String mSavedColor;
-
- LightweightThemeRunnable() {
- }
-
- LightweightThemeRunnable(final String headerURL, final String color) {
- mHeaderURL = headerURL;
- mColor = color;
- }
-
- private void loadFromPrefs() {
- SharedPreferences prefs = GeckoSharedPrefs.forProfile(mApplication);
- mSavedURL = prefs.getString(PREFS_URL, null);
- mSavedColor = prefs.getString(PREFS_COLOR, null);
- }
-
- private void saveToPrefs() {
- GeckoSharedPrefs.forProfile(mApplication)
- .edit()
- .putString(PREFS_URL, mHeaderURL)
- .putString(PREFS_COLOR, mColor)
- .apply();
-
- // Let's keep the saved data in sync.
- mSavedURL = mHeaderURL;
- mSavedColor = mColor;
- }
-
- @Override
- public void run() {
- // Load the data from preferences, if it exists.
- loadFromPrefs();
-
- if (TextUtils.isEmpty(mHeaderURL)) {
- // mHeaderURL is null is this is the early startup path. Use
- // the saved values, if we have any.
- mHeaderURL = mSavedURL;
- mColor = mSavedColor;
- if (TextUtils.isEmpty(mHeaderURL)) {
- // We don't have any saved values, so we probably don't have
- // any lightweight theme set yet.
- return;
- }
- } else if (TextUtils.equals(mHeaderURL, mSavedURL)) {
- // If we are already using the given header, just return
- // without doing any work.
- return;
- } else {
- // mHeaderURL and mColor probably need to be saved if we get here.
- saveToPrefs();
- }
-
- String croppedURL = mHeaderURL;
- int mark = croppedURL.indexOf('?');
- if (mark != -1) {
- croppedURL = croppedURL.substring(0, mark);
- }
-
- if (croppedURL.startsWith(ASSETS_PREFIX)) {
- onBitmapLoaded(loadFromAssets(croppedURL));
- } else {
- onBitmapLoaded(BitmapUtils.decodeUrl(croppedURL));
- }
- }
-
- private Bitmap loadFromAssets(String url) {
- InputStream stream = null;
-
- try {
- stream = mApplication.getAssets().open(url.substring(ASSETS_PREFIX.length()));
- return BitmapFactory.decodeStream(stream);
- } catch (IOException e) {
- return null;
- } finally {
- if (stream != null) {
- try {
- stream.close();
- } catch (IOException e) { }
- }
- }
- }
-
- private void onBitmapLoaded(final Bitmap bitmap) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- setLightweightTheme(bitmap, mColor);
- }
- });
- }
- }
-
- public LightweightTheme(Application application) {
- mApplication = application;
- mListeners = new ArrayList<OnChangeListener>();
-
- // unregister isn't needed as the lifetime is same as the application.
- EventDispatcher.getInstance().registerGeckoThreadListener(this,
- "LightweightTheme:Update",
- "LightweightTheme:Disable");
-
- ThreadUtils.postToBackgroundThread(new LightweightThemeRunnable());
- }
-
- public void addListener(final OnChangeListener listener) {
- // Don't inform the listeners that attached late.
- // Their onLayout() will take care of them before their onDraw();
- mListeners.add(listener);
- }
-
- public void removeListener(OnChangeListener listener) {
- mListeners.remove(listener);
- }
-
- @Override
- public void handleMessage(String event, JSONObject message) {
- try {
- if (event.equals("LightweightTheme:Update")) {
- JSONObject lightweightTheme = message.getJSONObject("data");
- final String headerURL = lightweightTheme.getString("headerURL");
- final String color = lightweightTheme.optString("accentcolor");
-
- ThreadUtils.postToBackgroundThread(new LightweightThemeRunnable(headerURL, color));
- } else if (event.equals("LightweightTheme:Disable")) {
- // Clear the saved data when a theme is disabled.
- // Called on the Gecko thread, but should be very lightweight.
- clearPrefs();
-
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- resetLightweightTheme();
- }
- });
- }
- } catch (Exception e) {
- Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
- }
- }
-
- /**
- * Clear the data stored in preferences for fast path loading during startup
- */
- private void clearPrefs() {
- GeckoSharedPrefs.forProfile(mApplication)
- .edit()
- .remove(PREFS_URL)
- .remove(PREFS_COLOR)
- .apply();
- }
-
- /**
- * Set a new lightweight theme with the given bitmap.
- * Note: This should be called on the UI thread to restrict accessing the
- * bitmap to a single thread.
- *
- * @param bitmap The bitmap used for the lightweight theme.
- * @param color The background/accent color used for the lightweight theme.
- */
- private void setLightweightTheme(Bitmap bitmap, String color) {
- if (bitmap == null || bitmap.getWidth() == 0 || bitmap.getHeight() == 0) {
- mBitmap = null;
- return;
- }
-
- // Get the max display dimension so we can crop or expand the theme.
- final int maxWidth = WindowUtils.getLargestDimension(mApplication);
-
- // The lightweight theme image's width and height.
- final int bitmapWidth = bitmap.getWidth();
- final int bitmapHeight = bitmap.getHeight();
-
- try {
- mColor = Color.parseColor(color);
- } catch (Exception e) {
- // Malformed or missing color.
- // Default to TRANSPARENT.
- mColor = Color.TRANSPARENT;
- }
-
- // Calculate the luminance to determine if it's a light or a dark theme.
- double luminance = (0.2125 * ((mColor & 0x00FF0000) >> 16)) +
- (0.7154 * ((mColor & 0x0000FF00) >> 8)) +
- (0.0721 * (mColor & 0x000000FF));
- mIsLight = luminance > 110;
-
- // The bitmap image might be smaller than the device's width.
- // If it's smaller, fill the extra space on the left with the dominant color.
- if (bitmapWidth >= maxWidth) {
- mBitmap = Bitmap.createBitmap(bitmap, bitmapWidth - maxWidth, 0, maxWidth, bitmapHeight);
- } else {
- Paint paint = new Paint();
- paint.setAntiAlias(true);
-
- // Create a bigger image that can fill the device width.
- // By creating a canvas for the bitmap, anything drawn on the canvas
- // will be drawn on the bitmap.
- mBitmap = Bitmap.createBitmap(maxWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(mBitmap);
-
- // Fill the canvas with dominant color.
- canvas.drawColor(mColor);
-
- // The image should be top-right aligned.
- Rect rect = new Rect();
- Gravity.apply(Gravity.TOP | Gravity.RIGHT,
- bitmapWidth,
- bitmapHeight,
- new Rect(0, 0, maxWidth, bitmapHeight),
- rect);
-
- // Draw the bitmap.
- canvas.drawBitmap(bitmap, null, rect, paint);
- }
-
- for (OnChangeListener listener : mListeners) {
- listener.onLightweightThemeChanged();
- }
- }
-
- /**
- * Reset the lightweight theme.
- * Note: This should be called on the UI thread to restrict accessing the
- * bitmap to a single thread.
- */
- private void resetLightweightTheme() {
- ThreadUtils.assertOnUiThread(AssertBehavior.NONE);
- if (mBitmap == null) {
- return;
- }
-
- // Reset the bitmap.
- mBitmap = null;
-
- for (OnChangeListener listener : mListeners) {
- listener.onLightweightThemeReset();
- }
- }
-
- /**
- * A lightweight theme is enabled only if there is an active bitmap.
- *
- * @return True if the theme is enabled.
- */
- public boolean isEnabled() {
- return (mBitmap != null);
- }
-
- /**
- * Based on the luminance of the domanint color, a theme is classified as light or dark.
- *
- * @return True if the theme is light.
- */
- public boolean isLightTheme() {
- return mIsLight;
- }
-
- /**
- * Crop the image based on the position of the view on the window.
- * Either the View or one of its ancestors might have scrolled or translated.
- * This value should be taken into account while mapping the View to the Bitmap.
- *
- * @param view The view requesting a cropped bitmap.
- */
- private Bitmap getCroppedBitmap(View view) {
- if (mBitmap == null || view == null) {
- return null;
- }
-
- // Get the global position of the view on the entire screen.
- Rect rect = new Rect();
- view.getGlobalVisibleRect(rect);
-
- // Get the activity's window position. This does an IPC call, may be expensive.
- Rect window = new Rect();
- view.getWindowVisibleDisplayFrame(window);
-
- // Calculate the coordinates for the cropped bitmap.
- int screenWidth = view.getContext().getResources().getDisplayMetrics().widthPixels;
- int left = mBitmap.getWidth() - screenWidth + rect.left;
- int right = mBitmap.getWidth() - screenWidth + rect.right;
- int top = rect.top - window.top;
- int bottom = rect.bottom - window.top;
-
- int offsetX = 0;
- int offsetY = 0;
-
- // Find if this view or any of its ancestors has been translated or scrolled.
- ViewParent parent;
- View curView = view;
- do {
- offsetX += (int) curView.getTranslationX() - curView.getScrollX();
- offsetY += (int) curView.getTranslationY() - curView.getScrollY();
-
- parent = curView.getParent();
-
- if (parent instanceof View) {
- curView = (View) parent;
- }
-
- } while (parent instanceof View);
-
- // Adjust the coordinates for the offset.
- left -= offsetX;
- right -= offsetX;
- top -= offsetY;
- bottom -= offsetY;
-
- // The either the required height may be less than the available image height or more than it.
- // If the height required is more, crop only the available portion on the image.
- int width = right - left;
- int height = (bottom > mBitmap.getHeight() ? mBitmap.getHeight() - top : bottom - top);
-
- // There is a chance that the view is not visible or doesn't fall within the phone's size.
- // In this case, 'rect' will have all values as '0'. Hence 'top' and 'bottom' may be negative,
- // and createBitmap() will fail.
- // The view will get a background in its next layout pass.
- try {
- return Bitmap.createBitmap(mBitmap, left, top, width, height);
- } catch (Exception e) {
- return null;
- }
- }
-
- /**
- * Converts the cropped bitmap to a BitmapDrawable and returns the same.
- *
- * @param view The view for which a background drawable is required.
- * @return Either the cropped bitmap as a Drawable or null.
- */
- public Drawable getDrawable(View view) {
- Bitmap bitmap = getCroppedBitmap(view);
- if (bitmap == null) {
- return null;
- }
-
- BitmapDrawable drawable = new BitmapDrawable(view.getContext().getResources(), bitmap);
- drawable.setGravity(Gravity.TOP | Gravity.RIGHT);
- drawable.setTileModeXY(Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
- return drawable;
- }
-
- /**
- * Converts the cropped bitmap to a LightweightThemeDrawable, placing it over the dominant color.
- *
- * @param view The view for which a background drawable is required.
- * @return Either the cropped bitmap as a Drawable or null.
- */
- public LightweightThemeDrawable getColorDrawable(View view) {
- return getColorDrawable(view, mColor, false);
- }
-
- /**
- * Converts the cropped bitmap to a LightweightThemeDrawable, placing it over the required color.
- *
- * @param view The view for which a background drawable is required.
- * @param color The color over which the drawable should be drawn.
- * @return Either the cropped bitmap as a Drawable or null.
- */
- public LightweightThemeDrawable getColorDrawable(View view, int color) {
- return getColorDrawable(view, color, false);
- }
-
- /**
- * Converts the cropped bitmap to a LightweightThemeDrawable, placing it over the required color.
- *
- * @param view The view for which a background drawable is required.
- * @param color The color over which the drawable should be drawn.
- * @param needsDominantColor A layer of dominant color is needed or not.
- * @return Either the cropped bitmap as a Drawable or null.
- */
- public LightweightThemeDrawable getColorDrawable(View view, int color, boolean needsDominantColor) {
- Bitmap bitmap = getCroppedBitmap(view);
- if (bitmap == null) {
- return null;
- }
-
- LightweightThemeDrawable drawable = new LightweightThemeDrawable(view.getContext().getResources(), bitmap);
- if (needsDominantColor) {
- drawable.setColorWithFilter(color, (mColor & 0x22FFFFFF));
- } else {
- drawable.setColor(color);
- }
-
- return drawable;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/lwt/LightweightThemeDrawable.java b/mobile/android/base/java/org/mozilla/gecko/lwt/LightweightThemeDrawable.java
deleted file mode 100644
index c0ae6eaed..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/lwt/LightweightThemeDrawable.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.lwt;
-
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BitmapShader;
-import android.graphics.Canvas;
-import android.graphics.ColorFilter;
-import android.graphics.ComposeShader;
-import android.graphics.LinearGradient;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
-import android.graphics.Rect;
-import android.graphics.Shader;
-import android.graphics.drawable.Drawable;
-
-/**
- * A special drawable used with lightweight themes. This draws a color
- * (with an optional color-filter) and a bitmap (with a linear gradient
- * to specify the alpha) in order.
- */
-public class LightweightThemeDrawable extends Drawable {
- private final Paint mPaint;
- private Paint mColorPaint;
-
- private final Bitmap mBitmap;
- private final Resources mResources;
-
- private int mStartColor;
- private int mEndColor;
-
- public LightweightThemeDrawable(Resources resources, Bitmap bitmap) {
- mBitmap = bitmap;
- mResources = resources;
-
- mPaint = new Paint();
- mPaint.setAntiAlias(true);
- mPaint.setStrokeWidth(0.0f);
- }
-
- @Override
- protected void onBoundsChange(Rect bounds) {
- super.onBoundsChange(bounds);
- initializeBitmapShader();
- }
-
- @Override
- public void draw(Canvas canvas) {
- // Draw the colors, if available.
- if (mColorPaint != null) {
- canvas.drawPaint(mColorPaint);
- }
-
- // Draw the bitmap.
- canvas.drawPaint(mPaint);
- }
-
- @Override
- public int getOpacity() {
- return PixelFormat.TRANSLUCENT;
- }
-
- @Override
- public void setAlpha(int alpha) {
- // A StateListDrawable will reset the alpha value with 255.
- // We cannot use to be the bitmap alpha.
- mPaint.setAlpha(alpha);
- }
-
- @Override
- public void setColorFilter(ColorFilter filter) {
- mPaint.setColorFilter(filter);
- }
-
- /**
- * Creates a paint that paint a particular color.
- *
- * Note that the given color should include an alpha value.
- *
- * @param color The color to be painted.
- */
- public void setColor(int color) {
- mColorPaint = new Paint(mPaint);
- mColorPaint.setColor(color);
- }
-
- /**
- * Creates a paint that paint a particular color, and a filter for the color.
- *
- * Note that the given color should include an alpha value.
- *
- * @param color The color to be painted.
- * @param filter The filter color to be applied using SRC_OVER mode.
- */
- public void setColorWithFilter(int color, int filter) {
- mColorPaint = new Paint(mPaint);
- mColorPaint.setColor(color);
- mColorPaint.setColorFilter(new PorterDuffColorFilter(filter, PorterDuff.Mode.SRC_OVER));
- }
-
- /**
- * Set the alpha for the linear gradient used with the bitmap's shader.
- *
- * @param startAlpha The starting alpha (0..255) value to be applied to the LinearGradient.
- * @param startAlpha The ending alpha (0..255) value to be applied to the LinearGradient.
- */
- public void setAlpha(int startAlpha, int endAlpha) {
- mStartColor = startAlpha << 24;
- mEndColor = endAlpha << 24;
- initializeBitmapShader();
- }
-
- private void initializeBitmapShader() {
- // A bitmap-shader to draw the bitmap.
- // Clamp mode will repeat the last row of pixels.
- // Hence its better to have an endAlpha of 0 for the linear-gradient.
- BitmapShader bitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
-
- // A linear-gradient to specify the opacity of the bitmap.
- LinearGradient gradient = new LinearGradient(0, 0, 0, mBitmap.getHeight(), mStartColor, mEndColor, Shader.TileMode.CLAMP);
-
- // Make a combined shader -- a performance win.
- // The linear-gradient is the 'SRC' and the bitmap-shader is the 'DST'.
- // Drawing the DST in the SRC will provide the opacity.
- mPaint.setShader(new ComposeShader(bitmapShader, gradient, PorterDuff.Mode.DST_IN));
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/mdns/MulticastDNSManager.java b/mobile/android/base/java/org/mozilla/gecko/mdns/MulticastDNSManager.java
deleted file mode 100644
index 6f23790b9..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/mdns/MulticastDNSManager.java
+++ /dev/null
@@ -1,535 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.mdns;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.GeckoRequest;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.net.nsd.NsdManager;
-import android.net.nsd.NsdServiceInfo;
-import android.support.annotation.UiThread;
-import android.util.Log;
-
-import java.net.InetAddress;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * This class is the bridge between XPCOM mDNS module and NsdManager.
- *
- * @See nsIDNSServiceDiscovery.idl
- */
-public abstract class MulticastDNSManager {
- protected static final String LOGTAG = "GeckoMDNSManager";
- private static MulticastDNSManager instance = null;
-
- public static MulticastDNSManager getInstance(final Context context) {
- if (instance == null) {
- instance = new DummyMulticastDNSManager();
- }
- return instance;
- }
-
- public abstract void init();
- public abstract void tearDown();
-}
-
-/**
- * Mix-in class for MulticastDNSManagers to call EventDispatcher.
- */
-class MulticastDNSEventManager {
- private NativeEventListener mListener = null;
- private boolean mEventsRegistered = false;
-
- MulticastDNSEventManager(NativeEventListener listener) {
- mListener = listener;
- }
-
- @UiThread
- public void init() {
- ThreadUtils.assertOnUiThread();
-
- if (mEventsRegistered || mListener == null) {
- return;
- }
-
- registerEvents();
- mEventsRegistered = true;
- }
-
- @UiThread
- public void tearDown() {
- ThreadUtils.assertOnUiThread();
-
- if (!mEventsRegistered || mListener == null) {
- return;
- }
-
- unregisterEvents();
- mEventsRegistered = false;
- }
-
- private void registerEvents() {
- EventDispatcher.getInstance().registerGeckoThreadListener(mListener,
- "NsdManager:DiscoverServices",
- "NsdManager:StopServiceDiscovery",
- "NsdManager:RegisterService",
- "NsdManager:UnregisterService",
- "NsdManager:ResolveService");
- }
-
- private void unregisterEvents() {
- EventDispatcher.getInstance().unregisterGeckoThreadListener(mListener,
- "NsdManager:DiscoverServices",
- "NsdManager:StopServiceDiscovery",
- "NsdManager:RegisterService",
- "NsdManager:UnregisterService",
- "NsdManager:ResolveService");
- }
-}
-
-class NsdMulticastDNSManager extends MulticastDNSManager implements NativeEventListener {
- private final NsdManager nsdManager;
- private final MulticastDNSEventManager mEventManager;
- private Map<String, DiscoveryListener> mDiscoveryListeners = null;
- private Map<String, RegistrationListener> mRegistrationListeners = null;
-
- @TargetApi(16)
- public NsdMulticastDNSManager(final Context context) {
- nsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE);
- mEventManager = new MulticastDNSEventManager(this);
- mDiscoveryListeners = new ConcurrentHashMap<String, DiscoveryListener>();
- mRegistrationListeners = new ConcurrentHashMap<String, RegistrationListener>();
- }
-
- @Override
- public void init() {
- mEventManager.init();
- }
-
- @Override
- public void tearDown() {
- mDiscoveryListeners.clear();
- mRegistrationListeners.clear();
-
- mEventManager.tearDown();
- }
-
- @Override
- public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) {
- Log.v(LOGTAG, "handleMessage: " + event);
-
- switch (event) {
- case "NsdManager:DiscoverServices": {
- DiscoveryListener listener = new DiscoveryListener(nsdManager);
- listener.discoverServices(message.getString("serviceType"), callback);
- mDiscoveryListeners.put(message.getString("uniqueId"), listener);
- break;
- }
- case "NsdManager:StopServiceDiscovery": {
- String uuid = message.getString("uniqueId");
- DiscoveryListener listener = mDiscoveryListeners.remove(uuid);
- if (listener == null) {
- Log.e(LOGTAG, "DiscoveryListener " + uuid + " was not found.");
- return;
- }
- listener.stopServiceDiscovery(callback);
- break;
- }
- case "NsdManager:RegisterService": {
- RegistrationListener listener = new RegistrationListener(nsdManager);
- listener.registerService(message.getInt("port"),
- message.optString("serviceName", android.os.Build.MODEL),
- message.getString("serviceType"),
- parseAttributes(message.optObjectArray("attributes", null)),
- callback);
- mRegistrationListeners.put(message.getString("uniqueId"), listener);
- break;
- }
- case "NsdManager:UnregisterService": {
- String uuid = message.getString("uniqueId");
- RegistrationListener listener = mRegistrationListeners.remove(uuid);
- if (listener == null) {
- Log.e(LOGTAG, "RegistrationListener " + uuid + " was not found.");
- return;
- }
- listener.unregisterService(callback);
- break;
- }
- case "NsdManager:ResolveService": {
- (new ResolveListener(nsdManager)).resolveService(message.getString("serviceName"),
- message.getString("serviceType"),
- callback);
- break;
- }
- }
- }
-
- private Map<String, String> parseAttributes(final NativeJSObject[] jsobjs) {
- if (jsobjs == null || jsobjs.length == 0 || !Versions.feature21Plus) {
- return null;
- }
-
- Map<String, String> attributes = new HashMap<String, String>(jsobjs.length);
- for (NativeJSObject obj : jsobjs) {
- attributes.put(obj.getString("name"), obj.getString("value"));
- }
-
- return attributes;
- }
-
- @TargetApi(16)
- public static JSONObject toJSON(final NsdServiceInfo serviceInfo) throws JSONException {
- JSONObject obj = new JSONObject();
-
- InetAddress host = serviceInfo.getHost();
- if (host != null) {
- obj.put("host", host.getCanonicalHostName());
- obj.put("address", host.getHostAddress());
- }
-
- int port = serviceInfo.getPort();
- if (port != 0) {
- obj.put("port", port);
- }
-
- String serviceName = serviceInfo.getServiceName();
- if (serviceName != null) {
- obj.put("serviceName", serviceName);
- }
-
- String serviceType = serviceInfo.getServiceType();
- if (serviceType != null) {
- obj.put("serviceType", serviceType);
- }
-
- return obj;
- }
-}
-
-class DummyMulticastDNSManager extends MulticastDNSManager implements NativeEventListener {
- static final int FAILURE_UNSUPPORTED = -65544;
- private final MulticastDNSEventManager mEventManager;
-
- public DummyMulticastDNSManager() {
- mEventManager = new MulticastDNSEventManager(this);
- }
-
- @Override
- public void init() {
- mEventManager.init();
- }
-
- @Override
- public void tearDown() {
- mEventManager.tearDown();
- }
-
- @Override
- public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) {
- Log.v(LOGTAG, "handleMessage: " + event);
- callback.sendError(FAILURE_UNSUPPORTED);
- }
-}
-
-@TargetApi(16)
-class DiscoveryListener implements NsdManager.DiscoveryListener {
- private static final String LOGTAG = "GeckoMDNSManager";
- private final NsdManager nsdManager;
-
- // Callbacks are called from different thread, and every callback can be called only once.
- private EventCallback mStartCallback = null;
- private EventCallback mStopCallback = null;
-
- DiscoveryListener(final NsdManager nsdManager) {
- this.nsdManager = nsdManager;
- }
-
- public void discoverServices(final String serviceType, final EventCallback callback) {
- synchronized (this) {
- mStartCallback = callback;
- }
- nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, this);
- }
-
- public void stopServiceDiscovery(final EventCallback callback) {
- synchronized (this) {
- mStopCallback = callback;
- }
- nsdManager.stopServiceDiscovery(this);
- }
-
- @Override
- public synchronized void onDiscoveryStarted(final String serviceType) {
- Log.d(LOGTAG, "onDiscoveryStarted: " + serviceType);
-
- EventCallback callback;
- synchronized (this) {
- callback = mStartCallback;
- }
-
- if (callback == null) {
- return;
- }
-
- callback.sendSuccess(serviceType);
- }
-
- @Override
- public synchronized void onStartDiscoveryFailed(final String serviceType, final int errorCode) {
- Log.e(LOGTAG, "onStartDiscoveryFailed: " + serviceType + "(" + errorCode + ")");
-
- EventCallback callback;
- synchronized (this) {
- callback = mStartCallback;
- }
-
- callback.sendError(errorCode);
- }
-
- @Override
- public synchronized void onDiscoveryStopped(final String serviceType) {
- Log.d(LOGTAG, "onDiscoveryStopped: " + serviceType);
-
- EventCallback callback;
- synchronized (this) {
- callback = mStopCallback;
- }
-
- if (callback == null) {
- return;
- }
-
- callback.sendSuccess(serviceType);
- }
-
- @Override
- public synchronized void onStopDiscoveryFailed(final String serviceType, final int errorCode) {
- Log.e(LOGTAG, "onStopDiscoveryFailed: " + serviceType + "(" + errorCode + ")");
-
- EventCallback callback;
- synchronized (this) {
- callback = mStopCallback;
- }
-
- if (callback == null) {
- return;
- }
-
- callback.sendError(errorCode);
- }
-
- @Override
- public void onServiceFound(final NsdServiceInfo serviceInfo) {
- Log.d(LOGTAG, "onServiceFound: " + serviceInfo.getServiceName());
- JSONObject json;
- try {
- json = NsdMulticastDNSManager.toJSON(serviceInfo);
- } catch (JSONException e) {
- throw new RuntimeException(e);
- }
- GeckoAppShell.sendRequestToGecko(new GeckoRequest("NsdManager:ServiceFound", json) {
- @Override
- public void onResponse(NativeJSObject nativeJSObject) {
- // don't care return value.
- }
- });
- }
-
- @Override
- public void onServiceLost(final NsdServiceInfo serviceInfo) {
- Log.d(LOGTAG, "onServiceLost: " + serviceInfo.getServiceName());
- JSONObject json;
- try {
- json = NsdMulticastDNSManager.toJSON(serviceInfo);
- } catch (JSONException e) {
- throw new RuntimeException(e);
- }
- GeckoAppShell.sendRequestToGecko(new GeckoRequest("NsdManager:ServiceLost", json) {
- @Override
- public void onResponse(NativeJSObject nativeJSObject) {
- // don't care return value.
- }
- });
- }
-}
-
-@TargetApi(16)
-class RegistrationListener implements NsdManager.RegistrationListener {
- private static final String LOGTAG = "GeckoMDNSManager";
- private final NsdManager nsdManager;
-
- // Callbacks are called from different thread, and every callback can be called only once.
- private EventCallback mStartCallback = null;
- private EventCallback mStopCallback = null;
-
- RegistrationListener(final NsdManager nsdManager) {
- this.nsdManager = nsdManager;
- }
-
- public void registerService(final int port, final String serviceName, final String serviceType, final Map<String, String> attributes, final EventCallback callback) {
- Log.d(LOGTAG, "registerService: " + serviceName + "." + serviceType + ":" + port);
-
- NsdServiceInfo serviceInfo = new NsdServiceInfo();
- serviceInfo.setPort(port);
- serviceInfo.setServiceName(serviceName);
- serviceInfo.setServiceType(serviceType);
- setAttributes(serviceInfo, attributes);
-
- synchronized (this) {
- mStartCallback = callback;
- }
- nsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, this);
- }
-
- @TargetApi(21)
- private void setAttributes(final NsdServiceInfo serviceInfo, final Map<String, String> attributes) {
- if (attributes == null || !Versions.feature21Plus) {
- return;
- }
-
- for (Map.Entry<String, String> entry : attributes.entrySet()) {
- serviceInfo.setAttribute(entry.getKey(), entry.getValue());
- }
- }
-
- public void unregisterService(final EventCallback callback) {
- Log.d(LOGTAG, "unregisterService");
- synchronized (this) {
- mStopCallback = callback;
- }
-
- nsdManager.unregisterService(this);
- }
-
- @Override
- public synchronized void onServiceRegistered(final NsdServiceInfo serviceInfo) {
- Log.d(LOGTAG, "onServiceRegistered: " + serviceInfo.getServiceName());
-
- EventCallback callback;
- synchronized (this) {
- callback = mStartCallback;
- }
-
- if (callback == null) {
- return;
- }
-
- try {
- callback.sendSuccess(NsdMulticastDNSManager.toJSON(serviceInfo));
- } catch (JSONException e) {
- throw new RuntimeException(e);
- }
- }
-
- @Override
- public synchronized void onRegistrationFailed(final NsdServiceInfo serviceInfo, final int errorCode) {
- Log.e(LOGTAG, "onRegistrationFailed: " + serviceInfo.getServiceName() + "(" + errorCode + ")");
-
- EventCallback callback;
- synchronized (this) {
- callback = mStartCallback;
- }
-
- callback.sendError(errorCode);
- }
-
- @Override
- public synchronized void onServiceUnregistered(final NsdServiceInfo serviceInfo) {
- Log.d(LOGTAG, "onServiceUnregistered: " + serviceInfo.getServiceName());
-
- EventCallback callback;
- synchronized (this) {
- callback = mStopCallback;
- }
-
- if (callback == null) {
- return;
- }
-
- try {
- callback.sendSuccess(NsdMulticastDNSManager.toJSON(serviceInfo));
- } catch (JSONException e) {
- throw new RuntimeException(e);
- }
- }
-
- @Override
- public synchronized void onUnregistrationFailed(final NsdServiceInfo serviceInfo, final int errorCode) {
- Log.e(LOGTAG, "onUnregistrationFailed: " + serviceInfo.getServiceName() + "(" + errorCode + ")");
-
- EventCallback callback;
- synchronized (this) {
- callback = mStopCallback;
- }
-
- if (callback == null) {
- return;
- }
-
- callback.sendError(errorCode);
- }
-}
-
-@TargetApi(16)
-class ResolveListener implements NsdManager.ResolveListener {
- private static final String LOGTAG = "GeckoMDNSManager";
- private final NsdManager nsdManager;
-
- // Callback is called from different thread, and the callback can be called only once.
- private EventCallback mCallback = null;
-
- public ResolveListener(final NsdManager nsdManager) {
- this.nsdManager = nsdManager;
- }
-
- public void resolveService(final String serviceName, final String serviceType, final EventCallback callback) {
- NsdServiceInfo serviceInfo = new NsdServiceInfo();
- serviceInfo.setServiceName(serviceName);
- serviceInfo.setServiceType(serviceType);
-
- mCallback = callback;
- nsdManager.resolveService(serviceInfo, this);
- }
-
-
- @Override
- public synchronized void onResolveFailed(final NsdServiceInfo serviceInfo, final int errorCode) {
- Log.e(LOGTAG, "onResolveFailed: " + serviceInfo.getServiceName() + "(" + errorCode + ")");
-
- if (mCallback == null) {
- return;
- }
- mCallback.sendError(errorCode);
- }
-
- @Override
- public synchronized void onServiceResolved(final NsdServiceInfo serviceInfo) {
- Log.d(LOGTAG, "onServiceResolved: " + serviceInfo.getServiceName());
-
- if (mCallback == null) {
- return;
- }
-
- try {
- mCallback.sendSuccess(NsdMulticastDNSManager.toJSON(serviceInfo));
- } catch (JSONException e) {
- throw new RuntimeException(e);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/media/AsyncCodec.java b/mobile/android/base/java/org/mozilla/gecko/media/AsyncCodec.java
deleted file mode 100644
index c9c620606..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/media/AsyncCodec.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.media;
-
-import android.media.MediaCodec.BufferInfo;
-import android.media.MediaFormat;
-import android.os.Handler;
-import android.view.Surface;
-
-import java.nio.ByteBuffer;
-
-// A wrapper interface that mimics the new {@link android.media.MediaCodec}
-// asynchronous mode API in Lollipop.
-public interface AsyncCodec {
- public interface Callbacks {
- void onInputBufferAvailable(AsyncCodec codec, int index);
- void onOutputBufferAvailable(AsyncCodec codec, int index, BufferInfo info);
- void onError(AsyncCodec codec, int error);
- void onOutputFormatChanged(AsyncCodec codec, MediaFormat format);
- }
-
- public abstract void setCallbacks(Callbacks callbacks, Handler handler);
- public abstract void configure(MediaFormat format, Surface surface, int flags);
- public abstract void start();
- public abstract void stop();
- public abstract void flush();
- public abstract void release();
- public abstract ByteBuffer getInputBuffer(int index);
- public abstract ByteBuffer getOutputBuffer(int index);
- public abstract void queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags);
- public abstract void releaseOutputBuffer(int index, boolean render);
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/media/AsyncCodecFactory.java b/mobile/android/base/java/org/mozilla/gecko/media/AsyncCodecFactory.java
deleted file mode 100644
index fd670e21b..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/media/AsyncCodecFactory.java
+++ /dev/null
@@ -1,14 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.media;
-
-import java.io.IOException;
-
-public final class AsyncCodecFactory {
- public static AsyncCodec create(String name) throws IOException {
- // TODO: create (to be implemented) LollipopAsyncCodec when running on Lollipop or later devices.
- return new JellyBeanAsyncCodec(name);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/media/AudioFocusAgent.java b/mobile/android/base/java/org/mozilla/gecko/media/AudioFocusAgent.java
deleted file mode 100644
index 93a63bcb5..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/media/AudioFocusAgent.java
+++ /dev/null
@@ -1,135 +0,0 @@
-package org.mozilla.gecko.media;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoAppShell;
-
-import android.content.Context;
-import android.content.Intent;
-import android.media.AudioManager;
-import android.media.AudioManager.OnAudioFocusChangeListener;
-import android.util.Log;
-
-public class AudioFocusAgent {
- private static final String LOGTAG = "AudioFocusAgent";
-
- private static Context mContext;
- private AudioManager mAudioManager;
- private OnAudioFocusChangeListener mAfChangeListener;
-
- public static final String OWN_FOCUS = "own_focus";
- public static final String LOST_FOCUS = "lost_focus";
- public static final String LOST_FOCUS_TRANSIENT = "lost_focus_transient";
-
- private String mAudioFocusState = LOST_FOCUS;
-
- @WrapForJNI(calledFrom = "gecko")
- public static void notifyStartedPlaying() {
- if (!isAttachedToContext()) {
- return;
- }
- Log.d(LOGTAG, "NotifyStartedPlaying");
- AudioFocusAgent.getInstance().requestAudioFocusIfNeeded();
- }
-
- @WrapForJNI(calledFrom = "gecko")
- public static void notifyStoppedPlaying() {
- if (!isAttachedToContext()) {
- return;
- }
- Log.d(LOGTAG, "NotifyStoppedPlaying");
- AudioFocusAgent.getInstance().abandonAudioFocusIfNeeded();
- }
-
- public synchronized void attachToContext(Context context) {
- if (isAttachedToContext()) {
- return;
- }
-
- mContext = context;
- mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
-
- mAfChangeListener = new OnAudioFocusChangeListener() {
- public void onAudioFocusChange(int focusChange) {
- switch (focusChange) {
- case AudioManager.AUDIOFOCUS_LOSS:
- Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_LOSS");
- notifyObservers("AudioFocusChanged", "lostAudioFocus");
- notifyMediaControlService(MediaControlService.ACTION_PAUSE_BY_AUDIO_FOCUS);
- mAudioFocusState = LOST_FOCUS;
- break;
- case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
- Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_LOSS_TRANSIENT");
- notifyObservers("AudioFocusChanged", "lostAudioFocusTransiently");
- notifyMediaControlService(MediaControlService.ACTION_PAUSE_BY_AUDIO_FOCUS);
- mAudioFocusState = LOST_FOCUS_TRANSIENT;
- break;
- case AudioManager.AUDIOFOCUS_GAIN:
- if (!mAudioFocusState.equals(LOST_FOCUS_TRANSIENT)) {
- return;
- }
- Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_GAIN");
- notifyObservers("AudioFocusChanged", "gainAudioFocus");
- notifyMediaControlService(MediaControlService.ACTION_RESUME_BY_AUDIO_FOCUS);
- mAudioFocusState = OWN_FOCUS;
- break;
- default:
- }
- }
- };
- notifyMediaControlService(MediaControlService.ACTION_INIT);
- }
-
- @RobocopTarget
- public static AudioFocusAgent getInstance() {
- return AudioFocusAgent.SingletonHolder.INSTANCE;
- }
-
- private static class SingletonHolder {
- private static final AudioFocusAgent INSTANCE = new AudioFocusAgent();
- }
-
- private static boolean isAttachedToContext() {
- return (mContext != null);
- }
-
- private void notifyObservers(String topic, String data) {
- GeckoAppShell.notifyObservers(topic, data);
- }
-
- private AudioFocusAgent() {}
-
- private void requestAudioFocusIfNeeded() {
- if (mAudioFocusState.equals(OWN_FOCUS)) {
- return;
- }
-
- int result = mAudioManager.requestAudioFocus(mAfChangeListener,
- AudioManager.STREAM_MUSIC,
- AudioManager.AUDIOFOCUS_GAIN);
-
- String focusMsg = (result == AudioManager.AUDIOFOCUS_GAIN) ?
- "AudioFocus request granted" : "AudioFoucs request failed";
- Log.d(LOGTAG, focusMsg);
- if (result == AudioManager.AUDIOFOCUS_GAIN) {
- mAudioFocusState = OWN_FOCUS;
- }
- }
-
- private void abandonAudioFocusIfNeeded() {
- if (!mAudioFocusState.equals(OWN_FOCUS)) {
- return;
- }
-
- Log.d(LOGTAG, "Abandon AudioFocus");
- mAudioManager.abandonAudioFocus(mAfChangeListener);
- mAudioFocusState = LOST_FOCUS;
- }
-
- private void notifyMediaControlService(String action) {
- Intent intent = new Intent(mContext, MediaControlService.class);
- intent.setAction(action);
- mContext.startService(intent);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/media/Codec.java b/mobile/android/base/java/org/mozilla/gecko/media/Codec.java
deleted file mode 100644
index b0a26dfb3..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/media/Codec.java
+++ /dev/null
@@ -1,366 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.media;
-
-import android.media.MediaCodec;
-import android.media.MediaCodecInfo;
-import android.media.MediaCodecList;
-import android.media.MediaFormat;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.os.TransactionTooLargeException;
-import android.util.Log;
-import android.view.Surface;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.util.LinkedList;
-import java.util.NoSuchElementException;
-import java.util.Queue;
-import java.util.concurrent.ConcurrentLinkedQueue;
-
-/* package */ final class Codec extends ICodec.Stub implements IBinder.DeathRecipient {
- private static final String LOGTAG = "GeckoRemoteCodec";
- private static final boolean DEBUG = false;
-
- public enum Error {
- DECODE, FATAL
- };
-
- private final class Callbacks implements AsyncCodec.Callbacks {
- private ICodecCallbacks mRemote;
- private boolean mHasInputCapacitySet;
- private boolean mHasOutputCapacitySet;
-
- public Callbacks(ICodecCallbacks remote) {
- mRemote = remote;
- }
-
- @Override
- public void onInputBufferAvailable(AsyncCodec codec, int index) {
- if (mFlushing) {
- // Flush invalidates all buffers.
- return;
- }
- if (!mHasInputCapacitySet) {
- int capacity = codec.getInputBuffer(index).capacity();
- if (capacity > 0) {
- mSamplePool.setInputBufferSize(capacity);
- mHasInputCapacitySet = true;
- }
- }
- if (!mInputProcessor.onBuffer(index)) {
- reportError(Error.FATAL, new Exception("FAIL: input buffer queue is full"));
- }
- }
-
- @Override
- public void onOutputBufferAvailable(AsyncCodec codec, int index, MediaCodec.BufferInfo info) {
- if (mFlushing) {
- // Flush invalidates all buffers.
- return;
- }
- ByteBuffer output = codec.getOutputBuffer(index);
- if (!mHasOutputCapacitySet) {
- int capacity = output.capacity();
- if (capacity > 0) {
- mSamplePool.setOutputBufferSize(capacity);
- mHasOutputCapacitySet = true;
- }
- }
- Sample copy = mSamplePool.obtainOutput(info);
- try {
- if (info.size > 0) {
- copy.buffer.readFromByteBuffer(output, info.offset, info.size);
- }
- mSentOutputs.add(copy);
- mRemote.onOutput(copy);
- } catch (IOException e) {
- Log.e(LOGTAG, "Fail to read output buffer:" + e.getMessage());
- outputDummy(info);
- } catch (TransactionTooLargeException ttle) {
- Log.e(LOGTAG, "Output is too large:" + ttle.getMessage());
- outputDummy(info);
- } catch (RemoteException e) {
- // Dead recipient.
- e.printStackTrace();
- }
-
- mCodec.releaseOutputBuffer(index, true);
- boolean eos = (info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
- if (DEBUG && eos) {
- Log.d(LOGTAG, "output EOS");
- }
- }
-
- private void outputDummy(MediaCodec.BufferInfo info) {
- try {
- if (DEBUG) Log.d(LOGTAG, "return dummy sample");
- mRemote.onOutput(Sample.create(null, info, null));
- } catch (RemoteException e) {
- // Dead recipient.
- e.printStackTrace();
- }
- }
-
- @Override
- public void onError(AsyncCodec codec, int error) {
- reportError(Error.FATAL, new Exception("codec error:" + error));
- }
-
- @Override
- public void onOutputFormatChanged(AsyncCodec codec, MediaFormat format) {
- try {
- mRemote.onOutputFormatChanged(new FormatParam(format));
- } catch (RemoteException re) {
- // Dead recipient.
- re.printStackTrace();
- }
- }
- }
-
- private final class InputProcessor {
- private Queue<Sample> mInputSamples = new LinkedList<>();
- private Queue<Integer> mAvailableInputBuffers = new LinkedList<>();
- private Queue<Sample> mDequeuedSamples = new LinkedList<>();
-
- private synchronized Sample onAllocate(int size) {
- Sample sample = mSamplePool.obtainInput(size);
- mDequeuedSamples.add(sample);
- return sample;
- }
-
- private synchronized boolean onSample(Sample sample) {
- if (sample == null) {
- return false;
- }
-
- if (!sample.isEOS()) {
- Sample temp = sample;
- sample = mDequeuedSamples.remove();
- sample.info = temp.info;
- sample.cryptoInfo = temp.cryptoInfo;
- temp.dispose();
- }
-
- if (!mInputSamples.offer(sample)) {
- return false;
- }
- feedSampleToBuffer();
- return true;
- }
-
- private synchronized boolean onBuffer(int index) {
- if (!mAvailableInputBuffers.offer(index)) {
- return false;
- }
- feedSampleToBuffer();
- return true;
- }
-
- private void feedSampleToBuffer() {
- while (!mAvailableInputBuffers.isEmpty() && !mInputSamples.isEmpty()) {
- int index = mAvailableInputBuffers.poll();
- int len = 0;
- Sample sample = mInputSamples.poll();
- long pts = sample.info.presentationTimeUs;
- int flags = sample.info.flags;
- if (!sample.isEOS() && sample.buffer != null) {
- len = sample.info.size;
- ByteBuffer buf = mCodec.getInputBuffer(index);
- try {
- sample.writeToByteBuffer(buf);
- mCallbacks.onInputExhausted();
- } catch (IOException e) {
- e.printStackTrace();
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- mSamplePool.recycleInput(sample);
- }
- mCodec.queueInputBuffer(index, 0, len, pts, flags);
- }
- }
-
- private synchronized void reset() {
- mInputSamples.clear();
- mAvailableInputBuffers.clear();
- }
- }
-
- private volatile ICodecCallbacks mCallbacks;
- private AsyncCodec mCodec;
- private InputProcessor mInputProcessor;
- private volatile boolean mFlushing = false;
- private SamplePool mSamplePool;
- private Queue<Sample> mSentOutputs = new ConcurrentLinkedQueue<>();
-
- public synchronized void setCallbacks(ICodecCallbacks callbacks) throws RemoteException {
- mCallbacks = callbacks;
- callbacks.asBinder().linkToDeath(this, 0);
- }
-
- // IBinder.DeathRecipient
- @Override
- public synchronized void binderDied() {
- Log.e(LOGTAG, "Callbacks is dead");
- try {
- release();
- } catch (RemoteException e) {
- // Nowhere to report the error.
- }
- }
-
- @Override
- public synchronized boolean configure(FormatParam format, Surface surface, int flags) throws RemoteException {
- if (mCallbacks == null) {
- Log.e(LOGTAG, "FAIL: callbacks must be set before calling configure()");
- return false;
- }
-
- if (mCodec != null) {
- if (DEBUG) Log.d(LOGTAG, "release existing codec: " + mCodec);
- releaseCodec();
- }
-
- if (DEBUG) Log.d(LOGTAG, "configure " + this);
-
- MediaFormat fmt = format.asFormat();
- String codecName = getDecoderForFormat(fmt);
- if (codecName == null) {
- Log.e(LOGTAG, "FAIL: cannot find codec");
- return false;
- }
-
- try {
- AsyncCodec codec = AsyncCodecFactory.create(codecName);
- codec.setCallbacks(new Callbacks(mCallbacks), null);
- codec.configure(fmt, surface, flags);
- mCodec = codec;
- mInputProcessor = new InputProcessor();
- mSamplePool = new SamplePool(codecName);
- if (DEBUG) Log.d(LOGTAG, codec.toString() + " created");
- return true;
- } catch (Exception e) {
- if (DEBUG) Log.d(LOGTAG, "FAIL: cannot create codec -- " + codecName);
- e.printStackTrace();
- return false;
- }
- }
-
- private void releaseCodec() {
- mInputProcessor.reset();
- try {
- mCodec.release();
- } catch (Exception e) {
- reportError(Error.FATAL, e);
- }
- mCodec = null;
- }
-
- private String getDecoderForFormat(MediaFormat format) {
- String mime = format.getString(MediaFormat.KEY_MIME);
- if (mime == null) {
- return null;
- }
- int numCodecs = MediaCodecList.getCodecCount();
- for (int i = 0; i < numCodecs; i++) {
- MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
- if (info.isEncoder()) {
- continue;
- }
- String[] types = info.getSupportedTypes();
- for (String t : types) {
- if (t.equalsIgnoreCase(mime)) {
- return info.getName();
- }
- }
- }
- return null;
- // TODO: API 21+ is simpler.
- //static MediaCodecList sCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
- //return sCodecList.findDecoderForFormat(format);
- }
-
- @Override
- public synchronized void start() throws RemoteException {
- if (DEBUG) Log.d(LOGTAG, "start " + this);
- mFlushing = false;
- try {
- mCodec.start();
- } catch (Exception e) {
- reportError(Error.FATAL, e);
- }
- }
-
- private void reportError(Error error, Exception e) {
- if (e != null) {
- e.printStackTrace();
- }
- try {
- mCallbacks.onError(error == Error.FATAL);
- } catch (RemoteException re) {
- re.printStackTrace();
- }
- }
-
- @Override
- public synchronized void stop() throws RemoteException {
- if (DEBUG) Log.d(LOGTAG, "stop " + this);
- try {
- mCodec.stop();
- } catch (Exception e) {
- reportError(Error.FATAL, e);
- }
- }
-
- @Override
- public synchronized void flush() throws RemoteException {
- mFlushing = true;
- if (DEBUG) Log.d(LOGTAG, "flush " + this);
- mInputProcessor.reset();
- try {
- mCodec.flush();
- } catch (Exception e) {
- reportError(Error.FATAL, e);
- }
-
- mFlushing = false;
- if (DEBUG) Log.d(LOGTAG, "flushed " + this);
- }
-
- @Override
- public synchronized Sample dequeueInput(int size) {
- return mInputProcessor.onAllocate(size);
- }
-
- @Override
- public synchronized void queueInput(Sample sample) throws RemoteException {
- if (!mInputProcessor.onSample(sample)) {
- reportError(Error.FATAL, new Exception("FAIL: input sample queue is full"));
- }
- }
-
- @Override
- public synchronized void releaseOutput(Sample sample) {
- try {
- mSamplePool.recycleOutput(mSentOutputs.remove());
- } catch (Exception e) {
- Log.e(LOGTAG, "failed to release output:" + sample);
- e.printStackTrace();
- }
- sample.dispose();
- }
-
- @Override
- public synchronized void release() throws RemoteException {
- if (DEBUG) Log.d(LOGTAG, "release " + this);
- releaseCodec();
- mSamplePool.reset();
- mSamplePool = null;
- mCallbacks.asBinder().unlinkToDeath(this, 0);
- mCallbacks = null;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/media/CodecProxy.java b/mobile/android/base/java/org/mozilla/gecko/media/CodecProxy.java
deleted file mode 100644
index 3025c14d0..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/media/CodecProxy.java
+++ /dev/null
@@ -1,191 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.media;
-
-import android.media.MediaCodec;
-import android.media.MediaCodec.BufferInfo;
-import android.media.MediaCodec.CryptoInfo;
-import android.media.MediaFormat;
-import android.os.DeadObjectException;
-import android.os.RemoteException;
-import android.util.Log;
-import android.view.Surface;
-
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.mozglue.JNIObject;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-
-// Proxy class of ICodec binder.
-public final class CodecProxy {
- private static final String LOGTAG = "GeckoRemoteCodecProxy";
- private static final boolean DEBUG = false;
-
- private ICodec mRemote;
- private FormatParam mFormat;
- private Surface mOutputSurface;
- private CallbacksForwarder mCallbacks;
-
- public interface Callbacks {
- void onInputExhausted();
- void onOutputFormatChanged(MediaFormat format);
- void onOutput(Sample output);
- void onError(boolean fatal);
- }
-
- @WrapForJNI
- public static class NativeCallbacks extends JNIObject implements Callbacks {
- public native void onInputExhausted();
- public native void onOutputFormatChanged(MediaFormat format);
- public native void onOutput(Sample output);
- public native void onError(boolean fatal);
-
- @Override // JNIObject
- protected native void disposeNative();
- }
-
- private class CallbacksForwarder extends ICodecCallbacks.Stub {
- private final Callbacks mCallbacks;
-
- CallbacksForwarder(Callbacks callbacks) {
- mCallbacks = callbacks;
- }
-
- @Override
- public void onInputExhausted() throws RemoteException {
- mCallbacks.onInputExhausted();
- }
-
- @Override
- public void onOutputFormatChanged(FormatParam format) throws RemoteException {
- mCallbacks.onOutputFormatChanged(format.asFormat());
- }
-
- @Override
- public void onOutput(Sample sample) throws RemoteException {
- mCallbacks.onOutput(sample);
- mRemote.releaseOutput(sample);
- sample.dispose();
- }
-
- @Override
- public void onError(boolean fatal) throws RemoteException {
- reportError(fatal);
- }
-
- public void reportError(boolean fatal) {
- mCallbacks.onError(fatal);
- }
- }
-
- @WrapForJNI
- public static CodecProxy create(MediaFormat format, Surface surface, Callbacks callbacks) {
- return RemoteManager.getInstance().createCodec(format, surface, callbacks);
- }
-
- public static CodecProxy createCodecProxy(MediaFormat format, Surface surface, Callbacks callbacks) {
- return new CodecProxy(format, surface, callbacks);
- }
-
- private CodecProxy(MediaFormat format, Surface surface, Callbacks callbacks) {
- mFormat = new FormatParam(format);
- mOutputSurface = surface;
- mCallbacks = new CallbacksForwarder(callbacks);
- }
-
- boolean init(ICodec remote) {
- try {
- remote.setCallbacks(mCallbacks);
- remote.configure(mFormat, mOutputSurface, 0);
- remote.start();
- } catch (RemoteException e) {
- e.printStackTrace();
- return false;
- }
-
- mRemote = remote;
- return true;
- }
-
- boolean deinit() {
- try {
- mRemote.stop();
- mRemote.release();
- mRemote = null;
- return true;
- } catch (RemoteException e) {
- e.printStackTrace();
- return false;
- }
- }
-
- @WrapForJNI
- public synchronized boolean input(ByteBuffer bytes, BufferInfo info, CryptoInfo cryptoInfo) {
- if (mRemote == null) {
- Log.e(LOGTAG, "cannot send input to an ended codec");
- return false;
- }
-
- try {
- Sample sample = (info.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) ?
- Sample.EOS : mRemote.dequeueInput(info.size).set(bytes, info, cryptoInfo);
- mRemote.queueInput(sample);
- sample.dispose();
- } catch (IOException e) {
- e.printStackTrace();
- return false;
- } catch (DeadObjectException e) {
- return false;
- } catch (RemoteException e) {
- e.printStackTrace();
- Log.e(LOGTAG, "fail to input sample: size=" + info.size +
- ", pts=" + info.presentationTimeUs +
- ", flags=" + Integer.toHexString(info.flags));
- return false;
- }
- return true;
- }
-
- @WrapForJNI
- public synchronized boolean flush() {
- if (mRemote == null) {
- Log.e(LOGTAG, "cannot flush an ended codec");
- return false;
- }
- try {
- if (DEBUG) Log.d(LOGTAG, "flush " + this);
- mRemote.flush();
- } catch (DeadObjectException e) {
- return false;
- } catch (RemoteException e) {
- e.printStackTrace();
- return false;
- }
- return true;
- }
-
- @WrapForJNI
- public synchronized boolean release() {
- if (mRemote == null) {
- Log.w(LOGTAG, "codec already ended");
- return true;
- }
- if (DEBUG) Log.d(LOGTAG, "release " + this);
- try {
- RemoteManager.getInstance().releaseCodec(this);
- } catch (DeadObjectException e) {
- return false;
- } catch (RemoteException e) {
- e.printStackTrace();
- return false;
- }
- return true;
- }
-
- public synchronized void reportError(boolean fatal) {
- mCallbacks.reportError(fatal);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/media/FormatParam.java b/mobile/android/base/java/org/mozilla/gecko/media/FormatParam.java
deleted file mode 100644
index c6762672d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/media/FormatParam.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.media;
-
-import android.media.MediaFormat;
-import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import java.nio.ByteBuffer;
-
-/** A wrapper to make {@link MediaFormat} parcelable.
- * Supports following keys:
- * <ul>
- * <li>{@link MediaFormat#KEY_MIME}</li>
- * <li>{@link MediaFormat#KEY_WIDTH}</li>
- * <li>{@link MediaFormat#KEY_HEIGHT}</li>
- * <li>{@link MediaFormat#KEY_CHANNEL_COUNT}</li>
- * <li>{@link MediaFormat#KEY_SAMPLE_RATE}</li>
- * <li>"csd-0"</li>
- * <li>"csd-1"</li>
- * </ul>
- */
-public final class FormatParam implements Parcelable {
- // Keys for codec specific config bits not exposed in {@link MediaFormat}.
- private static final String KEY_CONFIG_0 = "csd-0";
- private static final String KEY_CONFIG_1 = "csd-1";
-
- private MediaFormat mFormat;
-
- public MediaFormat asFormat() {
- return mFormat;
- }
-
- public FormatParam(MediaFormat format) {
- mFormat = format;
- }
-
- protected FormatParam(Parcel in) {
- mFormat = new MediaFormat();
- readFromParcel(in);
- }
-
- public static final Creator<FormatParam> CREATOR = new Creator<FormatParam>() {
- @Override
- public FormatParam createFromParcel(Parcel in) {
- return new FormatParam(in);
- }
-
- @Override
- public FormatParam[] newArray(int size) {
- return new FormatParam[size];
- }
- };
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- public void readFromParcel(Parcel in) {
- Bundle bundle = in.readBundle();
- fromBundle(bundle);
- }
-
- private void fromBundle(Bundle bundle) {
- if (bundle.containsKey(MediaFormat.KEY_MIME)) {
- mFormat.setString(MediaFormat.KEY_MIME,
- bundle.getString(MediaFormat.KEY_MIME));
- }
- if (bundle.containsKey(MediaFormat.KEY_WIDTH)) {
- mFormat.setInteger(MediaFormat.KEY_WIDTH,
- bundle.getInt(MediaFormat.KEY_WIDTH));
- }
- if (bundle.containsKey(MediaFormat.KEY_HEIGHT)) {
- mFormat.setInteger(MediaFormat.KEY_HEIGHT,
- bundle.getInt(MediaFormat.KEY_HEIGHT));
- }
- if (bundle.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) {
- mFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT,
- bundle.getInt(MediaFormat.KEY_CHANNEL_COUNT));
- }
- if (bundle.containsKey(MediaFormat.KEY_SAMPLE_RATE)) {
- mFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE,
- bundle.getInt(MediaFormat.KEY_SAMPLE_RATE));
- }
- if (bundle.containsKey(KEY_CONFIG_0)) {
- mFormat.setByteBuffer(KEY_CONFIG_0,
- ByteBuffer.wrap(bundle.getByteArray(KEY_CONFIG_0)));
- }
- if (bundle.containsKey(KEY_CONFIG_1)) {
- mFormat.setByteBuffer(KEY_CONFIG_1,
- ByteBuffer.wrap(bundle.getByteArray((KEY_CONFIG_1))));
- }
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeBundle(toBundle());
- }
-
- private Bundle toBundle() {
- Bundle bundle = new Bundle();
- if (mFormat.containsKey(MediaFormat.KEY_MIME)) {
- bundle.putString(MediaFormat.KEY_MIME, mFormat.getString(MediaFormat.KEY_MIME));
- }
- if (mFormat.containsKey(MediaFormat.KEY_WIDTH)) {
- bundle.putInt(MediaFormat.KEY_WIDTH, mFormat.getInteger(MediaFormat.KEY_WIDTH));
- }
- if (mFormat.containsKey(MediaFormat.KEY_HEIGHT)) {
- bundle.putInt(MediaFormat.KEY_HEIGHT, mFormat.getInteger(MediaFormat.KEY_HEIGHT));
- }
- if (mFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) {
- bundle.putInt(MediaFormat.KEY_CHANNEL_COUNT, mFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT));
- }
- if (mFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE)) {
- bundle.putInt(MediaFormat.KEY_SAMPLE_RATE, mFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE));
- }
- if (mFormat.containsKey(KEY_CONFIG_0)) {
- ByteBuffer bytes = mFormat.getByteBuffer(KEY_CONFIG_0);
- bundle.putByteArray(KEY_CONFIG_0,
- Sample.byteArrayFromBuffer(bytes, 0, bytes.capacity()));
- }
- if (mFormat.containsKey(KEY_CONFIG_1)) {
- ByteBuffer bytes = mFormat.getByteBuffer(KEY_CONFIG_1);
- bundle.putByteArray(KEY_CONFIG_1,
- Sample.byteArrayFromBuffer(bytes, 0, bytes.capacity()));
- }
- return bundle;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrm.java b/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrm.java
deleted file mode 100644
index 7b3bda3fd..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrm.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.media;
-
-import android.media.MediaCrypto;
-
-public interface GeckoMediaDrm {
- public interface Callbacks {
- void onSessionCreated(int createSessionToken,
- int promiseId,
- byte[] sessionId,
- byte[] request);
- void onSessionUpdated(int promiseId, byte[] sessionId);
- void onSessionClosed(int promiseId, byte[] sessionId);
- void onSessionMessage(byte[] sessionId,
- int sessionMessageType,
- byte[] request);
- void onSessionError(byte[] sessionId, String message);
- void onSessionBatchedKeyChanged(byte[] sessionId,
- SessionKeyInfo[] keyInfos);
- // All failure cases should go through this function.
- void onRejectPromise(int promiseId, String message);
- }
- void setCallbacks(Callbacks callbacks);
- void createSession(int createSessionToken,
- int promiseId,
- String initDataType,
- byte[] initData);
- void updateSession(int promiseId, String sessionId, byte[] response);
- void closeSession(int promiseId, String sessionId);
- void release();
- MediaCrypto getMediaCrypto();
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV21.java b/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV21.java
deleted file mode 100644
index 6ccaf80df..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV21.java
+++ /dev/null
@@ -1,627 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.media;
-
-import java.lang.*;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.net.URLEncoder;
-import java.nio.ByteBuffer;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.UUID;
-import java.util.ArrayDeque;
-
-import android.annotation.SuppressLint;
-import android.os.AsyncTask;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.media.MediaCrypto;
-import android.media.MediaCryptoException;
-import android.media.MediaDrm;
-import android.media.MediaDrmException;
-import android.util.Log;
-
-public class GeckoMediaDrmBridgeV21 implements GeckoMediaDrm {
- private static final String LOGTAG = "GeckoMediaDrmBridgeV21";
- private static final String INVALID_SESSION_ID = "Invalid";
- private static final String WIDEVINE_KEY_SYSTEM = "com.widevine.alpha";
- private static final boolean DEBUG = false;
- private static final UUID WIDEVINE_SCHEME_UUID =
- new UUID(0xedef8ba979d64aceL, 0xa3c827dcd51d21edL);
- // MediaDrm.KeyStatus information listener is supported on M+, adding a
- // dummy key id to report key status.
- private static final byte[] DUMMY_KEY_ID = new byte[] {0};
-
- private UUID mSchemeUUID;
- private Handler mHandler;
- private HandlerThread mHandlerThread;
- private ByteBuffer mCryptoSessionId;
-
- // mProvisioningPromiseId is great than 0 only during provisioning.
- private int mProvisioningPromiseId;
- private HashSet<ByteBuffer> mSessionIds;
- private HashMap<ByteBuffer, String> mSessionMIMETypes;
- private ArrayDeque<PendingCreateSessionData> mPendingCreateSessionDataQueue;
- private GeckoMediaDrm.Callbacks mCallbacks;
-
- private MediaCrypto mCrypto;
- protected MediaDrm mDrm;
-
- public static int LICENSE_REQUEST_INITIAL = 0; /*MediaKeyMessageType::License_request*/
- public static int LICENSE_REQUEST_RENEWAL = 1; /*MediaKeyMessageType::License_renewal*/
- public static int LICENSE_REQUEST_RELEASE = 2; /*MediaKeyMessageType::License_release*/
-
- // Store session data while provisioning
- private static class PendingCreateSessionData {
- public final int mToken;
- public final int mPromiseId;
- public final byte[] mInitData;
- public final String mMimeType;
-
- private PendingCreateSessionData(int token, int promiseId,
- byte[] initData, String mimeType) {
- mToken = token;
- mPromiseId = promiseId;
- mInitData = initData;
- mMimeType = mimeType;
- }
- }
-
- public boolean isSecureDecoderComonentRequired(String mimeType) {
- if (mCrypto != null) {
- return mCrypto.requiresSecureDecoderComponent(mimeType);
- }
- return false;
- }
-
- private static void assertTrue(boolean condition) {
- if (DEBUG && !condition) {
- throw new AssertionError("Expected condition to be true");
- }
- }
-
- @SuppressLint("WrongConstant")
- private void configureVendorSpecificProperty() {
- assertTrue(mDrm != null);
- // Support L3 for now
- mDrm.setPropertyString("securityLevel", "L3");
- // Refer to chromium, set multi-session mode for Widevine.
- if (mSchemeUUID.equals(WIDEVINE_SCHEME_UUID)) {
- mDrm.setPropertyString("sessionSharing", "enable");
- }
- }
-
- GeckoMediaDrmBridgeV21(String keySystem) throws Exception {
- if (DEBUG) Log.d(LOGTAG, "GeckoMediaDrmBridgeV21()");
-
- mProvisioningPromiseId = 0;
- mSessionIds = new HashSet<ByteBuffer>();
- mSessionMIMETypes = new HashMap<ByteBuffer, String>();
- mPendingCreateSessionDataQueue = new ArrayDeque<PendingCreateSessionData>();
-
- mSchemeUUID = convertKeySystemToSchemeUUID(keySystem);
- mCryptoSessionId = null;
-
- if (DEBUG) Log.d(LOGTAG, "mSchemeUUID : " + mSchemeUUID.toString());
-
- // The caller of GeckoMediaDrmBridgeV21 ctor should handle exceptions
- // threw by the following steps.
- mDrm = new MediaDrm(mSchemeUUID);
- configureVendorSpecificProperty();
- mDrm.setOnEventListener(new MediaDrmListener());
- }
-
- @Override
- public void setCallbacks(GeckoMediaDrm.Callbacks callbacks) {
- assertTrue(callbacks != null);
- mCallbacks = callbacks;
- }
-
- @Override
- public void createSession(int createSessionToken,
- int promiseId,
- String initDataType,
- byte[] initData) {
- if (DEBUG) Log.d(LOGTAG, "createSession()");
- if (mDrm == null) {
- onRejectPromise(promiseId, "MediaDrm instance doesn't exist !!");
- return;
- }
-
- if (mProvisioningPromiseId > 0 && mCrypto == null) {
- if (DEBUG) Log.d(LOGTAG, "Pending createSession because it's provisioning !");
- savePendingCreateSessionData(createSessionToken, promiseId,
- initData, initDataType);
- return;
- }
-
- ByteBuffer sessionId = null;
- String strSessionId = null;
- try {
- boolean hasMediaCrypto = ensureMediaCryptoCreated();
- if (!hasMediaCrypto) {
- onRejectPromise(promiseId, "MediaCrypto intance is not created !");
- return;
- }
-
- sessionId = openSession();
- if (sessionId == null) {
- onRejectPromise(promiseId, "Cannot get a session id from MediaDrm !");
- return;
- }
-
- MediaDrm.KeyRequest request = getKeyRequest(sessionId, initData, initDataType);
- if (request == null) {
- mDrm.closeSession(sessionId.array());
- onRejectPromise(promiseId, "Cannot get a key request from MediaDrm !");
- return;
- }
- onSessionCreated(createSessionToken,
- promiseId,
- sessionId.array(),
- request.getData());
- onSessionMessage(sessionId.array(),
- LICENSE_REQUEST_INITIAL,
- request.getData());
- mSessionMIMETypes.put(sessionId, initDataType);
- strSessionId = new String(sessionId.array());
- mSessionIds.add(sessionId);
- if (DEBUG) Log.d(LOGTAG, " StringID : " + strSessionId + " is put into mSessionIds ");
- } catch (android.media.NotProvisionedException e) {
- if (DEBUG) Log.d(LOGTAG, "Device not provisioned:" + e.getMessage());
- if (sessionId != null) {
- // The promise of this createSession will be either resolved
- // or rejected after provisioning.
- mDrm.closeSession(sessionId.array());
- }
- savePendingCreateSessionData(createSessionToken, promiseId,
- initData, initDataType);
- startProvisioning(promiseId);
- }
- }
-
- @Override
- public void updateSession(int promiseId,
- String sessionId,
- byte[] response) {
- if (DEBUG) Log.d(LOGTAG, "updateSession(), sessionId = " + sessionId);
- if (mDrm == null) {
- onRejectPromise(promiseId, "MediaDrm instance doesn't exist !!");
- return;
- }
-
- ByteBuffer session = ByteBuffer.wrap(sessionId.getBytes());
- if (!sessionExists(session)) {
- onRejectPromise(promiseId, "Invalid session during updateSession.");
- return;
- }
-
- try {
- final byte [] keySetId = mDrm.provideKeyResponse(session.array(), response);
- if (DEBUG) {
- HashMap<String, String> infoMap = mDrm.queryKeyStatus(session.array());
- for (String strKey : infoMap.keySet()) {
- String strValue = infoMap.get(strKey);
- Log.d(LOGTAG, "InfoMap : key(" + strKey + ")/value(" + strValue + ")");
- }
- }
- SessionKeyInfo[] keyInfos = new SessionKeyInfo[1];
- keyInfos[0] = new SessionKeyInfo(DUMMY_KEY_ID,
- MediaDrm.KeyStatus.STATUS_USABLE);
- onSessionBatchedKeyChanged(session.array(), keyInfos);
- if (DEBUG) Log.d(LOGTAG, "Key successfully added for session " + sessionId);
- onSessionUpdated(promiseId, session.array());
- return;
- } catch (android.media.NotProvisionedException e) {
- if (DEBUG) Log.d(LOGTAG, "Failed to provide key response:" + e.getMessage());
- onSessionError(session.array(), "Got NotProvisionedException.");
- onRejectPromise(promiseId, "Not provisioned during updateSession.");
- } catch (android.media.DeniedByServerException e) {
- if (DEBUG) Log.d(LOGTAG, "Failed to provide key response:" + e.getMessage());
- onSessionError(session.array(), "Got DeniedByServerException.");
- onRejectPromise(promiseId, "Denied by server during updateSession.");
- } catch (java.lang.IllegalStateException e) {
- if (DEBUG) Log.d(LOGTAG, "Exception when calling provideKeyResponse():" + e.getMessage());
- onSessionError(session.array(), "Got IllegalStateException.");
- onRejectPromise(promiseId, "Rejected during updateSession.");
- }
- release();
- return;
- }
-
- @Override
- public void closeSession(int promiseId, String sessionId) {
- if (DEBUG) Log.d(LOGTAG, "closeSession()");
- if (mDrm == null) {
- onRejectPromise(promiseId, "MediaDrm instance doesn't exist !!");
- return;
- }
-
- ByteBuffer session = ByteBuffer.wrap(sessionId.getBytes());
- mSessionIds.remove(session);
- mDrm.closeSession(session.array());
- onSessionClosed(promiseId, session.array());
- }
-
- @Override
- public void release() {
- if (DEBUG) Log.d(LOGTAG, "release()");
- if (mProvisioningPromiseId > 0) {
- onRejectPromise(mProvisioningPromiseId, "Releasing ... reject provisioning session.");
- mProvisioningPromiseId = 0;
- }
- while (!mPendingCreateSessionDataQueue.isEmpty()) {
- PendingCreateSessionData pendingData = mPendingCreateSessionDataQueue.poll();
- onRejectPromise(pendingData.mPromiseId, "Releasing ... reject all pending sessions.");
- }
- mPendingCreateSessionDataQueue = null;
-
- if (mDrm != null) {
- for (ByteBuffer session : mSessionIds) {
- mDrm.closeSession(session.array());
- }
- mDrm.release();
- mDrm = null;
- }
- mSessionIds.clear();
- mSessionIds = null;
- mSessionMIMETypes.clear();
- mSessionMIMETypes = null;
-
- mCryptoSessionId = null;
- if (mCrypto != null) {
- mCrypto.release();
- mCrypto = null;
- }
- if (mHandlerThread != null) {
- mHandlerThread.quitSafely();
- mHandlerThread = null;
- }
- mHandler = null;
- }
-
- @Override
- public MediaCrypto getMediaCrypto() {
- if (DEBUG) Log.d(LOGTAG, "getMediaCrypto()");
- return mCrypto;
- }
-
- protected void onSessionCreated(int createSessionToken,
- int promiseId,
- byte[] sessionId,
- byte[] request) {
- assertTrue(mCallbacks != null);
- mCallbacks.onSessionCreated(createSessionToken, promiseId, sessionId, request);
- }
-
- protected void onSessionUpdated(int promiseId, byte[] sessionId) {
- assertTrue(mCallbacks != null);
- mCallbacks.onSessionUpdated(promiseId, sessionId);
- }
-
- protected void onSessionClosed(int promiseId, byte[] sessionId) {
- assertTrue(mCallbacks != null);
- mCallbacks.onSessionClosed(promiseId, sessionId);
- }
-
- protected void onSessionMessage(byte[] sessionId,
- int sessionMessageType,
- byte[] request) {
- assertTrue(mCallbacks != null);
- mCallbacks.onSessionMessage(sessionId, sessionMessageType, request);
- }
-
- protected void onSessionError(byte[] sessionId, String message) {
- assertTrue(mCallbacks != null);
- mCallbacks.onSessionError(sessionId, message);
- }
-
- protected void onSessionBatchedKeyChanged(byte[] sessionId,
- SessionKeyInfo[] keyInfos) {
- assertTrue(mCallbacks != null);
- mCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos);
- }
-
- protected void onRejectPromise(int promiseId, String message) {
- assertTrue(mCallbacks != null);
- mCallbacks.onRejectPromise(promiseId, message);
- }
-
- private MediaDrm.KeyRequest getKeyRequest(ByteBuffer aSession,
- byte[] data,
- String mimeType)
- throws android.media.NotProvisionedException {
- if (mProvisioningPromiseId > 0) {
- // Now provisioning.
- return null;
- }
-
- try {
- HashMap<String, String> optionalParameters = new HashMap<String, String>();
- return mDrm.getKeyRequest(aSession.array(),
- data,
- mimeType,
- MediaDrm.KEY_TYPE_STREAMING,
- optionalParameters);
- } catch (Exception e) {
- Log.e(LOGTAG, "Got excpetion during MediaDrm.getKeyRequest", e);
- }
- return null;
- }
-
- private class MediaDrmListener implements MediaDrm.OnEventListener {
- @Override
- public void onEvent(MediaDrm mediaDrm, byte[] sessionArray, int event,
- int extra, byte[] data) {
- if (DEBUG) Log.d(LOGTAG, "MediaDrmListener.onEvent()");
- if (sessionArray == null) {
- if (DEBUG) Log.d(LOGTAG, "MediaDrmListener: Null session.");
- return;
- }
- ByteBuffer session = ByteBuffer.wrap(sessionArray);
- if (!sessionExists(session)) {
- if (DEBUG) Log.d(LOGTAG, "MediaDrmListener: Invalid session.");
- return;
- }
- // On L, these events are treated as exceptions and handled correspondingly.
- // Leaving this code block for logging message.
- String sessionId = new String(session.array());
- switch (event) {
- case MediaDrm.EVENT_PROVISION_REQUIRED:
- if (DEBUG) Log.d(LOGTAG, "MediaDrm.EVENT_PROVISION_REQUIRED");
- break;
- case MediaDrm.EVENT_KEY_REQUIRED:
- if (DEBUG) Log.d(LOGTAG, "MediaDrm.EVENT_KEY_REQUIRED");
- // No need to handle here if we're not in privacy mode.
- break;
- case MediaDrm.EVENT_KEY_EXPIRED:
- if (DEBUG) Log.d(LOGTAG, "MediaDrm.EVENT_KEY_EXPIRED, sessionId=" + sessionId);
- break;
- case MediaDrm.EVENT_VENDOR_DEFINED:
- if (DEBUG) Log.d(LOGTAG, "MediaDrm.EVENT_VENDOR_DEFINED, sessionId=" + sessionId);
- break;
- default:
- if (DEBUG) Log.d(LOGTAG, "Invalid DRM event " + event);
- return;
- }
- }
- }
-
- private ByteBuffer openSession() throws android.media.NotProvisionedException {
- try {
- byte[] sessionId = mDrm.openSession();
- // ByteBuffer.wrap() is backed by the byte[]. Make a clone here in
- // case the underlying byte[] is modified.
- return ByteBuffer.wrap(sessionId.clone());
- } catch (android.media.NotProvisionedException e) {
- // Throw NotProvisionedException so that we can startProvisioning().
- throw e;
- } catch (java.lang.RuntimeException e) {
- if (DEBUG) Log.d(LOGTAG, "Cannot open a new session:" + e.getMessage());
- release();
- return null;
- } catch (android.media.MediaDrmException e) {
- // Other MediaDrmExceptions (e.g. ResourceBusyException) are not
- // recoverable.
- release();
- return null;
- }
- }
-
- private boolean sessionExists(ByteBuffer session) {
- if (mCryptoSessionId == null) {
- if (DEBUG) Log.d(LOGTAG, "Session doesn't exist because media crypto session is not created.");
- return false;
- }
- if (session == null) {
- if (DEBUG) Log.d(LOGTAG, "Session is null, not in map !");
- return false;
- }
- return !session.equals(mCryptoSessionId) && mSessionIds.contains(session);
- }
-
- private class PostRequestTask extends AsyncTask<Void, Void, Void> {
- private static final String LOGTAG = "PostRequestTask";
-
- private int mPromiseId;
- private String mURL;
- private byte[] mDrmRequest;
- private byte[] mResponseBody;
-
- PostRequestTask(int promiseId, String url, byte[] drmRequest) {
- this.mPromiseId = promiseId;
- this.mURL = url;
- this.mDrmRequest = drmRequest;
- }
-
- @Override
- protected Void doInBackground(Void... params) {
- try {
- URL finalURL = new URL(mURL + "&signedRequest=" + URLEncoder.encode(new String(mDrmRequest), "UTF-8"));
- HttpURLConnection urlConnection = (HttpURLConnection) finalURL.openConnection();
- urlConnection.setRequestMethod("POST");
- if (DEBUG) Log.d(LOGTAG, "Provisioning, posting url =" + finalURL.toString());
-
- // Add data
- urlConnection.setRequestProperty("Accept", "*/*");
- urlConnection.setRequestProperty("User-Agent", getCDMUserAgent());
- urlConnection.setRequestProperty("Content-Type", "application/json");
-
- // Execute HTTP Post Request
- urlConnection.connect();
-
- int responseCode = urlConnection.getResponseCode();
- if (responseCode == HttpURLConnection.HTTP_OK) {
- BufferedReader in =
- new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
- String inputLine;
- StringBuffer response = new StringBuffer();
-
- while ((inputLine = in.readLine()) != null) {
- response.append(inputLine);
- }
- in.close();
- mResponseBody = String.valueOf(response).getBytes();
- if (DEBUG) Log.d(LOGTAG, "Provisioning, response received.");
- if (mResponseBody != null) Log.d(LOGTAG, "response length=" + mResponseBody.length);
- } else {
- Log.d(LOGTAG, "Provisioning, server returned HTTP error code :" + responseCode);
- }
- } catch (IOException e) {
- Log.e(LOGTAG, "Got exception during posting provisioning request ...", e);
- }
- return null;
- }
-
- @Override
- protected void onPostExecute(Void v) {
- onProvisionResponse(mPromiseId, mResponseBody);
- }
- }
-
- private boolean provideProvisionResponse(byte[] response) {
- if (response == null || response.length == 0) {
- if (DEBUG) Log.d(LOGTAG, "Invalid provision response.");
- return false;
- }
-
- try {
- mDrm.provideProvisionResponse(response);
- return true;
- } catch (android.media.DeniedByServerException e) {
- if (DEBUG) Log.d(LOGTAG, "Failed to provide provision response:" + e.getMessage());
- } catch (java.lang.IllegalStateException e) {
- if (DEBUG) Log.d(LOGTAG, "Failed to provide provision response:" + e.getMessage());
- }
- return false;
- }
-
- private void savePendingCreateSessionData(int token,
- int promiseId,
- byte[] initData,
- String mime) {
- if (DEBUG) Log.d(LOGTAG, "savePendingCreateSessionData, promiseId : " + promiseId);
- mPendingCreateSessionDataQueue.offer(new PendingCreateSessionData(token, promiseId, initData, mime));
- }
-
- private void processPendingCreateSessionData() {
- if (DEBUG) Log.d(LOGTAG, "processPendingCreateSessionData ... ");
-
- assertTrue(mProvisioningPromiseId == 0);
- try {
- while (!mPendingCreateSessionDataQueue.isEmpty()) {
- PendingCreateSessionData pendingData = mPendingCreateSessionDataQueue.poll();
- if (DEBUG) Log.d(LOGTAG, "processPendingCreateSessionData, promiseId : " + pendingData.mPromiseId);
-
- createSession(pendingData.mToken,
- pendingData.mPromiseId,
- pendingData.mMimeType,
- pendingData.mInitData);
- }
- } catch (Exception e) {
- Log.e(LOGTAG, "Got excpetion during processPendingCreateSessionData ...", e);
- }
- }
-
- private void resumePendingOperations() {
- if (mHandlerThread == null) {
- mHandlerThread = new HandlerThread("PendingSessionOpsThread");
- mHandlerThread.start();
- }
- if (mHandler == null) {
- mHandler = new Handler(mHandlerThread.getLooper());
- }
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- processPendingCreateSessionData();
- }
- });
- }
-
- // Only triggered when failed on {openSession, getKeyRequest}
- private void startProvisioning(int promiseId) {
- if (DEBUG) Log.d(LOGTAG, "startProvisioning()");
- if (mProvisioningPromiseId > 0) {
- // Already in provisioning.
- return;
- }
- try {
- mProvisioningPromiseId = promiseId;
- MediaDrm.ProvisionRequest request = mDrm.getProvisionRequest();
- PostRequestTask postTask =
- new PostRequestTask(promiseId, request.getDefaultUrl(), request.getData());
- postTask.execute();
- } catch (Exception e) {
- onRejectPromise(promiseId, "Exception happened in startProvisioning !");
- mProvisioningPromiseId = 0;
- }
- }
-
- private void onProvisionResponse(int promiseId, byte[] response) {
- if (DEBUG) Log.d(LOGTAG, "onProvisionResponse()");
-
- mProvisioningPromiseId = 0;
- boolean success = provideProvisionResponse(response);
- if (success) {
- // Promise will either be resovled / rejected in createSession during
- // resuming operations.
- resumePendingOperations();
- } else {
- onRejectPromise(promiseId, "Failed to provide provision response.");
- }
- }
-
- private boolean ensureMediaCryptoCreated() throws android.media.NotProvisionedException {
- if (mCrypto != null) {
- return true;
- }
- try {
- mCryptoSessionId = openSession();
- if (mCryptoSessionId == null) {
- if (DEBUG) Log.d(LOGTAG, "Cannot open session for MediaCrypto");
- return false;
- }
-
- if (MediaCrypto.isCryptoSchemeSupported(mSchemeUUID)) {
- final byte [] cryptoSessionId = mCryptoSessionId.array();
- mCrypto = new MediaCrypto(mSchemeUUID, cryptoSessionId);
- String strCryptoSessionId = new String(cryptoSessionId);
- mSessionIds.add(mCryptoSessionId);
- if (DEBUG) Log.d(LOGTAG, "MediaCrypto successfully created! - SId " + INVALID_SESSION_ID + ", " + strCryptoSessionId);
- return true;
- } else {
- if (DEBUG) Log.d(LOGTAG, "Cannot create MediaCrypto for unsupported scheme.");
- return false;
- }
- } catch (android.media.MediaCryptoException e) {
- if (DEBUG) Log.d(LOGTAG, "Cannot create MediaCrypto:" + e.getMessage());
- release();
- return false;
- } catch (android.media.NotProvisionedException e) {
- if (DEBUG) Log.d(LOGTAG, "ensureMediaCryptoCreated::Device not provisioned:" + e.getMessage());
- throw e;
- }
- }
-
- private UUID convertKeySystemToSchemeUUID(String keySystem) {
- if (WIDEVINE_KEY_SYSTEM.equals(keySystem)) {
- return WIDEVINE_SCHEME_UUID;
- }
- if (DEBUG) Log.d(LOGTAG, "Cannot convert unsupported key system : " + keySystem);
- return null;
- }
-
- private String getCDMUserAgent() {
- // This user agent is found and hard-coded in Android(L) source code and
- // Chromium project. Not sure if it's gonna change in the future.
- String ua = "Widevine CDM v1.0";
- return ua;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV23.java b/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV23.java
deleted file mode 100644
index 74144f28e..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV23.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.media;
-
-import android.annotation.TargetApi;
-import static android.os.Build.VERSION_CODES.M;
-import android.media.MediaDrm;
-import android.util.Log;
-import java.util.List;
-
-public class GeckoMediaDrmBridgeV23 extends GeckoMediaDrmBridgeV21 {
-
- private static final String LOGTAG = "GeckoMediaDrmBridgeV23";
- private static final boolean DEBUG = false;
-
- GeckoMediaDrmBridgeV23(String keySystem) throws Exception {
- super(keySystem);
- if (DEBUG) Log.d(LOGTAG, "GeckoMediaDrmBridgeV23 ctor");
- mDrm.setOnKeyStatusChangeListener(new KeyStatusChangeListener(), null);
- }
-
- @TargetApi(M)
- private class KeyStatusChangeListener implements MediaDrm.OnKeyStatusChangeListener {
- @Override
- public void onKeyStatusChange(MediaDrm mediaDrm,
- byte[] sessionId,
- List<MediaDrm.KeyStatus> keyInformation,
- boolean hasNewUsableKey) {
- if (DEBUG) Log.d(LOGTAG, "[onKeyStatusChange] hasNewUsableKey = " + hasNewUsableKey);
- if (keyInformation.size() == 0) {
- return;
- }
- SessionKeyInfo[] keyInfos = new SessionKeyInfo[keyInformation.size()];
- for (int i = 0; i < keyInformation.size(); i++) {
- MediaDrm.KeyStatus keyStatus = keyInformation.get(i);
- keyInfos[i] = new SessionKeyInfo(keyStatus.getKeyId(),
- keyStatus.getStatusCode());
- }
- onSessionBatchedKeyChanged(sessionId, keyInfos);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/media/JellyBeanAsyncCodec.java b/mobile/android/base/java/org/mozilla/gecko/media/JellyBeanAsyncCodec.java
deleted file mode 100644
index 3df01f1fe..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/media/JellyBeanAsyncCodec.java
+++ /dev/null
@@ -1,405 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.media;
-
-import android.media.MediaCodec;
-import android.media.MediaFormat;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
-import android.util.Log;
-import android.view.Surface;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-
-// Implement async API using MediaCodec sync mode (API v16).
-// This class uses internal worker thread/handler (mBufferPoller) to poll
-// input and output buffer and notifies the client through callbacks.
-final class JellyBeanAsyncCodec implements AsyncCodec {
- private static final String LOGTAG = "GeckoAsyncCodecAPIv16";
- private static final boolean DEBUG = false;
-
- private static final int ERROR_CODEC = -10000;
-
- private abstract class CancelableHandler extends Handler {
- private static final int MSG_CANCELLATION = 0x434E434C; // 'CNCL'
-
- protected CancelableHandler(Looper looper) {
- super(looper);
- }
-
- protected void cancel() {
- removeCallbacksAndMessages(null);
- sendEmptyMessage(MSG_CANCELLATION);
- // Wait until handleMessageLocked() is done.
- synchronized (this) { }
- }
-
- protected boolean isCanceled() {
- return hasMessages(MSG_CANCELLATION);
- }
-
- // Subclass should implement this and return true if it handles msg.
- // Warning: Never, ever call super.handleMessage() in this method!
- protected abstract boolean handleMessageLocked(Message msg);
-
- public final void handleMessage(Message msg) {
- // Block cancel() during handleMessageLocked().
- synchronized (this) {
- if (isCanceled() || handleMessageLocked(msg)) {
- return;
- }
- }
-
- switch (msg.what) {
- case MSG_CANCELLATION:
- // Just a marker. Nothing to do here.
- if (DEBUG) Log.d(LOGTAG, "handler " + this + " done cancellation, codec=" + JellyBeanAsyncCodec.this);
- break;
- default:
- super.handleMessage(msg);
- break;
- }
- }
- }
-
- // A handler to invoke AsyncCodec.Callbacks methods.
- private final class CallbackSender extends CancelableHandler {
- private static final int MSG_INPUT_BUFFER_AVAILABLE = 1;
- private static final int MSG_OUTPUT_BUFFER_AVAILABLE = 2;
- private static final int MSG_OUTPUT_FORMAT_CHANGE = 3;
- private static final int MSG_ERROR = 4;
- private Callbacks mCallbacks;
-
- private CallbackSender(Looper looper, Callbacks callbacks) {
- super(looper);
- mCallbacks = callbacks;
- }
-
- public void notifyInputBuffer(int index) {
- if (isCanceled()) {
- return;
- }
-
- Message msg = obtainMessage(MSG_INPUT_BUFFER_AVAILABLE);
- msg.arg1 = index;
- processMessage(msg);
- }
-
- private void processMessage(Message msg) {
- if (Looper.myLooper() == getLooper()) {
- handleMessage(msg);
- } else {
- sendMessage(msg);
- }
- }
-
- public void notifyOutputBuffer(int index, MediaCodec.BufferInfo info) {
- if (isCanceled()) {
- return;
- }
-
- Message msg = obtainMessage(MSG_OUTPUT_BUFFER_AVAILABLE, info);
- msg.arg1 = index;
- processMessage(msg);
- }
-
- public void notifyOutputFormat(MediaFormat format) {
- if (isCanceled()) {
- return;
- }
- processMessage(obtainMessage(MSG_OUTPUT_FORMAT_CHANGE, format));
- }
-
- public void notifyError(int result) {
- Log.e(LOGTAG, "codec error:" + result);
- processMessage(obtainMessage(MSG_ERROR, result, 0));
- }
-
- protected boolean handleMessageLocked(Message msg) {
- switch (msg.what) {
- case MSG_INPUT_BUFFER_AVAILABLE: // arg1: buffer index.
- mCallbacks.onInputBufferAvailable(JellyBeanAsyncCodec.this,
- msg.arg1);
- break;
- case MSG_OUTPUT_BUFFER_AVAILABLE: // arg1: buffer index, obj: info.
- mCallbacks.onOutputBufferAvailable(JellyBeanAsyncCodec.this,
- msg.arg1,
- (MediaCodec.BufferInfo)msg.obj);
- break;
- case MSG_OUTPUT_FORMAT_CHANGE: // obj: output format.
- mCallbacks.onOutputFormatChanged(JellyBeanAsyncCodec.this,
- (MediaFormat)msg.obj);
- break;
- case MSG_ERROR: // arg1: error code.
- mCallbacks.onError(JellyBeanAsyncCodec.this, msg.arg1);
- break;
- default:
- return false;
- }
-
- return true;
- }
- }
-
- // Handler to poll input and output buffers using dequeue(Input|Output)Buffer(),
- // with 10ms time-out. Once triggered and successfully gets a buffer, it
- // will schedule next polling until EOS or failure. To prevent it from
- // automatically polling more buffer, use cancel() it inherits from
- // CancelableHandler.
- private final class BufferPoller extends CancelableHandler {
- private static final int MSG_POLL_INPUT_BUFFERS = 1;
- private static final int MSG_POLL_OUTPUT_BUFFERS = 2;
-
- private static final long DEQUEUE_TIMEOUT_US = 10000;
-
- public BufferPoller(Looper looper) {
- super(looper);
- }
-
- private void schedulePollingIfNotCanceled(int what) {
- if (isCanceled()) {
- return;
- }
-
- schedulePolling(what);
- }
-
- private void schedulePolling(int what) {
- if (needsBuffer(what)) {
- sendEmptyMessage(what);
- }
- }
-
- private boolean needsBuffer(int what) {
- if (mOutputEnded && (what == MSG_POLL_OUTPUT_BUFFERS)) {
- return false;
- }
-
- if (mInputEnded && (what == MSG_POLL_INPUT_BUFFERS)) {
- return false;
- }
-
- return true;
- }
-
- protected boolean handleMessageLocked(Message msg) {
- try {
- switch (msg.what) {
- case MSG_POLL_INPUT_BUFFERS:
- pollInputBuffer();
- break;
- case MSG_POLL_OUTPUT_BUFFERS:
- pollOutputBuffer();
- break;
- default:
- return false;
- }
- } catch (IllegalStateException e) {
- e.printStackTrace();
- mCallbackSender.notifyError(ERROR_CODEC);
- }
-
- return true;
- }
-
- private void pollInputBuffer() {
- int result = mCodec.dequeueInputBuffer(DEQUEUE_TIMEOUT_US);
- if (result >= 0) {
- mCallbackSender.notifyInputBuffer(result);
- schedulePollingIfNotCanceled(BufferPoller.MSG_POLL_INPUT_BUFFERS);
- } else if (result != MediaCodec.INFO_TRY_AGAIN_LATER) {
- mCallbackSender.notifyError(result);
- }
- }
-
- private void pollOutputBuffer() {
- boolean dequeueMoreBuffer = true;
- MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
- int result = mCodec.dequeueOutputBuffer(info, DEQUEUE_TIMEOUT_US);
- if (result >= 0) {
- if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
- mOutputEnded = true;
- }
- mCallbackSender.notifyOutputBuffer(result, info);
- if (!hasMessages(MSG_POLL_INPUT_BUFFERS)) {
- schedulePollingIfNotCanceled(MSG_POLL_INPUT_BUFFERS);
- }
- } else if (result == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
- mOutputBuffers = mCodec.getOutputBuffers();
- } else if (result == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
- mCallbackSender.notifyOutputFormat(mCodec.getOutputFormat());
- } else if (result == MediaCodec.INFO_TRY_AGAIN_LATER) {
- // When input ended, keep polling remaining output buffer until EOS.
- dequeueMoreBuffer = mInputEnded;
- } else {
- mCallbackSender.notifyError(result);
- dequeueMoreBuffer = false;
- }
-
- if (dequeueMoreBuffer) {
- schedulePollingIfNotCanceled(MSG_POLL_OUTPUT_BUFFERS);
- }
- }
- }
-
- private MediaCodec mCodec;
- private ByteBuffer[] mInputBuffers;
- private ByteBuffer[] mOutputBuffers;
- private AsyncCodec.Callbacks mCallbacks;
- private CallbackSender mCallbackSender;
-
- private BufferPoller mBufferPoller;
- private volatile boolean mInputEnded;
- private volatile boolean mOutputEnded;
-
- // Must be called on a thread with looper.
- /* package */ JellyBeanAsyncCodec(String name) throws IOException {
- mCodec = MediaCodec.createByCodecName(name);
- initBufferPoller(name + " buffer poller");
- }
-
- private void initBufferPoller(String name) {
- if (mBufferPoller != null) {
- Log.e(LOGTAG, "poller already initialized");
- return;
- }
- HandlerThread thread = new HandlerThread(name);
- thread.start();
- mBufferPoller = new BufferPoller(thread.getLooper());
- if (DEBUG) Log.d(LOGTAG, "start poller for codec:" + this + ", thread=" + thread.getThreadId());
- }
-
- @Override
- public void setCallbacks(AsyncCodec.Callbacks callbacks, Handler handler) {
- if (callbacks == null) {
- return;
- }
-
- Looper looper = (handler == null) ? null : handler.getLooper();
- if (looper == null) {
- // Use this thread if no handler supplied.
- looper = Looper.myLooper();
- }
- if (looper == null) {
- // This thread has no looper. Use poller thread.
- looper = mBufferPoller.getLooper();
- }
- mCallbackSender = new CallbackSender(looper, callbacks);
- if (DEBUG) Log.d(LOGTAG, "setCallbacks(): sender=" + mCallbackSender);
- }
-
- @Override
- public void configure(MediaFormat format, Surface surface, int flags) {
- assertCallbacks();
-
- mCodec.configure(format, surface, null, flags);
- }
-
- private void assertCallbacks() {
- if (mCallbackSender == null) {
- throw new IllegalStateException(LOGTAG + ": callback must be supplied with setCallbacks().");
- }
- }
-
- @Override
- public void start() {
- assertCallbacks();
-
- mCodec.start();
- mInputEnded = false;
- mOutputEnded = false;
- mInputBuffers = mCodec.getInputBuffers();
- mOutputBuffers = mCodec.getOutputBuffers();
- mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_INPUT_BUFFERS);
- }
-
- @Override
- public final void queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags) {
- assertCallbacks();
-
- mInputEnded = (flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
-
- try {
- mCodec.queueInputBuffer(index, offset, size, presentationTimeUs, flags);
- } catch (IllegalStateException e) {
- e.printStackTrace();
- mCallbackSender.notifyError(ERROR_CODEC);
- return;
- }
-
- mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_INPUT_BUFFERS);
- mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_OUTPUT_BUFFERS);
- }
-
- @Override
- public final void releaseOutputBuffer(int index, boolean render) {
- assertCallbacks();
-
- mCodec.releaseOutputBuffer(index, render);
- }
-
- @Override
- public final ByteBuffer getInputBuffer(int index) {
- assertCallbacks();
-
- return mInputBuffers[index];
- }
-
- @Override
- public final ByteBuffer getOutputBuffer(int index) {
- assertCallbacks();
-
- return mOutputBuffers[index];
- }
-
- @Override
- public void flush() {
- assertCallbacks();
-
- mInputEnded = false;
- mOutputEnded = false;
- cancelPendingTasks();
- mCodec.flush();
- mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_INPUT_BUFFERS);
- }
-
- private void cancelPendingTasks() {
- mBufferPoller.cancel();
- mCallbackSender.cancel();
- }
-
- @Override
- public void stop() {
- assertCallbacks();
-
- cancelPendingTasks();
- mCodec.stop();
- }
-
- @Override
- public void release() {
- assertCallbacks();
-
- cancelPendingTasks();
- mCallbackSender = null;
- mCodec.release();
- stopBufferPoller();
- }
-
- private void stopBufferPoller() {
- if (mBufferPoller == null) {
- Log.e(LOGTAG, "no initialized poller.");
- return;
- }
-
- mBufferPoller.getLooper().quit();
- mBufferPoller = null;
-
- if (DEBUG) Log.d(LOGTAG, "stop poller " + this);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/media/LocalMediaDrmBridge.java b/mobile/android/base/java/org/mozilla/gecko/media/LocalMediaDrmBridge.java
deleted file mode 100644
index 2aad674b6..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/media/LocalMediaDrmBridge.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.media;
-import org.mozilla.gecko.AppConstants;
-
-import android.media.MediaCrypto;
-import android.util.Log;
-
-final class LocalMediaDrmBridge implements GeckoMediaDrm {
- private static final String LOGTAG = "GeckoLocalMediaDrmBridge";
- private static final boolean DEBUG = false;
- private GeckoMediaDrm mBridge = null;
- private CallbacksForwarder mCallbacksFwd;
-
- // Forward the callback calls from GeckoMediaDrmBridgeV{21,23}
- // to the callback MediaDrmProxy.Callbacks.
- private class CallbacksForwarder implements GeckoMediaDrm.Callbacks {
- private final GeckoMediaDrm.Callbacks mProxyCallbacks;
-
- CallbacksForwarder(GeckoMediaDrm.Callbacks callbacks) {
- assertTrue(callbacks != null);
- mProxyCallbacks = callbacks;
- }
-
- @Override
- public void onSessionCreated(int createSessionToken,
- int promiseId,
- byte[] sessionId,
- byte[] request) {
- assertTrue(mProxyCallbacks != null);
- mProxyCallbacks.onSessionCreated(createSessionToken,
- promiseId,
- sessionId,
- request);
- }
-
- @Override
- public void onSessionUpdated(int promiseId, byte[] sessionId) {
- assertTrue(mProxyCallbacks != null);
- mProxyCallbacks.onSessionUpdated(promiseId, sessionId);
- }
-
- @Override
- public void onSessionClosed(int promiseId, byte[] sessionId) {
- assertTrue(mProxyCallbacks != null);
- mProxyCallbacks.onSessionClosed(promiseId, sessionId);
- }
-
- @Override
- public void onSessionMessage(byte[] sessionId,
- int sessionMessageType,
- byte[] request) {
- assertTrue(mProxyCallbacks != null);
- mProxyCallbacks.onSessionMessage(sessionId, sessionMessageType, request);
- }
-
- @Override
- public void onSessionError(byte[] sessionId,
- String message) {
- assertTrue(mProxyCallbacks != null);
- mProxyCallbacks.onSessionError(sessionId, message);
- }
-
- @Override
- public void onSessionBatchedKeyChanged(byte[] sessionId,
- SessionKeyInfo[] keyInfos) {
- assertTrue(mProxyCallbacks != null);
- mProxyCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos);
- }
-
- @Override
- public void onRejectPromise(int promiseId, String message) {
- if (DEBUG) Log.d(LOGTAG, message);
- assertTrue(mProxyCallbacks != null);
- mProxyCallbacks.onRejectPromise(promiseId, message);
- }
- } // CallbacksForwarder
-
- private static void assertTrue(boolean condition) {
- if (DEBUG && !condition) {
- throw new AssertionError("Expected condition to be true");
- }
- }
-
- LocalMediaDrmBridge(String keySystem) throws Exception {
- if (AppConstants.Versions.preLollipop) {
- mBridge = null;
- } else if (AppConstants.Versions.feature21Plus &&
- AppConstants.Versions.preMarshmallow) {
- mBridge = new GeckoMediaDrmBridgeV21(keySystem);
- } else {
- mBridge = new GeckoMediaDrmBridgeV23(keySystem);
- }
- }
-
- @Override
- public synchronized void setCallbacks(Callbacks callbacks) {
- if (DEBUG) Log.d(LOGTAG, "setCallbacks()");
- mCallbacksFwd = new CallbacksForwarder(callbacks);
- assertTrue(mBridge != null);
- mBridge.setCallbacks(mCallbacksFwd);
- }
-
- @Override
- public synchronized void createSession(int createSessionToken,
- int promiseId,
- String initDataType,
- byte[] initData) {
- if (DEBUG) Log.d(LOGTAG, "createSession()");
- assertTrue(mCallbacksFwd != null);
- try {
- mBridge.createSession(createSessionToken, promiseId, initDataType, initData);
- } catch (Exception e) {
- Log.e(LOGTAG, "Failed to createSession.", e);
- mCallbacksFwd.onRejectPromise(promiseId, "Failed to createSession.");
- }
- }
-
- @Override
- public synchronized void updateSession(int promiseId, String sessionId, byte[] response) {
- if (DEBUG) Log.d(LOGTAG, "updateSession()");
- assertTrue(mCallbacksFwd != null);
- try {
- mBridge.updateSession(promiseId, sessionId, response);
- } catch (Exception e) {
- Log.e(LOGTAG, "Failed to updateSession.", e);
- mCallbacksFwd.onRejectPromise(promiseId, "Failed to updateSession.");
- }
- }
-
- @Override
- public synchronized void closeSession(int promiseId, String sessionId) {
- if (DEBUG) Log.d(LOGTAG, "closeSession()");
- assertTrue(mCallbacksFwd != null);
- try {
- mBridge.closeSession(promiseId, sessionId);
- } catch (Exception e) {
- Log.e(LOGTAG, "Failed to closeSession.", e);
- mCallbacksFwd.onRejectPromise(promiseId, "Failed to closeSession.");
- }
- }
-
- @Override
- public synchronized void release() {
- if (DEBUG) Log.d(LOGTAG, "release()");
- try {
- mBridge.release();
- mBridge = null;
- mCallbacksFwd = null;
- } catch (Exception e) {
- Log.e(LOGTAG, "Failed to release", e);
- }
- }
-
- @Override
- public synchronized MediaCrypto getMediaCrypto() {
- if (DEBUG) Log.d(LOGTAG, "getMediaCrypto()");
- return mBridge != null ? mBridge.getMediaCrypto() : null;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java b/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
deleted file mode 100644
index 2aa783050..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
+++ /dev/null
@@ -1,431 +0,0 @@
-package org.mozilla.gecko.media;
-
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.media.session.MediaController;
-import android.media.session.MediaSession;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.support.v4.app.NotificationManagerCompat;
-import android.util.Log;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.BrowserApp;
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.PrefsHelper;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import java.lang.ref.WeakReference;
-
-public class MediaControlService extends Service implements Tabs.OnTabsChangedListener {
- private static final String LOGTAG = "MediaControlService";
-
- public static final String ACTION_INIT = "action_init";
- public static final String ACTION_RESUME = "action_resume";
- public static final String ACTION_PAUSE = "action_pause";
- public static final String ACTION_STOP = "action_stop";
- public static final String ACTION_RESUME_BY_AUDIO_FOCUS = "action_resume_audio_focus";
- public static final String ACTION_PAUSE_BY_AUDIO_FOCUS = "action_pause_audio_focus";
-
- private static final int MEDIA_CONTROL_ID = 1;
- private static final String MEDIA_CONTROL_PREF = "dom.audiochannel.mediaControl";
-
- private String mActionState = ACTION_STOP;
-
- private MediaSession mSession;
- private MediaController mController;
-
- private PrefsHelper.PrefHandler mPrefsObserver;
- private final String[] mPrefs = { MEDIA_CONTROL_PREF };
-
- private boolean mInitialize = false;
- private boolean mIsMediaControlPrefOn = true;
-
- private static WeakReference<Tab> mTabReference = new WeakReference<>(null);
-
- private int minCoverSize;
- private int coverSize;
-
- @Override
- public void onCreate() {
- initialize();
- }
-
- @Override
- public void onDestroy() {
- shutdown();
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- handleIntent(intent);
- return START_NOT_STICKY;
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-
- @Override
- public boolean onUnbind(Intent intent) {
- mSession.release();
- return super.onUnbind(intent);
- }
-
- @Override
- public void onTaskRemoved(Intent rootIntent) {
- shutdown();
- }
-
- @Override
- public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
- if (!mInitialize) {
- return;
- }
-
- final Tab playingTab = mTabReference.get();
- switch (msg) {
- case MEDIA_PLAYING_CHANGE:
- // The 'MEDIA_PLAYING_CHANGE' would only be received when the
- // media starts or ends.
- if (playingTab != tab && tab.isMediaPlaying()) {
- mTabReference = new WeakReference<>(tab);
- notifyControlInterfaceChanged(ACTION_PAUSE);
- } else if (playingTab == tab && !tab.isMediaPlaying()) {
- notifyControlInterfaceChanged(ACTION_STOP);
- mTabReference = new WeakReference<>(null);
- }
- break;
- case MEDIA_PLAYING_RESUME:
- // user resume the paused-by-control media from page so that we
- // should make the control interface consistent.
- if (playingTab == tab && !isMediaPlaying()) {
- notifyControlInterfaceChanged(ACTION_PAUSE);
- }
- break;
- case CLOSED:
- if (playingTab == null || playingTab == tab) {
- // Remove the controls when the playing tab disappeared or was closed.
- notifyControlInterfaceChanged(ACTION_STOP);
- }
- break;
- case FAVICON:
- if (playingTab == tab) {
- final String actionForPendingIntent = isMediaPlaying() ?
- ACTION_PAUSE : ACTION_RESUME;
- notifyControlInterfaceChanged(actionForPendingIntent);
- }
- break;
- }
- }
-
- private boolean isMediaPlaying() {
- return mActionState.equals(ACTION_RESUME);
- }
-
- private void initialize() {
- if (mInitialize ||
- !isAndroidVersionLollopopOrHigher()) {
- return;
- }
-
- Log.d(LOGTAG, "initialize");
- getGeckoPreference();
- initMediaSession();
-
- coverSize = (int) getResources().getDimension(R.dimen.notification_media_cover);
- minCoverSize = getResources().getDimensionPixelSize(R.dimen.favicon_bg);
-
- Tabs.registerOnTabsChangedListener(this);
- mInitialize = true;
- }
-
- private void shutdown() {
- if (!mInitialize) {
- return;
- }
-
- Log.d(LOGTAG, "shutdown");
- notifyControlInterfaceChanged(ACTION_STOP);
- PrefsHelper.removeObserver(mPrefsObserver);
-
- Tabs.unregisterOnTabsChangedListener(this);
- mInitialize = false;
- stopSelf();
- }
-
- private boolean isAndroidVersionLollopopOrHigher() {
- return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
- }
-
- private void handleIntent(Intent intent) {
- if (intent == null || intent.getAction() == null || !mInitialize) {
- return;
- }
-
- Log.d(LOGTAG, "HandleIntent, action = " + intent.getAction() + ", actionState = " + mActionState);
- switch (intent.getAction()) {
- case ACTION_INIT :
- // This action is used to create a service and do the initialization,
- // the actual operation would be executed via control interface's
- // pending intent.
- break;
- case ACTION_RESUME :
- mController.getTransportControls().play();
- break;
- case ACTION_PAUSE :
- mController.getTransportControls().pause();
- break;
- case ACTION_STOP :
- mController.getTransportControls().stop();
- break;
- case ACTION_PAUSE_BY_AUDIO_FOCUS :
- mController.getTransportControls().sendCustomAction(ACTION_PAUSE_BY_AUDIO_FOCUS, null);
- break;
- case ACTION_RESUME_BY_AUDIO_FOCUS :
- mController.getTransportControls().sendCustomAction(ACTION_RESUME_BY_AUDIO_FOCUS, null);
- break;
- }
- }
-
- private void getGeckoPreference() {
- mPrefsObserver = new PrefsHelper.PrefHandlerBase() {
- @Override
- public void prefValue(String pref, boolean value) {
- if (pref.equals(MEDIA_CONTROL_PREF)) {
- mIsMediaControlPrefOn = value;
-
- // If media is playing, we just need to create or remove
- // the media control interface.
- if (mActionState.equals(ACTION_RESUME)) {
- notifyControlInterfaceChanged(mIsMediaControlPrefOn ?
- ACTION_PAUSE : ACTION_STOP);
- }
-
- // If turn off pref during pausing, except removing media
- // interface, we also need to stop the service and notify
- // gecko about that.
- if (mActionState.equals(ACTION_PAUSE) &&
- !mIsMediaControlPrefOn) {
- Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
- intent.setAction(ACTION_STOP);
- handleIntent(intent);
- }
- }
- }
- };
- PrefsHelper.addObserver(mPrefs, mPrefsObserver);
- }
-
- private void initMediaSession() {
- // Android MediaSession is introduced since version L.
- mSession = new MediaSession(getApplicationContext(),
- "fennec media session");
- mController = new MediaController(getApplicationContext(),
- mSession.getSessionToken());
-
- mSession.setCallback(new MediaSession.Callback() {
- @Override
- public void onCustomAction(String action, Bundle extras) {
- if (action.equals(ACTION_PAUSE_BY_AUDIO_FOCUS)) {
- Log.d(LOGTAG, "Controller, pause by audio focus changed");
- notifyControlInterfaceChanged(ACTION_RESUME);
- } else if (action.equals(ACTION_RESUME_BY_AUDIO_FOCUS)) {
- Log.d(LOGTAG, "Controller, resume by audio focus changed");
- notifyControlInterfaceChanged(ACTION_PAUSE);
- }
- }
-
- @Override
- public void onPlay() {
- Log.d(LOGTAG, "Controller, onPlay");
- super.onPlay();
- notifyControlInterfaceChanged(ACTION_PAUSE);
- notifyObservers("MediaControl", "resumeMedia");
- // To make sure we always own audio focus during playing.
- AudioFocusAgent.notifyStartedPlaying();
- }
-
- @Override
- public void onPause() {
- Log.d(LOGTAG, "Controller, onPause");
- super.onPause();
- notifyControlInterfaceChanged(ACTION_RESUME);
- notifyObservers("MediaControl", "mediaControlPaused");
- AudioFocusAgent.notifyStoppedPlaying();
- }
-
- @Override
- public void onStop() {
- Log.d(LOGTAG, "Controller, onStop");
- super.onStop();
- notifyControlInterfaceChanged(ACTION_STOP);
- notifyObservers("MediaControl", "mediaControlStopped");
- mTabReference = new WeakReference<>(null);
- }
- });
- }
-
- private void notifyObservers(String topic, String data) {
- GeckoAppShell.notifyObservers(topic, data);
- }
-
- private boolean isNeedToRemoveControlInterface(String action) {
- return action.equals(ACTION_STOP);
- }
-
- private void notifyControlInterfaceChanged(final String uiAction) {
- if (!mInitialize) {
- return;
- }
-
- Log.d(LOGTAG, "notifyControlInterfaceChanged, action = " + uiAction);
-
- if (isNeedToRemoveControlInterface(uiAction)) {
- stopForeground(false);
- NotificationManagerCompat.from(this).cancel(MEDIA_CONTROL_ID);
- setActionState(uiAction);
- return;
- }
-
- if (!mIsMediaControlPrefOn) {
- return;
- }
-
- final Tab tab = mTabReference.get();
-
- if (tab == null) {
- return;
- }
-
- setActionState(uiAction);
-
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- updateNotification(tab, uiAction);
- }
- });
- }
-
- private void setActionState(final String uiAction) {
- switch (uiAction) {
- case ACTION_PAUSE:
- mActionState = ACTION_RESUME;
- break;
- case ACTION_RESUME:
- mActionState = ACTION_PAUSE;
- break;
- case ACTION_STOP:
- mActionState = ACTION_STOP;
- break;
- }
- }
-
- private void updateNotification(Tab tab, String action) {
- ThreadUtils.assertNotOnUiThread();
-
- final Notification.MediaStyle style = new Notification.MediaStyle();
- style.setShowActionsInCompactView(0);
-
- final boolean isPlaying = isMediaPlaying();
- final int visibility = tab.isPrivate() ?
- Notification.VISIBILITY_PRIVATE : Notification.VISIBILITY_PUBLIC;
-
- final Notification notification = new Notification.Builder(this)
- .setSmallIcon(R.drawable.flat_icon)
- .setLargeIcon(generateCoverArt(tab))
- .setContentTitle(tab.getTitle())
- .setContentText(tab.getURL())
- .setContentIntent(createContentIntent(tab.getId()))
- .setDeleteIntent(createDeleteIntent())
- .setStyle(style)
- .addAction(createNotificationAction(action))
- .setOngoing(isPlaying)
- .setShowWhen(false)
- .setWhen(0)
- .setVisibility(visibility)
- .build();
-
- if (isPlaying) {
- startForeground(MEDIA_CONTROL_ID, notification);
- } else {
- stopForeground(false);
- NotificationManagerCompat.from(this)
- .notify(MEDIA_CONTROL_ID, notification);
- }
- }
-
- private Notification.Action createNotificationAction(String action) {
- boolean isPlayAction = action.equals(ACTION_RESUME);
-
- int icon = isPlayAction ? R.drawable.ic_media_play : R.drawable.ic_media_pause;
- String title = getString(isPlayAction ? R.string.media_play : R.string.media_pause);
-
- final Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
- intent.setAction(action);
- final PendingIntent pendingIntent = PendingIntent.getService(getApplicationContext(), 1, intent, 0);
-
- //noinspection deprecation - The new constructor is only for API > 23
- return new Notification.Action.Builder(icon, title, pendingIntent).build();
- }
-
- private PendingIntent createContentIntent(int tabId) {
- Intent intent = new Intent(getApplicationContext(), BrowserApp.class);
- intent.setAction(GeckoApp.ACTION_SWITCH_TAB);
- intent.putExtra("TabId", tabId);
- return PendingIntent.getActivity(getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- private PendingIntent createDeleteIntent() {
- Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
- intent.setAction(ACTION_STOP);
- return PendingIntent.getService(getApplicationContext(), 1, intent, 0);
- }
-
- private Bitmap generateCoverArt(Tab tab) {
- final Bitmap favicon = tab.getFavicon();
-
- // If we do not have a favicon or if it's smaller than 72 pixels then just use the default icon.
- if (favicon == null || favicon.getWidth() < minCoverSize || favicon.getHeight() < minCoverSize) {
- // Use the launcher icon as fallback
- return BitmapFactory.decodeResource(getResources(), R.drawable.notification_media);
- }
-
- // Favicon should at least have half of the size of the cover
- int width = Math.max(favicon.getWidth(), coverSize / 2);
- int height = Math.max(favicon.getHeight(), coverSize / 2);
-
- final Bitmap coverArt = Bitmap.createBitmap(coverSize, coverSize, Bitmap.Config.ARGB_8888);
- final Canvas canvas = new Canvas(coverArt);
- canvas.drawColor(0xFF777777);
-
- int left = Math.max(0, (coverArt.getWidth() / 2) - (width / 2));
- int right = Math.min(coverSize, left + width);
- int top = Math.max(0, (coverArt.getHeight() / 2) - (height / 2));
- int bottom = Math.min(coverSize, top + height);
-
- final Paint paint = new Paint();
- paint.setAntiAlias(true);
-
- canvas.drawBitmap(favicon,
- new Rect(0, 0, favicon.getWidth(), favicon.getHeight()),
- new Rect(left, top, right, bottom),
- paint);
-
- return coverArt;
- }
-} \ No newline at end of file
diff --git a/mobile/android/base/java/org/mozilla/gecko/media/MediaDrmProxy.java b/mobile/android/base/java/org/mozilla/gecko/media/MediaDrmProxy.java
deleted file mode 100644
index faca2389e..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/media/MediaDrmProxy.java
+++ /dev/null
@@ -1,307 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-
-package org.mozilla.gecko.media;
-
-import java.util.ArrayList;
-import java.util.UUID;
-
-import org.mozilla.gecko.mozglue.JNIObject;
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.AppConstants;
-
-import android.media.MediaCodecInfo;
-import android.media.MediaCodecList;
-import android.media.MediaCrypto;
-import android.media.MediaDrm;
-import android.util.Log;
-import android.os.Build;
-
-public final class MediaDrmProxy {
- private static final String LOGTAG = "GeckoMediaDrmProxy";
- private static final boolean DEBUG = false;
- private static final UUID WIDEVINE_SCHEME_UUID =
- new UUID(0xedef8ba979d64aceL, 0xa3c827dcd51d21edL);
-
- private static final String WIDEVINE_KEY_SYSTEM = "com.widevine.alpha";
- @WrapForJNI
- private static final String AAC = "audio/mp4a-latm";
- @WrapForJNI
- private static final String AVC = "video/avc";
- @WrapForJNI
- private static final String VORBIS = "audio/vorbis";
- @WrapForJNI
- private static final String VP8 = "video/x-vnd.on2.vp8";
- @WrapForJNI
- private static final String VP9 = "video/x-vnd.on2.vp9";
- @WrapForJNI
- private static final String OPUS = "audio/opus";
-
- // A flag to avoid using the native object that has been destroyed.
- private boolean mDestroyed;
- private GeckoMediaDrm mImpl;
- public static ArrayList<MediaDrmProxy> mProxyList = new ArrayList<MediaDrmProxy>();
-
- private static boolean isSystemSupported() {
- // Support versions >= LOLLIPOP
- if (AppConstants.Versions.preLollipop) {
- if (DEBUG) Log.d(LOGTAG, "System Not supported !!, current SDK version is " + Build.VERSION.SDK_INT);
- return false;
- }
- return true;
- }
-
- @WrapForJNI
- public static boolean isSchemeSupported(String keySystem) {
- if (!isSystemSupported()) {
- return false;
- }
- if (keySystem.equals(WIDEVINE_KEY_SYSTEM)) {
- return MediaDrm.isCryptoSchemeSupported(WIDEVINE_SCHEME_UUID)
- && MediaCrypto.isCryptoSchemeSupported(WIDEVINE_SCHEME_UUID);
- }
- if (DEBUG) Log.d(LOGTAG, "isSchemeSupported key sytem = " + keySystem);
- return false;
- }
-
- @WrapForJNI
- public static boolean IsCryptoSchemeSupported(String keySystem,
- String container) {
- if (!isSystemSupported()) {
- return false;
- }
- if (keySystem.equals(WIDEVINE_KEY_SYSTEM)) {
- return MediaDrm.isCryptoSchemeSupported(WIDEVINE_SCHEME_UUID, container);
- }
- if (DEBUG) Log.d(LOGTAG, "cannot decrypt key sytem = " + keySystem + ", container = " + container);
- return false;
- }
-
- @WrapForJNI
- public static boolean CanDecode(String mimeType) {
- for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
- MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
- if (info.isEncoder()) {
- continue;
- }
- for (String m : info.getSupportedTypes()) {
- if (m.equals(mimeType)) {
- return true;
- }
- }
- }
- if (DEBUG) Log.d(LOGTAG, "cannot decode mimetype = " + mimeType);
- return false;
- }
-
- // Interface for callback to native.
- public interface Callbacks {
- void onSessionCreated(int createSessionToken,
- int promiseId,
- byte[] sessionId,
- byte[] request);
-
- void onSessionUpdated(int promiseId, byte[] sessionId);
-
- void onSessionClosed(int promiseId, byte[] sessionId);
-
- void onSessionMessage(byte[] sessionId,
- int sessionMessageType,
- byte[] request);
-
- void onSessionError(byte[] sessionId,
- String message);
-
- // MediaDrm.KeyStatus is available in API level 23(M)
- // https://developer.android.com/reference/android/media/MediaDrm.KeyStatus.html
- // For compatibility between L and M above, we'll unwrap the KeyStatus structure
- // and store the keyid and status into SessionKeyInfo and pass to native(MediaDrmCDMProxy).
- void onSessionBatchedKeyChanged(byte[] sessionId,
- SessionKeyInfo[] keyInfos);
-
- void onRejectPromise(int promiseId,
- String message);
- } // Callbacks
-
- public static class NativeMediaDrmProxyCallbacks extends JNIObject implements Callbacks {
- @WrapForJNI(calledFrom = "gecko")
- NativeMediaDrmProxyCallbacks() {}
-
- @Override
- @WrapForJNI(dispatchTo = "gecko")
- public native void onSessionCreated(int createSessionToken,
- int promiseId,
- byte[] sessionId,
- byte[] request);
-
- @Override
- @WrapForJNI(dispatchTo = "gecko")
- public native void onSessionUpdated(int promiseId, byte[] sessionId);
-
- @Override
- @WrapForJNI(dispatchTo = "gecko")
- public native void onSessionClosed(int promiseId, byte[] sessionId);
-
- @Override
- @WrapForJNI(dispatchTo = "gecko")
- public native void onSessionMessage(byte[] sessionId,
- int sessionMessageType,
- byte[] request);
-
- @Override
- @WrapForJNI(dispatchTo = "gecko")
- public native void onSessionError(byte[] sessionId,
- String message);
-
- @Override
- @WrapForJNI(dispatchTo = "gecko")
- public native void onSessionBatchedKeyChanged(byte[] sessionId,
- SessionKeyInfo[] keyInfos);
-
- @Override
- @WrapForJNI(dispatchTo = "gecko")
- public native void onRejectPromise(int promiseId,
- String message);
-
- @Override // JNIObject
- protected void disposeNative() {
- throw new UnsupportedOperationException();
- }
- } // NativeMediaDrmProxyCallbacks
-
- // A proxy to callback from LocalMediaDrmBridge to native instance.
- public static class MediaDrmProxyCallbacks implements GeckoMediaDrm.Callbacks {
- private final Callbacks mNativeCallbacks;
- private final MediaDrmProxy mProxy;
-
- public MediaDrmProxyCallbacks(MediaDrmProxy proxy, Callbacks callbacks) {
- mNativeCallbacks = callbacks;
- mProxy = proxy;
- }
-
- @Override
- public void onSessionCreated(int createSessionToken,
- int promiseId,
- byte[] sessionId,
- byte[] request) {
- if (!mProxy.isDestroyed()) {
- mNativeCallbacks.onSessionCreated(createSessionToken,
- promiseId,
- sessionId,
- request);
- }
- }
-
- @Override
- public void onSessionUpdated(int promiseId, byte[] sessionId) {
- if (!mProxy.isDestroyed()) {
- mNativeCallbacks.onSessionUpdated(promiseId, sessionId);
- }
- }
-
- @Override
- public void onSessionClosed(int promiseId, byte[] sessionId) {
- if (!mProxy.isDestroyed()) {
- mNativeCallbacks.onSessionClosed(promiseId, sessionId);
- }
- }
-
- @Override
- public void onSessionMessage(byte[] sessionId,
- int sessionMessageType,
- byte[] request) {
- if (!mProxy.isDestroyed()) {
- mNativeCallbacks.onSessionMessage(sessionId, sessionMessageType, request);
- }
- }
-
- @Override
- public void onSessionError(byte[] sessionId,
- String message) {
- if (!mProxy.isDestroyed()) {
- mNativeCallbacks.onSessionError(sessionId, message);
- }
- }
-
- @Override
- public void onSessionBatchedKeyChanged(byte[] sessionId,
- SessionKeyInfo[] keyInfos) {
- if (!mProxy.isDestroyed()) {
- mNativeCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos);
- }
- }
-
- @Override
- public void onRejectPromise(int promiseId,
- String message) {
- if (!mProxy.isDestroyed()) {
- mNativeCallbacks.onRejectPromise(promiseId, message);
- }
- }
- } // MediaDrmProxyCallbacks
-
- public boolean isDestroyed() {
- return mDestroyed;
- }
-
- @WrapForJNI(calledFrom = "gecko")
- public static MediaDrmProxy create(String keySystem,
- Callbacks nativeCallbacks,
- boolean isRemote) {
- // TODO: Will implement {Local,Remote}MediaDrmBridge instantiation by
- // '''isRemote''' flag in Bug 1307818.
- MediaDrmProxy proxy = new MediaDrmProxy(keySystem, nativeCallbacks);
- return proxy;
- }
-
- MediaDrmProxy(String keySystem, Callbacks nativeCallbacks) {
- if (DEBUG) Log.d(LOGTAG, "Constructing MediaDrmProxy");
- // TODO: Bug 1306185 will implement the LocalMediaDrmBridge as an impl
- // of GeckoMediaDrm for in-process decoding mode.
- //mImpl = new LocalMediaDrmBridge(keySystem);
- mImpl.setCallbacks(new MediaDrmProxyCallbacks(this, nativeCallbacks));
- mProxyList.add(this);
- }
-
- @WrapForJNI
- private void createSession(int createSessionToken,
- int promiseId,
- String initDataType,
- byte[] initData) {
- if (DEBUG) Log.d(LOGTAG, "createSession, promiseId = " + promiseId);
- mImpl.createSession(createSessionToken,
- promiseId,
- initDataType,
- initData);
- }
-
- @WrapForJNI
- private void updateSession(int promiseId, String sessionId, byte[] response) {
- if (DEBUG) Log.d(LOGTAG, "updateSession, primiseId(" + promiseId + "sessionId(" + sessionId + ")");
- mImpl.updateSession(promiseId, sessionId, response);
- }
-
- @WrapForJNI
- private void closeSession(int promiseId, String sessionId) {
- if (DEBUG) Log.d(LOGTAG, "closeSession, primiseId(" + promiseId + "sessionId(" + sessionId + ")");
- mImpl.closeSession(promiseId, sessionId);
- }
-
- @WrapForJNI // Called when natvie object is destroyed.
- private void destroy() {
- if (DEBUG) Log.d(LOGTAG, "destroy!! Native object is destroyed.");
- if (mDestroyed) {
- return;
- }
- mDestroyed = true;
- release();
- }
-
- private void release() {
- if (DEBUG) Log.d(LOGTAG, "release");
- mProxyList.remove(this);
- mImpl.release();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/media/MediaManager.java b/mobile/android/base/java/org/mozilla/gecko/media/MediaManager.java
deleted file mode 100644
index fcb0fc659..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/media/MediaManager.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.media;
-
-import android.app.Service;
-import android.content.Intent;
-import android.os.Binder;
-import android.os.IBinder;
-import android.os.RemoteException;
-
-import org.mozilla.gecko.mozglue.GeckoLoader;
-
-public final class MediaManager extends Service {
- private static boolean sNativeLibLoaded;
-
- private Binder mBinder = new IMediaManager.Stub() {
- @Override
- public ICodec createCodec() throws RemoteException {
- return new Codec();
- }
-
- @Override
- public IMediaDrmBridge createRemoteMediaDrmBridge(String keySystem,
- String stubId)
- throws RemoteException {
- return new RemoteMediaDrmBridgeStub(keySystem, stubId);
- }
- };
-
- @Override
- public synchronized void onCreate() {
- if (!sNativeLibLoaded) {
- GeckoLoader.doLoadLibrary(this, "mozglue");
- sNativeLibLoaded = true;
- }
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return mBinder;
- }
-} \ No newline at end of file
diff --git a/mobile/android/base/java/org/mozilla/gecko/media/RemoteManager.java b/mobile/android/base/java/org/mozilla/gecko/media/RemoteManager.java
deleted file mode 100644
index 260ca73c1..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/media/RemoteManager.java
+++ /dev/null
@@ -1,224 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.media;
-
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.Telemetry;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.media.MediaFormat;
-import android.os.DeadObjectException;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.view.Surface;
-import android.util.Log;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.LinkedList;
-import java.util.List;
-
-public final class RemoteManager implements IBinder.DeathRecipient {
- private static final String LOGTAG = "GeckoRemoteManager";
- private static final boolean DEBUG = false;
- private static RemoteManager sRemoteManager = null;
-
- public synchronized static RemoteManager getInstance() {
- if (sRemoteManager == null) {
- sRemoteManager = new RemoteManager();
- }
-
- sRemoteManager.init();
- return sRemoteManager;
- }
-
- private List<CodecProxy> mProxies = new LinkedList<CodecProxy>();
- private volatile IMediaManager mRemote;
- private volatile CountDownLatch mConnectionLatch;
- private final ServiceConnection mConnection = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- if (DEBUG) Log.d(LOGTAG, "service connected");
- try {
- service.linkToDeath(RemoteManager.this, 0);
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- mRemote = IMediaManager.Stub.asInterface(service);
- if (mConnectionLatch != null) {
- mConnectionLatch.countDown();
- }
- }
-
- /**
- * Called when a connection to the Service has been lost. This typically
- * happens when the process hosting the service has crashed or been killed.
- * This does <em>not</em> remove the ServiceConnection itself -- this
- * binding to the service will remain active, and you will receive a call
- * to {@link #onServiceConnected} when the Service is next running.
- *
- * @param name The concrete component name of the service whose
- * connection has been lost.
- */
- @Override
- public void onServiceDisconnected(ComponentName name) {
- if (DEBUG) Log.d(LOGTAG, "service disconnected");
- mRemote.asBinder().unlinkToDeath(RemoteManager.this, 0);
- mRemote = null;
- if (mConnectionLatch != null) {
- mConnectionLatch.countDown();
- }
- }
- };
-
- private synchronized boolean init() {
- if (mRemote != null) {
- return true;
- }
-
- if (DEBUG) Log.d(LOGTAG, "init remote manager " + this);
- Context appCtxt = GeckoAppShell.getApplicationContext();
- if (DEBUG) Log.d(LOGTAG, "ctxt=" + appCtxt);
- appCtxt.bindService(new Intent(appCtxt, MediaManager.class),
- mConnection, Context.BIND_AUTO_CREATE);
- if (!waitConnection()) {
- appCtxt.unbindService(mConnection);
- return false;
- }
- return true;
- }
-
- private boolean waitConnection() {
- boolean ok = false;
-
- mConnectionLatch = new CountDownLatch(1);
- try {
- int retryCount = 0;
- while (retryCount < 5) {
- if (DEBUG) Log.d(LOGTAG, "waiting for connection latch:" + mConnectionLatch);
- mConnectionLatch.await(1, TimeUnit.SECONDS);
- if (mConnectionLatch.getCount() == 0) {
- break;
- }
- Log.w(LOGTAG, "Creator not connected in 1s. Try again.");
- retryCount++;
- }
- ok = true;
- } catch (InterruptedException e) {
- Log.e(LOGTAG, "service not connected in 5 seconds. Stop waiting.");
- e.printStackTrace();
- }
- mConnectionLatch = null;
-
- return ok;
- }
-
- public synchronized CodecProxy createCodec(MediaFormat format,
- Surface surface,
- CodecProxy.Callbacks callbacks) {
- if (mRemote == null) {
- if (DEBUG) Log.d(LOGTAG, "createCodec failed due to not initialize");
- return null;
- }
- try {
- ICodec remote = mRemote.createCodec();
- CodecProxy proxy = CodecProxy.createCodecProxy(format, surface, callbacks);
- if (proxy.init(remote)) {
- mProxies.add(proxy);
- return proxy;
- } else {
- return null;
- }
- } catch (RemoteException e) {
- e.printStackTrace();
- return null;
- }
- }
-
- private static final String MEDIA_DECODING_PROCESS_CRASH = "MEDIA_DECODING_PROCESS_CRASH";
- private void reportDecodingProcessCrash() {
- Telemetry.addToHistogram(MEDIA_DECODING_PROCESS_CRASH, 1);
- }
-
- public synchronized IMediaDrmBridge createRemoteMediaDrmBridge(String keySystem,
- String stubId) {
- if (mRemote == null) {
- if (DEBUG) Log.d(LOGTAG, "createRemoteMediaDrmBridge failed due to not initialize");
- return null;
- }
- try {
- IMediaDrmBridge remoteBridge =
- mRemote.createRemoteMediaDrmBridge(keySystem, stubId);
- return remoteBridge;
- } catch (RemoteException e) {
- Log.e(LOGTAG, "Got exception during createRemoteMediaDrmBridge().", e);
- return null;
- }
- }
-
- @Override
- public void binderDied() {
- Log.e(LOGTAG, "remote codec is dead");
- reportDecodingProcessCrash();
- handleRemoteDeath();
- }
-
- private synchronized void handleRemoteDeath() {
- // Wait for onServiceDisconnected()
- if (!waitConnection()) {
- notifyError(true);
- return;
- }
- // Restart
- if (init() && recoverRemoteCodec()) {
- notifyError(false);
- } else {
- notifyError(true);
- }
- }
-
- private synchronized void notifyError(boolean fatal) {
- for (CodecProxy proxy : mProxies) {
- proxy.reportError(fatal);
- }
- }
-
- private synchronized boolean recoverRemoteCodec() {
- if (DEBUG) Log.d(LOGTAG, "recover codec");
- boolean ok = true;
- try {
- for (CodecProxy proxy : mProxies) {
- ok &= proxy.init(mRemote.createCodec());
- }
- return ok;
- } catch (RemoteException e) {
- return false;
- }
- }
-
- public void releaseCodec(CodecProxy proxy) throws DeadObjectException, RemoteException {
- if (mRemote == null) {
- if (DEBUG) Log.d(LOGTAG, "releaseCodec called but not initialized yet");
- return;
- }
- proxy.deinit();
- synchronized (this) {
- if (mProxies.remove(proxy) && mProxies.isEmpty()) {
- release();
- }
- }
- }
-
- private void release() {
- if (DEBUG) Log.d(LOGTAG, "release remote manager " + this);
- Context appCtxt = GeckoAppShell.getApplicationContext();
- mRemote.asBinder().unlinkToDeath(this, 0);
- mRemote = null;
- appCtxt.unbindService(mConnection);
- }
-} // RemoteManager \ No newline at end of file
diff --git a/mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridge.java b/mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridge.java
deleted file mode 100644
index d65bb7872..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridge.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.media;
-
-import android.media.MediaCrypto;
-import android.util.Log;
-
-final class RemoteMediaDrmBridge implements GeckoMediaDrm {
- private static final String LOGTAG = "GeckoRemoteMediaDrmBridge";
- private static final boolean DEBUG = false;
- private CallbacksForwarder mCallbacksFwd;
- private IMediaDrmBridge mRemote;
-
- // Forward callbacks from remote bridge stub to MediaDrmProxy.
- private static class CallbacksForwarder extends IMediaDrmBridgeCallbacks.Stub {
- private final GeckoMediaDrm.Callbacks mProxyCallbacks;
- CallbacksForwarder(Callbacks callbacks) {
- assertTrue(callbacks != null);
- mProxyCallbacks = callbacks;
- }
-
- @Override
- public void onSessionCreated(int createSessionToken,
- int promiseId,
- byte[] sessionId,
- byte[] request) {
- mProxyCallbacks.onSessionCreated(createSessionToken,
- promiseId,
- sessionId,
- request);
- }
-
- @Override
- public void onSessionUpdated(int promiseId, byte[] sessionId) {
- mProxyCallbacks.onSessionUpdated(promiseId, sessionId);
- }
-
- @Override
- public void onSessionClosed(int promiseId, byte[] sessionId) {
- mProxyCallbacks.onSessionClosed(promiseId, sessionId);
- }
-
- @Override
- public void onSessionMessage(byte[] sessionId,
- int sessionMessageType,
- byte[] request) {
- mProxyCallbacks.onSessionMessage(sessionId, sessionMessageType, request);
- }
-
- @Override
- public void onSessionError(byte[] sessionId, String message) {
- mProxyCallbacks.onSessionError(sessionId, message);
- }
-
- @Override
- public void onSessionBatchedKeyChanged(byte[] sessionId,
- SessionKeyInfo[] keyInfos) {
- mProxyCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos);
- }
-
- @Override
- public void onRejectPromise(int promiseId, String message) {
- mProxyCallbacks.onRejectPromise(promiseId, message);
- }
- } // CallbacksForwarder
-
- /* package-private */ static void assertTrue(boolean condition) {
- if (DEBUG && !condition) {
- throw new AssertionError("Expected condition to be true");
- }
- }
-
- public RemoteMediaDrmBridge(IMediaDrmBridge remoteBridge) {
- assertTrue(remoteBridge != null);
- mRemote = remoteBridge;
- }
-
- @Override
- public synchronized void setCallbacks(Callbacks callbacks) {
- if (DEBUG) Log.d(LOGTAG, "setCallbacks()");
- assertTrue(callbacks != null);
- assertTrue(mRemote != null);
-
- mCallbacksFwd = new CallbacksForwarder(callbacks);
- try {
- mRemote.setCallbacks(mCallbacksFwd);
- } catch (Exception e) {
- Log.e(LOGTAG, "Got exception during setCallbacks", e);
- }
- }
-
- @Override
- public synchronized void createSession(int createSessionToken,
- int promiseId,
- String initDataType,
- byte[] initData) {
- if (DEBUG) Log.d(LOGTAG, "createSession()");
-
- try {
- mRemote.createSession(createSessionToken, promiseId, initDataType, initData);
- } catch (Exception e) {
- Log.e(LOGTAG, "Got exception while creating remote session.", e);
- mCallbacksFwd.onRejectPromise(promiseId, "Failed to create session.");
- }
- }
-
- @Override
- public synchronized void updateSession(int promiseId, String sessionId, byte[] response) {
- if (DEBUG) Log.d(LOGTAG, "updateSession()");
-
- try {
- mRemote.updateSession(promiseId, sessionId, response);
- } catch (Exception e) {
- Log.e(LOGTAG, "Got exception while updating remote session.", e);
- mCallbacksFwd.onRejectPromise(promiseId, "Failed to update session.");
- }
- }
-
- @Override
- public synchronized void closeSession(int promiseId, String sessionId) {
- if (DEBUG) Log.d(LOGTAG, "closeSession()");
-
- try {
- mRemote.closeSession(promiseId, sessionId);
- } catch (Exception e) {
- Log.e(LOGTAG, "Got exception while closing remote session.", e);
- mCallbacksFwd.onRejectPromise(promiseId, "Failed to close session.");
- }
- }
-
- @Override
- public synchronized void release() {
- if (DEBUG) Log.d(LOGTAG, "release()");
-
- try {
- mRemote.release();
- } catch (Exception e) {
- Log.e(LOGTAG, "Got exception while releasing RemoteDrmBridge.", e);
- }
- mRemote = null;
- mCallbacksFwd = null;
- }
-
- @Override
- public synchronized MediaCrypto getMediaCrypto() {
- if (DEBUG) Log.d(LOGTAG, "getMediaCrypto(), should not enter here!");
- assertTrue(false);
- return null;
- }
-} \ No newline at end of file
diff --git a/mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridgeStub.java b/mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridgeStub.java
deleted file mode 100644
index 8aed0f851..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridgeStub.java
+++ /dev/null
@@ -1,247 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.media;
-import org.mozilla.gecko.AppConstants;
-
-import java.util.ArrayList;
-
-import android.media.MediaCrypto;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.Log;
-
-final class RemoteMediaDrmBridgeStub extends IMediaDrmBridge.Stub implements IBinder.DeathRecipient {
- private static final String LOGTAG = "GeckoRemoteMediaDrmBridgeStub";
- private static final boolean DEBUG = false;
- private volatile IMediaDrmBridgeCallbacks mCallbacks = null;
-
- // Underlying bridge implmenetaion, i.e. GeckoMediaDrmBrdigeV21.
- private GeckoMediaDrm mBridge = null;
-
- // mStubId is initialized during stub construction. It should be a unique
- // string which is generated in MediaDrmProxy in Fennec App process and is
- // used for Codec to obtain corresponding MediaCrypto as input to achieve
- // decryption.
- // The generated stubId will be delivered to Codec via a code path starting
- // from MediaDrmProxy -> MediaDrmCDMProxy -> RemoteDataDecoder => IPC => Codec.
- private String mStubId = "";
-
- public static ArrayList<RemoteMediaDrmBridgeStub> mBridgeStubs =
- new ArrayList<RemoteMediaDrmBridgeStub>();
-
- private String getId() {
- return mStubId;
- }
-
- private MediaCrypto getMediaCryptoFromBridge() {
- return mBridge != null ? mBridge.getMediaCrypto() : null;
- }
-
- public static synchronized MediaCrypto getMediaCrypto(String stubId) {
- if (DEBUG) Log.d(LOGTAG, "getMediaCrypto()");
-
- for (int i = 0; i < mBridgeStubs.size(); i++) {
- if (mBridgeStubs.get(i) != null &&
- mBridgeStubs.get(i).getId().equals(stubId)) {
- return mBridgeStubs.get(i).getMediaCryptoFromBridge();
- }
- }
- return null;
- }
-
- // Callback to RemoteMediaDrmBridge.
- private final class Callbacks implements GeckoMediaDrm.Callbacks {
- private IMediaDrmBridgeCallbacks mRemoteCallbacks;
-
- public Callbacks(IMediaDrmBridgeCallbacks remote) {
- mRemoteCallbacks = remote;
- }
-
- @Override
- public void onSessionCreated(int createSessionToken,
- int promiseId,
- byte[] sessionId,
- byte[] request) {
- if (DEBUG) Log.d(LOGTAG, "onSessionCreated()");
- try {
- mRemoteCallbacks.onSessionCreated(createSessionToken,
- promiseId,
- sessionId,
- request);
- } catch (RemoteException e) {
- Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
- }
- }
-
- @Override
- public void onSessionUpdated(int promiseId, byte[] sessionId) {
- if (DEBUG) Log.d(LOGTAG, "onSessionUpdated()");
- try {
- mRemoteCallbacks.onSessionUpdated(promiseId, sessionId);
- } catch (RemoteException e) {
- Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
- }
- }
-
- @Override
- public void onSessionClosed(int promiseId, byte[] sessionId) {
- if (DEBUG) Log.d(LOGTAG, "onSessionClosed()");
- try {
- mRemoteCallbacks.onSessionClosed(promiseId, sessionId);
- } catch (RemoteException e) {
- Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
- }
- }
-
- @Override
- public void onSessionMessage(byte[] sessionId,
- int sessionMessageType,
- byte[] request) {
- if (DEBUG) Log.d(LOGTAG, "onSessionMessage()");
- try {
- mRemoteCallbacks.onSessionMessage(sessionId, sessionMessageType, request);
- } catch (RemoteException e) {
- Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
- }
- }
-
- @Override
- public void onSessionError(byte[] sessionId, String message) {
- if (DEBUG) Log.d(LOGTAG, "onSessionError()");
- try {
- mRemoteCallbacks.onSessionError(sessionId, message);
- } catch (RemoteException e) {
- Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
- }
- }
-
- @Override
- public void onSessionBatchedKeyChanged(byte[] sessionId,
- SessionKeyInfo[] keyInfos) {
- if (DEBUG) Log.d(LOGTAG, "onSessionBatchedKeyChanged()");
- try {
- mRemoteCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos);
- } catch (RemoteException e) {
- Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
- }
- }
-
- @Override
- public void onRejectPromise(int promiseId, String message) {
- if (DEBUG) Log.d(LOGTAG, "onRejectPromise()");
- try {
- mRemoteCallbacks.onRejectPromise(promiseId, message);
- } catch (RemoteException e) {
- Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
- }
- }
- }
-
- /* package-private */ void assertTrue(boolean condition) {
- if (DEBUG && !condition) {
- throw new AssertionError("Expected condition to be true");
- }
- }
-
- RemoteMediaDrmBridgeStub(String keySystem, String stubId) throws RemoteException {
- if (AppConstants.Versions.preLollipop) {
- Log.e(LOGTAG, "Pre-Lollipop should never enter here!!");
- throw new RemoteException("Error, unsupported version!");
- }
- try {
- if (AppConstants.Versions.feature21Plus &&
- AppConstants.Versions.preMarshmallow) {
- mBridge = new GeckoMediaDrmBridgeV21(keySystem);
- } else {
- mBridge = new GeckoMediaDrmBridgeV23(keySystem);
- }
- mStubId = stubId;
- mBridgeStubs.add(this);
- } catch (Exception e) {
- throw new RemoteException("RemoteMediaDrmBridgeStub cannot create bridge implementation.");
- }
- }
-
- @Override
- public synchronized void setCallbacks(IMediaDrmBridgeCallbacks callbacks) throws RemoteException {
- if (DEBUG) Log.d(LOGTAG, "setCallbacks()");
- assertTrue(mBridge != null);
- assertTrue(callbacks != null);
- mCallbacks = callbacks;
- callbacks.asBinder().linkToDeath(this, 0);
- mBridge.setCallbacks(new Callbacks(mCallbacks));
- }
-
- @Override
- public synchronized void createSession(int createSessionToken,
- int promiseId,
- String initDataType,
- byte[] initData) throws RemoteException {
- if (DEBUG) Log.d(LOGTAG, "createSession()");
- try {
- assertTrue(mCallbacks != null);
- assertTrue(mBridge != null);
- mBridge.createSession(createSessionToken,
- promiseId,
- initDataType,
- initData);
- } catch (Exception e) {
- Log.e(LOGTAG, "Failed to createSession.", e);
- mCallbacks.onRejectPromise(promiseId, "Failed to createSession.");
- }
- }
-
- @Override
- public synchronized void updateSession(int promiseId,
- String sessionId,
- byte[] response) throws RemoteException {
- if (DEBUG) Log.d(LOGTAG, "updateSession()");
- try {
- assertTrue(mCallbacks != null);
- assertTrue(mBridge != null);
- mBridge.updateSession(promiseId, sessionId, response);
- } catch (Exception e) {
- Log.e(LOGTAG, "Failed to updateSession.", e);
- mCallbacks.onRejectPromise(promiseId, "Failed to updateSession.");
- }
- }
-
- @Override
- public synchronized void closeSession(int promiseId, String sessionId) throws RemoteException {
- if (DEBUG) Log.d(LOGTAG, "closeSession()");
- try {
- assertTrue(mCallbacks != null);
- assertTrue(mBridge != null);
- mBridge.closeSession(promiseId, sessionId);
- } catch (Exception e) {
- Log.e(LOGTAG, "Failed to closeSession.", e);
- mCallbacks.onRejectPromise(promiseId, "Failed to closeSession.");
- }
- }
-
- // IBinder.DeathRecipient
- @Override
- public synchronized void binderDied() {
- Log.e(LOGTAG, "Binder died !!");
- try {
- release();
- } catch (Exception e) {
- Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
- }
- }
-
- @Override
- public synchronized void release() {
- if (DEBUG) Log.d(LOGTAG, "release()");
- mBridgeStubs.remove(this);
- if (mBridge != null) {
- mBridge.release();
- mBridge = null;
- }
- mCallbacks.asBinder().unlinkToDeath(this, 0);
- mCallbacks = null;
- mStubId = "";
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/media/Sample.java b/mobile/android/base/java/org/mozilla/gecko/media/Sample.java
deleted file mode 100644
index b7a98da8a..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/media/Sample.java
+++ /dev/null
@@ -1,264 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.media;
-
-import android.media.MediaCodec;
-import android.media.MediaCodec.BufferInfo;
-import android.media.MediaCodec.CryptoInfo;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.mozglue.SharedMemBuffer;
-import org.mozilla.gecko.mozglue.SharedMemory;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-
-// Parcelable carrying input/output sample data and info cross process.
-public final class Sample implements Parcelable {
- public static final Sample EOS;
- static {
- BufferInfo eosInfo = new BufferInfo();
- eosInfo.set(0, 0, Long.MIN_VALUE, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
- EOS = new Sample(null, eosInfo, null);
- }
-
- public interface Buffer extends Parcelable {
- int capacity();
- void readFromByteBuffer(ByteBuffer src, int offset, int size) throws IOException;
- void writeToByteBuffer(ByteBuffer dest, int offset, int size) throws IOException;
- void dispose();
- }
-
- private static final class ArrayBuffer implements Buffer {
- private byte[] mArray;
-
- public static final Creator<ArrayBuffer> CREATOR = new Creator<ArrayBuffer>() {
- @Override
- public ArrayBuffer createFromParcel(Parcel in) {
- return new ArrayBuffer(in);
- }
-
- @Override
- public ArrayBuffer[] newArray(int size) {
- return new ArrayBuffer[size];
- }
- };
-
- private ArrayBuffer(Parcel in) {
- mArray = in.createByteArray();
- }
-
- private ArrayBuffer(byte[] bytes) { mArray = bytes; }
-
- @Override
- public int describeContents() { return 0; }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeByteArray(mArray);
- }
-
- @Override
- public int capacity() {
- return mArray != null ? mArray.length : 0;
- }
-
- @Override
- public void readFromByteBuffer(ByteBuffer src, int offset, int size) throws IOException {
- src.position(offset);
- if (mArray == null || mArray.length != size) {
- mArray = new byte[size];
- }
- src.get(mArray, 0, size);
- }
-
- @Override
- public void writeToByteBuffer(ByteBuffer dest, int offset, int size) throws IOException {
- dest.put(mArray, offset, size);
- }
-
- @Override
- public void dispose() {
- mArray = null;
- }
- }
-
- public Buffer buffer;
- @WrapForJNI
- public BufferInfo info;
- public CryptoInfo cryptoInfo;
-
- public static Sample create() { return create(null, new BufferInfo(), null); }
-
- public static Sample create(ByteBuffer src, BufferInfo info, CryptoInfo cryptoInfo) {
- ArrayBuffer buffer = new ArrayBuffer(byteArrayFromBuffer(src, info.offset, info.size));
-
- BufferInfo bufferInfo = new BufferInfo();
- bufferInfo.set(0, info.size, info.presentationTimeUs, info.flags);
-
- return new Sample(buffer, bufferInfo, cryptoInfo);
- }
-
- public static Sample create(SharedMemory sharedMem) {
- return new Sample(new SharedMemBuffer(sharedMem), new BufferInfo(), null);
- }
-
- private Sample(Buffer bytes, BufferInfo info, CryptoInfo cryptoInfo) {
- buffer = bytes;
- this.info = info;
- this.cryptoInfo = cryptoInfo;
- }
-
- private Sample(Parcel in) {
- readInfo(in);
- readCrypto(in);
- buffer = in.readParcelable(Sample.class.getClassLoader());
- }
-
- private void readInfo(Parcel in) {
- int offset = in.readInt();
- int size = in.readInt();
- long pts = in.readLong();
- int flags = in.readInt();
-
- info = new BufferInfo();
- info.set(offset, size, pts, flags);
- }
-
- private void readCrypto(Parcel in) {
- int hasCryptoInfo = in.readInt();
- if (hasCryptoInfo == 0) {
- return;
- }
-
- byte[] iv = in.createByteArray();
- byte[] key = in.createByteArray();
- int mode = in.readInt();
- int[] numBytesOfClearData = in.createIntArray();
- int[] numBytesOfEncryptedData = in.createIntArray();
- int numSubSamples = in.readInt();
-
- cryptoInfo = new CryptoInfo();
- cryptoInfo.set(numSubSamples,
- numBytesOfClearData,
- numBytesOfEncryptedData,
- key,
- iv,
- mode);
- }
-
- public Sample set(ByteBuffer bytes, BufferInfo info, CryptoInfo cryptoInfo) throws IOException {
- if (bytes != null && info.size > 0) {
- buffer.readFromByteBuffer(bytes, info.offset, info.size);
- }
- this.info.set(0, info.size, info.presentationTimeUs, info.flags);
- this.cryptoInfo = cryptoInfo;
-
- return this;
- }
-
- public void dispose() {
- if (isEOS()) {
- return;
- }
-
- if (buffer != null) {
- buffer.dispose();
- buffer = null;
- }
- info = null;
- cryptoInfo = null;
- }
-
- public boolean isEOS() {
- return (this == EOS) ||
- ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0);
- }
-
- public static final Creator<Sample> CREATOR = new Creator<Sample>() {
- @Override
- public Sample createFromParcel(Parcel in) {
- return new Sample(in);
- }
-
- @Override
- public Sample[] newArray(int size) {
- return new Sample[size];
- }
- };
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int parcelableFlags) {
- writeInfo(dest);
- writeCrypto(dest);
- dest.writeParcelable(buffer, parcelableFlags);
- }
-
- private void writeInfo(Parcel dest) {
- dest.writeInt(info.offset);
- dest.writeInt(info.size);
- dest.writeLong(info.presentationTimeUs);
- dest.writeInt(info.flags);
- }
-
- private void writeCrypto(Parcel dest) {
- if (cryptoInfo != null) {
- dest.writeInt(1);
- dest.writeByteArray(cryptoInfo.iv);
- dest.writeByteArray(cryptoInfo.key);
- dest.writeInt(cryptoInfo.mode);
- dest.writeIntArray(cryptoInfo.numBytesOfClearData);
- dest.writeIntArray(cryptoInfo.numBytesOfEncryptedData);
- dest.writeInt(cryptoInfo.numSubSamples);
- } else {
- dest.writeInt(0);
- }
- }
-
- public static byte[] byteArrayFromBuffer(ByteBuffer buffer, int offset, int size) {
- if (buffer == null || buffer.capacity() == 0 || size == 0) {
- return null;
- }
- if (buffer.hasArray() && offset == 0 && buffer.array().length == size) {
- return buffer.array();
- }
- int length = Math.min(offset + size, buffer.capacity()) - offset;
- byte[] bytes = new byte[length];
- buffer.position(offset);
- buffer.get(bytes);
- return bytes;
- }
-
- @WrapForJNI
- public void writeToByteBuffer(ByteBuffer dest) throws IOException {
- if (buffer != null && dest != null && info.size > 0) {
- buffer.writeToByteBuffer(dest, info.offset, info.size);
- }
- }
-
- @Override
- public String toString() {
- if (isEOS()) {
- return "EOS sample";
- }
-
- StringBuilder str = new StringBuilder();
- str.append("{ buffer=").append(buffer).
- append(", info=").
- append("{ offset=").append(info.offset).
- append(", size=").append(info.size).
- append(", pts=").append(info.presentationTimeUs).
- append(", flags=").append(Integer.toHexString(info.flags)).append(" }").
- append(" }");
- return str.toString();
- }
-} \ No newline at end of file
diff --git a/mobile/android/base/java/org/mozilla/gecko/media/SamplePool.java b/mobile/android/base/java/org/mozilla/gecko/media/SamplePool.java
deleted file mode 100644
index 9041e3756..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/media/SamplePool.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.media;
-
-import android.media.MediaCodec;
-
-import org.mozilla.gecko.mozglue.SharedMemory;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-final class SamplePool {
- private final class Impl {
- private final String mName;
- private int mNextId = 0;
- private int mDefaultBufferSize = 4096;
- private final List<Sample> mRecycledSamples = new ArrayList<>();
-
- private Impl(String name) {
- mName = name;
- }
-
- private void setDefaultBufferSize(int size) {
- mDefaultBufferSize = size;
- }
-
- private synchronized Sample allocate(int size) {
- Sample sample;
- if (!mRecycledSamples.isEmpty()) {
- sample = mRecycledSamples.remove(0);
- sample.info.set(0, 0, 0, 0);
- } else {
- SharedMemory shm = null;
- try {
- shm = new SharedMemory(mNextId++, Math.max(size, mDefaultBufferSize));
- } catch (NoSuchMethodException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- }
- if (shm != null) {
- sample = Sample.create(shm);
- } else {
- sample = Sample.create();
- }
- }
-
- return sample;
- }
-
- private synchronized void recycle(Sample recycled) {
- if (recycled.buffer.capacity() >= mDefaultBufferSize) {
- mRecycledSamples.add(recycled);
- } else {
- recycled.dispose();
- }
- }
-
- private synchronized void clear() {
- for (Sample s : mRecycledSamples) {
- s.dispose();
- }
-
- mRecycledSamples.clear();
- }
-
- @Override
- protected void finalize() {
- clear();
- }
- }
-
- private final Impl mInputs;
- private final Impl mOutputs;
-
- /* package */ SamplePool(String name) {
- mInputs = new Impl(name + " input buffer pool");
- mOutputs = new Impl(name + " output buffer pool");
- }
-
- /* package */ void setInputBufferSize(int size) {
- mInputs.setDefaultBufferSize(size);
- }
-
- /* package */ void setOutputBufferSize(int size) {
- mOutputs.setDefaultBufferSize(size);
- }
-
- /* package */ Sample obtainInput(int size) {
- return mInputs.allocate(size);
- }
-
- /* package */ Sample obtainOutput(MediaCodec.BufferInfo info) {
- Sample output = mOutputs.allocate(info.size);
- output.info.set(0, info.size, info.presentationTimeUs, info.flags);
- return output;
- }
-
- /* package */ void recycleInput(Sample sample) {
- sample.cryptoInfo = null;
- mInputs.recycle(sample);
- }
-
- /* package */ void recycleOutput(Sample sample) {
- mOutputs.recycle(sample);
- }
-
- /* package */ void reset() {
- mInputs.clear();
- mOutputs.clear();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/media/SessionKeyInfo.java b/mobile/android/base/java/org/mozilla/gecko/media/SessionKeyInfo.java
deleted file mode 100644
index b41ef3625..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/media/SessionKeyInfo.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.media;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-import org.mozilla.gecko.annotation.WrapForJNI;
-
-public final class SessionKeyInfo implements Parcelable {
- @WrapForJNI
- public byte[] keyId;
-
- @WrapForJNI
- public int status;
-
- @WrapForJNI
- public SessionKeyInfo(byte[] keyId, int status) {
- this.keyId = keyId;
- this.status = status;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int parcelableFlags) {
- dest.writeByteArray(keyId);
- dest.writeInt(status);
- }
-
- public static final Creator<SessionKeyInfo> CREATOR = new Creator<SessionKeyInfo>() {
- @Override
- public SessionKeyInfo createFromParcel(Parcel in) {
- return new SessionKeyInfo(in);
- }
-
- @Override
- public SessionKeyInfo[] newArray(int size) {
- return new SessionKeyInfo[size];
- }
- };
-
- private SessionKeyInfo(Parcel src) {
- keyId = src.createByteArray();
- status = src.readInt();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/media/VideoPlayer.java b/mobile/android/base/java/org/mozilla/gecko/media/VideoPlayer.java
deleted file mode 100644
index 508b9d015..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/media/VideoPlayer.java
+++ /dev/null
@@ -1,204 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.media;
-
-import android.content.Context;
-
-import android.graphics.Color;
-
-import android.net.Uri;
-
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.Gravity;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-
-import android.widget.ImageButton;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.MediaController;
-import android.widget.VideoView;
-
-import org.mozilla.gecko.R;
-
-public class VideoPlayer extends FrameLayout {
- private VideoView video;
- private FullScreenMediaController controller;
- private FullScreenListener fullScreenListener;
-
- private boolean isFullScreen;
-
- public VideoPlayer(Context ctx) {
- this(ctx, null);
- }
-
- public VideoPlayer(Context ctx, AttributeSet attrs) {
- this(ctx, attrs, 0);
- }
-
- public VideoPlayer(Context ctx, AttributeSet attrs, int defStyle) {
- super(ctx, attrs, defStyle);
- setFullScreen(false);
- setVisibility(View.GONE);
- }
-
- public void start(Uri uri) {
- stop();
-
- video = new VideoView(getContext());
- controller = new FullScreenMediaController(getContext());
- video.setMediaController(controller);
- controller.setAnchorView(video);
-
- video.setVideoURI(uri);
-
- FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
- FrameLayout.LayoutParams.MATCH_PARENT,
- FrameLayout.LayoutParams.WRAP_CONTENT,
- Gravity.CENTER);
-
- addView(video, layoutParams);
- setVisibility(View.VISIBLE);
-
- video.setZOrderOnTop(true);
- video.start();
- }
-
- public boolean isPlaying() {
- return video != null;
- }
-
- public void stop() {
- if (video == null) {
- return;
- }
-
- removeAllViews();
- setVisibility(View.GONE);
- video.stopPlayback();
-
- video = null;
- controller = null;
- }
-
- public void setFullScreenListener(FullScreenListener listener) {
- fullScreenListener = listener;
- }
-
- public boolean isFullScreen() {
- return isFullScreen;
- }
-
- public void setFullScreen(boolean fullScreen) {
- isFullScreen = fullScreen;
- if (fullScreen) {
- setBackgroundColor(Color.BLACK);
- } else {
- setBackgroundResource(R.color.dark_transparent_overlay);
- }
-
- if (controller != null) {
- controller.setFullScreen(fullScreen);
- }
- }
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- if (event.isSystem()) {
- return super.onKeyDown(keyCode, event);
- }
- return true;
- }
-
- @Override
- public boolean onKeyUp(int keyCode, KeyEvent event) {
- if (event.isSystem()) {
- return super.onKeyUp(keyCode, event);
- }
- return true;
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- super.onTouchEvent(event);
- return true;
- }
-
- @Override
- public boolean onTrackballEvent(MotionEvent event) {
- super.onTrackballEvent(event);
- return true;
- }
-
- public interface FullScreenListener {
- void onFullScreenChanged(boolean fullScreen);
- }
-
- private class FullScreenMediaController extends MediaController {
- private ImageButton mButton;
-
- public FullScreenMediaController(Context ctx) {
- super(ctx);
-
- mButton = new ImageButton(getContext());
- mButton.setScaleType(ImageView.ScaleType.FIT_CENTER);
- mButton.setBackgroundColor(Color.TRANSPARENT);
- mButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- FullScreenMediaController.this.onFullScreenClicked();
- }
- });
-
- updateFullScreenButton(false);
- }
-
- public void setFullScreen(boolean fullScreen) {
- updateFullScreenButton(fullScreen);
- }
-
- private void updateFullScreenButton(boolean fullScreen) {
- mButton.setImageResource(fullScreen ? R.drawable.exit_fullscreen : R.drawable.fullscreen);
- }
-
- private void onFullScreenClicked() {
- if (VideoPlayer.this.fullScreenListener != null) {
- boolean fullScreen = !VideoPlayer.this.isFullScreen();
- VideoPlayer.this.fullScreenListener.onFullScreenChanged(fullScreen);
- }
- }
-
- @Override
- public void setAnchorView(final View view) {
- super.setAnchorView(view);
-
- // Add the fullscreen button here because this is where the parent class actually creates
- // the media buttons and their layout.
- //
- // http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/widget/MediaController.java#239
- //
- // The media buttons are in a horizontal linear layout which is itself packed into
- // a vertical layout. The vertical layout is the only child of the FrameLayout which
- // MediaController inherits from.
- LinearLayout child = (LinearLayout) getChildAt(0);
- LinearLayout buttons = (LinearLayout) child.getChildAt(0);
-
- LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
- LayoutParams.FILL_PARENT);
- params.gravity = Gravity.CENTER_VERTICAL;
-
- if (mButton.getParent() != null) {
- ((ViewGroup)mButton.getParent()).removeView(mButton);
- }
-
- buttons.addView(mButton, params);
- }
- }
-} \ No newline at end of file
diff --git a/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenu.java b/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenu.java
deleted file mode 100644
index 512f32002..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenu.java
+++ /dev/null
@@ -1,928 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.menu;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
-import org.mozilla.gecko.widget.GeckoActionProvider;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.util.SparseArray;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.SubMenu;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.BaseAdapter;
-import android.widget.LinearLayout;
-import android.widget.ListView;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class GeckoMenu extends ListView
- implements Menu,
- AdapterView.OnItemClickListener,
- GeckoMenuItem.OnShowAsActionChangedListener {
- private static final String LOGTAG = "GeckoMenu";
-
- /**
- * Controls whether off-UI-thread method calls in this class cause an
- * exception or just logging.
- */
- private static final AssertBehavior THREAD_ASSERT_BEHAVIOR = AppConstants.RELEASE_OR_BETA ? AssertBehavior.NONE : AssertBehavior.THROW;
-
- /*
- * A callback for a menu item click/long click event.
- */
- public static interface Callback {
- // Called when a menu item is clicked, with the actual menu item as the argument.
- public boolean onMenuItemClick(MenuItem item);
-
- // Called when a menu item is long-clicked, with the actual menu item as the argument.
- public boolean onMenuItemLongClick(MenuItem item);
- }
-
- /*
- * An interface for a presenter to show the menu.
- * Either an Activity or a View can be a presenter, that can watch for events
- * and show/hide menu.
- */
- public static interface MenuPresenter {
- // Open the menu.
- public void openMenu();
-
- // Show the actual view containing the menu items. This can either be a parent or sub-menu.
- public void showMenu(View menu);
-
- // Close the menu.
- public void closeMenu();
- }
-
- /*
- * An interface for a presenter of action-items.
- * Either an Activity or a View can be a presenter, that can watch for events
- * and add/remove action-items. If not ActionItemBarPresenter, the menu uses a
- * DefaultActionItemBar, that shows the action-items as a header over list-view.
- */
- public static interface ActionItemBarPresenter {
- // Add an action-item.
- public boolean addActionItem(View actionItem);
-
- // Remove an action-item.
- public void removeActionItem(View actionItem);
- }
-
- protected static final int NO_ID = 0;
-
- // List of all menu items.
- private final List<GeckoMenuItem> mItems;
-
- // Quick lookup array used to make a fast path in findItem.
- private final SparseArray<MenuItem> mItemsById;
-
- // Map of "always" action-items in action-bar and their views.
- private final Map<GeckoMenuItem, View> mPrimaryActionItems;
-
- // Map of "ifRoom" action-items in action-bar and their views.
- private final Map<GeckoMenuItem, View> mSecondaryActionItems;
-
- // Map of "collapseActionView" action-items in action-bar and their views.
- private final Map<GeckoMenuItem, View> mQuickShareActionItems;
-
- // Reference to a callback for menu events.
- private Callback mCallback;
-
- // Reference to menu presenter.
- private MenuPresenter mMenuPresenter;
-
- // Reference to "always" action-items bar in action-bar.
- private ActionItemBarPresenter mPrimaryActionItemBar;
-
- // Reference to "ifRoom" action-items bar in action-bar.
- private final ActionItemBarPresenter mSecondaryActionItemBar;
-
- // Reference to "collapseActionView" action-items bar in action-bar.
- private final ActionItemBarPresenter mQuickShareActionItemBar;
-
- // Adapter to hold the list of menu items.
- private final MenuItemsAdapter mAdapter;
-
- // Show/hide icons in the list.
- boolean mShowIcons;
-
- public GeckoMenu(Context context) {
- this(context, null);
- }
-
- public GeckoMenu(Context context, AttributeSet attrs) {
- this(context, attrs, R.attr.geckoMenuListViewStyle);
- }
-
- public GeckoMenu(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
-
- setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
- LayoutParams.MATCH_PARENT));
-
- // Attach an adapter.
- mAdapter = new MenuItemsAdapter();
- setAdapter(mAdapter);
- setOnItemClickListener(this);
-
- mItems = new ArrayList<GeckoMenuItem>();
- mItemsById = new SparseArray<MenuItem>();
- mPrimaryActionItems = new HashMap<GeckoMenuItem, View>();
- mSecondaryActionItems = new HashMap<GeckoMenuItem, View>();
- mQuickShareActionItems = new HashMap<GeckoMenuItem, View>();
-
- mPrimaryActionItemBar = (DefaultActionItemBar) LayoutInflater.from(context).inflate(R.layout.menu_action_bar, null);
- mSecondaryActionItemBar = (DefaultActionItemBar) LayoutInflater.from(context).inflate(R.layout.menu_secondary_action_bar, null);
- mQuickShareActionItemBar = (DefaultActionItemBar) LayoutInflater.from(context).inflate(R.layout.menu_secondary_action_bar, null);
- }
-
- private static void assertOnUiThread() {
- ThreadUtils.assertOnUiThread(THREAD_ASSERT_BEHAVIOR);
- }
-
- @Override
- public MenuItem add(CharSequence title) {
- GeckoMenuItem menuItem = new GeckoMenuItem(this, NO_ID, 0, title);
- addItem(menuItem);
- return menuItem;
- }
-
- @Override
- public MenuItem add(int groupId, int itemId, int order, int titleRes) {
- GeckoMenuItem menuItem = new GeckoMenuItem(this, itemId, order, titleRes);
- addItem(menuItem);
- return menuItem;
- }
-
- @Override
- public MenuItem add(int titleRes) {
- GeckoMenuItem menuItem = new GeckoMenuItem(this, NO_ID, 0, titleRes);
- addItem(menuItem);
- return menuItem;
- }
-
- @Override
- public MenuItem add(int groupId, int itemId, int order, CharSequence title) {
- GeckoMenuItem menuItem = new GeckoMenuItem(this, itemId, order, title);
- addItem(menuItem);
- return menuItem;
- }
-
- private void addItem(GeckoMenuItem menuItem) {
- assertOnUiThread();
- menuItem.setOnShowAsActionChangedListener(this);
- mAdapter.addMenuItem(menuItem);
- mItems.add(menuItem);
- }
-
- private boolean addActionItem(final GeckoMenuItem menuItem) {
- assertOnUiThread();
- menuItem.setOnShowAsActionChangedListener(this);
-
- final View actionView = menuItem.getActionView();
- final int actionEnum = menuItem.getActionEnum();
- boolean added = false;
-
- if (actionEnum == GeckoMenuItem.SHOW_AS_ACTION_ALWAYS) {
- if (mPrimaryActionItems.size() == 0 &&
- mPrimaryActionItemBar instanceof DefaultActionItemBar) {
- // Reset the adapter before adding the header view to a list.
- setAdapter(null);
- addHeaderView((DefaultActionItemBar) mPrimaryActionItemBar);
- setAdapter(mAdapter);
- }
-
- if (added = mPrimaryActionItemBar.addActionItem(actionView)) {
- mPrimaryActionItems.put(menuItem, actionView);
- mItems.add(menuItem);
- }
- } else if (actionEnum == GeckoMenuItem.SHOW_AS_ACTION_IF_ROOM) {
- if (mSecondaryActionItems.size() == 0) {
- // Reset the adapter before adding the header view to a list.
- setAdapter(null);
- addHeaderView((DefaultActionItemBar) mSecondaryActionItemBar);
- setAdapter(mAdapter);
- }
-
- if (added = mSecondaryActionItemBar.addActionItem(actionView)) {
- mSecondaryActionItems.put(menuItem, actionView);
- mItems.add(menuItem);
- }
- } else if (actionEnum == GeckoMenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) {
- if (actionView instanceof MenuItemSwitcherLayout) {
- final MenuItemSwitcherLayout quickShareView = (MenuItemSwitcherLayout) actionView;
-
- // We don't want to add the quick share bar if we don't have any quick share items.
- if (quickShareView.getActionButtonCount() > 0 &&
- (added = mQuickShareActionItemBar.addActionItem(quickShareView))) {
- if (mQuickShareActionItems.size() == 0) {
- // Reset the adapter before adding the header view to a list.
- setAdapter(null);
- addHeaderView((DefaultActionItemBar) mQuickShareActionItemBar);
- setAdapter(mAdapter);
- }
-
- mQuickShareActionItems.put(menuItem, quickShareView);
- mItems.add(menuItem);
- }
- }
- }
-
- // Set the listeners.
- if (actionView instanceof MenuItemActionBar) {
- ((MenuItemActionBar) actionView).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- handleMenuItemClick(menuItem);
- }
- });
- ((MenuItemActionBar) actionView).setOnLongClickListener(new View.OnLongClickListener() {
- @Override
- public boolean onLongClick(View view) {
- if (handleMenuItemLongClick(menuItem)) {
- GeckoAppShell.vibrateOnHapticFeedbackEnabled(getResources().getIntArray(R.array.long_press_vibrate_msec));
- return true;
- }
- return false;
- }
- });
- } else if (actionView instanceof MenuItemSwitcherLayout) {
- ((MenuItemSwitcherLayout) actionView).setMenuItemClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- handleMenuItemClick(menuItem);
- }
- });
- ((MenuItemSwitcherLayout) actionView).setMenuItemLongClickListener(new View.OnLongClickListener() {
- @Override
- public boolean onLongClick(View view) {
- if (handleMenuItemLongClick(menuItem)) {
- GeckoAppShell.vibrateOnHapticFeedbackEnabled(getResources().getIntArray(R.array.long_press_vibrate_msec));
- return true;
- }
- return false;
- }
- });
- }
-
- return added;
- }
-
- @Override
- public int addIntentOptions(int groupId, int itemId, int order, ComponentName caller, Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems) {
- return 0;
- }
-
- @Override
- public SubMenu addSubMenu(int groupId, int itemId, int order, CharSequence title) {
- MenuItem menuItem = add(groupId, itemId, order, title);
- return addSubMenu(menuItem);
- }
-
- @Override
- public SubMenu addSubMenu(int groupId, int itemId, int order, int titleRes) {
- MenuItem menuItem = add(groupId, itemId, order, titleRes);
- return addSubMenu(menuItem);
- }
-
- @Override
- public SubMenu addSubMenu(CharSequence title) {
- MenuItem menuItem = add(title);
- return addSubMenu(menuItem);
- }
-
- @Override
- public SubMenu addSubMenu(int titleRes) {
- MenuItem menuItem = add(titleRes);
- return addSubMenu(menuItem);
- }
-
- private SubMenu addSubMenu(MenuItem menuItem) {
- GeckoSubMenu subMenu = new GeckoSubMenu(getContext());
- subMenu.setMenuItem(menuItem);
- subMenu.setCallback(mCallback);
- subMenu.setMenuPresenter(mMenuPresenter);
- ((GeckoMenuItem) menuItem).setSubMenu(subMenu);
- return subMenu;
- }
-
- private void removePrimaryActionBarView() {
- // Reset the adapter before removing the header view from a list.
- setAdapter(null);
- removeHeaderView((DefaultActionItemBar) mPrimaryActionItemBar);
- setAdapter(mAdapter);
- }
-
- private void removeSecondaryActionBarView() {
- // Reset the adapter before removing the header view from a list.
- setAdapter(null);
- removeHeaderView((DefaultActionItemBar) mSecondaryActionItemBar);
- setAdapter(mAdapter);
- }
-
- private void removeQuickShareActionBarView() {
- // Reset the adapter before removing the header view from a list.
- setAdapter(null);
- removeHeaderView((DefaultActionItemBar) mQuickShareActionItemBar);
- setAdapter(mAdapter);
- }
-
- @Override
- public void clear() {
- assertOnUiThread();
- for (GeckoMenuItem menuItem : mItems) {
- if (menuItem.hasSubMenu()) {
- SubMenu sub = menuItem.getSubMenu();
- if (sub == null) {
- continue;
- }
- try {
- sub.clear();
- } catch (Exception ex) {
- Log.e(LOGTAG, "Couldn't clear submenu.", ex);
- }
- }
- }
-
- mAdapter.clear();
- mItems.clear();
-
- /*
- * Reinflating the menu will re-add any action items to the toolbar, so
- * remove the old ones. This also ensures that any text associated with
- * these is switched to the correct locale.
- */
- if (mPrimaryActionItemBar != null) {
- for (View item : mPrimaryActionItems.values()) {
- mPrimaryActionItemBar.removeActionItem(item);
- }
- }
- mPrimaryActionItems.clear();
-
- if (mSecondaryActionItemBar != null) {
- for (View item : mSecondaryActionItems.values()) {
- mSecondaryActionItemBar.removeActionItem(item);
- }
- }
- mSecondaryActionItems.clear();
-
- if (mQuickShareActionItemBar != null) {
- for (View item : mQuickShareActionItems.values()) {
- mQuickShareActionItemBar.removeActionItem(item);
- }
- }
- mQuickShareActionItems.clear();
-
- // Remove the view, too -- the first addActionItem will re-add it,
- // and this is simpler than changing that logic.
- if (mPrimaryActionItemBar instanceof DefaultActionItemBar) {
- removePrimaryActionBarView();
- }
-
- removeSecondaryActionBarView();
- removeQuickShareActionBarView();
- }
-
- @Override
- public void close() {
- if (mMenuPresenter != null)
- mMenuPresenter.closeMenu();
- }
-
- private void showMenu(View viewForMenu) {
- if (mMenuPresenter != null)
- mMenuPresenter.showMenu(viewForMenu);
- }
-
- @Override
- public MenuItem findItem(int id) {
- assertOnUiThread();
- MenuItem quickItem = mItemsById.get(id);
- if (quickItem != null) {
- return quickItem;
- }
-
- for (GeckoMenuItem menuItem : mItems) {
- if (menuItem.getItemId() == id) {
- mItemsById.put(id, menuItem);
- return menuItem;
- } else if (menuItem.hasSubMenu()) {
- if (!menuItem.hasActionProvider()) {
- SubMenu subMenu = menuItem.getSubMenu();
- MenuItem item = subMenu.findItem(id);
- if (item != null) {
- mItemsById.put(id, item);
- return item;
- }
- }
- }
- }
- return null;
- }
-
- @Override
- public MenuItem getItem(int index) {
- if (index < mItems.size())
- return mItems.get(index);
-
- return null;
- }
-
- @Override
- public boolean hasVisibleItems() {
- assertOnUiThread();
- for (GeckoMenuItem menuItem : mItems) {
- if (menuItem.isVisible() &&
- !mPrimaryActionItems.containsKey(menuItem) &&
- !mSecondaryActionItems.containsKey(menuItem) &&
- !mQuickShareActionItems.containsKey(menuItem))
- return true;
- }
-
- return false;
- }
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- // Close the menu if it is open and the hardware menu key is pressed.
- if (keyCode == KeyEvent.KEYCODE_MENU && isShown()) {
- close();
- return true;
- }
-
- return super.onKeyDown(keyCode, event);
- }
-
- @Override
- public boolean isShortcutKey(int keyCode, KeyEvent event) {
- return true;
- }
-
- @Override
- public boolean performIdentifierAction(int id, int flags) {
- return false;
- }
-
- @Override
- public boolean performShortcut(int keyCode, KeyEvent event, int flags) {
- return false;
- }
-
- @Override
- public void removeGroup(int groupId) {
- }
-
- @Override
- public void removeItem(int id) {
- assertOnUiThread();
- GeckoMenuItem item = (GeckoMenuItem) findItem(id);
- if (item == null)
- return;
-
- // Remove it from the cache.
- mItemsById.remove(id);
-
- // Remove it from any sub-menu.
- for (GeckoMenuItem menuItem : mItems) {
- if (menuItem.hasSubMenu()) {
- SubMenu subMenu = menuItem.getSubMenu();
- if (subMenu != null && subMenu.findItem(id) != null) {
- subMenu.removeItem(id);
- return;
- }
- }
- }
-
- // Remove it from own menu.
- if (mPrimaryActionItems.containsKey(item)) {
- if (mPrimaryActionItemBar != null)
- mPrimaryActionItemBar.removeActionItem(mPrimaryActionItems.get(item));
-
- mPrimaryActionItems.remove(item);
- mItems.remove(item);
-
- if (mPrimaryActionItems.size() == 0 &&
- mPrimaryActionItemBar instanceof DefaultActionItemBar) {
- removePrimaryActionBarView();
- }
-
- return;
- }
-
- if (mSecondaryActionItems.containsKey(item)) {
- if (mSecondaryActionItemBar != null)
- mSecondaryActionItemBar.removeActionItem(mSecondaryActionItems.get(item));
-
- mSecondaryActionItems.remove(item);
- mItems.remove(item);
-
- if (mSecondaryActionItems.size() == 0) {
- removeSecondaryActionBarView();
- }
-
- return;
- }
-
- if (mQuickShareActionItems.containsKey(item)) {
- if (mQuickShareActionItemBar != null)
- mQuickShareActionItemBar.removeActionItem(mQuickShareActionItems.get(item));
-
- mQuickShareActionItems.remove(item);
- mItems.remove(item);
-
- if (mQuickShareActionItems.size() == 0) {
- removeQuickShareActionBarView();
- }
-
- return;
- }
-
- mAdapter.removeMenuItem(item);
- mItems.remove(item);
- }
-
- @Override
- public void setGroupCheckable(int group, boolean checkable, boolean exclusive) {
- }
-
- @Override
- public void setGroupEnabled(int group, boolean enabled) {
- }
-
- @Override
- public void setGroupVisible(int group, boolean visible) {
- }
-
- @Override
- public void setQwertyMode(boolean isQwerty) {
- }
-
- @Override
- public int size() {
- return mItems.size();
- }
-
- @Override
- public boolean hasActionItemBar() {
- return (mPrimaryActionItemBar != null) &&
- (mSecondaryActionItemBar != null) &&
- (mQuickShareActionItemBar != null);
- }
-
- @Override
- public void onShowAsActionChanged(GeckoMenuItem item) {
- removeItem(item.getItemId());
-
- if (item.isActionItem() && addActionItem(item)) {
- return;
- }
-
- addItem(item);
- }
-
- public void onItemChanged(GeckoMenuItem item) {
- assertOnUiThread();
- if (item.isActionItem()) {
- final View actionView;
- final int actionEnum = item.getActionEnum();
- if (actionEnum == GeckoMenuItem.SHOW_AS_ACTION_ALWAYS) {
- actionView = mPrimaryActionItems.get(item);
- } else if (actionEnum == GeckoMenuItem.SHOW_AS_ACTION_IF_ROOM) {
- actionView = mSecondaryActionItems.get(item);
- } else {
- actionView = mQuickShareActionItems.get(item);
- }
-
- if (actionView != null) {
- if (item.isVisible()) {
- actionView.setVisibility(View.VISIBLE);
- if (actionView instanceof MenuItemActionBar) {
- ((MenuItemActionBar) actionView).initialize(item);
- } else {
- ((MenuItemSwitcherLayout) actionView).initialize(item);
- }
- } else {
- actionView.setVisibility(View.GONE);
- }
- }
- } else {
- mAdapter.notifyDataSetChanged();
- }
- }
-
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- // We might be showing headers. Account them while using the position.
- position -= getHeaderViewsCount();
-
- GeckoMenuItem item = mAdapter.getItem(position);
- handleMenuItemClick(item);
- }
-
- void handleMenuItemClick(GeckoMenuItem item) {
- if (!item.isEnabled())
- return;
-
- if (item.invoke()) {
- close();
- } else if (item.hasSubMenu()) {
- // Refresh the submenu for the provider.
- GeckoActionProvider provider = item.getGeckoActionProvider();
- if (provider != null) {
- GeckoSubMenu subMenu = new GeckoSubMenu(getContext());
- subMenu.setShowIcons(true);
- provider.onPrepareSubMenu(subMenu);
- item.setSubMenu(subMenu);
- }
-
- // Show the submenu.
- GeckoSubMenu subMenu = (GeckoSubMenu) item.getSubMenu();
- showMenu(subMenu);
- } else {
- close();
- mCallback.onMenuItemClick(item);
- }
- }
-
- boolean handleMenuItemLongClick(GeckoMenuItem item) {
- if (!item.isEnabled()) {
- return false;
- }
-
- if (mCallback != null) {
- if (mCallback.onMenuItemLongClick(item)) {
- close();
- return true;
- }
- }
- return false;
- }
-
- public Callback getCallback() {
- return mCallback;
- }
-
- public MenuPresenter getMenuPresenter() {
- return mMenuPresenter;
- }
-
- public void setCallback(Callback callback) {
- mCallback = callback;
-
- // Update the submenus just in case this changes on the fly.
- for (GeckoMenuItem menuItem : mItems) {
- if (menuItem.hasSubMenu()) {
- GeckoSubMenu subMenu = (GeckoSubMenu) menuItem.getSubMenu();
- subMenu.setCallback(mCallback);
- }
- }
- }
-
- public void setMenuPresenter(MenuPresenter presenter) {
- mMenuPresenter = presenter;
-
- // Update the submenus just in case this changes on the fly.
- for (GeckoMenuItem menuItem : mItems) {
- if (menuItem.hasSubMenu()) {
- GeckoSubMenu subMenu = (GeckoSubMenu) menuItem.getSubMenu();
- subMenu.setMenuPresenter(mMenuPresenter);
- }
- }
- }
-
- public void setActionItemBarPresenter(ActionItemBarPresenter presenter) {
- mPrimaryActionItemBar = presenter;
- }
-
- public void setShowIcons(boolean show) {
- if (mShowIcons != show) {
- mShowIcons = show;
- mAdapter.notifyDataSetChanged();
- }
- }
-
- // Action Items are added to the header view by default.
- // URL bar can register itself as a presenter, in case it has a different place to show them.
- public static class DefaultActionItemBar extends LinearLayout
- implements ActionItemBarPresenter {
- private final int mRowHeight;
- private float mWeightSum;
-
- public DefaultActionItemBar(Context context) {
- this(context, null);
- }
-
- public DefaultActionItemBar(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- mRowHeight = getResources().getDimensionPixelSize(R.dimen.menu_item_row_height);
- }
-
- @Override
- public boolean addActionItem(View actionItem) {
- ViewGroup.LayoutParams actualParams = actionItem.getLayoutParams();
- LinearLayout.LayoutParams params;
-
- if (actualParams != null) {
- params = new LinearLayout.LayoutParams(actionItem.getLayoutParams());
- params.width = 0;
- } else {
- params = new LinearLayout.LayoutParams(0, mRowHeight);
- }
-
- if (actionItem instanceof MenuItemSwitcherLayout) {
- params.weight = ((MenuItemSwitcherLayout) actionItem).getChildCount();
- } else {
- params.weight = 1.0f;
- }
-
- mWeightSum += params.weight;
-
- actionItem.setLayoutParams(params);
- addView(actionItem);
- setWeightSum(mWeightSum);
- return true;
- }
-
- @Override
- public void removeActionItem(View actionItem) {
- if (indexOfChild(actionItem) != -1) {
- LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) actionItem.getLayoutParams();
- mWeightSum -= params.weight;
- removeView(actionItem);
- }
- }
- }
-
- // Adapter to bind menu items to the list.
- private class MenuItemsAdapter extends BaseAdapter {
- private static final int VIEW_TYPE_DEFAULT = 0;
- private static final int VIEW_TYPE_ACTION_MODE = 1;
-
- private final List<GeckoMenuItem> mItems;
-
- public MenuItemsAdapter() {
- mItems = new ArrayList<GeckoMenuItem>();
- }
-
- @Override
- public int getCount() {
- if (mItems == null)
- return 0;
-
- int visibleCount = 0;
- for (GeckoMenuItem item : mItems) {
- if (item.isVisible())
- visibleCount++;
- }
-
- return visibleCount;
- }
-
- @Override
- public GeckoMenuItem getItem(int position) {
- for (GeckoMenuItem item : mItems) {
- if (item.isVisible()) {
- position--;
-
- if (position < 0)
- return item;
- }
- }
-
- return null;
- }
-
- @Override
- public long getItemId(int position) {
- return position;
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- GeckoMenuItem item = getItem(position);
- GeckoMenuItem.Layout view = null;
-
- // Try to re-use the view.
- if (convertView == null && getItemViewType(position) == VIEW_TYPE_DEFAULT) {
- view = new MenuItemDefault(parent.getContext(), null);
- } else {
- view = (GeckoMenuItem.Layout) convertView;
- }
-
- if (view == null || view instanceof MenuItemSwitcherLayout) {
- // Always get from the menu item.
- // This will ensure that the default activity is refreshed.
- view = (MenuItemSwitcherLayout) item.getActionView();
-
- // ListView will not perform an item click if the row has a focusable view in it.
- // Hence, forward the click event on the menu item in the action-view to the ListView.
- final View actionView = (View) view;
- final int pos = position;
- final long id = getItemId(position);
- ((MenuItemSwitcherLayout) view).setMenuItemClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- GeckoMenu listView = GeckoMenu.this;
- listView.performItemClick(actionView, pos + listView.getHeaderViewsCount(), id);
- }
- });
- }
-
- // Initialize the view.
- view.setShowIcon(mShowIcons);
- view.initialize(item);
- return (View) view;
- }
-
- @Override
- public int getItemViewType(int position) {
- return getItem(position).getGeckoActionProvider() == null ? VIEW_TYPE_DEFAULT : VIEW_TYPE_ACTION_MODE;
- }
-
- @Override
- public int getViewTypeCount() {
- return 2;
- }
-
- @Override
- public boolean hasStableIds() {
- return false;
- }
-
- @Override
- public boolean areAllItemsEnabled() {
- // Setting this to true is a workaround to fix disappearing
- // dividers in the menu (bug 963249).
- return true;
- }
-
- @Override
- public boolean isEnabled(int position) {
- // Setting this to true is a workaround to fix disappearing
- // dividers in the menu in L (bug 1050780).
- return true;
- }
-
- public void addMenuItem(GeckoMenuItem menuItem) {
- if (mItems.contains(menuItem))
- return;
-
- // Insert it in proper order.
- int index = 0;
- for (GeckoMenuItem item : mItems) {
- if (item.getOrder() > menuItem.getOrder()) {
- mItems.add(index, menuItem);
- notifyDataSetChanged();
- return;
- } else {
- index++;
- }
- }
-
- // Add the menuItem at the end.
- mItems.add(menuItem);
- notifyDataSetChanged();
- }
-
- public void removeMenuItem(GeckoMenuItem menuItem) {
- // Remove it from the list.
- mItems.remove(menuItem);
- notifyDataSetChanged();
- }
-
- public void clear() {
- mItemsById.clear();
- mItems.clear();
- notifyDataSetChanged();
- }
-
- public GeckoMenuItem getMenuItem(int id) {
- for (GeckoMenuItem item : mItems) {
- if (item.getItemId() == id)
- return item;
- }
-
- return null;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenuInflater.java b/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenuInflater.java
deleted file mode 100644
index dfcb31c5f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenuInflater.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.menu;
-
-import java.io.IOException;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.R;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.content.res.XmlResourceParser;
-import android.util.AttributeSet;
-import android.util.Xml;
-import android.view.InflateException;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.SubMenu;
-
-public class GeckoMenuInflater extends MenuInflater {
- private static final String TAG_MENU = "menu";
- private static final String TAG_ITEM = "item";
- private static final int NO_ID = 0;
-
- private final Context mContext;
-
- // Private class to hold the parsed menu item.
- private class ParsedItem {
- public int id;
- public int order;
- public CharSequence title;
- public int iconRes;
- public boolean checkable;
- public boolean checked;
- public boolean visible;
- public boolean enabled;
- public int showAsAction;
- public boolean hasSubMenu;
- }
-
- public GeckoMenuInflater(Context context) {
- super(context);
- mContext = context;
- }
-
- @Override
- public void inflate(int menuRes, Menu menu) {
-
- // This does not check for a well-formed XML.
-
- XmlResourceParser parser = null;
- try {
- parser = mContext.getResources().getXml(menuRes);
- AttributeSet attrs = Xml.asAttributeSet(parser);
-
- parseMenu(parser, attrs, menu);
-
- } catch (XmlPullParserException | IOException e) {
- throw new InflateException("Error inflating menu XML", e);
- } finally {
- if (parser != null)
- parser.close();
- }
- }
-
- private void parseMenu(XmlResourceParser parser, AttributeSet attrs, Menu menu)
- throws XmlPullParserException, IOException {
- ParsedItem item = null;
-
- String tag;
- int eventType = parser.getEventType();
-
- do {
- tag = parser.getName();
-
- switch (eventType) {
- case XmlPullParser.START_TAG:
- if (tag.equals(TAG_ITEM)) {
- // Parse the menu item.
- item = new ParsedItem();
- parseItem(item, attrs);
- } else if (tag.equals(TAG_MENU)) {
- if (item != null) {
- // Add the submenu item.
- SubMenu subMenu = menu.addSubMenu(NO_ID, item.id, item.order, item.title);
- item.hasSubMenu = true;
-
- // Set the menu item in main menu.
- MenuItem menuItem = subMenu.getItem();
- setValues(item, menuItem);
-
- // Start parsing the sub menu.
- parseMenu(parser, attrs, subMenu);
- }
- }
- break;
-
- case XmlPullParser.END_TAG:
- if (parser.getName().equals(TAG_ITEM)) {
- if (!item.hasSubMenu) {
- // Add the item.
- MenuItem menuItem = menu.add(NO_ID, item.id, item.order, item.title);
- setValues(item, menuItem);
- }
- } else if (tag.equals(TAG_MENU)) {
- return;
- }
- break;
- }
-
- eventType = parser.next();
-
- } while (eventType != XmlPullParser.END_DOCUMENT);
- }
-
- public void parseItem(ParsedItem item, AttributeSet attrs) {
- TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.MenuItem);
-
- item.id = a.getResourceId(R.styleable.MenuItem_android_id, NO_ID);
- item.order = a.getInt(R.styleable.MenuItem_android_orderInCategory, 0);
- item.title = a.getText(R.styleable.MenuItem_android_title);
- item.checkable = a.getBoolean(R.styleable.MenuItem_android_checkable, false);
- item.checked = a.getBoolean(R.styleable.MenuItem_android_checked, false);
- item.visible = a.getBoolean(R.styleable.MenuItem_android_visible, true);
- item.enabled = a.getBoolean(R.styleable.MenuItem_android_enabled, true);
- item.hasSubMenu = false;
- item.iconRes = a.getResourceId(R.styleable.MenuItem_android_icon, 0);
- item.showAsAction = a.getInt(R.styleable.MenuItem_android_showAsAction, 0);
-
- a.recycle();
- }
-
- public void setValues(ParsedItem item, MenuItem menuItem) {
- // We are blocking any presenter updates during inflation.
- GeckoMenuItem geckoItem = null;
- if (menuItem instanceof GeckoMenuItem) {
- geckoItem = (GeckoMenuItem) menuItem;
- }
-
- if (geckoItem != null) {
- geckoItem.stopDispatchingChanges();
- }
-
- menuItem.setChecked(item.checked)
- .setVisible(item.visible)
- .setEnabled(item.enabled)
- .setCheckable(item.checkable)
- .setIcon(item.iconRes);
-
- menuItem.setShowAsAction(item.showAsAction);
-
- if (geckoItem != null) {
- // We don't need to allow presenter updates during inflation,
- // so we use the weak form of re-enabling changes.
- geckoItem.resumeDispatchingChanges();
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenuItem.java b/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenuItem.java
deleted file mode 100644
index 21df4208d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenuItem.java
+++ /dev/null
@@ -1,472 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.menu;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.ResourceDrawableUtils;
-import org.mozilla.gecko.widget.GeckoActionProvider;
-
-import android.content.Intent;
-import android.graphics.drawable.Drawable;
-import android.text.TextUtils;
-import android.view.ActionProvider;
-import android.view.ContextMenu;
-import android.view.MenuItem;
-import android.view.SubMenu;
-import android.view.View;
-
-public class GeckoMenuItem implements MenuItem {
- private static final int SHARE_BAR_HISTORY_SIZE = 2;
-
- // These values mirror MenuItem values that are only available on API >= 11.
- public static final int SHOW_AS_ACTION_NEVER = 0;
- public static final int SHOW_AS_ACTION_IF_ROOM = 1;
- public static final int SHOW_AS_ACTION_ALWAYS = 2;
- public static final int SHOW_AS_ACTION_WITH_TEXT = 4;
- public static final int SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW = 8;
-
- // A View that can show a MenuItem should be able to initialize from
- // the properties of the MenuItem.
- public static interface Layout {
- public void initialize(GeckoMenuItem item);
- public void setShowIcon(boolean show);
- }
-
- public static interface OnShowAsActionChangedListener {
- public boolean hasActionItemBar();
- public void onShowAsActionChanged(GeckoMenuItem item);
- }
-
- private final int mId;
- private final int mOrder;
- private View mActionView;
- private int mActionEnum;
- private CharSequence mTitle;
- private CharSequence mTitleCondensed;
- private boolean mCheckable;
- private boolean mChecked;
- private boolean mVisible = true;
- private boolean mEnabled = true;
- private Drawable mIcon;
- private int mIconRes;
- private GeckoActionProvider mActionProvider;
- private GeckoSubMenu mSubMenu;
- private MenuItem.OnMenuItemClickListener mMenuItemClickListener;
- final GeckoMenu mMenu;
- OnShowAsActionChangedListener mShowAsActionChangedListener;
-
- private volatile boolean mShouldDispatchChanges = true;
- private volatile boolean mDidChange;
-
- public GeckoMenuItem(GeckoMenu menu, int id, int order, int titleRes) {
- mMenu = menu;
- mId = id;
- mOrder = order;
- mTitle = mMenu.getResources().getString(titleRes);
- }
-
- public GeckoMenuItem(GeckoMenu menu, int id, int order, CharSequence title) {
- mMenu = menu;
- mId = id;
- mOrder = order;
- mTitle = title;
- }
-
- /**
- * Stop dispatching item changed events to presenters until
- * [start|resume]DispatchingItemsChanged() is called. Useful when
- * many menu operations are going to be performed as a batch.
- */
- public void stopDispatchingChanges() {
- mDidChange = false;
- mShouldDispatchChanges = false;
- }
-
- /**
- * Resume dispatching item changed events to presenters. This method
- * will NOT call onItemChanged if any menu operations were queued.
- * Only future menu operations will call onItemChanged. Useful for
- * sequelching presenter updates.
- */
- public void resumeDispatchingChanges() {
- mShouldDispatchChanges = true;
- }
-
- /**
- * Start dispatching item changed events to presenters. This method
- * will call onItemChanged if any menu operations were queued.
- */
- public void startDispatchingChanges() {
- if (mDidChange) {
- mMenu.onItemChanged(this);
- }
- mShouldDispatchChanges = true;
- }
-
- @Override
- public boolean collapseActionView() {
- return false;
- }
-
- @Override
- public boolean expandActionView() {
- return false;
- }
-
- public boolean hasActionProvider() {
- return (mActionProvider != null);
- }
-
- public int getActionEnum() {
- return mActionEnum;
- }
-
- public GeckoActionProvider getGeckoActionProvider() {
- return mActionProvider;
- }
-
- @Override
- public ActionProvider getActionProvider() {
- return null;
- }
-
- @Override
- public View getActionView() {
- if (mActionProvider != null) {
- return mActionProvider.onCreateActionView(SHARE_BAR_HISTORY_SIZE,
- GeckoActionProvider.ActionViewType.DEFAULT);
- }
-
- return mActionView;
- }
-
- @Override
- public char getAlphabeticShortcut() {
- return 0;
- }
-
- @Override
- public int getGroupId() {
- return 0;
- }
-
- @Override
- public Drawable getIcon() {
- if (mIcon == null) {
- if (mIconRes != 0)
- return ResourceDrawableUtils.getDrawable(mMenu.getContext(), mIconRes);
- else
- return null;
- } else {
- return mIcon;
- }
- }
-
- @Override
- public Intent getIntent() {
- return null;
- }
-
- @Override
- public int getItemId() {
- return mId;
- }
-
- @Override
- public ContextMenu.ContextMenuInfo getMenuInfo() {
- return null;
- }
-
- @Override
- public char getNumericShortcut() {
- return 0;
- }
-
- @Override
- public int getOrder() {
- return mOrder;
- }
-
- @Override
- public SubMenu getSubMenu() {
- return mSubMenu;
- }
-
- @Override
- public CharSequence getTitle() {
- return mTitle;
- }
-
- @Override
- public CharSequence getTitleCondensed() {
- return mTitleCondensed;
- }
-
- @Override
- public boolean hasSubMenu() {
- if (mActionProvider != null)
- return mActionProvider.hasSubMenu();
-
- return (mSubMenu != null);
- }
-
- public boolean isActionItem() {
- return (mActionEnum > 0);
- }
-
- @Override
- public boolean isActionViewExpanded() {
- return false;
- }
-
- @Override
- public boolean isCheckable() {
- return mCheckable;
- }
-
- @Override
- public boolean isChecked() {
- return mChecked;
- }
-
- @Override
- public boolean isEnabled() {
- return mEnabled;
- }
-
- @Override
- public boolean isVisible() {
- return mVisible;
- }
-
- @Override
- public MenuItem setActionProvider(ActionProvider actionProvider) {
- return this;
- }
-
- public MenuItem setActionProvider(GeckoActionProvider actionProvider) {
- mActionProvider = actionProvider;
- if (mActionProvider != null) {
- actionProvider.setOnTargetSelectedListener(new GeckoActionProvider.OnTargetSelectedListener() {
- @Override
- public void onTargetSelected() {
- mMenu.close();
-
- // Refresh the menu item to show the high frequency apps.
- mShowAsActionChangedListener.onShowAsActionChanged(GeckoMenuItem.this);
- }
- });
- }
-
- mShowAsActionChangedListener.onShowAsActionChanged(this);
- return this;
- }
-
- @Override
- public MenuItem setActionView(int resId) {
- return this;
- }
-
- @Override
- public MenuItem setActionView(View view) {
- return this;
- }
-
- @Override
- public MenuItem setAlphabeticShortcut(char alphaChar) {
- return this;
- }
-
- @Override
- public MenuItem setCheckable(boolean checkable) {
- if (mCheckable != checkable) {
- mCheckable = checkable;
- if (mShouldDispatchChanges) {
- mMenu.onItemChanged(this);
- } else {
- mDidChange = true;
- }
- }
- return this;
- }
-
- @Override
- public MenuItem setChecked(boolean checked) {
- if (mChecked != checked) {
- mChecked = checked;
- if (mShouldDispatchChanges) {
- mMenu.onItemChanged(this);
- } else {
- mDidChange = true;
- }
- }
- return this;
- }
-
- @Override
- public MenuItem setEnabled(boolean enabled) {
- if (mEnabled != enabled) {
- mEnabled = enabled;
- if (mShouldDispatchChanges) {
- mMenu.onItemChanged(this);
- } else {
- mDidChange = true;
- }
- }
- return this;
- }
-
- @Override
- public MenuItem setIcon(Drawable icon) {
- if (mIcon != icon) {
- mIcon = icon;
- if (mShouldDispatchChanges) {
- mMenu.onItemChanged(this);
- } else {
- mDidChange = true;
- }
- }
- return this;
- }
-
- @Override
- public MenuItem setIcon(int iconRes) {
- if (mIconRes != iconRes) {
- mIconRes = iconRes;
- if (mShouldDispatchChanges) {
- mMenu.onItemChanged(this);
- } else {
- mDidChange = true;
- }
- }
- return this;
- }
-
- @Override
- public MenuItem setIntent(Intent intent) {
- return this;
- }
-
- @Override
- public MenuItem setNumericShortcut(char numericChar) {
- return this;
- }
-
- @Override
- public MenuItem setOnActionExpandListener(MenuItem.OnActionExpandListener listener) {
- return this;
- }
-
- @Override
- public MenuItem setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener menuItemClickListener) {
- mMenuItemClickListener = menuItemClickListener;
- return this;
- }
-
- @Override
- public MenuItem setShortcut(char numericChar, char alphaChar) {
- return this;
- }
-
- @Override
- public void setShowAsAction(int actionEnum) {
- setShowAsAction(actionEnum, 0);
- }
-
- public void setShowAsAction(int actionEnum, int style) {
- if (mShowAsActionChangedListener == null)
- return;
-
- if (mActionEnum == actionEnum)
- return;
-
- if (actionEnum > 0) {
- if (!mShowAsActionChangedListener.hasActionItemBar())
- return;
-
- if (!hasActionProvider()) {
- // Change the type to just an icon
- MenuItemActionBar actionView;
- if (style != 0) {
- actionView = new MenuItemActionBar(mMenu.getContext(), null, style);
- } else {
- if (actionEnum == SHOW_AS_ACTION_ALWAYS) {
- actionView = new MenuItemActionBar(mMenu.getContext());
- } else {
- actionView = new MenuItemActionBar(mMenu.getContext(), null, R.attr.menuItemSecondaryActionBarStyle);
- }
- }
-
- actionView.initialize(this);
- mActionView = actionView;
- }
-
- mActionEnum = actionEnum;
- }
-
- mShowAsActionChangedListener.onShowAsActionChanged(this);
- }
-
- @Override
- public MenuItem setShowAsActionFlags(int actionEnum) {
- return this;
- }
-
- public MenuItem setSubMenu(GeckoSubMenu subMenu) {
- mSubMenu = subMenu;
- return this;
- }
-
- @Override
- public MenuItem setTitle(CharSequence title) {
- if (!TextUtils.equals(mTitle, title)) {
- mTitle = title;
- if (mShouldDispatchChanges) {
- mMenu.onItemChanged(this);
- } else {
- mDidChange = true;
- }
- }
- return this;
- }
-
- @Override
- public MenuItem setTitle(int title) {
- CharSequence newTitle = mMenu.getResources().getString(title);
- return setTitle(newTitle);
- }
-
- @Override
- public MenuItem setTitleCondensed(CharSequence title) {
- mTitleCondensed = title;
- return this;
- }
-
- @Override
- public MenuItem setVisible(boolean visible) {
- // Action views are not normal menu items and visibility can get out
- // of sync unless we dispatch whenever required.
- if (isActionItem() || mVisible != visible) {
- mVisible = visible;
- if (mShouldDispatchChanges) {
- mMenu.onItemChanged(this);
- } else {
- mDidChange = true;
- }
- }
- return this;
- }
-
- public boolean invoke() {
- if (mMenuItemClickListener != null)
- return mMenuItemClickListener.onMenuItemClick(this);
- else
- return false;
- }
-
- public void setOnShowAsActionChangedListener(OnShowAsActionChangedListener listener) {
- mShowAsActionChangedListener = listener;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/menu/GeckoSubMenu.java b/mobile/android/base/java/org/mozilla/gecko/menu/GeckoSubMenu.java
deleted file mode 100644
index d774bdd37..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/menu/GeckoSubMenu.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.menu;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.MenuItem;
-import android.view.SubMenu;
-import android.view.View;
-
-public class GeckoSubMenu extends GeckoMenu
- implements SubMenu {
- private static final String LOGTAG = "GeckoSubMenu";
-
- // MenuItem associated with this submenu.
- private MenuItem mMenuItem;
-
- public GeckoSubMenu(Context context) {
- super(context);
- }
-
- public GeckoSubMenu(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public GeckoSubMenu(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-
- @Override
- public void clearHeader() {
- }
-
- public SubMenu setMenuItem(MenuItem item) {
- mMenuItem = item;
- return this;
- }
-
- @Override
- public MenuItem getItem() {
- return mMenuItem;
- }
-
- @Override
- public SubMenu setHeaderIcon(Drawable icon) {
- return this;
- }
-
- @Override
- public SubMenu setHeaderIcon(int iconRes) {
- return this;
- }
-
- @Override
- public SubMenu setHeaderTitle(CharSequence title) {
- return this;
- }
-
- @Override
- public SubMenu setHeaderTitle(int titleRes) {
- return this;
- }
-
- @Override
- public SubMenu setHeaderView(View view) {
- return this;
- }
-
- @Override
- public SubMenu setIcon(Drawable icon) {
- return this;
- }
-
- @Override
- public SubMenu setIcon(int iconRes) {
- return this;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/menu/MenuItemActionBar.java b/mobile/android/base/java/org/mozilla/gecko/menu/MenuItemActionBar.java
deleted file mode 100644
index 882187dd6..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/menu/MenuItemActionBar.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.menu;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.ResourceDrawableUtils;
-import org.mozilla.gecko.widget.themed.ThemedImageButton;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-
-public class MenuItemActionBar extends ThemedImageButton
- implements GeckoMenuItem.Layout {
- private static final String LOGTAG = "GeckoMenuItemActionBar";
-
- public MenuItemActionBar(Context context) {
- this(context, null);
- }
-
- public MenuItemActionBar(Context context, AttributeSet attrs) {
- this(context, attrs, R.attr.menuItemActionBarStyle);
- }
-
- public MenuItemActionBar(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-
- @Override
- public void initialize(GeckoMenuItem item) {
- if (item == null)
- return;
-
- setIcon(item.getIcon());
- setTitle(item.getTitle());
- setEnabled(item.isEnabled());
- setId(item.getItemId());
- }
-
- void setIcon(Drawable icon) {
- if (icon == null) {
- setVisibility(GONE);
- } else {
- setVisibility(VISIBLE);
- setImageDrawable(icon);
- }
- }
-
- void setIcon(int icon) {
- setIcon((icon == 0) ? null : ResourceDrawableUtils.getDrawable(getContext(), icon));
- }
-
- void setTitle(CharSequence title) {
- // set accessibility contentDescription here
- setContentDescription(title);
- }
-
- @Override
- public void setShowIcon(boolean show) {
- // Do nothing.
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/menu/MenuItemDefault.java b/mobile/android/base/java/org/mozilla/gecko/menu/MenuItemDefault.java
deleted file mode 100644
index 5b5069334..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/menu/MenuItemDefault.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.menu;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.ResourceDrawableUtils;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.widget.TextView;
-
-public class MenuItemDefault extends TextView
- implements GeckoMenuItem.Layout {
- private static final int[] STATE_MORE = new int[] { R.attr.state_more };
- private static final int[] STATE_CHECKED = new int[] { android.R.attr.state_checkable, android.R.attr.state_checked };
- private static final int[] STATE_UNCHECKED = new int[] { android.R.attr.state_checkable };
-
- private Drawable mIcon;
- private final Drawable mState;
- private static Rect sIconBounds;
-
- private boolean mCheckable;
- private boolean mChecked;
- private boolean mHasSubMenu;
- private boolean mShowIcon;
-
- public MenuItemDefault(Context context) {
- this(context, null);
- }
-
- public MenuItemDefault(Context context, AttributeSet attrs) {
- this(context, attrs, R.attr.menuItemDefaultStyle);
- }
-
- public MenuItemDefault(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
-
- Resources res = getResources();
- int width = res.getDimensionPixelSize(R.dimen.menu_item_row_width);
- int height = res.getDimensionPixelSize(R.dimen.menu_item_row_height);
- setMinimumWidth(width);
- setMinimumHeight(height);
-
- int stateIconSize = res.getDimensionPixelSize(R.dimen.menu_item_state_icon);
- Rect stateIconBounds = new Rect(0, 0, stateIconSize, stateIconSize);
-
- mState = res.getDrawable(R.drawable.menu_item_state).mutate();
- mState.setBounds(stateIconBounds);
-
- if (sIconBounds == null) {
- int iconSize = res.getDimensionPixelSize(R.dimen.menu_item_icon);
- sIconBounds = new Rect(0, 0, iconSize, iconSize);
- }
-
- setCompoundDrawables(mIcon, null, mState, null);
- }
-
- @Override
- public int[] onCreateDrawableState(int extraSpace) {
- final int[] drawableState = super.onCreateDrawableState(extraSpace + 2);
-
- if (mHasSubMenu)
- mergeDrawableStates(drawableState, STATE_MORE);
- else if (mCheckable && mChecked)
- mergeDrawableStates(drawableState, STATE_CHECKED);
- else if (mCheckable && !mChecked)
- mergeDrawableStates(drawableState, STATE_UNCHECKED);
-
- return drawableState;
- }
-
- @Override
- public void initialize(GeckoMenuItem item) {
- if (item == null)
- return;
-
- setTitle(item.getTitle());
- setIcon(item.getIcon());
- setEnabled(item.isEnabled());
- setCheckable(item.isCheckable());
- setChecked(item.isChecked());
- setSubMenuIndicator(item.hasSubMenu());
- }
-
- private void refreshIcon() {
- setCompoundDrawables(mShowIcon ? mIcon : null, null, mState, null);
- }
-
- void setIcon(Drawable icon) {
- mIcon = icon;
-
- if (mIcon != null) {
- mIcon.setBounds(sIconBounds);
- mIcon.setAlpha(isEnabled() ? 255 : 99);
- }
-
- refreshIcon();
- }
-
- void setIcon(int icon) {
- setIcon((icon == 0) ? null : ResourceDrawableUtils.getDrawable(getContext(), icon));
- }
-
- void setTitle(CharSequence title) {
- setText(title);
- }
-
- @Override
- public void setEnabled(boolean enabled) {
- super.setEnabled(enabled);
-
- if (mIcon != null)
- mIcon.setAlpha(enabled ? 255 : 99);
-
- if (mState != null)
- mState.setAlpha(enabled ? 255 : 99);
- }
-
- private void setCheckable(boolean checkable) {
- if (mCheckable != checkable) {
- mCheckable = checkable;
- refreshDrawableState();
- }
- }
-
- private void setChecked(boolean checked) {
- if (mChecked != checked) {
- mChecked = checked;
- refreshDrawableState();
- }
- }
-
- @Override
- public void setShowIcon(boolean show) {
- if (mShowIcon != show) {
- mShowIcon = show;
- refreshIcon();
- }
- }
-
- void setSubMenuIndicator(boolean hasSubMenu) {
- if (mHasSubMenu != hasSubMenu) {
- mHasSubMenu = hasSubMenu;
- refreshDrawableState();
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/menu/MenuItemSwitcherLayout.java b/mobile/android/base/java/org/mozilla/gecko/menu/MenuItemSwitcherLayout.java
deleted file mode 100644
index d01f52687..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/menu/MenuItemSwitcherLayout.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.menu;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.mozilla.gecko.R;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.ImageButton;
-import android.widget.LinearLayout;
-
-/**
- * This class is a container view for menu items that:
- * * Shows text if there is enough space and there are
- * no action buttons ({@link #mActionButtons}).
- * * Shows an icon if there is not enough space for text,
- * or there are action buttons.
- */
-public class MenuItemSwitcherLayout extends LinearLayout
- implements GeckoMenuItem.Layout,
- View.OnClickListener {
- private final MenuItemDefault mMenuItem;
- private final MenuItemActionBar mMenuButton;
- private final List<ImageButton> mActionButtons;
- private final List<View.OnClickListener> mActionButtonListeners = new ArrayList<View.OnClickListener>();
-
- public MenuItemSwitcherLayout(Context context) {
- this(context, null);
- }
-
- public MenuItemSwitcherLayout(Context context, AttributeSet attrs) {
- this(context, attrs, R.attr.menuItemSwitcherLayoutStyle);
- }
-
- @TargetApi(14)
- public MenuItemSwitcherLayout(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs);
-
- LayoutInflater.from(context).inflate(R.layout.menu_item_switcher_layout, this);
- mMenuItem = (MenuItemDefault) findViewById(R.id.menu_item);
- mMenuButton = (MenuItemActionBar) findViewById(R.id.menu_item_button);
- mActionButtons = new ArrayList<ImageButton>();
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- final int width = right - left;
-
- final View parent = (View) getParent();
- final int parentPadding = parent.getPaddingLeft() + parent.getPaddingRight();
- final int horizontalSpaceAvailableInParent = parent.getMeasuredWidth() - parentPadding;
-
- // Check if there is another View sharing horizontal
- // space with this View in the parent.
- if (width < horizontalSpaceAvailableInParent || mActionButtons.size() != 0) {
- // Use the icon.
- mMenuItem.setVisibility(View.GONE);
- mMenuButton.setVisibility(View.VISIBLE);
- } else {
- // Use the button.
- mMenuItem.setVisibility(View.VISIBLE);
- mMenuButton.setVisibility(View.GONE);
- }
-
- super.onLayout(changed, left, top, right, bottom);
- }
-
- @Override
- public void initialize(GeckoMenuItem item) {
- if (item == null) {
- return;
- }
-
- mMenuItem.initialize(item);
- mMenuButton.initialize(item);
- setEnabled(item.isEnabled());
- }
-
- @Override
- public void setEnabled(boolean enabled) {
- super.setEnabled(enabled);
- mMenuItem.setEnabled(enabled);
- mMenuButton.setEnabled(enabled);
-
- for (ImageButton button : mActionButtons) {
- button.setEnabled(enabled);
- button.setAlpha(enabled ? 255 : 99);
- }
- }
-
- public void setMenuItemClickListener(View.OnClickListener listener) {
- mMenuItem.setOnClickListener(listener);
- mMenuButton.setOnClickListener(listener);
- }
-
- public void setMenuItemLongClickListener(View.OnLongClickListener listener) {
- mMenuItem.setOnLongClickListener(listener);
- mMenuButton.setOnLongClickListener(listener);
- }
-
- public void addActionButtonClickListener(View.OnClickListener listener) {
- mActionButtonListeners.add(listener);
- }
-
- @Override
- public void setShowIcon(boolean show) {
- mMenuItem.setShowIcon(show);
- }
-
- public void setIcon(Drawable icon) {
- mMenuItem.setIcon(icon);
- mMenuButton.setIcon(icon);
- }
-
- public void setIcon(int icon) {
- mMenuItem.setIcon(icon);
- mMenuButton.setIcon(icon);
- }
-
- public void setTitle(CharSequence title) {
- mMenuItem.setTitle(title);
- mMenuButton.setContentDescription(title);
- }
-
- public void setSubMenuIndicator(boolean hasSubMenu) {
- mMenuItem.setSubMenuIndicator(hasSubMenu);
- }
-
- public void addActionButton(Drawable drawable, CharSequence label) {
- // If this is the first icon, retain the text.
- // If not, make the menu item an icon.
- final int count = mActionButtons.size();
- mMenuItem.setVisibility(View.GONE);
- mMenuButton.setVisibility(View.VISIBLE);
-
- if (drawable != null) {
- ImageButton button = new ImageButton(getContext(), null, R.attr.menuItemSecondaryActionBarStyle);
- button.setImageDrawable(drawable);
- button.setContentDescription(label);
- button.setOnClickListener(this);
- button.setTag(count);
-
- final int height = (int) (getResources().getDimension(R.dimen.menu_item_row_height));
- LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, height);
- params.weight = 1.0f;
- button.setLayoutParams(params);
-
- // Place action buttons to the right of the actual menu item
- mActionButtons.add(button);
- addView(button, count + 1);
- }
- }
-
- protected int getActionButtonCount() {
- return mActionButtons.size();
- }
-
- @Override
- public void onClick(View view) {
- for (View.OnClickListener listener : mActionButtonListeners) {
- listener.onClick(view);
- }
- }
-
- /**
- * Update the styles if this view is being used in the context menus.
- *
- * Ideally, we just use different layout files and styles to set this, but
- * MenuItemSwitcherLayout is too integrated into GeckoActionProvider to provide
- * an easy separation so instead I provide this hack. I'm sorry.
- */
- public void initContextMenuStyles() {
- final int defaultContextMenuPadding = getContext().getResources().getDimensionPixelOffset(
- R.dimen.context_menu_item_horizontal_padding);
- mMenuItem.setPadding(defaultContextMenuPadding, getPaddingTop(),
- defaultContextMenuPadding, getPaddingBottom());
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/menu/MenuPanel.java b/mobile/android/base/java/org/mozilla/gecko/menu/MenuPanel.java
deleted file mode 100644
index ce4da8b7f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/menu/MenuPanel.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.menu;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.ViewGroup;
-import android.view.accessibility.AccessibilityEvent;
-import android.widget.LinearLayout;
-
-/**
- * The outer container for the custom menu. On phones with h/w menu button,
- * this is given to Android which inflates it to the right panel. On phones
- * with s/w menu button, this is added to a MenuPopup.
- */
-public class MenuPanel extends LinearLayout {
- public MenuPanel(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- int width = (int) context.getResources().getDimension(R.dimen.menu_item_row_width);
- setLayoutParams(new ViewGroup.LayoutParams(width, ViewGroup.LayoutParams.WRAP_CONTENT));
- }
-
- @Override
- public boolean dispatchPopulateAccessibilityEvent (AccessibilityEvent event) {
- onPopulateAccessibilityEvent(event);
-
- return true;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/menu/MenuPopup.java b/mobile/android/base/java/org/mozilla/gecko/menu/MenuPopup.java
deleted file mode 100644
index 227cc7630..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/menu/MenuPopup.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.menu;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.graphics.Color;
-import android.graphics.drawable.ColorDrawable;
-import android.support.v7.widget.CardView;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-import android.widget.PopupWindow;
-
-/**
- * A popup to show the inflated MenuPanel.
- */
-public class MenuPopup extends PopupWindow {
- private final CardView mPanel;
-
- private final int mPopupWidth;
-
- public MenuPopup(Context context) {
- super(context);
-
- setFocusable(true);
-
- mPopupWidth = context.getResources().getDimensionPixelSize(R.dimen.menu_popup_width);
-
- // Setting a null background makes the popup to not close on touching outside.
- setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
- setWindowLayoutMode(ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT);
-
- LayoutInflater inflater = LayoutInflater.from(context);
- mPanel = (CardView) inflater.inflate(R.layout.menu_popup, null);
- setContentView(mPanel);
-
- setAnimationStyle(R.style.PopupAnimation);
- }
-
- /**
- * Adds the panel with the menu to its content.
- *
- * @param view The panel view with the menu to be shown.
- */
- public void setPanelView(View view) {
- view.setLayoutParams(new FrameLayout.LayoutParams(mPopupWidth,
- FrameLayout.LayoutParams.WRAP_CONTENT));
-
- mPanel.removeAllViews();
- mPanel.addView(view);
- }
-
- /**
- * A small little offset.
- */
- @Override
- public void showAsDropDown(View anchor) {
- // Set a height, so that the popup will not be displayed below the bottom of the screen.
- // We use the exact height of the internal content, which is the technique described in
- // http://stackoverflow.com/a/7698709
- setHeight(mPanel.getHeight());
-
- // Attempt to align the center of the popup with the center of the anchor. If the anchor is
- // near the edge of the screen, the popup will just align with the edge of the screen.
- final int xOffset = anchor.getWidth() / 2 - mPopupWidth / 2;
- showAsDropDown(anchor, xOffset, -anchor.getHeight());
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/mozglue/SharedMemBuffer.java b/mobile/android/base/java/org/mozilla/gecko/mozglue/SharedMemBuffer.java
deleted file mode 100644
index cf22685c2..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/mozglue/SharedMemBuffer.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-package org.mozilla.gecko.mozglue;
-
-import android.os.Parcel;
-
-import org.mozilla.gecko.media.Sample;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-
-public final class SharedMemBuffer implements Sample.Buffer {
- private SharedMemory mSharedMem;
-
- /* package */
- public SharedMemBuffer(SharedMemory sharedMem) {
- mSharedMem = sharedMem;
- }
-
- protected SharedMemBuffer(Parcel in) {
- mSharedMem = in.readParcelable(Sample.class.getClassLoader());
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeParcelable(mSharedMem, flags);
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- public static final Creator<SharedMemBuffer> CREATOR = new Creator<SharedMemBuffer>() {
- @Override
- public SharedMemBuffer createFromParcel(Parcel in) {
- return new SharedMemBuffer(in);
- }
-
- @Override
- public SharedMemBuffer[] newArray(int size) {
- return new SharedMemBuffer[size];
- }
- };
-
- @Override
- public int capacity() {
- return mSharedMem != null ? mSharedMem.getSize() : 0;
- }
-
- @Override
- public void readFromByteBuffer(ByteBuffer src, int offset, int size) throws IOException {
- if (!src.isDirect()) {
- throw new IOException("SharedMemBuffer only support reading from direct byte buffer.");
- }
- nativeReadFromDirectBuffer(src, mSharedMem.getPointer(), offset, size);
- }
-
- private native static void nativeReadFromDirectBuffer(ByteBuffer src, long dest, int offset, int size);
-
- @Override
- public void writeToByteBuffer(ByteBuffer dest, int offset, int size) throws IOException {
- if (!dest.isDirect()) {
- throw new IOException("SharedMemBuffer only support writing to direct byte buffer.");
- }
- nativeWriteToDirectBuffer(mSharedMem.getPointer(), dest, offset, size);
- }
-
- private native static void nativeWriteToDirectBuffer(long src, ByteBuffer dest, int offset, int size);
-
- @Override
- public void dispose() {
- if (mSharedMem != null) {
- mSharedMem.dispose();
- mSharedMem = null;
- }
- }
-
- @Override public String toString() { return "Buffer: " + mSharedMem; }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/mozglue/SharedMemory.java b/mobile/android/base/java/org/mozilla/gecko/mozglue/SharedMemory.java
deleted file mode 100644
index bc43a2755..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/mozglue/SharedMemory.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.mozglue;
-
-import android.os.MemoryFile;
-import android.os.Parcel;
-import android.os.ParcelFileDescriptor;
-import android.os.Parcelable;
-import android.util.Log;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.lang.reflect.Method;
-
-public class SharedMemory implements Parcelable {
- private static final String LOGTAG = "GeckoShmem";
- private static Method sGetFDMethod = null; // MemoryFile.getFileDescriptor() is hidden. :(
- private ParcelFileDescriptor mDescriptor;
- private int mSize;
- private int mId;
- private long mHandle; // The native pointer.
- private boolean mIsMapped;
- private MemoryFile mBackedFile;
-
- static {
- try {
- sGetFDMethod = MemoryFile.class.getDeclaredMethod("getFileDescriptor");
- } catch (NoSuchMethodException e) {
- e.printStackTrace();
- }
- }
-
- private SharedMemory(Parcel in) {
- mDescriptor = in.readFileDescriptor();
- mSize = in.readInt();
- mId = in.readInt();
- }
-
- public static final Creator<SharedMemory> CREATOR = new Creator<SharedMemory>() {
- @Override
- public SharedMemory createFromParcel(Parcel in) {
- return new SharedMemory(in);
- }
-
- @Override
- public SharedMemory[] newArray(int size) {
- return new SharedMemory[size];
- }
- };
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- // We don't want ParcelFileDescriptor.writeToParcel() to close the fd.
- dest.writeFileDescriptor(mDescriptor.getFileDescriptor());
- dest.writeInt(mSize);
- dest.writeInt(mId);
- }
-
- public SharedMemory(int id, int size) throws NoSuchMethodException, IOException {
- if (sGetFDMethod == null) {
- throw new NoSuchMethodException("MemoryFile.getFileDescriptor() doesn't exist.");
- }
- mBackedFile = new MemoryFile(null, size);
- try {
- FileDescriptor fd = (FileDescriptor)sGetFDMethod.invoke(mBackedFile);
- mDescriptor = ParcelFileDescriptor.dup(fd);
- mSize = size;
- mId = id;
- mBackedFile.allowPurging(false);
- } catch (Exception e) {
- e.printStackTrace();
- close();
- throw new IOException(e.getMessage());
- }
- }
-
- public void flush() {
- if (mBackedFile == null) {
- close();
- }
- }
-
- public void close() {
- if (mIsMapped) {
- unmap(mHandle, mSize);
- mHandle = 0;
- }
-
- if (mDescriptor != null) {
- try {
- mDescriptor.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- mDescriptor = null;
- }
- }
-
- // Should only be called by process that allocates shared memory.
- public void dispose() {
- if (!isValid()) {
- return;
- }
-
- close();
-
- if (mBackedFile != null) {
- mBackedFile.close();
- mBackedFile = null;
- }
- }
-
- private native void unmap(long address, int size);
-
- public boolean isValid() { return mDescriptor != null; }
-
- public int getSize() { return mSize; }
-
- private int getFD() {
- return isValid() ? mDescriptor.getFd() : -1;
- }
-
- public long getPointer() {
- if (!isValid()) {
- return 0;
- }
-
- if (!mIsMapped) {
- mHandle = map(getFD(), mSize);
- if (mHandle != 0) {
- mIsMapped = true;
- }
- }
- return mHandle;
- }
-
- private native long map(int fd, int size);
-
- @Override
- protected void finalize() throws Throwable {
- if (mBackedFile != null) {
- Log.w(LOGTAG, "dispose() not called before finalizing");
- }
- dispose();
-
- super.finalize();
- }
-
- @Override
- public String toString() {
- return "SHM(" + getSize() + " bytes): id=" + mId + ", backing=" + mBackedFile + ",fd=" + mDescriptor;
- }
-
- @Override
- public boolean equals(Object that) {
- return (this == that) ||
- ((that instanceof SharedMemory) && (hashCode() == that.hashCode()));
- }
-
- @Override
- public int hashCode() {
- return mId;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationClient.java b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationClient.java
deleted file mode 100644
index c46c01050..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationClient.java
+++ /dev/null
@@ -1,324 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.notifications;
-
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.support.v4.app.NotificationCompat;
-import android.support.v4.app.NotificationManagerCompat;
-import android.util.Log;
-
-import java.util.HashMap;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoService;
-import org.mozilla.gecko.NotificationListener;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.gfx.BitmapUtils;
-
-/**
- * Client for posting notifications.
- */
-public final class NotificationClient implements NotificationListener {
- private static final String LOGTAG = "GeckoNotificationClient";
- /* package */ static final String CLICK_ACTION = AppConstants.ANDROID_PACKAGE_NAME + ".NOTIFICATION_CLICK";
- /* package */ static final String CLOSE_ACTION = AppConstants.ANDROID_PACKAGE_NAME + ".NOTIFICATION_CLOSE";
- /* package */ static final String PERSISTENT_INTENT_EXTRA = "persistentIntent";
-
- private final Context mContext;
- private final NotificationManagerCompat mNotificationManager;
-
- private final HashMap<String, Notification> mNotifications = new HashMap<>();
-
- /**
- * Notification associated with this service's foreground state.
- *
- * {@link android.app.Service#startForeground(int, android.app.Notification)}
- * associates the foreground with exactly one notification from the service.
- * To keep Fennec alive during downloads (and to make sure it can be killed
- * once downloads are complete), we make sure that the foreground is always
- * associated with an active progress notification if and only if at least
- * one download is in progress.
- */
- private String mForegroundNotification;
-
- public NotificationClient(Context context) {
- mContext = context.getApplicationContext();
- mNotificationManager = NotificationManagerCompat.from(mContext);
- }
-
- @Override // NotificationListener
- public void showNotification(String name, String cookie, String title,
- String text, String host, String imageUrl) {
- showNotification(name, cookie, title, text, host, imageUrl, /* data */ null);
- }
-
- @Override // NotificationListener
- public void showPersistentNotification(String name, String cookie, String title,
- String text, String host, String imageUrl,
- String data) {
- showNotification(name, cookie, title, text, host, imageUrl, data != null ? data : "");
- }
-
- private void showNotification(String name, String cookie, String title,
- String text, String host, String imageUrl,
- String persistentData) {
- // Put the strings into the intent as an URI
- // "alert:?name=<name>&cookie=<cookie>"
- String packageName = AppConstants.ANDROID_PACKAGE_NAME;
- String className = AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS;
- if (GeckoAppShell.getGeckoInterface() != null) {
- final ComponentName comp = GeckoAppShell.getGeckoInterface()
- .getActivity().getComponentName();
- packageName = comp.getPackageName();
- className = comp.getClassName();
- }
- final Uri dataUri = (new Uri.Builder())
- .scheme("moz-notification")
- .authority(packageName)
- .path(className)
- .appendQueryParameter("name", name)
- .appendQueryParameter("cookie", cookie)
- .build();
-
- final Intent clickIntent = new Intent(CLICK_ACTION);
- clickIntent.setClass(mContext, NotificationReceiver.class);
- clickIntent.setData(dataUri);
-
- if (persistentData != null) {
- final Intent persistentIntent = GeckoService.getIntentToCreateServices(
- mContext, "persistent-notification-click", persistentData);
- clickIntent.putExtra(PERSISTENT_INTENT_EXTRA, persistentIntent);
- }
-
- final PendingIntent clickPendingIntent = PendingIntent.getBroadcast(
- mContext, 0, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT);
-
- final Intent closeIntent = new Intent(CLOSE_ACTION);
- closeIntent.setClass(mContext, NotificationReceiver.class);
- closeIntent.setData(dataUri);
-
- if (persistentData != null) {
- final Intent persistentIntent = GeckoService.getIntentToCreateServices(
- mContext, "persistent-notification-close", persistentData);
- closeIntent.putExtra(PERSISTENT_INTENT_EXTRA, persistentIntent);
- }
-
- final PendingIntent closePendingIntent = PendingIntent.getBroadcast(
- mContext, 0, closeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
-
- add(name, imageUrl, host, title, text, clickPendingIntent, closePendingIntent);
- GeckoAppShell.onNotificationShow(name, cookie);
- }
-
- @Override // NotificationListener
- public void closeNotification(String name)
- {
- remove(name);
- }
-
- /**
- * Adds a notification; used for web notifications.
- *
- * @param name the unique name of the notification
- * @param imageUrl URL of the image to use
- * @param alertTitle title of the notification
- * @param alertText text of the notification
- * @param contentIntent Intent used when the notification is clicked
- * @param deleteIntent Intent used when the notification is closed
- */
- private void add(final String name, final String imageUrl, final String host,
- final String alertTitle, final String alertText,
- final PendingIntent contentIntent, final PendingIntent deleteIntent) {
- final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext)
- .setContentTitle(alertTitle)
- .setContentText(alertText)
- .setSmallIcon(R.drawable.ic_status_logo)
- .setContentIntent(contentIntent)
- .setDeleteIntent(deleteIntent)
- .setAutoCancel(true)
- .setStyle(new NotificationCompat.BigTextStyle()
- .bigText(alertText)
- .setSummaryText(host));
-
- // Fetch icon.
- if (!imageUrl.isEmpty()) {
- final Bitmap image = BitmapUtils.decodeUrl(imageUrl);
- builder.setLargeIcon(image);
- }
-
- builder.setWhen(System.currentTimeMillis());
- final Notification notification = builder.build();
-
- synchronized (this) {
- mNotifications.put(name, notification);
- }
-
- mNotificationManager.notify(name, 0, notification);
- }
-
- /**
- * Adds a notification; used for Fennec app notifications.
- *
- * @param name the unique name of the notification
- * @param notification the Notification to add
- */
- public synchronized void add(final String name, final Notification notification) {
- final boolean ongoing = isOngoing(notification);
-
- if (ongoing != isOngoing(mNotifications.get(name))) {
- // In order to change notification from ongoing to non-ongoing, or vice versa,
- // we have to remove the previous notification, because ongoing notifications
- // use a different id value than non-ongoing notifications.
- onNotificationClose(name);
- }
-
- mNotifications.put(name, notification);
-
- if (!ongoing) {
- mNotificationManager.notify(name, 0, notification);
- return;
- }
-
- // Ongoing
- if (mForegroundNotification == null) {
- setForegroundNotificationLocked(name, notification);
- } else if (mForegroundNotification.equals(name)) {
- // Shortcut to update the current foreground notification, instead of
- // going through the service.
- mNotificationManager.notify(R.id.foregroundNotification, notification);
- }
- }
-
- /**
- * Updates a notification.
- *
- * @param name Name of existing notification
- * @param progress progress of item being updated
- * @param progressMax max progress of item being updated
- * @param alertText text of the notification
- */
- public void update(final String name, final long progress,
- final long progressMax, final String alertText) {
- Notification notification;
- synchronized (this) {
- notification = mNotifications.get(name);
- }
- if (notification == null) {
- return;
- }
-
- notification = new NotificationCompat.Builder(mContext)
- .setContentText(alertText)
- .setSmallIcon(notification.icon)
- .setWhen(notification.when)
- .setContentIntent(notification.contentIntent)
- .setProgress((int) progressMax, (int) progress, false)
- .build();
-
- add(name, notification);
- }
-
- /* package */ synchronized Notification onNotificationClose(final String name) {
- mNotificationManager.cancel(name, 0);
-
- final Notification notification = mNotifications.remove(name);
- if (notification != null) {
- updateForegroundNotificationLocked(name);
- }
- return notification;
- }
-
- /**
- * Removes a notification.
- *
- * @param name Name of existing notification
- */
- public synchronized void remove(final String name) {
- final Notification notification = onNotificationClose(name);
- if (notification == null || notification.deleteIntent == null) {
- return;
- }
-
- // Canceling the notification doesn't trigger the delete intent, so we
- // have to trigger it manually.
- try {
- notification.deleteIntent.send();
- } catch (final PendingIntent.CanceledException e) {
- // Ignore.
- }
- }
-
- /**
- * Determines whether the service is done.
- *
- * The service is considered finished when all notifications have been
- * removed.
- *
- * @return whether all notifications have been removed
- */
- public synchronized boolean isDone() {
- return mNotifications.isEmpty();
- }
-
- /**
- * Determines whether a notification should hold a foreground service to keep Gecko alive
- *
- * @param name the name of the notification to check
- * @return whether the notification is ongoing
- */
- public synchronized boolean isOngoing(final String name) {
- return isOngoing(mNotifications.get(name));
- }
-
- /**
- * Determines whether a notification should hold a foreground service to keep Gecko alive
- *
- * @param notification the notification to check
- * @return whether the notification is ongoing
- */
- public boolean isOngoing(final Notification notification) {
- if (notification != null && (notification.flags & Notification.FLAG_ONGOING_EVENT) != 0) {
- return true;
- }
- return false;
- }
-
- private void setForegroundNotificationLocked(final String name,
- final Notification notification) {
- mForegroundNotification = name;
-
- final Intent intent = new Intent(mContext, NotificationService.class);
- intent.putExtra(NotificationService.EXTRA_NOTIFICATION, notification);
- mContext.startService(intent);
- }
-
- private void updateForegroundNotificationLocked(final String oldName) {
- if (mForegroundNotification == null || !mForegroundNotification.equals(oldName)) {
- return;
- }
-
- // If we're removing the notification associated with the
- // foreground, we need to pick another active notification to act
- // as the foreground notification.
- for (final String name : mNotifications.keySet()) {
- final Notification notification = mNotifications.get(name);
- if (isOngoing(notification)) {
- setForegroundNotificationLocked(name, notification);
- return;
- }
- }
-
- setForegroundNotificationLocked(null, null);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationHelper.java b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationHelper.java
deleted file mode 100644
index 1e33031b5..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationHelper.java
+++ /dev/null
@@ -1,366 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.notifications;
-
-import java.util.HashMap;
-import java.util.Iterator;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.gfx.BitmapUtils;
-import org.mozilla.gecko.mozglue.SafeIntent;
-import org.mozilla.gecko.util.GeckoEventListener;
-
-import android.app.PendingIntent;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.support.v4.app.NotificationCompat;
-import android.util.Log;
-
-public final class NotificationHelper implements GeckoEventListener {
- public static final String HELPER_BROADCAST_ACTION = AppConstants.ANDROID_PACKAGE_NAME + ".helperBroadcastAction";
-
- public static final String NOTIFICATION_ID = "NotificationHelper_ID";
- private static final String LOGTAG = "GeckoNotificationHelper";
- private static final String HELPER_NOTIFICATION = "helperNotif";
-
- // Attributes mandatory to be used while sending a notification from js.
- private static final String TITLE_ATTR = "title";
- private static final String TEXT_ATTR = "text";
- /* package */ static final String ID_ATTR = "id";
- private static final String SMALLICON_ATTR = "smallIcon";
-
- // Attributes that can be used while sending a notification from js.
- private static final String PROGRESS_VALUE_ATTR = "progress_value";
- private static final String PROGRESS_MAX_ATTR = "progress_max";
- private static final String PROGRESS_INDETERMINATE_ATTR = "progress_indeterminate";
- private static final String LIGHT_ATTR = "light";
- private static final String ONGOING_ATTR = "ongoing";
- private static final String WHEN_ATTR = "when";
- private static final String PRIORITY_ATTR = "priority";
- private static final String LARGE_ICON_ATTR = "largeIcon";
- private static final String ACTIONS_ATTR = "actions";
- private static final String ACTION_ID_ATTR = "buttonId";
- private static final String ACTION_TITLE_ATTR = "title";
- private static final String ACTION_ICON_ATTR = "icon";
- private static final String PERSISTENT_ATTR = "persistent";
- private static final String HANDLER_ATTR = "handlerKey";
- private static final String COOKIE_ATTR = "cookie";
- static final String EVENT_TYPE_ATTR = "eventType";
-
- private static final String NOTIFICATION_SCHEME = "moz-notification";
-
- private static final String BUTTON_EVENT = "notification-button-clicked";
- private static final String CLICK_EVENT = "notification-clicked";
- static final String CLEARED_EVENT = "notification-cleared";
-
- static final String ORIGINAL_EXTRA_COMPONENT = "originalComponent";
-
- private final Context mContext;
-
- // Holds a list of notifications that should be cleared if the Fennec Activity is shut down.
- // Will not include ongoing or persistent notifications that are tied to Gecko's lifecycle.
- private HashMap<String, String> mClearableNotifications;
-
- private boolean mInitialized;
- private static NotificationHelper sInstance;
-
- private NotificationHelper(Context context) {
- mContext = context;
- }
-
- public void init() {
- if (mInitialized) {
- return;
- }
-
- mClearableNotifications = new HashMap<String, String>();
- EventDispatcher.getInstance().registerGeckoThreadListener(this,
- "Notification:Show",
- "Notification:Hide");
- mInitialized = true;
- }
-
- public static NotificationHelper getInstance(Context context) {
- // If someone else created this singleton, but didn't initialize it, something has gone wrong.
- if (sInstance != null && !sInstance.mInitialized) {
- throw new IllegalStateException("NotificationHelper was created by someone else but not initialized");
- }
-
- if (sInstance == null) {
- sInstance = new NotificationHelper(context.getApplicationContext());
- }
- return sInstance;
- }
-
- @Override
- public void handleMessage(String event, JSONObject message) {
- if (event.equals("Notification:Show")) {
- showNotification(message);
- } else if (event.equals("Notification:Hide")) {
- hideNotification(message);
- }
- }
-
- public boolean isHelperIntent(Intent i) {
- return i.getBooleanExtra(HELPER_NOTIFICATION, false);
- }
-
- public static void getArgsAndSendNotificationIntent(SafeIntent intent) {
- final JSONObject args = new JSONObject();
- final Uri data = intent.getData();
-
- final String notificationType = data.getQueryParameter(EVENT_TYPE_ATTR);
-
- try {
- args.put(ID_ATTR, data.getQueryParameter(ID_ATTR));
- args.put(EVENT_TYPE_ATTR, notificationType);
- args.put(HANDLER_ATTR, data.getQueryParameter(HANDLER_ATTR));
- args.put(COOKIE_ATTR, intent.getStringExtra(COOKIE_ATTR));
-
- if (BUTTON_EVENT.equals(notificationType)) {
- final String actionName = data.getQueryParameter(ACTION_ID_ATTR);
- args.put(ACTION_ID_ATTR, actionName);
- }
-
- Log.i(LOGTAG, "Send " + args.toString());
- GeckoAppShell.notifyObservers("Notification:Event", args.toString());
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error building JSON notification arguments.", e);
- }
- }
-
- public void handleNotificationIntent(SafeIntent i) {
- final Uri data = i.getData();
- final String notificationType = data.getQueryParameter(EVENT_TYPE_ATTR);
- final String id = data.getQueryParameter(ID_ATTR);
- if (id == null || notificationType == null) {
- Log.e(LOGTAG, "handleNotificationEvent: invalid intent parameters");
- return;
- }
-
- getArgsAndSendNotificationIntent(i);
-
- // If the notification was clicked, we are closing it. This must be executed after
- // sending the event to js side because when the notification is canceled no event can be
- // handled.
- if (CLICK_EVENT.equals(notificationType) && !i.getBooleanExtra(ONGOING_ATTR, false)) {
- // The handler and cookie parameters are optional.
- final String handler = data.getQueryParameter(HANDLER_ATTR);
- final String cookie = i.getStringExtra(COOKIE_ATTR);
- hideNotification(id, handler, cookie);
- }
- }
-
- private Uri.Builder getNotificationBuilder(JSONObject message, String type) {
- Uri.Builder b = new Uri.Builder();
- b.scheme(NOTIFICATION_SCHEME).appendQueryParameter(EVENT_TYPE_ATTR, type);
-
- try {
- final String id = message.getString(ID_ATTR);
- b.appendQueryParameter(ID_ATTR, id);
- } catch (JSONException ex) {
- Log.i(LOGTAG, "buildNotificationPendingIntent, error parsing", ex);
- }
-
- try {
- final String id = message.getString(HANDLER_ATTR);
- b.appendQueryParameter(HANDLER_ATTR, id);
- } catch (JSONException ex) {
- Log.i(LOGTAG, "Notification doesn't have a handler");
- }
-
- return b;
- }
-
- private Intent buildNotificationIntent(JSONObject message, Uri.Builder builder) {
- Intent notificationIntent = new Intent(HELPER_BROADCAST_ACTION);
- final boolean ongoing = message.optBoolean(ONGOING_ATTR);
- notificationIntent.putExtra(ONGOING_ATTR, ongoing);
-
- final Uri dataUri = builder.build();
- notificationIntent.setData(dataUri);
- notificationIntent.putExtra(HELPER_NOTIFICATION, true);
- notificationIntent.putExtra(COOKIE_ATTR, message.optString(COOKIE_ATTR));
-
- // All intents get routed through the notificationReceiver. That lets us bail if we don't want to start Gecko
- final ComponentName name = new ComponentName(mContext, GeckoAppShell.getGeckoInterface().getActivity().getClass());
- notificationIntent.putExtra(ORIGINAL_EXTRA_COMPONENT, name);
-
- return notificationIntent;
- }
-
- private PendingIntent buildNotificationPendingIntent(JSONObject message, String type) {
- Uri.Builder builder = getNotificationBuilder(message, type);
- final Intent notificationIntent = buildNotificationIntent(message, builder);
- return PendingIntent.getBroadcast(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- private PendingIntent buildButtonClickPendingIntent(JSONObject message, JSONObject action) {
- Uri.Builder builder = getNotificationBuilder(message, BUTTON_EVENT);
- try {
- // Action name must be in query uri, otherwise buttons pending intents
- // would be collapsed.
- if (action.has(ACTION_ID_ATTR)) {
- builder.appendQueryParameter(ACTION_ID_ATTR, action.getString(ACTION_ID_ATTR));
- } else {
- Log.i(LOGTAG, "button event with no name");
- }
- } catch (JSONException ex) {
- Log.i(LOGTAG, "buildNotificationPendingIntent, error parsing", ex);
- }
- final Intent notificationIntent = buildNotificationIntent(message, builder);
- PendingIntent res = PendingIntent.getBroadcast(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
- return res;
- }
-
- private void showNotification(JSONObject message) {
- NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext);
-
- // These attributes are required
- final String id;
- try {
- builder.setContentTitle(message.getString(TITLE_ATTR));
- builder.setContentText(message.getString(TEXT_ATTR));
- id = message.getString(ID_ATTR);
- } catch (JSONException ex) {
- Log.i(LOGTAG, "Error parsing", ex);
- return;
- }
-
- Uri imageUri = Uri.parse(message.optString(SMALLICON_ATTR));
- builder.setSmallIcon(BitmapUtils.getResource(mContext, imageUri));
-
- JSONArray light = message.optJSONArray(LIGHT_ATTR);
- if (light != null && light.length() == 3) {
- try {
- builder.setLights(light.getInt(0),
- light.getInt(1),
- light.getInt(2));
- } catch (JSONException ex) {
- Log.i(LOGTAG, "Error parsing", ex);
- }
- }
-
- boolean ongoing = message.optBoolean(ONGOING_ATTR);
- builder.setOngoing(ongoing);
-
- if (message.has(WHEN_ATTR)) {
- long when = message.optLong(WHEN_ATTR);
- builder.setWhen(when);
- }
-
- if (message.has(PRIORITY_ATTR)) {
- int priority = message.optInt(PRIORITY_ATTR);
- builder.setPriority(priority);
- }
-
- if (message.has(LARGE_ICON_ATTR)) {
- Bitmap b = BitmapUtils.getBitmapFromDataURI(message.optString(LARGE_ICON_ATTR));
- builder.setLargeIcon(b);
- }
-
- if (message.has(PROGRESS_VALUE_ATTR) &&
- message.has(PROGRESS_MAX_ATTR) &&
- message.has(PROGRESS_INDETERMINATE_ATTR)) {
- try {
- final int progress = message.getInt(PROGRESS_VALUE_ATTR);
- final int progressMax = message.getInt(PROGRESS_MAX_ATTR);
- final boolean progressIndeterminate = message.getBoolean(PROGRESS_INDETERMINATE_ATTR);
- builder.setProgress(progressMax, progress, progressIndeterminate);
- } catch (JSONException ex) {
- Log.i(LOGTAG, "Error parsing", ex);
- }
- }
-
- JSONArray actions = message.optJSONArray(ACTIONS_ATTR);
- if (actions != null) {
- try {
- for (int i = 0; i < actions.length(); i++) {
- JSONObject action = actions.getJSONObject(i);
- final PendingIntent pending = buildButtonClickPendingIntent(message, action);
- final String actionTitle = action.getString(ACTION_TITLE_ATTR);
- final Uri actionImage = Uri.parse(action.optString(ACTION_ICON_ATTR));
- builder.addAction(BitmapUtils.getResource(mContext, actionImage),
- actionTitle,
- pending);
- }
- } catch (JSONException ex) {
- Log.i(LOGTAG, "Error parsing", ex);
- }
- }
-
- PendingIntent pi = buildNotificationPendingIntent(message, CLICK_EVENT);
- builder.setContentIntent(pi);
- PendingIntent deletePendingIntent = buildNotificationPendingIntent(message, CLEARED_EVENT);
- builder.setDeleteIntent(deletePendingIntent);
-
- ((NotificationClient) GeckoAppShell.getNotificationListener()).add(id, builder.build());
-
- boolean persistent = message.optBoolean(PERSISTENT_ATTR);
- // We add only not persistent notifications to the list since we want to purge only
- // them when geckoapp is destroyed.
- if (!persistent && !mClearableNotifications.containsKey(id)) {
- mClearableNotifications.put(id, message.toString());
- }
- }
-
- private void hideNotification(JSONObject message) {
- final String id;
- final String handler;
- final String cookie;
- try {
- id = message.getString("id");
- handler = message.optString("handlerKey");
- cookie = message.optString("cookie");
- } catch (JSONException ex) {
- Log.i(LOGTAG, "Error parsing", ex);
- return;
- }
-
- hideNotification(id, handler, cookie);
- }
-
- private void closeNotification(String id, String handlerKey, String cookie) {
- ((NotificationClient) GeckoAppShell.getNotificationListener()).remove(id);
- }
-
- public void hideNotification(String id, String handlerKey, String cookie) {
- mClearableNotifications.remove(id);
- closeNotification(id, handlerKey, cookie);
- }
-
- private void clearAll() {
- for (Iterator<String> i = mClearableNotifications.keySet().iterator(); i.hasNext();) {
- final String id = i.next();
- final String json = mClearableNotifications.get(id);
- i.remove();
-
- JSONObject obj;
- try {
- obj = new JSONObject(json);
- } catch (JSONException ex) {
- obj = new JSONObject();
- }
-
- closeNotification(id, obj.optString(HANDLER_ATTR), obj.optString(COOKIE_ATTR));
- }
- }
-
- public static void destroy() {
- if (sInstance != null) {
- sInstance.clearAll();
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationReceiver.java b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationReceiver.java
deleted file mode 100644
index c3dd43297..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationReceiver.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.notifications;
-
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoThread;
-import org.mozilla.gecko.mozglue.SafeIntent;
-
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.util.Log;
-
-/**
- * Broadcast receiver for Notifications. Will forward them to GeckoApp (and start Gecko) if they're clicked.
- * If they're being dismissed, it will not start Gecko, but may forward them to JS if Gecko is running.
- * This is also the only entry point for notification intents.
- */
-public class NotificationReceiver extends BroadcastReceiver {
- private static final String LOGTAG = "GeckoNotificationReceiver";
-
- public void onReceive(Context context, Intent intent) {
- final Uri data = intent.getData();
- if (data == null) {
- Log.e(LOGTAG, "handleNotificationEvent: empty data");
- return;
- }
-
- final String action = intent.getAction();
- if (NotificationClient.CLICK_ACTION.equals(action) ||
- NotificationClient.CLOSE_ACTION.equals(action)) {
- onNotificationClientAction(context, action, data, intent);
- return;
- }
-
- final String notificationType = data.getQueryParameter(NotificationHelper.EVENT_TYPE_ATTR);
- if (notificationType == null) {
- return;
- }
-
- // In case the user swiped out the notification, we empty the id set.
- if (NotificationHelper.CLEARED_EVENT.equals(notificationType)) {
- // If Gecko isn't running, we throw away events where the notification was cancelled.
- // i.e. Don't bug the user if they're just closing a bunch of notifications.
- if (GeckoThread.isRunning()) {
- NotificationHelper.getArgsAndSendNotificationIntent(new SafeIntent(intent));
- }
-
- final NotificationClient client = (NotificationClient)
- GeckoAppShell.getNotificationListener();
- client.onNotificationClose(data.getQueryParameter(NotificationHelper.ID_ATTR));
- return;
- }
-
- forwardMessageToActivity(intent, context);
- }
-
- private void forwardMessageToActivity(final Intent intent, final Context context) {
- final ComponentName name = intent.getExtras().getParcelable(NotificationHelper.ORIGINAL_EXTRA_COMPONENT);
- intent.setComponent(name);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- context.startActivity(intent);
- }
-
- private void onNotificationClientAction(final Context context, final String action,
- final Uri data, final Intent intent) {
- final String name = data.getQueryParameter("name");
- final String cookie = data.getQueryParameter("cookie");
- final Intent persistentIntent = (Intent)
- intent.getParcelableExtra(NotificationClient.PERSISTENT_INTENT_EXTRA);
-
- if (persistentIntent != null) {
- // Go through GeckoService for persistent notifications.
- context.startService(persistentIntent);
- }
-
- if (NotificationClient.CLICK_ACTION.equals(action)) {
- GeckoAppShell.onNotificationClick(name, cookie);
-
- if (persistentIntent != null) {
- // Don't launch GeckoApp if it's a background persistent notification.
- return;
- }
-
- final Intent appIntent = new Intent(GeckoApp.ACTION_ALERT_CALLBACK);
- appIntent.setComponent(new ComponentName(
- data.getAuthority(), data.getPath().substring(1))); // exclude leading slash.
- appIntent.setData(data);
- appIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- context.startActivity(appIntent);
-
- } else if (NotificationClient.CLOSE_ACTION.equals(action)) {
- GeckoAppShell.onNotificationClose(name, cookie);
-
- final NotificationClient client = (NotificationClient)
- GeckoAppShell.getNotificationListener();
- client.onNotificationClose(name);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationService.java b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationService.java
deleted file mode 100644
index 04b94cd1a..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationService.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.notifications;
-
-import android.app.Notification;
-import android.app.Service;
-import android.content.Intent;
-import android.os.IBinder;
-
-import org.mozilla.gecko.R;
-
-public final class NotificationService extends Service {
- public static final String EXTRA_NOTIFICATION = "notification";
-
- @Override // Service
- public int onStartCommand(final Intent intent, final int flags, final int startId) {
- final Notification notification = intent.getParcelableExtra(EXTRA_NOTIFICATION);
- if (notification != null) {
- // Start foreground notification.
- startForeground(R.id.foregroundNotification, notification);
- return START_NOT_STICKY;
- }
-
- // Stop foreground notification
- stopForeground(true);
- stopSelfResult(startId);
- return START_NOT_STICKY;
- }
-
- @Override // Service
- public IBinder onBind(final Intent intent) {
- return null;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/notifications/WhatsNewReceiver.java b/mobile/android/base/java/org/mozilla/gecko/notifications/WhatsNewReceiver.java
deleted file mode 100644
index 6e799bf74..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/notifications/WhatsNewReceiver.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.notifications;
-
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.support.v4.app.NotificationCompat;
-import android.text.TextUtils;
-
-import com.keepsafe.switchboard.SwitchBoard;
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.Locales;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-import org.mozilla.gecko.Experiments;
-
-import java.util.Locale;
-
-public class WhatsNewReceiver extends BroadcastReceiver {
-
- public static final String EXTRA_WHATSNEW_NOTIFICATION = "whatsnew_notification";
- private static final String ACTION_NOTIFICATION_CANCELLED = "notification_cancelled";
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (ACTION_NOTIFICATION_CANCELLED.equals(intent.getAction())) {
- Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.NOTIFICATION, EXTRA_WHATSNEW_NOTIFICATION);
- return;
- }
-
- final String dataString = intent.getDataString();
- if (TextUtils.isEmpty(dataString) || !dataString.contains(AppConstants.ANDROID_PACKAGE_NAME)) {
- return;
- }
-
- if (!SwitchBoard.isInExperiment(context, Experiments.WHATSNEW_NOTIFICATION)) {
- return;
- }
-
- if (!isPreferenceEnabled(context)) {
- return;
- }
-
- showWhatsNewNotification(context);
- }
-
- private boolean isPreferenceEnabled(Context context) {
- return GeckoSharedPrefs.forApp(context).getBoolean(GeckoPreferences.PREFS_NOTIFICATIONS_WHATS_NEW, true);
- }
-
- private void showWhatsNewNotification(Context context) {
- final Notification notification = new NotificationCompat.Builder(context)
- .setContentTitle(context.getString(R.string.whatsnew_notification_title))
- .setContentText(context.getString(R.string.whatsnew_notification_summary))
- .setSmallIcon(R.drawable.ic_status_logo)
- .setAutoCancel(true)
- .setContentIntent(getContentIntent(context))
- .setDeleteIntent(getDeleteIntent(context))
- .build();
-
- final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- final int notificationID = EXTRA_WHATSNEW_NOTIFICATION.hashCode();
- notificationManager.notify(notificationID, notification);
-
- Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.NOTIFICATION, EXTRA_WHATSNEW_NOTIFICATION);
- }
-
- private PendingIntent getContentIntent(Context context) {
- final String link = context.getString(R.string.whatsnew_notification_url,
- AppConstants.MOZ_APP_VERSION,
- AppConstants.OS_TARGET,
- Locales.getLanguageTag(Locale.getDefault()));
-
- final Intent i = new Intent(Intent.ACTION_VIEW);
- i.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
- i.setData(Uri.parse(link));
- i.putExtra(EXTRA_WHATSNEW_NOTIFICATION, true);
-
- return PendingIntent.getActivity(context, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- private PendingIntent getDeleteIntent(Context context) {
- final Intent i = new Intent(context, WhatsNewReceiver.class);
- i.setAction(ACTION_NOTIFICATION_CANCELLED);
-
- return PendingIntent.getBroadcast(context, 0, i, PendingIntent.FLAG_CANCEL_CURRENT);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/OverlayConstants.java b/mobile/android/base/java/org/mozilla/gecko/overlays/OverlayConstants.java
deleted file mode 100644
index 16f5560d3..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/overlays/OverlayConstants.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.overlays;
-
-/**
- * Constants used by the share handler service (and clients).
- * The intent API used by the service is defined herein.
- */
-public class OverlayConstants {
- /*
- * OverlayIntentHandler service intent actions.
- */
-
- /*
- * Causes the service to broadcast an intent containing state necessary for proper display of
- * a UI to select a target share method.
- *
- * Intent parameters:
- *
- * None.
- */
- public static final String ACTION_PREPARE_SHARE = "org.mozilla.gecko.overlays.ACTION_PREPARE_SHARE";
-
- /*
- * Action for sharing a page.
- *
- * Intent parameters:
- *
- * $EXTRA_URL: URL of page to share. (required)
- * $EXTRA_SHARE_METHOD: Method(s) via which to share this url/title combination. Can be either a
- * ShareType or a ShareType[]
- * $EXTRA_TITLE: Title of page to share (optional)
- * $EXTRA_PARAMETERS: Parcelable of extra data to pass to the ShareMethod (optional)
- */
- public static final String ACTION_SHARE = "org.mozilla.gecko.overlays.ACTION_SHARE";
-
- /*
- * OverlayIntentHandler service intent extra field keys.
- */
-
- // The URL/title of the page being shared
- public static final String EXTRA_URL = "URL";
- public static final String EXTRA_TITLE = "TITLE";
-
- // The optional extra Parcelable parameters for a ShareMethod.
- public static final String EXTRA_PARAMETERS = "EXTRA";
-
- // The extra field key used for holding the ShareMethod.Type we wish to use for an operation.
- public static final String EXTRA_SHARE_METHOD = "SHARE_METHOD";
-
- /*
- * ShareMethod UI event intent constants. Broadcast by ShareMethods using LocalBroadcastManager
- * when state has changed that requires an update of any currently-displayed share UI.
- */
-
- /*
- * Action for a ShareMethod UI event.
- *
- * Intent parameters:
- *
- * $EXTRA_SHARE_METHOD: The ShareType to which this event relates.
- * ... ShareType-specific parameters as desired... (optional)
- */
- public static final String SHARE_METHOD_UI_EVENT = "org.mozilla.gecko.overlays.ACTION_SHARE_METHOD_UI_EVENT";
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/service/OverlayActionService.java b/mobile/android/base/java/org/mozilla/gecko/overlays/service/OverlayActionService.java
deleted file mode 100644
index 7182fcce7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/overlays/service/OverlayActionService.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.overlays.service;
-
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.os.IBinder;
-import android.util.Log;
-
-import org.mozilla.gecko.overlays.service.sharemethods.AddBookmark;
-import org.mozilla.gecko.overlays.service.sharemethods.SendTab;
-import org.mozilla.gecko.overlays.service.sharemethods.ShareMethod;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import java.util.EnumMap;
-import java.util.Map;
-
-import static org.mozilla.gecko.overlays.OverlayConstants.ACTION_PREPARE_SHARE;
-import static org.mozilla.gecko.overlays.OverlayConstants.ACTION_SHARE;
-
-/**
- * A service to receive requests from overlays to perform actions.
- * See OverlayConstants for details of the intent API supported by this service.
- *
- * Currently supported operations are:
- *
- * Add bookmark*
- * Send tab (delegates to Sync's existing handler)
- * Future: Load page in background.
- *
- * * Neither of these incur a page fetch on the service... yet. That will require headless Gecko,
- * something we're yet to have. Refactoring Gecko as a service itself and restructing the rest of
- * the app to talk to it seems like the way to go there.
- */
-public class OverlayActionService extends Service {
- private static final String LOGTAG = "GeckoOverlayService";
-
- // Map used for selecting the appropriate helper object when handling a share.
- final Map<ShareMethod.Type, ShareMethod> shareTypes = new EnumMap<>(ShareMethod.Type.class);
-
- // Map relating Strings representing share types to the corresponding ShareMethods.
- // Share methods are initialised (and shown in the UI) in the order they are given here.
- // This map is used to look up the appropriate ShareMethod when handling a request, as well as
- // for identifying which ShareMethod needs re-initialising in response to such an intent (which
- // will be necessary in situations such as the deletion of Sync accounts).
-
- // Not a bindable service.
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- if (intent == null) {
- return START_NOT_STICKY;
- }
-
- // Dispatch intent to appropriate method according to its action.
- String action = intent.getAction();
-
- switch (action) {
- case ACTION_SHARE:
- handleShare(intent);
- break;
- case ACTION_PREPARE_SHARE:
- initShareMethods(getApplicationContext());
- break;
- default:
- throw new IllegalArgumentException("Unsupported intent action: " + action);
- }
-
- return START_NOT_STICKY;
- }
-
- /**
- * Reinitialise all ShareMethods, causing them to broadcast any UI update events necessary.
- */
- private void initShareMethods(final Context context) {
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- shareTypes.clear();
-
- shareTypes.put(ShareMethod.Type.ADD_BOOKMARK, new AddBookmark(context));
- shareTypes.put(ShareMethod.Type.SEND_TAB, new SendTab(context));
- }
- });
- }
-
- public void handleShare(final Intent intent) {
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- ShareData shareData;
- try {
- shareData = ShareData.fromIntent(intent);
- } catch (IllegalArgumentException e) {
- Log.e(LOGTAG, "Error parsing share intent: ", e);
- return;
- }
-
- ShareMethod shareMethod = shareTypes.get(shareData.shareMethodType);
-
- final ShareMethod.Result result = shareMethod.handle(shareData);
- // Dispatch the share to the targeted ShareMethod.
- switch (result) {
- case SUCCESS:
- Log.d(LOGTAG, "Share was successful");
- break;
- case TRANSIENT_FAILURE:
- // Fall-through
- case PERMANENT_FAILURE:
- Log.e(LOGTAG, "Share failed: " + result);
- break;
- default:
- throw new IllegalStateException("Unknown share method result code: " + result);
- }
- }
- });
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/service/ShareData.java b/mobile/android/base/java/org/mozilla/gecko/overlays/service/ShareData.java
deleted file mode 100644
index df233d74a..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/overlays/service/ShareData.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.overlays.service;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.Parcelable;
-import org.mozilla.gecko.overlays.OverlayConstants;
-import org.mozilla.gecko.overlays.service.sharemethods.ShareMethod;
-
-import static org.mozilla.gecko.overlays.OverlayConstants.EXTRA_SHARE_METHOD;
-
-/**
- * Class to hold information related to a particular request to perform a share.
- */
-public class ShareData {
- private static final String LOGTAG = "GeckoShareRequest";
-
- public final String url;
- public final String title;
- public final Parcelable extra;
- public final ShareMethod.Type shareMethodType;
-
- public ShareData(String url, String title, Parcelable extra, ShareMethod.Type shareMethodType) {
- if (url == null) {
- throw new IllegalArgumentException("Null url passed to ShareData!");
- }
-
- this.url = url;
- this.title = title;
- this.extra = extra;
- this.shareMethodType = shareMethodType;
- }
-
- public static ShareData fromIntent(Intent intent) {
- Bundle extras = intent.getExtras();
-
- // Fish the parameters out of the Intent.
- final String url = extras.getString(OverlayConstants.EXTRA_URL);
- final String title = extras.getString(OverlayConstants.EXTRA_TITLE);
- final Parcelable extra = extras.getParcelable(OverlayConstants.EXTRA_PARAMETERS);
- ShareMethod.Type shareMethodType = (ShareMethod.Type) extras.get(EXTRA_SHARE_METHOD);
-
- return new ShareData(url, title, extra, shareMethodType);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/AddBookmark.java b/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/AddBookmark.java
deleted file mode 100644
index 71931e683..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/AddBookmark.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.overlays.service.sharemethods;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.LocalBrowserDB;
-import org.mozilla.gecko.overlays.service.ShareData;
-
-public class AddBookmark extends ShareMethod {
- private static final String LOGTAG = "GeckoAddBookmark";
-
- @Override
- public Result handle(ShareData shareData) {
- ContentResolver resolver = context.getContentResolver();
-
- LocalBrowserDB browserDB = new LocalBrowserDB(GeckoProfile.DEFAULT_PROFILE);
- browserDB.addBookmark(resolver, shareData.title, shareData.url);
-
- return Result.SUCCESS;
- }
-
- public AddBookmark(Context context) {
- super(context);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/SendTab.java b/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/SendTab.java
deleted file mode 100644
index 5abcbd99f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/SendTab.java
+++ /dev/null
@@ -1,296 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.overlays.service.sharemethods;
-
-import android.accounts.Account;
-import android.accounts.AccountManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.database.Cursor;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.support.v4.content.LocalBroadcastManager;
-import android.util.Log;
-
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.db.RemoteClient;
-import org.mozilla.gecko.db.TabsAccessor;
-import org.mozilla.gecko.fxa.FxAccountConstants;
-import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
-import org.mozilla.gecko.fxa.login.State;
-import org.mozilla.gecko.overlays.OverlayConstants;
-import org.mozilla.gecko.overlays.service.ShareData;
-import org.mozilla.gecko.sync.CommandProcessor;
-import org.mozilla.gecko.sync.CommandRunner;
-import org.mozilla.gecko.sync.GlobalSession;
-import org.mozilla.gecko.sync.SyncConfiguration;
-
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-/**
- * ShareMethod implementation to handle Sync's "Send tab to device" mechanism.
- * See OverlayConstants for documentation of OverlayIntentHandler service intent API (which is how
- * this class is chiefly interacted with).
- */
-public class SendTab extends ShareMethod {
- private static final String LOGTAG = "GeckoSendTab";
-
- // Key used in the extras Bundle in the share intent used for a send tab ShareMethod.
- public static final String SEND_TAB_TARGET_DEVICES = "SEND_TAB_TARGET_DEVICES";
-
- // Key used in broadcast intent from SendTab ShareMethod specifying available RemoteClients.
- public static final String EXTRA_REMOTE_CLIENT_RECORDS = "RECORDS";
-
- // The intent we should dispatch when the button for this ShareMethod is tapped, instead of
- // taking the normal action (e.g., "Set up Sync!")
- public static final String OVERRIDE_INTENT = "OVERRIDE_INTENT";
-
- private Set<String> validGUIDs;
-
- // A TabSender appropriate to the account type we're connected to.
- private TabSender tabSender;
-
- @Override
- public Result handle(ShareData shareData) {
- if (shareData.extra == null) {
- Log.e(LOGTAG, "No target devices specified!");
-
- // Retrying with an identical lack of devices ain't gonna fix it...
- return Result.PERMANENT_FAILURE;
- }
-
- String[] targetGUIDs = ((Bundle) shareData.extra).getStringArray(SEND_TAB_TARGET_DEVICES);
-
- // Ensure all target GUIDs are devices we actually know about.
- if (!validGUIDs.containsAll(Arrays.asList(targetGUIDs))) {
- // Find the set of invalid GUIDs to provide a nice error message.
- Log.e(LOGTAG, "Not all provided GUIDs are real devices:");
- for (String targetGUID : targetGUIDs) {
- if (!validGUIDs.contains(targetGUID)) {
- Log.e(LOGTAG, "Invalid GUID: " + targetGUID);
- }
- }
-
- return Result.PERMANENT_FAILURE;
- }
-
- Log.i(LOGTAG, "Send tab handler invoked.");
-
- final CommandProcessor processor = CommandProcessor.getProcessor();
-
- final String accountGUID = tabSender.getAccountGUID();
- Log.d(LOGTAG, "Retrieved local account GUID '" + accountGUID + "'.");
-
- if (accountGUID == null) {
- Log.e(LOGTAG, "Cannot determine account GUID");
-
- // It's not completely out of the question that a background sync might come along and
- // fix everything for us...
- return Result.TRANSIENT_FAILURE;
- }
-
- // Queue up the share commands for each destination device.
- // Remember that ShareMethod.handle is always run on the background thread, so the database
- // access here is of no concern.
- for (int i = 0; i < targetGUIDs.length; i++) {
- processor.sendURIToClientForDisplay(shareData.url, targetGUIDs[i], shareData.title, accountGUID, context);
- }
-
- // Request an immediate sync to push these new commands to the network ASAP.
- Log.i(LOGTAG, "Requesting immediate clients stage sync.");
- tabSender.sync();
-
- return Result.SUCCESS;
- // ... Probably.
- }
-
- /**
- * Get an Intent suitable for broadcasting the UI state of this ShareMethod.
- * The caller shall populate the intent with the actual state.
- */
- private Intent getUIStateIntent() {
- Intent uiStateIntent = new Intent(OverlayConstants.SHARE_METHOD_UI_EVENT);
- uiStateIntent.putExtra(OverlayConstants.EXTRA_SHARE_METHOD, (Parcelable) Type.SEND_TAB);
- return uiStateIntent;
- }
-
- /**
- * Broadcast the given intent to any UIs that may be listening.
- */
- private void broadcastUIState(Intent uiStateIntent) {
- LocalBroadcastManager.getInstance(context).sendBroadcast(uiStateIntent);
- }
-
- /**
- * Load the state of the user's Firefox Sync accounts and broadcast it to any registered
- * listeners. This will cause any UIs that may exist that depend on this information to update.
- */
- public SendTab(Context aContext) {
- super(aContext);
- // Initialise the UI state intent...
-
- // Determine if the user has a new or old style sync account and load the available sync
- // clients for it.
- final AccountManager accountManager = AccountManager.get(context);
- final Account[] fxAccounts = accountManager.getAccountsByType(FxAccountConstants.ACCOUNT_TYPE);
-
- if (fxAccounts.length > 0) {
- final AndroidFxAccount fxAccount = new AndroidFxAccount(context, fxAccounts[0]);
- if (fxAccount.getState().getNeededAction() != State.Action.None) {
- // We have a Firefox Account, but it's definitely not able to send a tab
- // right now. Redirect to the status activity.
- Log.w(LOGTAG, "Firefox Account named like " + fxAccount.getObfuscatedEmail() +
- " needs action before it can send a tab; redirecting to status activity.");
-
- setOverrideIntentAction(FxAccountConstants.ACTION_FXA_STATUS);
- return;
- }
-
- tabSender = new FxAccountTabSender(fxAccount);
-
- updateClientList(tabSender);
-
- Log.i(LOGTAG, "Allowing tab send for Firefox Account.");
- registerDisplayURICommand();
- return;
- }
-
- // Have registered UIs offer to set up a Firefox Account.
- setOverrideIntentAction(FxAccountConstants.ACTION_FXA_GET_STARTED);
- }
-
- /**
- * Load the list of Sync clients that are not this device using the given TabSender.
- */
- private void updateClientList(TabSender tabSender) {
- Collection<RemoteClient> otherClients = getOtherClients(tabSender);
-
- // Put the list of RemoteClients into the uiStateIntent and broadcast it.
- RemoteClient[] records = new RemoteClient[otherClients.size()];
- records = otherClients.toArray(records);
-
- validGUIDs = new HashSet<>();
-
- for (RemoteClient client : otherClients) {
- validGUIDs.add(client.guid);
- }
-
- if (validGUIDs.isEmpty()) {
- // Guess we'd better override. We have no clients.
- // This does the broadcast for us.
- setOverrideIntentAction(FxAccountConstants.ACTION_FXA_GET_STARTED);
- return;
- }
-
- Intent uiStateIntent = getUIStateIntent();
- uiStateIntent.putExtra(EXTRA_REMOTE_CLIENT_RECORDS, records);
- broadcastUIState(uiStateIntent);
- }
-
- /**
- * Record our intention to redirect the user to a different activity when they attempt to share
- * with us, usually because we found something wrong with their Sync account (a need to login,
- * register, etc.)
- * This will be recorded in the OVERRIDE_INTENT field of the UI broadcast. Consumers should
- * dispatch this intent instead of attempting to share with this ShareMethod whenever it is
- * non-null.
- *
- * @param action to launch instead of invoking a share.
- */
- protected void setOverrideIntentAction(final String action) {
- Intent intent = new Intent(action);
- // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
- // the soft keyboard not being shown for the started activity. Why, Android, why?
- intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
- Intent uiStateIntent = getUIStateIntent();
- uiStateIntent.putExtra(OVERRIDE_INTENT, intent);
-
- broadcastUIState(uiStateIntent);
- }
-
- private static void registerDisplayURICommand() {
- final CommandProcessor processor = CommandProcessor.getProcessor();
- processor.registerCommand("displayURI", new CommandRunner(3) {
- @Override
- public void executeCommand(final GlobalSession session, List<String> args) {
- CommandProcessor.displayURI(args, session.getContext());
- }
- });
- }
-
- /**
- * @return A collection of unique remote clients sorted by most recently used.
- */
- protected Collection<RemoteClient> getOtherClients(final TabSender sender) {
- if (sender == null) {
- Log.w(LOGTAG, "No tab sender when fetching other client IDs.");
- return Collections.emptyList();
- }
-
- final BrowserDB browserDB = BrowserDB.from(context);
- final TabsAccessor tabsAccessor = browserDB.getTabsAccessor();
- final Cursor remoteTabsCursor = tabsAccessor.getRemoteClientsByRecencyCursor(context);
- try {
- if (remoteTabsCursor.getCount() == 0) {
- return Collections.emptyList();
- }
- return tabsAccessor.getClientsWithoutTabsByRecencyFromCursor(remoteTabsCursor);
- } finally {
- remoteTabsCursor.close();
- }
- }
-
- /**
- * Inteface for interacting with Sync accounts. Used to hide the difference in implementation
- * between FXA and "old sync" accounts when sending tabs.
- */
- private interface TabSender {
- public static final String[] STAGES_TO_SYNC = new String[] { "clients", "tabs" };
-
- /**
- * @return Return null if the account isn't correctly initialized. Return
- * the account GUID otherwise.
- */
- String getAccountGUID();
-
- /**
- * Sync this account, specifying only clients and tabs as the engines to sync.
- */
- void sync();
- }
-
- private static class FxAccountTabSender implements TabSender {
- private final AndroidFxAccount fxAccount;
-
- public FxAccountTabSender(AndroidFxAccount fxa) {
- fxAccount = fxa;
- }
-
- @Override
- public String getAccountGUID() {
- try {
- final SharedPreferences prefs = fxAccount.getSyncPrefs();
- return prefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null);
- } catch (Exception e) {
- Log.w(LOGTAG, "Could not get Firefox Account parameters or preferences; aborting.");
- return null;
- }
- }
-
- @Override
- public void sync() {
- fxAccount.requestImmediateSync(STAGES_TO_SYNC, null);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/ShareMethod.java b/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/ShareMethod.java
deleted file mode 100644
index 768176d63..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/ShareMethod.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.overlays.service.sharemethods;
-
-import android.content.Context;
-import android.os.Parcel;
-import android.os.Parcelable;
-import org.mozilla.gecko.overlays.service.ShareData;
-
-/**
- * Represents a method of sharing a URL/title. Add a bookmark? Send to a device? Add to reading list?
- */
-public abstract class ShareMethod {
- protected final Context context;
-
- public ShareMethod(Context aContext) {
- context = aContext;
- }
-
- /**
- * Perform a share for the given title/URL combination. Called on the background thread by the
- * handler service when a request is made. The "extra" parameter is provided should a ShareMethod
- * desire to handle the share differently based on some additional parameters.
- *
- * @param title The page title for the page being shared. May be null if none can be found.
- * @param url The URL of the page to be shared. Never null.
- * @param extra A Parcelable of ShareMethod-specific parameters that may be provided by the
- * caller. Generally null, but this field may be used to provide extra input to
- * the ShareMethod (such as the device to share to in the case of SendTab).
- * @return true if the attempt to share was a success. False in the event of an error.
- */
- public abstract Result handle(ShareData shareData);
-
- /**
- * Enum representing the possible results of performing a share.
- */
- public static enum Result {
- // Victory!
- SUCCESS,
-
- // Failure, but retrying the same action again might lead to success.
- TRANSIENT_FAILURE,
-
- // Failure, and you're not going to succeed until you reinitialise the ShareMethod (ie.
- // until you repeat the entire share action). Examples include broken Sync accounts, or
- // Sync accounts with no valid target devices (so the only way to fix this is to add some
- // and try again: pushing a retry button isn't sane).
- PERMANENT_FAILURE
- }
-
- /**
- * Enum representing types of ShareMethod. Parcelable so it may be efficiently used in Intents.
- */
- public static enum Type implements Parcelable {
- ADD_BOOKMARK,
- SEND_TAB;
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(final Parcel dest, final int flags) {
- dest.writeInt(ordinal());
- }
-
- public static final Creator<Type> CREATOR = new Creator<Type>() {
- @Override
- public Type createFromParcel(final Parcel source) {
- return Type.values()[source.readInt()];
- }
-
- @Override
- public Type[] newArray(final int size) {
- return new Type[size];
- }
- };
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/OverlayDialogButton.java b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/OverlayDialogButton.java
deleted file mode 100644
index 8b7bc872b..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/OverlayDialogButton.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.overlays.ui;
-
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.animation.Animation;
-import android.view.animation.AnimationUtils;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-/**
- * A button in the share overlay, such as the "Add to Reading List" button.
- * Has an associated icon and label, and two states: enabled and disabled.
- *
- * When disabled, tapping results in a "pop" animation causing the icon to pulse. When enabled,
- * tapping calls the OnClickListener set by the consumer in the usual way.
- */
-public class OverlayDialogButton extends LinearLayout {
- private static final String LOGTAG = "GeckoOverlayDialogButton";
-
- // We can't use super.isEnabled(), since we want to stay clickable in disabled state.
- private boolean isEnabled = true;
-
- private final ImageView iconView;
- private final TextView labelView;
-
- private String enabledText = "";
- private String disabledText = "";
-
- private OnClickListener enabledOnClickListener;
-
- public OverlayDialogButton(Context context) {
- this(context, null);
- }
-
- public OverlayDialogButton(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- setOrientation(LinearLayout.HORIZONTAL);
-
- LayoutInflater.from(context).inflate(R.layout.overlay_share_button, this);
-
- iconView = (ImageView) findViewById(R.id.overlaybtn_icon);
- labelView = (TextView) findViewById(R.id.overlaybtn_label);
-
- super.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
-
- if (isEnabled) {
- if (enabledOnClickListener != null) {
- enabledOnClickListener.onClick(v);
- } else {
- Log.e(LOGTAG, "enabledOnClickListener is null.");
- }
- } else {
- Animation anim = AnimationUtils.loadAnimation(getContext(), R.anim.overlay_pop);
- iconView.startAnimation(anim);
- }
- }
- });
-
- final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.OverlayDialogButton);
-
- Drawable drawable = typedArray.getDrawable(R.styleable.OverlayDialogButton_drawable);
- if (drawable != null) {
- setDrawable(drawable);
- }
-
- String disabledText = typedArray.getString(R.styleable.OverlayDialogButton_disabledText);
- if (disabledText != null) {
- this.disabledText = disabledText;
- }
-
- String enabledText = typedArray.getString(R.styleable.OverlayDialogButton_enabledText);
- if (enabledText != null) {
- this.enabledText = enabledText;
- }
-
- typedArray.recycle();
-
- setEnabled(true);
- }
-
- public void setDrawable(Drawable drawable) {
- iconView.setImageDrawable(drawable);
- }
-
- public void setText(String text) {
- labelView.setText(text);
- }
-
- @Override
- public void setOnClickListener(OnClickListener listener) {
- enabledOnClickListener = listener;
- }
-
- /**
- * Set the enabledness state of this view. We don't call super.setEnabled, as we want to remain
- * clickable even in the disabled state (but with a different click listener).
- */
- @Override
- public void setEnabled(boolean enabled) {
- isEnabled = enabled;
- iconView.setEnabled(enabled);
- labelView.setEnabled(enabled);
-
- if (enabled) {
- setText(enabledText);
- } else {
- setText(disabledText);
- }
- }
-
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabDeviceListArrayAdapter.java b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabDeviceListArrayAdapter.java
deleted file mode 100644
index 08e9c59f5..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabDeviceListArrayAdapter.java
+++ /dev/null
@@ -1,185 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.overlays.ui;
-
-import java.util.Collection;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.RemoteClient;
-import org.mozilla.gecko.overlays.ui.SendTabList.State;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-
-public class SendTabDeviceListArrayAdapter extends ArrayAdapter<RemoteClient> {
- @SuppressWarnings("unused")
- private static final String LOGTAG = "GeckoSendTabAdapter";
-
- private State currentState;
-
- // String to display when in a "button-like" special state. Instead of using a
- // RemoteClient we override the rendering using this string.
- private String dummyRecordName;
-
- private final SendTabTargetSelectedListener listener;
-
- private Collection<RemoteClient> records;
-
- // The AlertDialog to show in the event the record is pressed while in the SHOW_DEVICES state.
- // This will show the user a prompt to select a device from a longer list of devices.
- private AlertDialog dialog;
-
- public SendTabDeviceListArrayAdapter(Context context, SendTabTargetSelectedListener aListener) {
- super(context, R.layout.overlay_share_send_tab_item, R.id.overlaybtn_label);
-
- listener = aListener;
-
- // We do this manually and avoid multiple notifications when doing compound operations.
- setNotifyOnChange(false);
- }
-
- /**
- * Get an array of the contents of this adapter were it in the LIST state.
- * Useful for determining the "real" contents of the adapter.
- */
- public RemoteClient[] toArray() {
- return records.toArray(new RemoteClient[records.size()]);
- }
-
- public void setRemoteClientsList(Collection<RemoteClient> remoteClientsList) {
- records = remoteClientsList;
- updateRecordList();
- }
-
- /**
- * Ensure the contents of the Adapter are synchronised with the `records` field. This may not
- * be the case if records has recently changed, or if we have experienced a state change.
- */
- public void updateRecordList() {
- if (currentState != State.LIST) {
- return;
- }
-
- clear();
-
- setNotifyOnChange(false); // So we don't notify for each add.
- addAll(records);
-
- notifyDataSetChanged();
- }
-
- @Override
- public View getView(final int position, View convertView, ViewGroup parent) {
- final Context context = getContext();
-
- // Reuse View objects if they exist.
- OverlayDialogButton row = (OverlayDialogButton) convertView;
- if (row == null) {
- row = (OverlayDialogButton) View.inflate(context, R.layout.overlay_share_send_tab_item, null);
- }
-
- // The first view in the list has a unique style.
- if (position == 0) {
- row.setBackgroundResource(R.drawable.overlay_share_button_background_first);
- } else {
- row.setBackgroundResource(R.drawable.overlay_share_button_background);
- }
-
- if (currentState != State.LIST) {
- // If we're in a special "Button-like" state, use the override string and a generic icon.
- final Drawable sendTabIcon = context.getResources().getDrawable(R.drawable.shareplane);
- row.setText(dummyRecordName);
- row.setDrawable(sendTabIcon);
- }
-
- // If we're just a button to launch the dialog, set the listener and abort.
- if (currentState == State.SHOW_DEVICES) {
- row.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View view) {
- dialog.show();
- }
- });
-
- return row;
- }
-
- // The remaining states delegate to the SentTabTargetSelectedListener.
- final RemoteClient remoteClient = getItem(position);
- if (currentState == State.LIST) {
- final Drawable clientIcon = context.getResources().getDrawable(getImage(remoteClient));
- row.setText(remoteClient.name);
- row.setDrawable(clientIcon);
-
- final String listenerGUID = remoteClient.guid;
-
- row.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View view) {
- listener.onSendTabTargetSelected(listenerGUID);
- }
- });
- } else {
- row.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View view) {
- listener.onSendTabActionSelected();
- }
- });
- }
-
- return row;
- }
-
- private static int getImage(RemoteClient record) {
- if ("mobile".equals(record.deviceType)) {
- return R.drawable.device_mobile;
- }
-
- return R.drawable.device_desktop;
- }
-
- public void switchState(State newState) {
- if (currentState == newState) {
- return;
- }
-
- currentState = newState;
-
- switch (newState) {
- case LIST:
- updateRecordList();
- break;
- case NONE:
- showDummyRecord(getContext().getResources().getString(R.string.overlay_share_send_tab_btn_label));
- break;
- case SHOW_DEVICES:
- showDummyRecord(getContext().getResources().getString(R.string.overlay_share_send_other));
- break;
- default:
- throw new IllegalStateException("Unexpected state transition: " + newState);
- }
- }
-
- /**
- * Set the dummy override string to the given value and clear the list.
- */
- private void showDummyRecord(String name) {
- dummyRecordName = name;
- clear();
- add(null);
- notifyDataSetChanged();
- }
-
- public void setDialog(AlertDialog aDialog) {
- dialog = aDialog;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabList.java b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabList.java
deleted file mode 100644
index 4fc6caaa9..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabList.java
+++ /dev/null
@@ -1,150 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.overlays.ui;
-
-import static org.mozilla.gecko.overlays.ui.SendTabList.State.LOADING;
-import static org.mozilla.gecko.overlays.ui.SendTabList.State.SHOW_DEVICES;
-
-import java.util.Arrays;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.db.RemoteClient;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.util.AttributeSet;
-import android.widget.ListAdapter;
-import android.widget.ListView;
-
-/**
- * The SendTab button has a few different states depending on the available devices (and whether
- * we've loaded them yet...)
- *
- * Initially, the view resembles a disabled button. (the LOADING state)
- * Once state is loaded from Sync's database, we know how many devices the user may send their tab
- * to.
- *
- * If there are no targets, the user was found to not have a Sync account, or their Sync account is
- * in a state that prevents it from being able to send a tab, we enter the NONE state and display
- * a generic button which launches an appropriate activity to fix the situation when tapped (such
- * as the set up Sync wizard).
- *
- * If the number of targets does not MAX_INLINE_SYNC_TARGETS, we present a button for each of them.
- * (the LIST state)
- *
- * Otherwise, we enter the SHOW_DEVICES state, in which we display a "Send to other devices" button
- * that takes the user to a menu for selecting a target device from their complete list of many
- * devices.
- */
-public class SendTabList extends ListView {
- @SuppressWarnings("unused")
- private static final String LOGTAG = "GeckoSendTabList";
-
- // The maximum number of target devices to show in the main list. Further devices are available
- // from a secondary menu.
- public static final int MAXIMUM_INLINE_ELEMENTS = R.integer.number_of_inline_share_devices;
-
- private SendTabDeviceListArrayAdapter clientListAdapter;
-
- // Listener to fire when a share target is selected (either directly or via the prompt)
- private SendTabTargetSelectedListener listener;
-
- private final State currentState = LOADING;
-
- /**
- * Enum defining the states this view may occupy.
- */
- public enum State {
- // State when no sync targets exist (a generic "Send to Firefox Sync" button which launches
- // an activity to set it up)
- NONE,
-
- // As NONE, but disabled. Initial state. Used until we get information from Sync about what
- // we really want.
- LOADING,
-
- // A list of devices to share to.
- LIST,
-
- // A single button prompting the user to select a device to share to.
- SHOW_DEVICES
- }
-
- public SendTabList(Context context) {
- super(context);
- }
-
- public SendTabList(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public void setAdapter(ListAdapter adapter) {
- if (!(adapter instanceof SendTabDeviceListArrayAdapter)) {
- throw new IllegalArgumentException("adapter must be a SendTabDeviceListArrayAdapter instance");
- }
-
- clientListAdapter = (SendTabDeviceListArrayAdapter) adapter;
- super.setAdapter(adapter);
- }
-
- public void setSendTabTargetSelectedListener(SendTabTargetSelectedListener aListener) {
- listener = aListener;
- }
-
- public void switchState(State state) {
- if (state == currentState) {
- return;
- }
-
- clientListAdapter.switchState(state);
- if (state == SHOW_DEVICES) {
- clientListAdapter.setDialog(getDialog());
- }
- }
-
- public void setSyncClients(final RemoteClient[] c) {
- final RemoteClient[] clients = c == null ? new RemoteClient[0] : c;
-
- clientListAdapter.setRemoteClientsList(Arrays.asList(clients));
- }
-
- /**
- * Get an AlertDialog listing all devices, allowing the user to select the one they want.
- * Used when more than MAXIMUM_INLINE_ELEMENTS devices are found (to avoid displaying them all
- * inline and looking crazy).
- */
- public AlertDialog getDialog() {
- final Context context = getContext();
-
- final AlertDialog.Builder builder = new AlertDialog.Builder(context);
-
- final RemoteClient[] records = clientListAdapter.toArray();
- final String[] dialogElements = new String[records.length];
-
- for (int i = 0; i < records.length; i++) {
- dialogElements[i] = records[i].name;
- }
-
- builder.setTitle(R.string.overlay_share_select_device)
- .setItems(dialogElements, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int index) {
- listener.onSendTabTargetSelected(records[index].guid);
- }
- })
- .setOnCancelListener(new DialogInterface.OnCancelListener() {
- @Override
- public void onCancel(DialogInterface dialogInterface) {
- Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.SHARE_OVERLAY, "device_selection_cancel");
- }
- });
-
- return builder.create();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabTargetSelectedListener.java b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabTargetSelectedListener.java
deleted file mode 100644
index 79da526da..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabTargetSelectedListener.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
- */
-package org.mozilla.gecko.overlays.ui;
-
-/**
- * Interface for classes that wish to listen for the selection of an element from a SendTabList.
- */
-public interface SendTabTargetSelectedListener {
- /**
- * Called when a row in the SendTabList is clicked.
- *
- * @param targetGUID The GUID of the ClientRecord the element represents (if any, otherwise null)
- */
- public void onSendTabTargetSelected(String targetGUID);
-
- /**
- * Called when the overall Send Tab item is clicked.
- *
- * This implies that the clients list was unavailable.
- */
- public void onSendTabActionSelected();
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/ShareDialog.java b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/ShareDialog.java
deleted file mode 100644
index 156fdda2a..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/overlays/ui/ShareDialog.java
+++ /dev/null
@@ -1,493 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.overlays.ui;
-
-import java.net.URISyntaxException;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.Locales;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.db.LocalBrowserDB;
-import org.mozilla.gecko.db.RemoteClient;
-import org.mozilla.gecko.overlays.OverlayConstants;
-import org.mozilla.gecko.overlays.service.OverlayActionService;
-import org.mozilla.gecko.overlays.service.sharemethods.SendTab;
-import org.mozilla.gecko.overlays.service.sharemethods.ShareMethod;
-import org.mozilla.gecko.sync.setup.activities.WebURLFinder;
-import org.mozilla.gecko.util.IntentUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.UIAsyncTask;
-
-import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.support.v4.content.LocalBroadcastManager;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.animation.AnimationSet;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.animation.Animation;
-import android.view.animation.AnimationUtils;
-import android.widget.TextView;
-import android.widget.Toast;
-
-/**
- * A transparent activity that displays the share overlay.
- */
-public class ShareDialog extends Locales.LocaleAwareActivity implements SendTabTargetSelectedListener {
-
- private enum State {
- DEFAULT,
- DEVICES_ONLY // Only display the device list.
- }
-
- private static final String LOGTAG = "GeckoShareDialog";
-
- /** Flag to indicate that we should always show the device list; specific to this release channel. **/
- public static final String INTENT_EXTRA_DEVICES_ONLY =
- AppConstants.ANDROID_PACKAGE_NAME + ".intent.extra.DEVICES_ONLY";
-
- /** The maximum number of devices we'll show in the dialog when in State.DEFAULT. **/
- private static final int MAXIMUM_INLINE_DEVICES = 2;
-
- private State state;
-
- private SendTabList sendTabList;
- private OverlayDialogButton bookmarkButton;
-
- // The bookmark button drawable set from XML - we need this to reset state.
- private Drawable bookmarkButtonDrawable;
-
- private String url;
- private String title;
-
- // The override intent specified by SendTab (if any). See SendTab.java.
- private Intent sendTabOverrideIntent;
-
- // Flag set during animation to prevent animation multiple-start.
- private boolean isAnimating;
-
- // BroadcastReceiver to receive callbacks from ShareMethods which are changing state.
- private final BroadcastReceiver uiEventListener = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- ShareMethod.Type originShareMethod = intent.getParcelableExtra(OverlayConstants.EXTRA_SHARE_METHOD);
- switch (originShareMethod) {
- case SEND_TAB:
- handleSendTabUIEvent(intent);
- break;
- default:
- throw new IllegalArgumentException("UIEvent broadcast from ShareMethod that isn't thought to support such broadcasts.");
- }
- }
- };
-
- /**
- * Called when a UI event broadcast is received from the SendTab ShareMethod.
- */
- protected void handleSendTabUIEvent(Intent intent) {
- sendTabOverrideIntent = intent.getParcelableExtra(SendTab.OVERRIDE_INTENT);
-
- RemoteClient[] remoteClientRecords = (RemoteClient[]) intent.getParcelableArrayExtra(SendTab.EXTRA_REMOTE_CLIENT_RECORDS);
-
- // Escape hatch: we don't show the option to open this dialog in this state so this should
- // never be run. However, due to potential inconsistencies in synced client state
- // (e.g. bug 1122302 comment 47), we might fail.
- if (state == State.DEVICES_ONLY &&
- (remoteClientRecords == null || remoteClientRecords.length == 0)) {
- Log.e(LOGTAG, "In state: " + State.DEVICES_ONLY + " and received 0 synced clients. Finishing...");
- Toast.makeText(this, getResources().getText(R.string.overlay_no_synced_devices), Toast.LENGTH_SHORT)
- .show();
- finish();
- return;
- }
-
- sendTabList.setSyncClients(remoteClientRecords);
-
- if (state == State.DEVICES_ONLY ||
- remoteClientRecords == null ||
- remoteClientRecords.length <= MAXIMUM_INLINE_DEVICES) {
- // Show the list of devices in-line.
- sendTabList.switchState(SendTabList.State.LIST);
-
- // The first item in the list has a unique style. If there are no items
- // in the list, the next button appears to be the first item in the list.
- //
- // Note: a more thorough implementation would add this
- // (and other non-ListView buttons) into a custom ListView.
- if (remoteClientRecords == null || remoteClientRecords.length == 0) {
- bookmarkButton.setBackgroundResource(
- R.drawable.overlay_share_button_background_first);
- }
- return;
- }
-
- // Just show a button to launch the list of devices to choose from.
- sendTabList.switchState(SendTabList.State.SHOW_DEVICES);
- }
-
- @Override
- protected void onDestroy() {
- // Remove the listener when the activity is destroyed: we no longer care.
- // Note: The activity can be destroyed without onDestroy being called. However, this occurs
- // only when the application is killed, something which also kills the registered receiver
- // list, and the service, and everything else: so we don't care.
- LocalBroadcastManager.getInstance(this).unregisterReceiver(uiEventListener);
-
- super.onDestroy();
- }
-
- /**
- * Show a toast indicating we were started with no URL, and then stop.
- */
- private void abortDueToNoURL() {
- Log.e(LOGTAG, "Unable to process shared intent. No URL found!");
-
- // Display toast notifying the user of failure (most likely a developer who screwed up
- // trying to send a share intent).
- Toast toast = Toast.makeText(this, getResources().getText(R.string.overlay_share_no_url), Toast.LENGTH_SHORT);
- toast.show();
- finish();
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.overlay_share_dialog);
-
- LocalBroadcastManager.getInstance(this).registerReceiver(uiEventListener,
- new IntentFilter(OverlayConstants.SHARE_METHOD_UI_EVENT));
-
- // Send tab.
- sendTabList = (SendTabList) findViewById(R.id.overlay_send_tab_btn);
-
- // Register ourselves as both the listener and the context for the Adapter.
- final SendTabDeviceListArrayAdapter adapter = new SendTabDeviceListArrayAdapter(this, this);
- sendTabList.setAdapter(adapter);
- sendTabList.setSendTabTargetSelectedListener(this);
-
- bookmarkButton = (OverlayDialogButton) findViewById(R.id.overlay_share_bookmark_btn);
-
- bookmarkButtonDrawable = bookmarkButton.getBackground();
-
- // Bookmark button
- bookmarkButton = (OverlayDialogButton) findViewById(R.id.overlay_share_bookmark_btn);
- bookmarkButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- addBookmark();
- }
- });
- }
-
- @Override
- protected void onResume() {
- super.onResume();
-
- final Intent intent = getIntent();
-
- state = intent.getBooleanExtra(INTENT_EXTRA_DEVICES_ONLY, false) ?
- State.DEVICES_ONLY : State.DEFAULT;
-
- // If the Activity is being reused, we need to reset the state. Ideally, we create a
- // new instance for each call, but Android L breaks this (bug 1137928).
- sendTabList.switchState(SendTabList.State.LOADING);
- bookmarkButton.setBackgroundDrawable(bookmarkButtonDrawable);
-
- // The URL is usually hiding somewhere in the extra text. Extract it.
- final String extraText = IntentUtils.getStringExtraSafe(intent, Intent.EXTRA_TEXT);
- if (TextUtils.isEmpty(extraText)) {
- abortDueToNoURL();
- return;
- }
-
- final String pageUrl = new WebURLFinder(extraText).bestWebURL();
- if (TextUtils.isEmpty(pageUrl)) {
- abortDueToNoURL();
- return;
- }
-
- // Have the service start any initialisation work that's necessary for us to show the correct
- // UI. The results of such work will come in via the BroadcastListener.
- Intent serviceStartupIntent = new Intent(this, OverlayActionService.class);
- serviceStartupIntent.setAction(OverlayConstants.ACTION_PREPARE_SHARE);
- startService(serviceStartupIntent);
-
- // Start the slide-up animation.
- getWindow().setWindowAnimations(0);
- final Animation anim = AnimationUtils.loadAnimation(this, R.anim.overlay_slide_up);
- findViewById(R.id.sharedialog).startAnimation(anim);
-
- // If provided, we use the subject text to give us something nice to display.
- // If not, we wing it with the URL.
-
- // TODO: Consider polling Fennec databases to find better information to display.
- final String subjectText = intent.getStringExtra(Intent.EXTRA_SUBJECT);
-
- final String telemetryExtras = "title=" + (subjectText != null);
- if (subjectText != null) {
- ((TextView) findViewById(R.id.title)).setText(subjectText);
- }
-
- Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.SHARE_OVERLAY, telemetryExtras);
-
- title = subjectText;
- url = pageUrl;
-
- // Set the subtitle text on the view and cause it to marquee if it's too long (which it will
- // be, since it's a URL).
- final TextView subtitleView = (TextView) findViewById(R.id.subtitle);
- subtitleView.setText(pageUrl);
- subtitleView.setEllipsize(TextUtils.TruncateAt.MARQUEE);
- subtitleView.setSingleLine(true);
- subtitleView.setMarqueeRepeatLimit(5);
- subtitleView.setSelected(true);
-
- final View titleView = findViewById(R.id.title);
-
- if (state == State.DEVICES_ONLY) {
- bookmarkButton.setVisibility(View.GONE);
-
- titleView.setOnClickListener(null);
- subtitleView.setOnClickListener(null);
- return;
- }
-
- bookmarkButton.setVisibility(View.VISIBLE);
-
- // Configure buttons.
- final View.OnClickListener launchBrowser = new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- ShareDialog.this.launchBrowser();
- }
- };
-
- titleView.setOnClickListener(launchBrowser);
- subtitleView.setOnClickListener(launchBrowser);
-
- final LocalBrowserDB browserDB = new LocalBrowserDB(getCurrentProfile());
- setButtonState(url, browserDB);
- }
-
- @Override
- protected void onNewIntent(final Intent intent) {
- super.onNewIntent(intent);
-
- // The intent returned by getIntent is not updated automatically.
- setIntent(intent);
- }
-
- /**
- * Sets the state of the bookmark/reading list buttons: they are disabled if the given URL is
- * already in the corresponding list.
- */
- private void setButtonState(final String pageURL, final LocalBrowserDB browserDB) {
- new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
- // Flags to hold the result
- boolean isBookmark;
-
- @Override
- protected Void doInBackground() {
- final ContentResolver contentResolver = getApplicationContext().getContentResolver();
-
- isBookmark = browserDB.isBookmark(contentResolver, pageURL);
-
- return null;
- }
-
- @Override
- protected void onPostExecute(Void aVoid) {
- findViewById(R.id.overlay_share_bookmark_btn).setEnabled(!isBookmark);
- }
- }.execute();
- }
-
- /**
- * Helper method to get an overlay service intent populated with the data held in this dialog.
- */
- private Intent getServiceIntent(ShareMethod.Type method) {
- final Intent serviceIntent = new Intent(this, OverlayActionService.class);
- serviceIntent.setAction(OverlayConstants.ACTION_SHARE);
-
- serviceIntent.putExtra(OverlayConstants.EXTRA_SHARE_METHOD, (Parcelable) method);
- serviceIntent.putExtra(OverlayConstants.EXTRA_URL, url);
- serviceIntent.putExtra(OverlayConstants.EXTRA_TITLE, title);
-
- return serviceIntent;
- }
-
- @Override
- public void finish() {
- finish(true);
- }
-
- private void finish(final boolean shouldOverrideAnimations) {
- super.finish();
- if (shouldOverrideAnimations) {
- // Don't perform an activity-dismiss animation.
- overridePendingTransition(0, 0);
- }
- }
-
- /*
- * Button handlers. Send intents to the background service responsible for processing requests
- * on Fennec in the background. (a nice extensible mechanism for "doing stuff without properly
- * launching Fennec").
- */
-
- @Override
- public void onSendTabActionSelected() {
- // This requires an override intent.
- if (sendTabOverrideIntent == null) {
- throw new IllegalStateException("sendTabOverrideIntent must not be null");
- }
-
- startActivity(sendTabOverrideIntent);
- finish();
- }
-
- @Override
- public void onSendTabTargetSelected(String targetGUID) {
- // targetGUID being null with no override intent should be an impossible state.
- if (targetGUID == null) {
- throw new IllegalStateException("targetGUID must not be null");
- }
-
- Intent serviceIntent = getServiceIntent(ShareMethod.Type.SEND_TAB);
-
- // Currently, only one extra parameter is necessary (the GUID of the target device).
- Bundle extraParameters = new Bundle();
-
- // Future: Handle multiple-selection. Bug 1061297.
- extraParameters.putStringArray(SendTab.SEND_TAB_TARGET_DEVICES, new String[] { targetGUID });
-
- serviceIntent.putExtra(OverlayConstants.EXTRA_PARAMETERS, extraParameters);
-
- startService(serviceIntent);
- animateOut(true);
-
- Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.SHARE_OVERLAY, "sendtab");
- }
-
- public void addBookmark() {
- startService(getServiceIntent(ShareMethod.Type.ADD_BOOKMARK));
- animateOut(true);
-
- Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.SHARE_OVERLAY, "bookmark");
- }
-
- public void launchBrowser() {
- try {
- // This can launch in the guest profile. Sorry.
- final Intent i = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
- i.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
- startActivity(i);
- } catch (URISyntaxException e) {
- // Nothing much we can do.
- } finally {
- // Since we're changing apps, users expect the default app switch animations.
- finish(false);
- }
- }
-
- private String getCurrentProfile() {
- return GeckoProfile.DEFAULT_PROFILE;
- }
-
- /**
- * Slide the overlay down off the screen, display
- * a check (if given), and finish the activity.
- */
- private void animateOut(final boolean shouldDisplayConfirmation) {
- if (isAnimating) {
- return;
- }
-
- isAnimating = true;
- final Animation slideOutAnim = AnimationUtils.loadAnimation(this, R.anim.overlay_slide_down);
-
- final Animation animationToFinishActivity;
- if (!shouldDisplayConfirmation) {
- animationToFinishActivity = slideOutAnim;
- } else {
- final View check = findViewById(R.id.check);
- check.setVisibility(View.VISIBLE);
- final Animation checkEntryAnim = AnimationUtils.loadAnimation(this, R.anim.overlay_check_entry);
- final Animation checkExitAnim = AnimationUtils.loadAnimation(this, R.anim.overlay_check_exit);
- checkExitAnim.setStartOffset(checkEntryAnim.getDuration() + 500);
-
- final AnimationSet checkAnimationSet = new AnimationSet(this, null);
- checkAnimationSet.addAnimation(checkEntryAnim);
- checkAnimationSet.addAnimation(checkExitAnim);
-
- check.startAnimation(checkAnimationSet);
- animationToFinishActivity = checkExitAnim;
- }
-
- findViewById(R.id.sharedialog).startAnimation(slideOutAnim);
- animationToFinishActivity.setAnimationListener(new Animation.AnimationListener() {
- @Override
- public void onAnimationStart(Animation animation) { /* Unused. */ }
-
- @Override
- public void onAnimationEnd(Animation animation) {
- finish();
- }
-
- @Override
- public void onAnimationRepeat(Animation animation) { /* Unused. */ }
- });
-
- // Allows the user to dismiss the animation early.
- setFullscreenFinishOnClickListener();
- }
-
- /**
- * Sets a fullscreen {@link #finish()} click listener. We do this rather than attaching an
- * onClickListener to the root View because in that case, we need to remove all of the
- * existing listeners, which is less robust.
- */
- private void setFullscreenFinishOnClickListener() {
- final View clickTarget = findViewById(R.id.fullscreen_click_target);
- clickTarget.setVisibility(View.VISIBLE);
- clickTarget.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- finish();
- }
- });
- }
-
- /**
- * Close the dialog if back is pressed.
- */
- @Override
- public void onBackPressed() {
- animateOut(false);
- Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.SHARE_OVERLAY);
- }
-
- /**
- * Close the dialog if the anything that isn't a button is tapped.
- */
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- animateOut(false);
- Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.SHARE_OVERLAY);
- return true;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/AlignRightLinkPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/AlignRightLinkPreference.java
deleted file mode 100644
index b68a018f2..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/AlignRightLinkPreference.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.util.AttributeSet;
-
-class AlignRightLinkPreference extends LinkPreference {
-
- public AlignRightLinkPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- setLayoutResource(R.layout.preference_rightalign_icon);
- }
-
- public AlignRightLinkPreference(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- setLayoutResource(R.layout.preference_rightalign_icon);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/AndroidImport.java b/mobile/android/base/java/org/mozilla/gecko/preferences/AndroidImport.java
deleted file mode 100644
index bb71ce78b..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/AndroidImport.java
+++ /dev/null
@@ -1,230 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import android.content.ContentValues;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.os.Build;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.BrowserContract.Bookmarks;
-import org.mozilla.gecko.db.LocalBrowserDB;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.icons.IconsHelper;
-import org.mozilla.gecko.icons.storage.DiskStorage;
-
-import android.content.ContentProviderOperation;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.OperationApplicationException;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.RemoteException;
-import android.provider.BaseColumns;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.util.ArrayList;
-
-public class AndroidImport implements Runnable {
- /**
- * The Android M SDK removed several fields and methods from android.provider.Browser. This class is used as a
- * replacement to support building with the new SDK but at the same time still use these fields on lower Android
- * versions.
- */
- private static class LegacyBrowserProvider {
- public static final Uri BOOKMARKS_URI = Uri.parse("content://browser/bookmarks");
-
- // Incomplete: This are just the fields we currently use in our code base
- public static class BookmarkColumns implements BaseColumns {
- public static final String URL = "url";
- public static final String VISITS = "visits";
- public static final String DATE = "date";
- public static final String BOOKMARK = "bookmark";
- public static final String TITLE = "title";
- public static final String CREATED = "created";
- public static final String FAVICON = "favicon";
- }
- }
-
- public static final Uri SAMSUNG_BOOKMARKS_URI = Uri.parse("content://com.sec.android.app.sbrowser.browser/bookmarks");
- public static final Uri SAMSUNG_HISTORY_URI = Uri.parse("content://com.sec.android.app.sbrowser.browser/history");
- public static final String SAMSUNG_MANUFACTURER = "samsung";
-
- private static final String LOGTAG = "AndroidImport";
- private final Context mContext;
- private final Runnable mOnDoneRunnable;
- private final ArrayList<ContentProviderOperation> mOperations;
- private final ContentResolver mCr;
- private final LocalBrowserDB mDB;
- private final boolean mImportBookmarks;
- private final boolean mImportHistory;
-
- public AndroidImport(Context context, Runnable onDoneRunnable,
- boolean doBookmarks, boolean doHistory) {
- mContext = context;
- mOnDoneRunnable = onDoneRunnable;
- mOperations = new ArrayList<ContentProviderOperation>();
- mCr = mContext.getContentResolver();
- mDB = new LocalBrowserDB(GeckoProfile.get(context).getName());
- mImportBookmarks = doBookmarks;
- mImportHistory = doHistory;
- }
-
- public void mergeBookmarks() {
- Cursor cursor = null;
- try {
- cursor = query(LegacyBrowserProvider.BOOKMARKS_URI,
- SAMSUNG_BOOKMARKS_URI,
- LegacyBrowserProvider.BookmarkColumns.BOOKMARK + " = 1");
-
- if (cursor != null) {
- final int faviconCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.FAVICON);
- final int titleCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.TITLE);
- final int urlCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.URL);
- // http://code.google.com/p/android/issues/detail?id=17969
- final int createCol = cursor.getColumnIndex(LegacyBrowserProvider.BookmarkColumns.CREATED);
-
- cursor.moveToFirst();
- while (!cursor.isAfterLast()) {
- String url = cursor.getString(urlCol);
- String title = cursor.getString(titleCol);
- long created;
- if (createCol >= 0) {
- created = cursor.getLong(createCol);
- } else {
- created = System.currentTimeMillis();
- }
- // Need to set it to the current time so Sync picks it up.
- long modified = System.currentTimeMillis();
- byte[] data = cursor.getBlob(faviconCol);
- mDB.updateBookmarkInBatch(mCr, mOperations,
- url, title, null, -1,
- created, modified,
- BrowserContract.Bookmarks.DEFAULT_POSITION,
- null, Bookmarks.TYPE_BOOKMARK);
- if (data != null) {
- storeBitmap(data, url);
- }
- cursor.moveToNext();
- }
- }
- } finally {
- if (cursor != null)
- cursor.close();
- }
-
- flushBatchOperations();
- }
-
- public void mergeHistory() {
- ArrayList<ContentValues> visitsToSynthesize = new ArrayList<>();
- Cursor cursor = null;
- try {
- cursor = query (LegacyBrowserProvider.BOOKMARKS_URI,
- SAMSUNG_HISTORY_URI,
- LegacyBrowserProvider.BookmarkColumns.BOOKMARK + " = 0 AND " +
- LegacyBrowserProvider.BookmarkColumns.VISITS + " > 0");
-
- if (cursor != null) {
- final int dateCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.DATE);
- final int faviconCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.FAVICON);
- final int titleCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.TITLE);
- final int urlCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.URL);
- final int visitsCol = cursor.getColumnIndexOrThrow(LegacyBrowserProvider.BookmarkColumns.VISITS);
-
- cursor.moveToFirst();
- while (!cursor.isAfterLast()) {
- String url = cursor.getString(urlCol);
- String title = cursor.getString(titleCol);
- long date = cursor.getLong(dateCol);
- int visits = cursor.getInt(visitsCol);
- byte[] data = cursor.getBlob(faviconCol);
- mDB.updateHistoryInBatch(mCr, mOperations, url, title, date, visits);
- if (data != null) {
- storeBitmap(data, url);
- }
- ContentValues visitData = new ContentValues();
- visitData.put(LocalBrowserDB.HISTORY_VISITS_DATE, date);
- visitData.put(LocalBrowserDB.HISTORY_VISITS_URL, url);
- visitData.put(LocalBrowserDB.HISTORY_VISITS_COUNT, visits);
- visitsToSynthesize.add(visitData);
- cursor.moveToNext();
- }
- }
- } finally {
- if (cursor != null)
- cursor.close();
- }
-
- flushBatchOperations();
-
- // Now that we have flushed history records, we need to synthesize individual visits. We have
- // gathered information about all of the visits we need to synthesize into visitsForSynthesis.
- mDB.insertVisitsFromImportHistoryInBatch(mCr, mOperations, visitsToSynthesize);
-
- flushBatchOperations();
- }
-
- private void storeBitmap(byte[] data, String url) {
- if (TextUtils.isEmpty(url) || data == null) {
- return;
- }
-
- final Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
- if (bitmap == null) {
- return;
- }
-
- final String iconUrl = IconsHelper.guessDefaultFaviconURL(url);
- if (iconUrl == null) {
- return;
- }
-
- final DiskStorage storage = DiskStorage.get(mContext);
-
- storage.putIcon(url, bitmap);
- storage.putMapping(url, iconUrl);
- }
-
- protected Cursor query(Uri mainUri, Uri fallbackUri, String condition) {
- final Cursor cursor = mCr.query(mainUri, null, condition, null, null);
- if (Build.MANUFACTURER.equals(SAMSUNG_MANUFACTURER) && (cursor == null || cursor.getCount() == 0)) {
- if (cursor != null) {
- cursor.close();
- }
- return mCr.query(fallbackUri, null, null, null, null);
- }
- return cursor;
- }
-
- protected void flushBatchOperations() {
- Log.d(LOGTAG, "Flushing " + mOperations.size() + " DB operations");
- try {
- // We don't really care for the results, this is best-effort.
- mCr.applyBatch(BrowserContract.AUTHORITY, mOperations);
- } catch (RemoteException e) {
- Log.e(LOGTAG, "Remote exception while updating db: ", e);
- } catch (OperationApplicationException e) {
- // Bug 716729 means this happens even in normal circumstances
- Log.d(LOGTAG, "Error while applying database updates: ", e);
- }
- mOperations.clear();
- }
-
- @Override
- public void run() {
- if (mImportBookmarks) {
- mergeBookmarks();
- }
- if (mImportHistory) {
- mergeHistory();
- }
-
- mOnDoneRunnable.run();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/AndroidImportPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/AndroidImportPreference.java
deleted file mode 100644
index 0f1d3ec3f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/AndroidImportPreference.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.restrictions.Restrictable;
-import org.mozilla.gecko.restrictions.Restrictions;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import java.util.Set;
-
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.preference.Preference;
-import android.util.AttributeSet;
-import android.util.Log;
-
-class AndroidImportPreference extends MultiPrefMultiChoicePreference {
- private static final String LOGTAG = "AndroidImport";
- public static final String PREF_KEY = "android.not_a_preference.import_android";
- private static final String PREF_KEY_PREFIX = "import_android.data.";
- private final Context mContext;
-
- public static class Handler implements GeckoPreferences.PrefHandler {
- public boolean setupPref(Context context, Preference pref) {
- // Feature disabled on devices running Android M+ (Bug 1183559)
- return Versions.preMarshmallow && Restrictions.isAllowed(context, Restrictable.IMPORT_SETTINGS);
- }
-
- public void onChange(Context context, Preference pref, Object newValue) { }
- }
-
- public AndroidImportPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- mContext = context;
- }
-
- @Override
- protected void onDialogClosed(boolean positiveResult) {
- super.onDialogClosed(positiveResult);
-
- if (!positiveResult)
- return;
-
- boolean bookmarksChecked = false;
- boolean historyChecked = false;
-
- Set<String> values = getValues();
-
- for (String value : values) {
- // Import checkbox values are stored in Android prefs to
- // remember their check states. The key names are import_android.data.X
- String key = value.substring(PREF_KEY_PREFIX.length());
- if ("bookmarks".equals(key)) {
- bookmarksChecked = true;
- } else if ("history".equals(key)) {
- historyChecked = true;
- }
- }
-
- runImport(bookmarksChecked, historyChecked);
- }
-
- protected void runImport(final boolean doBookmarks, final boolean doHistory) {
- Log.i(LOGTAG, "Importing Android history/bookmarks");
- if (!doBookmarks && !doHistory) {
- return;
- }
-
- final String dialogTitle;
- if (doBookmarks && doHistory) {
- dialogTitle = mContext.getString(R.string.bookmarkhistory_import_both);
- } else if (doBookmarks) {
- dialogTitle = mContext.getString(R.string.bookmarkhistory_import_bookmarks);
- } else {
- dialogTitle = mContext.getString(R.string.bookmarkhistory_import_history);
- }
-
- final ProgressDialog dialog =
- ProgressDialog.show(mContext,
- dialogTitle,
- mContext.getString(R.string.bookmarkhistory_import_wait),
- true);
-
- final Runnable stopCallback = new Runnable() {
- @Override
- public void run() {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- dialog.dismiss();
- }
- });
- }
- };
-
- ThreadUtils.postToBackgroundThread(
- // Constructing AndroidImport may need finding the profile,
- // which hits disk, so it needs to go into a Runnable too.
- new Runnable() {
- @Override
- public void run() {
- new AndroidImport(mContext, stopCallback, doBookmarks, doHistory).run();
- }
- }
- );
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/AppCompatPreferenceActivity.java b/mobile/android/base/java/org/mozilla/gecko/preferences/AppCompatPreferenceActivity.java
deleted file mode 100644
index fb4a8f751..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/AppCompatPreferenceActivity.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.mozilla.gecko.preferences;
-
-
-import android.content.res.Configuration;
-import android.os.Bundle;
-import android.preference.PreferenceActivity;
-import android.support.annotation.LayoutRes;
-import android.support.annotation.Nullable;
-import android.support.v7.app.ActionBar;
-import android.support.v7.app.AppCompatDelegate;
-import android.support.v7.widget.Toolbar;
-import android.view.MenuInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
- * to be used with AppCompat.
- *
- * This technique can be used with an {@link android.app.Activity} class, not just
- * {@link android.preference.PreferenceActivity}.
- *
- * This class was directly imported (without any modifications) from Android SDK examples, at:
- * https://android.googlesource.com/platform/development/+/master/samples/Support7Demos/src/com/example/android/supportv7/app/AppCompatPreferenceActivity.java
- */
-public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
- private AppCompatDelegate mDelegate;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- getDelegate().installViewFactory();
- getDelegate().onCreate(savedInstanceState);
- super.onCreate(savedInstanceState);
- }
- @Override
- protected void onPostCreate(Bundle savedInstanceState) {
- super.onPostCreate(savedInstanceState);
- getDelegate().onPostCreate(savedInstanceState);
- }
- public ActionBar getSupportActionBar() {
- return getDelegate().getSupportActionBar();
- }
- public void setSupportActionBar(@Nullable Toolbar toolbar) {
- getDelegate().setSupportActionBar(toolbar);
- }
- @Override
- public MenuInflater getMenuInflater() {
- return getDelegate().getMenuInflater();
- }
- @Override
- public void setContentView(@LayoutRes int layoutResID) {
- getDelegate().setContentView(layoutResID);
- }
- @Override
- public void setContentView(View view) {
- getDelegate().setContentView(view);
- }
- @Override
- public void setContentView(View view, ViewGroup.LayoutParams params) {
- getDelegate().setContentView(view, params);
- }
- @Override
- public void addContentView(View view, ViewGroup.LayoutParams params) {
- getDelegate().addContentView(view, params);
- }
- @Override
- protected void onPostResume() {
- super.onPostResume();
- getDelegate().onPostResume();
- }
- @Override
- protected void onTitleChanged(CharSequence title, int color) {
- super.onTitleChanged(title, color);
- getDelegate().setTitle(title);
- }
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- getDelegate().onConfigurationChanged(newConfig);
- }
- @Override
- protected void onStop() {
- super.onStop();
- getDelegate().onStop();
- }
- @Override
- protected void onDestroy() {
- super.onDestroy();
- getDelegate().onDestroy();
- }
- public void invalidateOptionsMenu() {
- getDelegate().invalidateOptionsMenu();
- }
- private AppCompatDelegate getDelegate() {
- if (mDelegate == null) {
- mDelegate = AppCompatDelegate.create(this, null);
- }
- return mDelegate;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/ClearOnShutdownPref.java b/mobile/android/base/java/org/mozilla/gecko/preferences/ClearOnShutdownPref.java
deleted file mode 100644
index 5218cd06d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/ClearOnShutdownPref.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import java.util.HashSet;
-import java.util.Set;
-
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.util.PrefUtils;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.preference.Preference;
-
-public class ClearOnShutdownPref implements GeckoPreferences.PrefHandler {
- public static final String PREF = GeckoPreferences.NON_PREF_PREFIX + "history.clear_on_exit";
-
- @Override
- public boolean setupPref(Context context, Preference pref) {
- // The pref is initialized asynchronously. Read the pref explicitly
- // here to make sure we have the data.
- final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context);
- final Set<String> clearItems = PrefUtils.getStringSet(prefs, PREF, new HashSet<String>());
- ((ListCheckboxPreference) pref).setChecked(clearItems.size() > 0);
- return true;
- }
-
- @Override
- @SuppressWarnings("unchecked")
- public void onChange(Context context, Preference pref, Object newValue) {
- final Set<String> vals = (Set<String>) newValue;
- ((ListCheckboxPreference) pref).setChecked(vals.size() > 0);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/CustomCheckBoxPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/CustomCheckBoxPreference.java
deleted file mode 100644
index 2934ca88e..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/CustomCheckBoxPreference.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import android.content.Context;
-import android.preference.CheckBoxPreference;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.TextView;
-
-/**
- * Represents a Checkbox element in a preference menu.
- * The title of the Checkbox can be larger than the view.
- * In this case, it will be displayed in 2 or more lines.
- * The default behavior of the class CheckBoxPreference
- * doesn't wrap the title.
- */
-
-public class CustomCheckBoxPreference extends CheckBoxPreference {
-
- public CustomCheckBoxPreference(Context context) {
- super(context);
- }
-
- public CustomCheckBoxPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- public CustomCheckBoxPreference(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-
- @Override
- protected void onBindView(View view) {
- super.onBindView(view);
- final TextView title = (TextView) view.findViewById(android.R.id.title);
- if (title != null) {
- title.setSingleLine(false);
- title.setEllipsize(null);
- }
- }
-
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/CustomListCategory.java b/mobile/android/base/java/org/mozilla/gecko/preferences/CustomListCategory.java
deleted file mode 100644
index ee5a46bef..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/CustomListCategory.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import android.content.Context;
-import android.preference.PreferenceCategory;
-import android.util.AttributeSet;
-
-public abstract class CustomListCategory extends PreferenceCategory {
- protected CustomListPreference mDefaultReference;
-
- public CustomListCategory(Context context) {
- super(context);
- }
-
- public CustomListCategory(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public CustomListCategory(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-
- @Override
- protected void onAttachedToActivity() {
- super.onAttachedToActivity();
-
- setOrderingAsAdded(true);
- }
-
- /**
- * Set the default to some available list item. Used if the current default is removed or
- * disabled.
- */
- protected void setFallbackDefault() {
- if (getPreferenceCount() > 0) {
- CustomListPreference aItem = (CustomListPreference) getPreference(0);
- setDefault(aItem);
- }
- }
-
- /**
- * Removes the given item from the set of available list items.
- * This only updates the UI, so callers are responsible for persisting any state.
- *
- * @param item The given item to remove.
- */
- public void uninstall(CustomListPreference item) {
- removePreference(item);
- if (item == mDefaultReference) {
- // If the default is being deleted, set a new default.
- setFallbackDefault();
- }
- }
-
- /**
- * Sets the given item as the current default.
- * This only updates the UI, so callers are responsible for persisting any state.
- *
- * @param item The intended new default.
- */
- public void setDefault(CustomListPreference item) {
- if (mDefaultReference != null) {
- mDefaultReference.setIsDefault(false);
- }
-
- item.setIsDefault(true);
- mDefaultReference = item;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/CustomListPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/CustomListPreference.java
deleted file mode 100644
index 8b7e0e7b3..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/CustomListPreference.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import org.mozilla.gecko.R;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.res.Resources;
-import android.preference.Preference;
-import android.view.View;
-import android.widget.TextView;
-
-/**
- * Represents an element in a <code>CustomListCategory</code> preference menu.
- * This preference con display a dialog when clicked, and also supports
- * being set as a default item within the preference list category.
- */
-
-public abstract class CustomListPreference extends Preference implements View.OnLongClickListener {
- protected String LOGTAG = "CustomListPreference";
-
- // Indices of the buttons of the Dialog.
- public static final int INDEX_SET_DEFAULT_BUTTON = 0;
-
- // Dialog item labels.
- private String[] mDialogItems;
-
- // Dialog displayed when this element is tapped.
- protected AlertDialog mDialog;
-
- // Cache label to avoid repeated use of the resource system.
- protected final String LABEL_IS_DEFAULT;
- protected final String LABEL_SET_AS_DEFAULT;
- protected final String LABEL_REMOVE;
-
- protected boolean mIsDefault;
-
- // Enclosing parent category that contains this preference.
- protected final CustomListCategory mParentCategory;
-
- /**
- * Create a preference object to represent a list preference that is attached to
- * a category.
- *
- * @param context The activity context we operate under.
- * @param parentCategory The PreferenceCategory this object exists within.
- */
- public CustomListPreference(Context context, CustomListCategory parentCategory) {
- super(context);
-
- mParentCategory = parentCategory;
- setLayoutResource(getPreferenceLayoutResource());
-
- setOnPreferenceClickListener(new OnPreferenceClickListener() {
- @Override
- public boolean onPreferenceClick(Preference preference) {
- CustomListPreference sPref = (CustomListPreference) preference;
- sPref.showDialog();
- return true;
- }
- });
-
- Resources res = getContext().getResources();
-
- // Fetch these strings now, instead of every time we ever want to relabel a button.
- LABEL_IS_DEFAULT = res.getString(R.string.pref_default);
- LABEL_SET_AS_DEFAULT = res.getString(R.string.pref_dialog_set_default);
- LABEL_REMOVE = res.getString(R.string.pref_dialog_remove);
- }
-
- /**
- * Returns the Android resource id for the layout.
- */
- protected abstract int getPreferenceLayoutResource();
-
- /**
- * Set whether this object's UI should display this as the default item.
- * Note: This must be called from the UI thread because it touches the view hierarchy.
- *
- * To ensure proper ordering, this method should only be called after this Preference
- * is added to the PreferenceCategory.
- *
- * @param isDefault Flag indicating if this represents the default list item.
- */
- public void setIsDefault(boolean isDefault) {
- mIsDefault = isDefault;
- if (isDefault) {
- setOrder(0);
- setSummary(LABEL_IS_DEFAULT);
- } else {
- setOrder(1);
- setSummary("");
- }
- }
-
- private String[] getCachedDialogItems() {
- if (mDialogItems == null) {
- mDialogItems = createDialogItems();
- }
- return mDialogItems;
- }
-
- /**
- * Returns the strings to be displayed in the dialog.
- */
- abstract protected String[] createDialogItems();
-
- /**
- * Display a dialog for this preference, when the preference is clicked.
- */
- public void showDialog() {
- final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
- builder.setTitle(getTitle().toString());
- builder.setItems(getCachedDialogItems(), new DialogInterface.OnClickListener() {
- // Forward relevant events to the container class for handling.
- @Override
- public void onClick(DialogInterface dialog, int indexClicked) {
- hideDialog();
- onDialogIndexClicked(indexClicked);
- }
- });
-
- configureDialogBuilder(builder);
-
- // We have to construct the dialog itself on the UI thread.
- mDialog = builder.create();
- mDialog.setOnShowListener(new DialogInterface.OnShowListener() {
- // Called when the dialog is shown (so we're finally able to manipulate button enabledness).
- @Override
- public void onShow(DialogInterface dialog) {
- configureShownDialog();
- }
- });
- mDialog.show();
- }
-
- /**
- * (Optional) Configure the AlertDialog builder.
- */
- protected void configureDialogBuilder(AlertDialog.Builder builder) {
- return;
- }
-
- abstract protected void onDialogIndexClicked(int index);
-
- /**
- * Disables buttons in the shown AlertDialog as required. The button elements are not created
- * until after show is called, so this method has to be called from the onShowListener above.
- * @see this.showDialog
- */
- protected void configureShownDialog() {
- // If this is already the default list item, disable the button for setting this as the default.
- final TextView defaultButton = (TextView) mDialog.getListView().getChildAt(INDEX_SET_DEFAULT_BUTTON);
- if (mIsDefault) {
- defaultButton.setEnabled(false);
-
- // Failure to unregister this listener leads to tapping the button dismissing the dialog
- // without doing anything.
- defaultButton.setOnClickListener(null);
- }
- }
-
- /**
- * Hide the dialog we previously created, if any.
- */
- public void hideDialog() {
- if (mDialog != null && mDialog.isShowing()) {
- mDialog.dismiss();
- }
- }
-
- @Override
- public boolean onLongClick(View view) {
- // Show the preference dialog on long-press.
- showDialog();
- return true;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/DistroSharedPrefsImport.java b/mobile/android/base/java/org/mozilla/gecko/preferences/DistroSharedPrefsImport.java
deleted file mode 100644
index 1e235640e..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/DistroSharedPrefsImport.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.distribution.Distribution;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.util.Log;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.Iterator;
-
-public class DistroSharedPrefsImport {
-
- public static final String LOGTAG = DistroSharedPrefsImport.class.getSimpleName();
-
- public static void importPreferences(final Context context, final Distribution distribution) {
- if (distribution == null) {
- return;
- }
-
- final JSONObject preferences = distribution.getAndroidPreferences();
- if (preferences.length() == 0) {
- return;
- }
-
- final Iterator<?> keys = preferences.keys();
- final SharedPreferences.Editor sharedPreferences = GeckoSharedPrefs.forProfile(context).edit();
-
- while (keys.hasNext()) {
- final String key = (String) keys.next();
- final Object value;
- try {
- value = preferences.get(key);
- } catch (JSONException e) {
- Log.e(LOGTAG, "Unable to completely process Android Preferences JSON.", e);
- continue;
- }
-
- // We currently don't support Float preferences.
- if (value instanceof String) {
- sharedPreferences.putString(GeckoPreferences.NON_PREF_PREFIX + key, (String) value);
- } else if (value instanceof Boolean) {
- sharedPreferences.putBoolean(GeckoPreferences.NON_PREF_PREFIX + key, (boolean) value);
- } else if (value instanceof Integer) {
- sharedPreferences.putInt(GeckoPreferences.NON_PREF_PREFIX + key, (int) value);
- } else if (value instanceof Long) {
- sharedPreferences.putLong(GeckoPreferences.NON_PREF_PREFIX + key, (long) value);
- } else {
- Log.d(LOGTAG, "Unknown preference value type whilst importing android preferences from distro file.");
- }
- }
- sharedPreferences.apply();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/FontSizePreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/FontSizePreference.java
deleted file mode 100644
index c77c2cc23..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/FontSizePreference.java
+++ /dev/null
@@ -1,192 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import org.mozilla.gecko.R;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Color;
-import android.preference.DialogPreference;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.ScrollView;
-import android.widget.TextView;
-
-import java.util.HashMap;
-
-class FontSizePreference extends DialogPreference {
- private static final String LOGTAG = "FontSizePreference";
- private static final int TWIP_TO_PT_RATIO = 20; // 20 twip = 1 point.
- private static final int PREVIEW_FONT_SIZE_UNIT = TypedValue.COMPLEX_UNIT_PT;
- private static final int DEFAULT_FONT_INDEX = 2;
-
- private final Context mContext;
- /** Container for mPreviewFontView to allow for scrollable padding at the top of the view. */
- private ScrollView mScrollingContainer;
- private TextView mPreviewFontView;
- private Button mIncreaseFontButton;
- private Button mDecreaseFontButton;
-
- private final String[] mFontTwipValues;
- private final String[] mFontSizeNames; // Ex: "Small".
- /** Index into the above arrays for the saved preference value (from Gecko). */
- private int mSavedFontIndex = DEFAULT_FONT_INDEX;
- /** Index into the above arrays for the currently displayed font size (the preview). */
- private int mPreviewFontIndex = mSavedFontIndex;
- private final HashMap<String, Integer> mFontTwipToIndexMap;
-
- public FontSizePreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- mContext = context;
-
- final Resources res = mContext.getResources();
- mFontTwipValues = res.getStringArray(R.array.pref_font_size_values);
- mFontSizeNames = res.getStringArray(R.array.pref_font_size_entries);
- mFontTwipToIndexMap = new HashMap<String, Integer>();
- for (int i = 0; i < mFontTwipValues.length; ++i) {
- mFontTwipToIndexMap.put(mFontTwipValues[i], i);
- }
- }
-
- @Override
- protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
- final LayoutInflater inflater =
- (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- View dialogView = inflater.inflate(R.layout.font_size_preference, null);
- initInternalViews(dialogView);
- updatePreviewFontSize(mFontTwipValues[mPreviewFontIndex]);
-
- builder.setTitle(null);
- builder.setView(dialogView);
- }
-
- /** Saves relevant views to instance variables and initializes their settings. */
- private void initInternalViews(View dialogView) {
- mScrollingContainer = (ScrollView) dialogView.findViewById(R.id.scrolling_container);
- // Background cannot be set in XML (see bug 783597 - TODO: Change this to XML when bug is fixed).
- mScrollingContainer.setBackgroundColor(Color.WHITE);
- mPreviewFontView = (TextView) dialogView.findViewById(R.id.preview);
-
- mDecreaseFontButton = (Button) dialogView.findViewById(R.id.decrease_preview_font_button);
- mIncreaseFontButton = (Button) dialogView.findViewById(R.id.increase_preview_font_button);
- setButtonState(mPreviewFontIndex);
- mDecreaseFontButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- mPreviewFontIndex = Math.max(mPreviewFontIndex - 1, 0);
- updatePreviewFontSize(mFontTwipValues[mPreviewFontIndex]);
- mIncreaseFontButton.setEnabled(true);
- // If we reached the minimum index, disable the button.
- if (mPreviewFontIndex == 0) {
- mDecreaseFontButton.setEnabled(false);
- }
- }
- });
- mIncreaseFontButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- mPreviewFontIndex = Math.min(mPreviewFontIndex + 1, mFontTwipValues.length - 1);
- updatePreviewFontSize(mFontTwipValues[mPreviewFontIndex]);
-
- mDecreaseFontButton.setEnabled(true);
- // If we reached the maximum index, disable the button.
- if (mPreviewFontIndex == mFontTwipValues.length - 1) {
- mIncreaseFontButton.setEnabled(false);
- }
- }
- });
- }
-
- @Override
- protected void onDialogClosed(boolean positiveResult) {
- super.onDialogClosed(positiveResult);
- if (!positiveResult) {
- mPreviewFontIndex = mSavedFontIndex;
- return;
- }
- mSavedFontIndex = mPreviewFontIndex;
- final String twipVal = mFontTwipValues[mSavedFontIndex];
- final OnPreferenceChangeListener prefChangeListener = getOnPreferenceChangeListener();
- if (prefChangeListener == null) {
- Log.e(LOGTAG, "PreferenceChangeListener is null. FontSizePreference will not be saved to Gecko.");
- return;
- }
- prefChangeListener.onPreferenceChange(this, twipVal);
- }
-
- /**
- * Finds the index of the given twip value and sets it as the saved preference value. Also the
- * current preview text size to the given value. Does not update the mPreviewFontView text size.
- */
- protected void setSavedFontSize(String twip) {
- final Integer index = mFontTwipToIndexMap.get(twip);
- if (index != null) {
- mSavedFontIndex = index;
- mPreviewFontIndex = mSavedFontIndex;
- return;
- }
- resetSavedFontSizeToDefault();
- Log.e(LOGTAG, "setSavedFontSize: Given font size does not exist in twip values map. Reverted to default font size.");
- }
-
- /**
- * Updates the mPreviewFontView to the given text size, resets the container's scroll to the top
- * left, and invalidates the view. Does not update the font indices.
- */
- private void updatePreviewFontSize(String twip) {
- float pt = convertTwipStrToPT(twip);
- // Android will not render a font size of 0 pt but for Gecko, 0 twip turns off font
- // inflation. Thus we special case 0 twip to display a renderable font size.
- if (pt == 0) {
- // Android adds an inexplicable extra margin on the smallest font size so to get around
- // this, we reinflate the view.
- ViewGroup parentView = (ViewGroup) mScrollingContainer.getParent();
- parentView.removeAllViews();
- final LayoutInflater inflater =
- (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- View dialogView = inflater.inflate(R.layout.font_size_preference, parentView);
- initInternalViews(dialogView);
- mPreviewFontView.setTextSize(PREVIEW_FONT_SIZE_UNIT, 1);
- } else {
- mPreviewFontView.setTextSize(PREVIEW_FONT_SIZE_UNIT, pt);
- }
- mScrollingContainer.scrollTo(0, 0);
- }
-
- /**
- * Resets the font indices to the default value. Does not update the mPreviewFontView text size.
- */
- private void resetSavedFontSizeToDefault() {
- mSavedFontIndex = DEFAULT_FONT_INDEX;
- mPreviewFontIndex = mSavedFontIndex;
- }
-
- private void setButtonState(int index) {
- if (index == 0) {
- mDecreaseFontButton.setEnabled(false);
- } else if (index == mFontTwipValues.length - 1) {
- mIncreaseFontButton.setEnabled(false);
- }
- }
-
- /**
- * Returns the name of the font size (ex: "Small") at the currently saved preference value.
- */
- protected String getSavedFontSizeName() {
- return mFontSizeNames[mSavedFontIndex];
- }
-
- private float convertTwipStrToPT(String twip) {
- return Float.parseFloat(twip) / TWIP_TO_PT_RATIO;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferenceFragment.java b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferenceFragment.java
deleted file mode 100644
index 6be9e6ea5..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferenceFragment.java
+++ /dev/null
@@ -1,296 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import java.util.Locale;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.BrowserLocaleManager;
-import org.mozilla.gecko.GeckoApplication;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.LocaleManager;
-import org.mozilla.gecko.PrefsHelper;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.TelemetryContract.Method;
-import org.mozilla.gecko.fxa.AccountLoader;
-import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
-
-import android.accounts.Account;
-import android.app.ActionBar;
-import android.app.Activity;
-import android.app.LoaderManager;
-import android.content.Context;
-import android.content.Loader;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.os.Bundle;
-import android.preference.PreferenceActivity;
-import android.preference.PreferenceFragment;
-import android.preference.PreferenceScreen;
-import android.util.Log;
-import android.view.Menu;
-import android.view.MenuInflater;
-
-import com.squareup.leakcanary.RefWatcher;
-
-/* A simple implementation of PreferenceFragment for large screen devices
- * This will strip category headers (so that they aren't shown to the user twice)
- * as well as initializing Gecko prefs when a fragment is shown.
-*/
-public class GeckoPreferenceFragment extends PreferenceFragment {
-
- public static final int ACCOUNT_LOADER_ID = 1;
- private AccountLoaderCallbacks accountLoaderCallbacks;
- private SyncPreference syncPreference;
-
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- Log.d(LOGTAG, "onConfigurationChanged: " + newConfig.locale);
-
- final Activity context = getActivity();
-
- final LocaleManager localeManager = BrowserLocaleManager.getInstance();
- final Locale changed = localeManager.onSystemConfigurationChanged(context, getResources(), newConfig, lastLocale);
- if (changed != null) {
- applyLocale(changed);
- }
- }
-
- private static final String LOGTAG = "GeckoPreferenceFragment";
- private PrefsHelper.PrefHandler mPrefsRequest;
- private Locale lastLocale = Locale.getDefault();
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- // Write prefs to our custom GeckoSharedPrefs file.
- getPreferenceManager().setSharedPreferencesName(GeckoSharedPrefs.APP_PREFS_NAME);
-
- int res = getResource();
- if (res == R.xml.preferences) {
- Telemetry.startUISession(TelemetryContract.Session.SETTINGS);
- } else {
- final String resourceName = getArguments().getString("resource");
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, Method.SETTINGS, resourceName);
- }
-
- // Display a menu for Search preferences.
- if (res == R.xml.preferences_search) {
- setHasOptionsMenu(true);
- }
-
- addPreferencesFromResource(res);
-
- PreferenceScreen screen = getPreferenceScreen();
- setPreferenceScreen(screen);
- mPrefsRequest = ((GeckoPreferences)getActivity()).setupPreferences(screen);
- syncPreference = (SyncPreference) findPreference(GeckoPreferences.PREFS_SYNC);
- }
-
- /**
- * Return the title to use for this preference fragment.
- *
- * We only return titles for the preference screens that are
- * launched directly, and thus might need to be redisplayed.
- *
- * This method sets the title that you see on non-multi-pane devices.
- */
- private String getTitle() {
- final int res = getResource();
- if (res == R.xml.preferences) {
- return getString(R.string.settings_title);
- }
-
- // We can launch this category from the Data Reporting notification.
- if (res == R.xml.preferences_privacy) {
- return getString(R.string.pref_category_privacy_short);
- }
-
- // We can launch this category from the the magnifying glass in the quick search bar.
- if (res == R.xml.preferences_search) {
- return getString(R.string.pref_category_search);
- }
-
- // Launched as action from content notifications.
- if (res == R.xml.preferences_notifications) {
- return getString(R.string.pref_category_notifications);
- }
-
- return null;
- }
-
- /**
- * Return the header id for this preference fragment. This allows
- * us to select the correct header when launching a preference
- * screen directly.
- *
- * We only return titles for the preference screens that are
- * launched directly.
- */
- private int getHeader() {
- final int res = getResource();
- if (res == R.xml.preferences) {
- return R.id.pref_header_general;
- }
-
- // We can launch this category from the Data Reporting notification.
- if (res == R.xml.preferences_privacy) {
- return R.id.pref_header_privacy;
- }
-
- // We can launch this category from the the magnifying glass in the quick search bar.
- if (res == R.xml.preferences_search) {
- return R.id.pref_header_search;
- }
-
- // Launched as action from content notifications.
- if (res == R.xml.preferences_notifications) {
- return R.id.pref_header_notifications;
- }
-
- return -1;
- }
-
- private void updateTitle() {
- final String newTitle = getTitle();
- if (newTitle == null) {
- Log.d(LOGTAG, "No new title to show.");
- return;
- }
-
- final GeckoPreferences activity = (GeckoPreferences) getActivity();
- if (activity.isMultiPane()) {
- // In a multi-pane activity, the title is "Settings", and the action
- // bar is along the top of the screen. We don't want to change those.
- activity.showBreadCrumbs(newTitle, newTitle);
- activity.switchToHeader(getHeader());
- return;
- }
-
- Log.v(LOGTAG, "Setting activity title to " + newTitle);
- activity.setTitle(newTitle);
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- accountLoaderCallbacks = new AccountLoaderCallbacks();
- getLoaderManager().initLoader(ACCOUNT_LOADER_ID, null, accountLoaderCallbacks);
- }
-
- @Override
- public void onResume() {
- // This is a little delicate. Ensure that you do nothing prior to
- // super.onResume that you wouldn't do in onCreate.
- applyLocale(Locale.getDefault());
- super.onResume();
-
- // Force reload as the account may have been deleted while the app was in background.
- getLoaderManager().restartLoader(ACCOUNT_LOADER_ID, null, accountLoaderCallbacks);
- }
-
- private void applyLocale(final Locale currentLocale) {
- final Context context = getActivity().getApplicationContext();
-
- BrowserLocaleManager.getInstance().updateConfiguration(context, currentLocale);
-
- if (!currentLocale.equals(lastLocale)) {
- // Locales differ. Let's redisplay.
- Log.d(LOGTAG, "Locale changed: " + currentLocale);
- this.lastLocale = currentLocale;
-
- // Rebuild the list to reflect the current locale.
- getPreferenceScreen().removeAll();
- addPreferencesFromResource(getResource());
- }
-
- // Fix the parent title regardless.
- updateTitle();
- }
-
- /*
- * Get the resource from Fragment arguments and return it.
- *
- * If no resource can be found, return the resource id of the default preference screen.
- */
- private int getResource() {
- int resid = 0;
-
- final String resourceName = getArguments().getString("resource");
- final Activity activity = getActivity();
-
- if (resourceName != null) {
- // Fetch resource id by resource name.
- final Resources resources = activity.getResources();
- final String packageName = activity.getPackageName();
- resid = resources.getIdentifier(resourceName, "xml", packageName);
- }
-
- if (resid == 0) {
- // The resource was invalid. Use the default resource.
- Log.e(LOGTAG, "Failed to find resource: " + resourceName + ". Displaying default settings.");
-
- boolean isMultiPane = ((GeckoPreferences) activity).isMultiPane();
- resid = isMultiPane ? R.xml.preferences_general_tablet : R.xml.preferences;
- }
-
- return resid;
- }
-
- @Override
- public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
- super.onCreateOptionsMenu(menu, inflater);
- inflater.inflate(R.menu.preferences_search_menu, menu);
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- if (mPrefsRequest != null) {
- PrefsHelper.removeObserver(mPrefsRequest);
- mPrefsRequest = null;
- }
-
- final int res = getResource();
- if (res == R.xml.preferences) {
- Telemetry.stopUISession(TelemetryContract.Session.SETTINGS);
- }
-
- GeckoApplication.watchReference(getActivity(), this);
- }
-
- private class AccountLoaderCallbacks implements LoaderManager.LoaderCallbacks<Account> {
- @Override
- public Loader<Account> onCreateLoader(int id, Bundle args) {
- return new AccountLoader(getActivity());
- }
-
- @Override
- public void onLoadFinished(Loader<Account> loader, Account account) {
- if (syncPreference == null) {
- return;
- }
-
- if (account == null) {
- syncPreference.update(null);
- return;
- }
-
- syncPreference.update(new AndroidFxAccount(getActivity(), account));
- }
-
- @Override
- public void onLoaderReset(Loader<Account> loader) {
- if (syncPreference != null) {
- syncPreference.update(null);
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
deleted file mode 100644
index aab5be2de..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
+++ /dev/null
@@ -1,1514 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import org.json.JSONArray;
-import org.mozilla.gecko.AboutPages;
-import org.mozilla.gecko.AdjustConstants;
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.BrowserApp;
-import org.mozilla.gecko.BrowserLocaleManager;
-import org.mozilla.gecko.DataReportingNotification;
-import org.mozilla.gecko.DynamicToolbar;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoActivityStatus;
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoApplication;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.LocaleManager;
-import org.mozilla.gecko.Locales;
-import org.mozilla.gecko.PrefsHelper;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.SnackbarBuilder;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.TelemetryContract.Method;
-import org.mozilla.gecko.activitystream.ActivityStream;
-import org.mozilla.gecko.background.common.GlobalConstants;
-import org.mozilla.gecko.db.BrowserContract.SuggestedSites;
-import org.mozilla.gecko.feeds.FeedService;
-import org.mozilla.gecko.feeds.action.CheckForUpdatesAction;
-import org.mozilla.gecko.permissions.Permissions;
-import org.mozilla.gecko.restrictions.Restrictable;
-import org.mozilla.gecko.restrictions.Restrictions;
-import org.mozilla.gecko.tabqueue.TabQueueHelper;
-import org.mozilla.gecko.tabqueue.TabQueuePrompt;
-import org.mozilla.gecko.updater.UpdateService;
-import org.mozilla.gecko.updater.UpdateServiceHelper;
-import org.mozilla.gecko.util.ContextUtils;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.HardwareUtils;
-import org.mozilla.gecko.util.InputOptionsUtils;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.annotation.TargetApi;
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.app.Fragment;
-import android.app.FragmentManager;
-import android.app.NotificationManager;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
-import android.content.res.Configuration;
-import android.Manifest;
-import android.os.Build;
-import android.os.Bundle;
-import android.preference.CheckBoxPreference;
-import android.preference.EditTextPreference;
-import android.preference.ListPreference;
-import android.preference.Preference;
-import android.preference.Preference.OnPreferenceChangeListener;
-import android.preference.Preference.OnPreferenceClickListener;
-import android.preference.PreferenceActivity;
-import android.preference.PreferenceGroup;
-import android.preference.SwitchPreference;
-import android.preference.TwoStatePreference;
-import android.support.design.widget.Snackbar;
-import android.support.design.widget.TextInputLayout;
-import android.support.v4.content.LocalBroadcastManager;
-import android.support.v7.app.ActionBar;
-import android.text.Editable;
-import android.text.InputType;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.util.Log;
-import android.view.MenuItem;
-import android.view.View;
-import android.widget.AdapterView;
-import android.widget.EditText;
-import android.widget.LinearLayout;
-import android.widget.ListAdapter;
-import android.widget.ListView;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-
-public class GeckoPreferences
-extends AppCompatPreferenceActivity
-implements
-GeckoActivityStatus,
-NativeEventListener,
-OnPreferenceChangeListener,
-OnSharedPreferenceChangeListener
-{
- private static final String LOGTAG = "GeckoPreferences";
-
- // We have a white background, which makes transitions on
- // some devices look bad. Don't use transitions on those
- // devices.
- private static final boolean NO_TRANSITIONS = HardwareUtils.IS_KINDLE_DEVICE;
- private static final int NO_SUCH_ID = 0;
-
- public static final String NON_PREF_PREFIX = "android.not_a_preference.";
- public static final String INTENT_EXTRA_RESOURCES = "resource";
- public static final String PREFS_TRACKING_PROTECTION_PROMPT_SHOWN = NON_PREF_PREFIX + "trackingProtectionPromptShown";
- public static String PREFS_HEALTHREPORT_UPLOAD_ENABLED = NON_PREF_PREFIX + "healthreport.uploadEnabled";
- public static final String PREFS_SYNC = NON_PREF_PREFIX + "sync";
-
- private static boolean sIsCharEncodingEnabled;
- private boolean mInitialized;
- private PrefsHelper.PrefHandler mPrefsRequest;
- private List<Header> mHeaders;
-
- // These match keys in resources/xml*/preferences*.xml
- private static final String PREFS_SEARCH_RESTORE_DEFAULTS = NON_PREF_PREFIX + "search.restore_defaults";
- private static final String PREFS_DATA_REPORTING_PREFERENCES = NON_PREF_PREFIX + "datareporting.preferences";
- private static final String PREFS_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";
- private static final String PREFS_CRASHREPORTER_ENABLED = "datareporting.crashreporter.submitEnabled";
- private static final String PREFS_MENU_CHAR_ENCODING = "browser.menu.showCharacterEncoding";
- private static final String PREFS_MP_ENABLED = "privacy.masterpassword.enabled";
- private static final String PREFS_UPDATER_AUTODOWNLOAD = "app.update.autodownload";
- private static final String PREFS_UPDATER_URL = "app.update.url.android";
- private static final String PREFS_GEO_REPORTING = NON_PREF_PREFIX + "app.geo.reportdata";
- private static final String PREFS_GEO_LEARN_MORE = NON_PREF_PREFIX + "geo.learn_more";
- private static final String PREFS_HEALTHREPORT_LINK = NON_PREF_PREFIX + "healthreport.link";
- private static final String PREFS_DEVTOOLS_REMOTE_USB_ENABLED = "devtools.remote.usb.enabled";
- private static final String PREFS_DEVTOOLS_REMOTE_WIFI_ENABLED = "devtools.remote.wifi.enabled";
- private static final String PREFS_DEVTOOLS_REMOTE_LINK = NON_PREF_PREFIX + "remote_debugging.link";
- private static final String PREFS_TRACKING_PROTECTION = "privacy.trackingprotection.state";
- private static final String PREFS_TRACKING_PROTECTION_PB = "privacy.trackingprotection.pbmode.enabled";
- private static final String PREFS_ZOOMED_VIEW_ENABLED = "ui.zoomedview.enabled";
- public static final String PREFS_VOICE_INPUT_ENABLED = NON_PREF_PREFIX + "voice_input_enabled";
- public static final String PREFS_QRCODE_ENABLED = NON_PREF_PREFIX + "qrcode_enabled";
- private static final String PREFS_TRACKING_PROTECTION_PRIVATE_BROWSING = "privacy.trackingprotection.pbmode.enabled";
- private static final String PREFS_TRACKING_PROTECTION_LEARN_MORE = NON_PREF_PREFIX + "trackingprotection.learn_more";
- private static final String PREFS_CLEAR_PRIVATE_DATA = NON_PREF_PREFIX + "privacy.clear";
- private static final String PREFS_CLEAR_PRIVATE_DATA_EXIT = NON_PREF_PREFIX + "history.clear_on_exit";
- private static final String PREFS_SCREEN_ADVANCED = NON_PREF_PREFIX + "advanced_screen";
- public static final String PREFS_HOMEPAGE = NON_PREF_PREFIX + "homepage";
- public static final String PREFS_HOMEPAGE_PARTNER_COPY = GeckoPreferences.PREFS_HOMEPAGE + ".partner";
- public static final String PREFS_HISTORY_SAVED_SEARCH = NON_PREF_PREFIX + "search.search_history.enabled";
- private static final String PREFS_FAQ_LINK = NON_PREF_PREFIX + "faq.link";
- private static final String PREFS_FEEDBACK_LINK = NON_PREF_PREFIX + "feedback.link";
- public static final String PREFS_NOTIFICATIONS_CONTENT = NON_PREF_PREFIX + "notifications.content";
- public static final String PREFS_NOTIFICATIONS_CONTENT_LEARN_MORE = NON_PREF_PREFIX + "notifications.content.learn_more";
- public static final String PREFS_NOTIFICATIONS_WHATS_NEW = NON_PREF_PREFIX + "notifications.whats_new";
- public static final String PREFS_APP_UPDATE_LAST_BUILD_ID = "app.update.last_build_id";
- public static final String PREFS_READ_PARTNER_CUSTOMIZATIONS_PROVIDER = NON_PREF_PREFIX + "distribution.read_partner_customizations_provider";
- public static final String PREFS_READ_PARTNER_BOOKMARKS_PROVIDER = NON_PREF_PREFIX + "distribution.read_partner_bookmarks_provider";
- public static final String PREFS_CUSTOM_TABS = NON_PREF_PREFIX + "customtabs";
- public static final String PREFS_ACTIVITY_STREAM = NON_PREF_PREFIX + "activitystream";
- public static final String PREFS_CATEGORY_EXPERIMENTAL_FEATURES = NON_PREF_PREFIX + "category_experimental";
-
- private static final String ACTION_STUMBLER_UPLOAD_PREF = "STUMBLER_PREF";
-
-
- // This isn't a Gecko pref, even if it looks like one.
- private static final String PREFS_BROWSER_LOCALE = "locale";
-
- public static final String PREFS_RESTORE_SESSION = NON_PREF_PREFIX + "restoreSession3";
- public static final String PREFS_RESTORE_SESSION_FROM_CRASH = "browser.sessionstore.resume_from_crash";
- public static final String PREFS_RESTORE_SESSION_MAX_CRASH_RESUMES = "browser.sessionstore.max_resumed_crashes";
- public static final String PREFS_TAB_QUEUE = NON_PREF_PREFIX + "tab_queue";
- public static final String PREFS_TAB_QUEUE_LAST_SITE = NON_PREF_PREFIX + "last_site";
- public static final String PREFS_TAB_QUEUE_LAST_TIME = NON_PREF_PREFIX + "last_time";
-
- private static final String PREFS_DYNAMIC_TOOLBAR = "browser.chrome.dynamictoolbar";
-
- // These values are chosen to be distinct from other Activity constants.
- private static final int REQUEST_CODE_PREF_SCREEN = 5;
- private static final int RESULT_CODE_EXIT_SETTINGS = 6;
-
- // Result code used when a locale preference changes.
- // Callers can recognize this code to refresh themselves to
- // accommodate a locale change.
- public static final int RESULT_CODE_LOCALE_DID_CHANGE = 7;
-
- private static final int REQUEST_CODE_TAB_QUEUE = 8;
-
- private final Map<String, PrefHandler> HANDLERS;
- {
- final HashMap<String, PrefHandler> tempHandlers = new HashMap<>(2);
- tempHandlers.put(ClearOnShutdownPref.PREF, new ClearOnShutdownPref());
- tempHandlers.put(AndroidImportPreference.PREF_KEY, new AndroidImportPreference.Handler());
- HANDLERS = Collections.unmodifiableMap(tempHandlers);
- }
-
- private SwitchPreference tabQueuePreference;
-
- /**
- * Track the last locale so we know whether to redisplay.
- */
- private Locale lastLocale = Locale.getDefault();
- private boolean localeSwitchingIsEnabled;
-
- private void startActivityForResultChoosingTransition(final Intent intent, final int requestCode) {
- startActivityForResult(intent, requestCode);
- if (NO_TRANSITIONS) {
- overridePendingTransition(0, 0);
- }
- }
-
- private void finishChoosingTransition() {
- finish();
- if (NO_TRANSITIONS) {
- overridePendingTransition(0, 0);
- }
- }
- private void updateActionBarTitle(int title) {
- final String newTitle = getString(title);
- if (newTitle != null) {
- Log.v(LOGTAG, "Setting action bar title to " + newTitle);
-
- setTitle(newTitle);
- }
- }
-
- /**
- * We only call this method for pre-HC versions of Android.
- */
- private void updateTitleForPrefsResource(int res) {
- // At present we only need to do this for non-leaf prefs views
- // and the locale switcher itself.
- int title = -1;
- if (res == R.xml.preferences) {
- title = R.string.settings_title;
- } else if (res == R.xml.preferences_locale) {
- title = R.string.pref_category_language;
- } else if (res == R.xml.preferences_vendor) {
- title = R.string.pref_category_vendor;
- } else if (res == R.xml.preferences_general) {
- title = R.string.pref_category_general;
- } else if (res == R.xml.preferences_search) {
- title = R.string.pref_category_search;
- }
- if (title != -1) {
- setTitle(title);
- }
- }
-
- private void onLocaleChanged(Locale newLocale) {
- Log.d(LOGTAG, "onLocaleChanged: " + newLocale);
-
- BrowserLocaleManager.getInstance().updateConfiguration(getApplicationContext(), newLocale);
- this.lastLocale = newLocale;
-
- if (isMultiPane()) {
- // This takes care of the left pane.
- invalidateHeaders();
-
- // Detach and reattach the current prefs pane so that it
- // reflects the new locale.
- final FragmentManager fragmentManager = getFragmentManager();
- int id = getResources().getIdentifier("android:id/prefs", null, null);
- final Fragment current = fragmentManager.findFragmentById(id);
- if (current != null) {
- fragmentManager.beginTransaction()
- .disallowAddToBackStack()
- .detach(current)
- .attach(current)
- .commitAllowingStateLoss();
- } else {
- Log.e(LOGTAG, "No prefs fragment to reattach!");
- }
-
- // Because Android just rebuilt the activity itself with the
- // old language, we need to update the top title and other
- // wording again.
- if (onIsMultiPane()) {
- updateActionBarTitle(R.string.settings_title);
- }
-
- // Update the title to for the preference pane that we're currently showing.
- final int titleId = getIntent().getExtras().getInt(PreferenceActivity.EXTRA_SHOW_FRAGMENT_TITLE);
- if (titleId != NO_SUCH_ID) {
- setTitle(titleId);
- } else {
- throw new IllegalStateException("Title id not found in intent bundle extras");
- }
-
- // Don't finish the activity -- we just reloaded all of the
- // individual parts! -- but when it returns, make sure that the
- // caller knows the locale changed.
- setResult(RESULT_CODE_LOCALE_DID_CHANGE);
- return;
- }
-
- refreshSuggestedSites();
-
- // Cause the current fragment to redisplay, the hard way.
- // This avoids nonsense with trying to reach inside fragments and force them
- // to redisplay themselves.
- // We also don't need to update the title.
- final Intent intent = (Intent) getIntent().clone();
- intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
- startActivityForResultChoosingTransition(intent, REQUEST_CODE_PREF_SCREEN);
-
- setResult(RESULT_CODE_LOCALE_DID_CHANGE);
- finishChoosingTransition();
- }
-
- private void checkLocale() {
- final Locale currentLocale = Locale.getDefault();
- Log.v(LOGTAG, "Checking locale: " + currentLocale + " vs " + lastLocale);
- if (currentLocale.equals(lastLocale)) {
- return;
- }
-
- onLocaleChanged(currentLocale);
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- // Apply the current user-selected locale, if necessary.
- checkLocale();
-
- // Track this so we can decide whether to show locale options.
- // See also the workaround below for Bug 1015209.
- localeSwitchingIsEnabled = BrowserLocaleManager.getInstance().isEnabled();
-
- // For Android v11+ where we use Fragments (v11+ only due to bug 866352),
- // check that PreferenceActivity.EXTRA_SHOW_FRAGMENT has been set
- // (or set it) before super.onCreate() is called so Android can display
- // the correct Fragment resource.
- // Note: this seems to only be required for non-multipane devices, multipane
- // manages to automatically select the correct fragments.
- if (!getIntent().hasExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT)) {
- // Set up the default fragment if there is no explicit fragment to show.
- setupTopLevelFragmentIntent();
- }
-
- // We must call this before setTitle to avoid crashes. Most devices don't seem to care
- // (we used to call onCreate later), however the ASUS TF300T (running 4.2) crashes
- // with an NPE in android.support.v7.app.AppCompatDelegateImplV7.ensureSubDecor(), and it's
- // likely other strange devices (other Asus devices, some Samsungs) could do the same.
- super.onCreate(savedInstanceState);
-
- if (onIsMultiPane()) {
- // So that Android doesn't put the fragment title (or nothing at
- // all) in the action bar.
- updateActionBarTitle(R.string.settings_title);
-
- if (Build.VERSION.SDK_INT < 13) {
- // Affected by Bug 1015209 -- no detach/attach.
- // If we try rejigging fragments, we'll crash, so don't
- // enable locale switching at all.
- localeSwitchingIsEnabled = false;
- throw new IllegalStateException("foobar");
- }
- }
-
- // Use setResourceToOpen to specify these extras.
- Bundle intentExtras = getIntent().getExtras();
-
- EventDispatcher.getInstance().registerGeckoThreadListener(this,
- "Sanitize:Finished",
- "Snackbar:Show");
-
- // Add handling for long-press click.
- // This is only for Android 3.0 and below (which use the long-press-context-menu paradigm).
- final ListView mListView = getListView();
- mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
- @Override
- public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
- // Call long-click handler if it the item implements it.
- final ListAdapter listAdapter = ((ListView) parent).getAdapter();
- final Object listItem = listAdapter.getItem(position);
-
- // Only CustomListPreference handles long clicks.
- if (listItem instanceof CustomListPreference && listItem instanceof View.OnLongClickListener) {
- final View.OnLongClickListener longClickListener = (View.OnLongClickListener) listItem;
- return longClickListener.onLongClick(view);
- }
- return false;
- }
- });
-
- // N.B., if we ever need to redisplay the locale selection UI without
- // just finishing and recreating the activity, right here we'll need to
- // capture EXTRA_SHOW_FRAGMENT_TITLE from the intent and store the title ID.
-
- // If launched from notification, explicitly cancel the notification.
- if (intentExtras != null && intentExtras.containsKey(DataReportingNotification.ALERT_NAME_DATAREPORTING_NOTIFICATION)) {
- Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, Method.NOTIFICATION, "settings-data-choices");
- NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.cancel(DataReportingNotification.ALERT_NAME_DATAREPORTING_NOTIFICATION.hashCode());
- }
-
- // Launched from "Notifications settings" action button in a notification.
- if (intentExtras != null && intentExtras.containsKey(CheckForUpdatesAction.EXTRA_CONTENT_NOTIFICATION)) {
- Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(this));
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, Method.BUTTON, "notification-settings");
- Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(this));
- }
- }
-
- /**
- * Set intent to display top-level settings fragment,
- * and show the correct title.
- */
- private void setupTopLevelFragmentIntent() {
- Intent intent = getIntent();
- // Check intent to determine settings screen to display.
- Bundle intentExtras = intent.getExtras();
- Bundle fragmentArgs = new Bundle();
- // Add resource argument to fragment if it exists.
- if (intentExtras != null && intentExtras.containsKey(INTENT_EXTRA_RESOURCES)) {
- String resourceName = intentExtras.getString(INTENT_EXTRA_RESOURCES);
- fragmentArgs.putString(INTENT_EXTRA_RESOURCES, resourceName);
- } else {
- // Use top-level settings screen.
- if (!onIsMultiPane()) {
- fragmentArgs.putString(INTENT_EXTRA_RESOURCES, "preferences");
- } else {
- fragmentArgs.putString(INTENT_EXTRA_RESOURCES, "preferences_general_tablet");
- }
- }
-
- // Build fragment intent.
- intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT, GeckoPreferenceFragment.class.getName());
- intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, fragmentArgs);
- // Used to get fragment title when locale changes (see onLocaleChanged method above)
- intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT_TITLE, R.string.settings_title);
- }
-
- @Override
- public boolean isValidFragment(String fragmentName) {
- return GeckoPreferenceFragment.class.getName().equals(fragmentName);
- }
-
- @TargetApi(11)
- @Override
- public void onBuildHeaders(List<Header> target) {
- if (onIsMultiPane()) {
- loadHeadersFromResource(R.xml.preference_headers, target);
-
- Iterator<Header> iterator = target.iterator();
-
- while (iterator.hasNext()) {
- Header header = iterator.next();
-
- if (header.id == R.id.pref_header_advanced && !Restrictions.isAllowed(this, Restrictable.ADVANCED_SETTINGS)) {
- iterator.remove();
- } else if (header.id == R.id.pref_header_clear_private_data
- && !Restrictions.isAllowed(this, Restrictable.CLEAR_HISTORY)) {
- iterator.remove();
- }
- }
-
- mHeaders = target;
- }
- }
-
- @TargetApi(11)
- public void switchToHeader(int id) {
- if (mHeaders == null) {
- // Can't switch to a header if there are no headers!
- return;
- }
-
- for (Header header : mHeaders) {
- if (header.id == id) {
- switchToHeader(header);
- return;
- }
- }
- }
-
- @Override
- public void onWindowFocusChanged(boolean hasFocus) {
- if (!hasFocus || mInitialized)
- return;
-
- mInitialized = true;
- }
-
- @Override
- public void onBackPressed() {
- super.onBackPressed();
-
- if (NO_TRANSITIONS) {
- overridePendingTransition(0, 0);
- }
-
- Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, Method.BACK, "settings");
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
-
- EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
- "Sanitize:Finished",
- "Snackbar:Show");
-
- if (mPrefsRequest != null) {
- PrefsHelper.removeObserver(mPrefsRequest);
- mPrefsRequest = null;
- }
- }
-
- @Override
- public void onPause() {
- // Symmetric with onResume.
- if (isMultiPane()) {
- SharedPreferences prefs = GeckoSharedPrefs.forApp(this);
- prefs.unregisterOnSharedPreferenceChangeListener(this);
- }
-
- super.onPause();
-
- if (getApplication() instanceof GeckoApplication) {
- ((GeckoApplication) getApplication()).onActivityPause(this);
- }
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- if (getApplication() instanceof GeckoApplication) {
- ((GeckoApplication) getApplication()).onActivityResume(this);
- }
-
- // Watch prefs, otherwise we don't reliably get told when they change.
- // See documentation for onSharedPreferenceChange for more.
- // Inexplicably only needed on tablet.
- if (isMultiPane()) {
- SharedPreferences prefs = GeckoSharedPrefs.forApp(this);
- prefs.registerOnSharedPreferenceChangeListener(this);
- }
- }
-
- @Override
- public void startActivity(Intent intent) {
- // For settings, we want to be able to pass results up the chain
- // of preference screens so Settings can behave as a single unit.
- // Specifically, when we open a link, we want to back out of all
- // the settings screens.
- // We need to start nested PreferenceScreens withStartActivityForResult().
- // Android doesn't let us do that (see Preference.onClick), so we're overriding here.
- startActivityForResultChoosingTransition(intent, REQUEST_CODE_PREF_SCREEN);
- }
-
- @Override
- public void startWithFragment(String fragmentName, Bundle args,
- Fragment resultTo, int resultRequestCode, int titleRes, int shortTitleRes) {
- Log.v(LOGTAG, "Starting with fragment: " + fragmentName + ", title " + titleRes);
-
- // Overriding because we want to use startActivityForResult for Fragment intents.
- Intent intent = onBuildStartFragmentIntent(fragmentName, args, titleRes, shortTitleRes);
- if (resultTo == null) {
- startActivityForResultChoosingTransition(intent, REQUEST_CODE_PREF_SCREEN);
- } else {
- resultTo.startActivityForResult(intent, resultRequestCode);
- if (NO_TRANSITIONS) {
- overridePendingTransition(0, 0);
- }
- }
- }
-
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- // We might have just returned from a settings activity that allows us
- // to switch locales, so reflect any change that occurred.
- checkLocale();
-
- switch (requestCode) {
- case REQUEST_CODE_PREF_SCREEN:
- switch (resultCode) {
- case RESULT_CODE_EXIT_SETTINGS:
- updateActionBarTitle(R.string.settings_title);
-
- // Pass this result up to the parent activity.
- setResult(RESULT_CODE_EXIT_SETTINGS);
- finishChoosingTransition();
- break;
- }
- break;
- case REQUEST_CODE_TAB_QUEUE:
- if (TabQueueHelper.processTabQueuePromptResponse(resultCode, this)) {
- tabQueuePreference.setChecked(true);
- }
- break;
- }
- }
-
- @Override
- public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
- Permissions.onRequestPermissionsResult(this, permissions, grantResults);
- }
-
- @Override
- public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) {
- try {
- switch (event) {
- case "Sanitize:Finished":
- boolean success = message.getBoolean("success");
- final int stringRes = success ? R.string.private_data_success : R.string.private_data_fail;
-
- SnackbarBuilder.builder(GeckoPreferences.this)
- .message(stringRes)
- .duration(Snackbar.LENGTH_LONG)
- .buildAndShow();
- break;
- case "Snackbar:Show":
- SnackbarBuilder.builder(this)
- .fromEvent(message)
- .callback(callback)
- .buildAndShow();
- break;
- }
- } catch (Exception e) {
- Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
- }
- }
-
- /**
- * Initialize all of the preferences (native of Gecko ones) for this screen.
- *
- * @param prefs The android.preference.PreferenceGroup to initialize
- * @return The integer id for the PrefsHelper.PrefHandlerBase listener added
- * to monitor changes to Gecko prefs.
- */
- public PrefsHelper.PrefHandler setupPreferences(PreferenceGroup prefs) {
- ArrayList<String> list = new ArrayList<String>();
- setupPreferences(prefs, list);
- return getGeckoPreferences(prefs, list);
- }
-
- /**
- * Recursively loop through a PreferenceGroup. Initialize native Android prefs,
- * and build a list of Gecko preferences in the passed in prefs array
- *
- * @param preferences The android.preference.PreferenceGroup to initialize
- * @param prefs An ArrayList to fill with Gecko preferences that need to be
- * initialized
- * @return The integer id for the PrefsHelper.PrefHandlerBase listener added
- * to monitor changes to Gecko prefs.
- */
- private void setupPreferences(PreferenceGroup preferences, ArrayList<String> prefs) {
- for (int i = 0; i < preferences.getPreferenceCount(); i++) {
- final Preference pref = preferences.getPreference(i);
-
- // Eliminate locale switching if necessary.
- // This logic will need to be extended when
- // content language selection (Bug 881510) is implemented.
- if (!localeSwitchingIsEnabled &&
- "preferences_locale".equals(pref.getExtras().getString("resource"))) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
-
- String key = pref.getKey();
- if (pref instanceof PreferenceGroup) {
- // If datareporting is disabled, remove UI.
- if (PREFS_DATA_REPORTING_PREFERENCES.equals(key)) {
- if (!AppConstants.MOZ_DATA_REPORTING || !Restrictions.isAllowed(this, Restrictable.DATA_CHOICES)) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
- } else if (PREFS_SCREEN_ADVANCED.equals(key) &&
- !Restrictions.isAllowed(this, Restrictable.ADVANCED_SETTINGS)) {
- preferences.removePreference(pref);
- i--;
- continue;
- } else if (PREFS_CATEGORY_EXPERIMENTAL_FEATURES.equals(key)
- && !AppConstants.MOZ_ANDROID_ACTIVITY_STREAM
- && !AppConstants.MOZ_ANDROID_CUSTOM_TABS) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
- setupPreferences((PreferenceGroup) pref, prefs);
- } else {
- if (HANDLERS.containsKey(key)) {
- PrefHandler handler = HANDLERS.get(key);
- if (!handler.setupPref(this, pref)) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
- }
-
- pref.setOnPreferenceChangeListener(this);
- if (PREFS_UPDATER_AUTODOWNLOAD.equals(key)) {
- if (!AppConstants.MOZ_UPDATER || ContextUtils.isInstalledFromGooglePlay(this)) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
- } else if (PREFS_TRACKING_PROTECTION.equals(key)) {
- // Remove UI for global TP pref in non-Nightly builds.
- if (!AppConstants.NIGHTLY_BUILD) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
- } else if (PREFS_TRACKING_PROTECTION_PB.equals(key)) {
- // Remove UI for private-browsing-only TP pref in Nightly builds.
- if (AppConstants.NIGHTLY_BUILD) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
- } else if (PREFS_TELEMETRY_ENABLED.equals(key)) {
- if (!AppConstants.MOZ_TELEMETRY_REPORTING || !Restrictions.isAllowed(this, Restrictable.DATA_CHOICES)) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
- } else if (PREFS_HEALTHREPORT_UPLOAD_ENABLED.equals(key) ||
- PREFS_HEALTHREPORT_LINK.equals(key)) {
- if (!AppConstants.MOZ_SERVICES_HEALTHREPORT || !Restrictions.isAllowed(this, Restrictable.DATA_CHOICES)) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
- } else if (PREFS_GEO_REPORTING.equals(key) ||
- PREFS_GEO_LEARN_MORE.equals(key)) {
- if (!AppConstants.MOZ_STUMBLER_BUILD_TIME_ENABLED || !Restrictions.isAllowed(this, Restrictable.DATA_CHOICES)) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
- } else if (PREFS_DEVTOOLS_REMOTE_USB_ENABLED.equals(key)) {
- if (!Restrictions.isAllowed(this, Restrictable.REMOTE_DEBUGGING)) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
- } else if (PREFS_DEVTOOLS_REMOTE_WIFI_ENABLED.equals(key)) {
- if (!Restrictions.isAllowed(this, Restrictable.REMOTE_DEBUGGING)) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
- if (!InputOptionsUtils.supportsQrCodeReader(getApplicationContext())) {
- // WiFi debugging requires a QR code reader
- pref.setEnabled(false);
- pref.setSummary(getString(R.string.pref_developer_remotedebugging_wifi_disabled_summary));
- continue;
- }
- } else if (PREFS_DEVTOOLS_REMOTE_LINK.equals(key)) {
- // Remove the "Learn more" link if remote debugging is disabled
- if (!Restrictions.isAllowed(this, Restrictable.REMOTE_DEBUGGING)) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
- } else if (PREFS_RESTORE_SESSION.equals(key) ||
- PREFS_BROWSER_LOCALE.equals(key)) {
- // Set the summary string to the current entry. The summary
- // for other list prefs will be set in the PrefsHelper
- // callback, but since this pref doesn't live in Gecko, we
- // need to handle it separately.
- ListPreference listPref = (ListPreference) pref;
- CharSequence selectedEntry = listPref.getEntry();
- listPref.setSummary(selectedEntry);
- continue;
- } else if (PREFS_SYNC.equals(key)) {
- // Don't show sync prefs while in guest mode.
- if (!Restrictions.isAllowed(this, Restrictable.MODIFY_ACCOUNTS)) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
- } else if (PREFS_SEARCH_RESTORE_DEFAULTS.equals(key)) {
- pref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
- @Override
- public boolean onPreferenceClick(Preference preference) {
- GeckoPreferences.this.restoreDefaultSearchEngines();
- Telemetry.sendUIEvent(TelemetryContract.Event.SEARCH_RESTORE_DEFAULTS, Method.LIST_ITEM);
- return true;
- }
- });
- } else if (PREFS_TAB_QUEUE.equals(key)) {
- tabQueuePreference = (SwitchPreference) pref;
- // Only show tab queue pref on nightly builds with the tab queue build flag.
- if (!TabQueueHelper.TAB_QUEUE_ENABLED) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
- } else if (PREFS_ZOOMED_VIEW_ENABLED.equals(key)) {
- if (!AppConstants.NIGHTLY_BUILD) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
- } else if (PREFS_VOICE_INPUT_ENABLED.equals(key)) {
- if (!InputOptionsUtils.supportsVoiceRecognizer(getApplicationContext(), getResources().getString(R.string.voicesearch_prompt))) {
- // Remove UI for voice input on non nightly builds.
- preferences.removePreference(pref);
- i--;
- continue;
- }
- } else if (PREFS_QRCODE_ENABLED.equals(key)) {
- if (!InputOptionsUtils.supportsQrCodeReader(getApplicationContext())) {
- // Remove UI for qr code input on non nightly builds
- preferences.removePreference(pref);
- i--;
- continue;
- }
- } else if (PREFS_TRACKING_PROTECTION_PRIVATE_BROWSING.equals(key)) {
- if (!Restrictions.isAllowed(this, Restrictable.PRIVATE_BROWSING)) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
- } else if (PREFS_TRACKING_PROTECTION_LEARN_MORE.equals(key)) {
- if (!Restrictions.isAllowed(this, Restrictable.PRIVATE_BROWSING)) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
- } else if (PREFS_MP_ENABLED.equals(key)) {
- if (!Restrictions.isAllowed(this, Restrictable.MASTER_PASSWORD)) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
- } else if (PREFS_CLEAR_PRIVATE_DATA.equals(key) || PREFS_CLEAR_PRIVATE_DATA_EXIT.equals(key)) {
- if (!Restrictions.isAllowed(this, Restrictable.CLEAR_HISTORY)) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
- } else if (PREFS_HOMEPAGE.equals(key)) {
- String setUrl = GeckoSharedPrefs.forProfile(getBaseContext()).getString(PREFS_HOMEPAGE, AboutPages.HOME);
- setHomePageSummary(pref, setUrl);
- pref.setOnPreferenceChangeListener(this);
- } else if (PREFS_FAQ_LINK.equals(key)) {
- // Format the FAQ link
- final String VERSION = AppConstants.MOZ_APP_VERSION;
- final String OS = AppConstants.OS_TARGET;
- final String LOCALE = Locales.getLanguageTag(Locale.getDefault());
-
- final String url = getResources().getString(R.string.faq_link, VERSION, OS, LOCALE);
- ((LinkPreference) pref).setUrl(url);
- } else if (PREFS_FEEDBACK_LINK.equals(key)) {
- // Format the feedback link. We can't easily use this "app.feedbackURL"
- // Gecko preference because the URL must be formatted.
- final String url = getResources().getString(R.string.feedback_link, AppConstants.MOZ_APP_VERSION, AppConstants.MOZ_UPDATE_CHANNEL);
- ((LinkPreference) pref).setUrl(url);
- } else if (PREFS_DYNAMIC_TOOLBAR.equals(key)) {
- if (DynamicToolbar.isForceDisabled()) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
- } else if (PREFS_NOTIFICATIONS_CONTENT.equals(key) ||
- PREFS_NOTIFICATIONS_CONTENT_LEARN_MORE.equals(key)) {
- if (!FeedService.isInExperiment(this)) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
- } else if (PREFS_CUSTOM_TABS.equals(key) && !AppConstants.MOZ_ANDROID_CUSTOM_TABS) {
- preferences.removePreference(pref);
- i--;
- continue;
- } else if (PREFS_ACTIVITY_STREAM.equals(key) && !ActivityStream.isUserEligible(this)) {
- preferences.removePreference(pref);
- i--;
- continue;
- }
-
- // Some Preference UI elements are not actually preferences,
- // but they require a key to work correctly. For example,
- // "Clear private data" requires a key for its state to be
- // saved when the orientation changes. It uses the
- // "android.not_a_preference.privacy.clear" key - which doesn't
- // exist in Gecko - to satisfy this requirement.
- if (isGeckoPref(key)) {
- prefs.add(key);
- }
- }
- }
- }
-
- private void setHomePageSummary(Preference pref, String value) {
- if (!TextUtils.isEmpty(value)) {
- pref.setSummary(value);
- } else {
- pref.setSummary(AboutPages.HOME);
- }
- }
-
- private boolean isGeckoPref(String key) {
- if (TextUtils.isEmpty(key)) {
- return false;
- }
-
- if (key.startsWith(NON_PREF_PREFIX)) {
- return false;
- }
-
- if (key.equals(PREFS_BROWSER_LOCALE)) {
- return false;
- }
-
- return true;
- }
-
- /**
- * Restore default search engines in Gecko and retrigger a search engine refresh.
- */
- protected void restoreDefaultSearchEngines() {
- GeckoAppShell.notifyObservers("SearchEngines:RestoreDefaults", null);
-
- // Send message to Gecko to get engines. SearchPreferenceCategory listens for the response.
- GeckoAppShell.notifyObservers("SearchEngines:GetVisible", null);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- int itemId = item.getItemId();
- switch (itemId) {
- case android.R.id.home:
- finishChoosingTransition();
- return true;
- }
-
- // Generated R.id.* apparently aren't constant expressions, so they can't be switched.
- if (itemId == R.id.restore_defaults) {
- restoreDefaultSearchEngines();
- Telemetry.sendUIEvent(TelemetryContract.Event.SEARCH_RESTORE_DEFAULTS, Method.MENU);
- return true;
- }
-
- return super.onOptionsItemSelected(item);
- }
-
- final private int DIALOG_CREATE_MASTER_PASSWORD = 0;
- final private int DIALOG_REMOVE_MASTER_PASSWORD = 1;
-
- public static void setCharEncodingState(boolean enabled) {
- sIsCharEncodingEnabled = enabled;
- }
-
- public static boolean getCharEncodingState() {
- return sIsCharEncodingEnabled;
- }
-
- public static void broadcastAction(final Context context, final Intent intent) {
- fillIntentWithProfileInfo(context, intent);
- LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
- }
-
- private static void fillIntentWithProfileInfo(final Context context, final Intent intent) {
- // There is a race here, but GeckoProfile returns the default profile
- // when Gecko is not explicitly running for a different profile. In a
- // multi-profile world, this will need to be updated (possibly to
- // broadcast settings for all profiles). See Bug 882182.
- GeckoProfile profile = GeckoProfile.get(context);
- if (profile != null) {
- intent.putExtra("profileName", profile.getName())
- .putExtra("profilePath", profile.getDir().getAbsolutePath());
- }
- }
-
- /**
- * Broadcast the provided value as the value of the
- * <code>PREFS_GEO_REPORTING</code> pref.
- */
- public static void broadcastStumblerPref(final Context context, final boolean value) {
- Intent intent = new Intent(ACTION_STUMBLER_UPLOAD_PREF)
- .putExtra("pref", PREFS_GEO_REPORTING)
- .putExtra("branch", GeckoSharedPrefs.APP_PREFS_NAME)
- .putExtra("enabled", value)
- .putExtra("moz_mozilla_api_key", AppConstants.MOZ_MOZILLA_API_KEY);
- if (GeckoAppShell.getGeckoInterface() != null) {
- intent.putExtra("user_agent", GeckoAppShell.getGeckoInterface().getDefaultUAString());
- }
- broadcastAction(context, intent);
- }
-
- /**
- * Broadcast the current value of the
- * <code>PREFS_GEO_REPORTING</code> pref.
- */
- public static void broadcastStumblerPref(final Context context) {
- final boolean value = getBooleanPref(context, PREFS_GEO_REPORTING, false);
- broadcastStumblerPref(context, value);
- }
-
- /**
- * Return the value of the named preference in the default preferences file.
- *
- * This corresponds to the storage that backs preferences.xml.
- * @param context a <code>Context</code>; the
- * <code>PreferenceActivity</code> will suffice, but this
- * method is intended to be called from other contexts
- * within the application, not just this <code>Activity</code>.
- * @param name the name of the preference to retrieve.
- * @param def the default value to return if the preference is not present.
- * @return the value of the preference, or the default.
- */
- public static boolean getBooleanPref(final Context context, final String name, boolean def) {
- final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
- return prefs.getBoolean(name, def);
- }
-
- /**
- * Immediately handle the user's selection of a browser locale.
- *
- * Earlier locale-handling code did this with centralized logic in
- * GeckoApp, delegating to LocaleManager for persistence and refreshing
- * the activity as necessary.
- *
- * We no longer handle this by sending a message to GeckoApp, for
- * several reasons:
- *
- * * GeckoApp might not be running. Activities don't always stick around.
- * A Java bridge message might not be handled.
- * * We need to adapt the preferences UI to the locale ourselves.
- * * The user might not hit Back (or Up) -- they might hit Home and never
- * come back.
- *
- * We handle the case of the user returning to the browser via the
- * onActivityResult mechanism: see {@link BrowserApp#onActivityResult(int, int, Intent)}.
- */
- private boolean onLocaleSelected(final String currentLocale, final String newValue) {
- final Context context = getApplicationContext();
-
- // LocaleManager operations need to occur on the background thread.
- // ... but activity operations need to occur on the UI thread. So we
- // have nested runnables.
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- final LocaleManager localeManager = BrowserLocaleManager.getInstance();
-
- if (TextUtils.isEmpty(newValue)) {
- BrowserLocaleManager.getInstance().resetToSystemLocale(context);
- Telemetry.sendUIEvent(TelemetryContract.Event.LOCALE_BROWSER_RESET);
- } else {
- if (null == localeManager.setSelectedLocale(context, newValue)) {
- localeManager.updateConfiguration(context, Locale.getDefault());
- }
- Telemetry.sendUIEvent(TelemetryContract.Event.LOCALE_BROWSER_UNSELECTED, Method.NONE,
- currentLocale == null ? "unknown" : currentLocale);
- Telemetry.sendUIEvent(TelemetryContract.Event.LOCALE_BROWSER_SELECTED, Method.NONE, newValue);
- }
-
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- onLocaleChanged(Locale.getDefault());
- }
- });
- }
- });
-
- return true;
- }
-
- private void refreshSuggestedSites() {
- final ContentResolver cr = getApplicationContext().getContentResolver();
-
- // This will force all active suggested sites cursors
- // to request a refresh (e.g. cursor loaders).
- cr.notifyChange(SuggestedSites.CONTENT_URI, null);
- }
-
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
-
- Log.d(LOGTAG, "onConfigurationChanged: " + newConfig.locale);
-
- if (lastLocale.equals(newConfig.locale)) {
- Log.d(LOGTAG, "Old locale same as new locale. Short-circuiting.");
- return;
- }
-
- final LocaleManager localeManager = BrowserLocaleManager.getInstance();
- final Locale changed = localeManager.onSystemConfigurationChanged(this, getResources(), newConfig, lastLocale);
- if (changed != null) {
- onLocaleChanged(changed);
- }
- }
-
- /**
- * Implementation for the {@link OnSharedPreferenceChangeListener} interface,
- * which we use to watch changes in our prefs file.
- *
- * This is reliably called whenever the pref changes, which is not the case
- * for multiple consecutive changes in the case of onPreferenceChange.
- *
- * Note that this listener is not always registered: we use it only on
- * tablets, Honeycomb and up, where we'll have a multi-pane view and prefs
- * changing multiple times.
- */
- @Override
- public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
- if (PREFS_BROWSER_LOCALE.equals(key)) {
- onLocaleSelected(Locales.getLanguageTag(lastLocale),
- sharedPreferences.getString(key, null));
- }
- }
-
- public interface PrefHandler {
- // Allows the pref to do any initialization it needs. Return false to have the pref removed
- // from the prefs screen entirely.
- public boolean setupPref(Context context, Preference pref);
- public void onChange(Context context, Preference pref, Object newValue);
- }
-
- private void recordSettingChangeTelemetry(String prefName, Object newValue) {
- final String value;
- if (newValue instanceof Boolean) {
- value = (Boolean) newValue ? "1" : "0";
- } else if (prefName.equals(PREFS_HOMEPAGE)) {
- // Don't record the user's homepage preference.
- value = "*";
- } else {
- value = newValue.toString();
- }
-
- final JSONArray extras = new JSONArray();
- extras.put(prefName);
- extras.put(value);
- Telemetry.sendUIEvent(TelemetryContract.Event.EDIT, Method.SETTINGS, extras.toString());
- }
-
- @Override
- public boolean onPreferenceChange(Preference preference, Object newValue) {
- final String prefName = preference.getKey();
- Log.i(LOGTAG, "Changed " + prefName + " = " + newValue);
- recordSettingChangeTelemetry(prefName, newValue);
-
- if (PREFS_MP_ENABLED.equals(prefName)) {
- showDialog((Boolean) newValue ? DIALOG_CREATE_MASTER_PASSWORD : DIALOG_REMOVE_MASTER_PASSWORD);
-
- // We don't want the "use master password" pref to change until the
- // user has gone through the dialog.
- return false;
- }
-
- if (PREFS_HOMEPAGE.equals(prefName)) {
- setHomePageSummary(preference, String.valueOf(newValue));
- }
-
- if (PREFS_BROWSER_LOCALE.equals(prefName)) {
- // Even though this is a list preference, we don't want to handle it
- // below, so we return here.
- return onLocaleSelected(Locales.getLanguageTag(lastLocale), (String) newValue);
- }
-
- if (PREFS_MENU_CHAR_ENCODING.equals(prefName)) {
- setCharEncodingState(((String) newValue).equals("true"));
- } else if (PREFS_UPDATER_AUTODOWNLOAD.equals(prefName)) {
- UpdateServiceHelper.setAutoDownloadPolicy(this, UpdateService.AutoDownloadPolicy.get((String) newValue));
- } else if (PREFS_UPDATER_URL.equals(prefName)) {
- UpdateServiceHelper.setUpdateUrl(this, (String) newValue);
- } else if (PREFS_HEALTHREPORT_UPLOAD_ENABLED.equals(prefName)) {
- final Boolean newBooleanValue = (Boolean) newValue;
- AdjustConstants.getAdjustHelper().setEnabled(newBooleanValue);
- } else if (PREFS_GEO_REPORTING.equals(prefName)) {
- if ((Boolean) newValue) {
- enableStumbler((CheckBoxPreference) preference);
- return false;
- } else {
- broadcastStumblerPref(GeckoPreferences.this, false);
- return true;
- }
- } else if (PREFS_TAB_QUEUE.equals(prefName)) {
- if ((Boolean) newValue && !TabQueueHelper.canDrawOverlays(this)) {
- Intent promptIntent = new Intent(this, TabQueuePrompt.class);
- startActivityForResult(promptIntent, REQUEST_CODE_TAB_QUEUE);
- return false;
- }
- } else if (PREFS_NOTIFICATIONS_CONTENT.equals(prefName)) {
- FeedService.setup(this);
- } else if (PREFS_ACTIVITY_STREAM.equals(prefName)) {
- ThreadUtils.postDelayedToUiThread(new Runnable() {
- @Override
- public void run() {
- GeckoAppShell.scheduleRestart();
- }
- }, 1000);
- } else if (HANDLERS.containsKey(prefName)) {
- PrefHandler handler = HANDLERS.get(prefName);
- handler.onChange(this, preference, newValue);
- }
-
- // Send Gecko-side pref changes to Gecko
- if (isGeckoPref(prefName)) {
- PrefsHelper.setPref(prefName, newValue, true /* flush */);
- }
-
- if (preference instanceof ListPreference) {
- // We need to find the entry for the new value
- int newIndex = ((ListPreference) preference).findIndexOfValue((String) newValue);
- CharSequence newEntry = ((ListPreference) preference).getEntries()[newIndex];
- ((ListPreference) preference).setSummary(newEntry);
- } else if (preference instanceof LinkPreference) {
- setResult(RESULT_CODE_EXIT_SETTINGS);
- finishChoosingTransition();
- } else if (preference instanceof FontSizePreference) {
- final FontSizePreference fontSizePref = (FontSizePreference) preference;
- fontSizePref.setSummary(fontSizePref.getSavedFontSizeName());
- }
-
- return true;
- }
-
- private void enableStumbler(final CheckBoxPreference preference) {
- Permissions
- .from(this)
- .withPermissions(Manifest.permission.ACCESS_FINE_LOCATION)
- .onUIThread()
- .andFallback(new Runnable() {
- @Override
- public void run() {
- preference.setChecked(false);
- }
- })
- .run(new Runnable() {
- @Override
- public void run() {
- preference.setChecked(true);
- broadcastStumblerPref(GeckoPreferences.this, true);
- }
- });
- }
-
- private TextInputLayout getTextBox(int aHintText) {
- final EditText input = new EditText(this);
- int inputtype = InputType.TYPE_CLASS_TEXT;
- inputtype |= InputType.TYPE_TEXT_VARIATION_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
- input.setInputType(inputtype);
-
- input.setHint(aHintText);
-
- final TextInputLayout layout = new TextInputLayout(this);
- layout.addView(input);
-
- return layout;
- }
-
- private class PasswordTextWatcher implements TextWatcher {
- EditText input1;
- EditText input2;
- AlertDialog dialog;
-
- PasswordTextWatcher(EditText aInput1, EditText aInput2, AlertDialog aDialog) {
- input1 = aInput1;
- input2 = aInput2;
- dialog = aDialog;
- }
-
- @Override
- public void afterTextChanged(Editable s) {
- if (dialog == null)
- return;
-
- String text1 = input1.getText().toString();
- String text2 = input2.getText().toString();
- boolean disabled = TextUtils.isEmpty(text1) || TextUtils.isEmpty(text2) || !text1.equals(text2);
- dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(!disabled);
- }
-
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) { }
- }
-
- private class EmptyTextWatcher implements TextWatcher {
- EditText input;
- AlertDialog dialog;
-
- EmptyTextWatcher(EditText aInput, AlertDialog aDialog) {
- input = aInput;
- dialog = aDialog;
- }
-
- @Override
- public void afterTextChanged(Editable s) {
- if (dialog == null)
- return;
-
- String text = input.getText().toString();
- boolean disabled = TextUtils.isEmpty(text);
- dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(!disabled);
- }
-
- @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
- protected Dialog onCreateDialog(int id) {
- AlertDialog.Builder builder = new AlertDialog.Builder(this);
- LinearLayout linearLayout = new LinearLayout(this);
- linearLayout.setOrientation(LinearLayout.VERTICAL);
- AlertDialog dialog;
- switch (id) {
- case DIALOG_CREATE_MASTER_PASSWORD:
- final TextInputLayout inputLayout1 = getTextBox(R.string.masterpassword_password);
- final TextInputLayout inputLayout2 = getTextBox(R.string.masterpassword_confirm);
- linearLayout.addView(inputLayout1);
- linearLayout.addView(inputLayout2);
-
- final EditText input1 = inputLayout1.getEditText();
- final EditText input2 = inputLayout2.getEditText();
-
- builder.setTitle(R.string.masterpassword_create_title)
- .setView((View) linearLayout)
- .setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- PrefsHelper.setPref(PREFS_MP_ENABLED,
- input1.getText().toString(),
- /* flush */ true);
- }
- })
- .setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- return;
- }
- });
- dialog = builder.create();
- dialog.setOnShowListener(new DialogInterface.OnShowListener() {
- @Override
- public void onShow(DialogInterface dialog) {
- input1.setText("");
- input2.setText("");
- input1.requestFocus();
- }
- });
-
- PasswordTextWatcher watcher = new PasswordTextWatcher(input1, input2, dialog);
- input1.addTextChangedListener((TextWatcher) watcher);
- input2.addTextChangedListener((TextWatcher) watcher);
-
- break;
- case DIALOG_REMOVE_MASTER_PASSWORD:
- final TextInputLayout inputLayout = getTextBox(R.string.masterpassword_password);
- linearLayout.addView(inputLayout);
- final EditText input = inputLayout.getEditText();
-
- builder.setTitle(R.string.masterpassword_remove_title)
- .setView((View) linearLayout)
- .setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- PrefsHelper.setPref(PREFS_MP_ENABLED, input.getText().toString());
- }
- })
- .setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- return;
- }
- });
- dialog = builder.create();
- dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
- @Override
- public void onDismiss(DialogInterface dialog) {
- input.setText("");
- }
- });
- dialog.setOnShowListener(new DialogInterface.OnShowListener() {
- @Override
- public void onShow(DialogInterface dialog) {
- input.setText("");
- }
- });
- input.addTextChangedListener(new EmptyTextWatcher(input, dialog));
- break;
- default:
- return null;
- }
-
- return dialog;
- }
-
- // Initialize preferences by requesting the preference values from Gecko
- private static class PrefCallbacks extends PrefsHelper.PrefHandlerBase {
- private final PreferenceGroup screen;
-
- public PrefCallbacks(final PreferenceGroup screen) {
- this.screen = screen;
- }
-
- private Preference getField(String prefName) {
- return screen.findPreference(prefName);
- }
-
- @Override
- public void prefValue(String prefName, final boolean value) {
- final TwoStatePreference pref = (TwoStatePreference) getField(prefName);
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- if (pref.isChecked() != value) {
- pref.setChecked(value);
- }
- }
- });
- }
-
- @Override
- public void prefValue(String prefName, final String value) {
- final Preference pref = getField(prefName);
- if (pref instanceof EditTextPreference) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- ((EditTextPreference) pref).setText(value);
- }
- });
- } else if (pref instanceof ListPreference) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- ((ListPreference) pref).setValue(value);
- // Set the summary string to the current entry
- CharSequence selectedEntry = ((ListPreference) pref).getEntry();
- ((ListPreference) pref).setSummary(selectedEntry);
- }
- });
- } else if (pref instanceof FontSizePreference) {
- final FontSizePreference fontSizePref = (FontSizePreference) pref;
- fontSizePref.setSavedFontSize(value);
- final String fontSizeName = fontSizePref.getSavedFontSizeName();
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- fontSizePref.setSummary(fontSizeName); // Ex: "Small".
- }
- });
- }
- }
-
- @Override
- public void prefValue(String prefName, final int value) {
- final Preference pref = getField(prefName);
- Log.w(LOGTAG, "Unhandled int value for pref [" + pref + "]");
- }
-
- @Override
- public void finish() {
- // enable all preferences once we have them from gecko
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- screen.setEnabled(true);
- }
- });
- }
- }
-
- private PrefsHelper.PrefHandler getGeckoPreferences(final PreferenceGroup screen,
- ArrayList<String> prefs) {
- final PrefsHelper.PrefHandler prefHandler = new PrefCallbacks(screen);
- final String[] prefNames = prefs.toArray(new String[prefs.size()]);
- PrefsHelper.addObserver(prefNames, prefHandler);
- return prefHandler;
- }
-
- @Override
- public boolean isGeckoActivityOpened() {
- return false;
- }
-
- /**
- * Given an Intent instance, add extras to specify which settings section to
- * open.
- *
- * resource should be a valid Android XML resource identifier.
- *
- * The mechanism to open a section differs based on Android version.
- */
- public static void setResourceToOpen(final Intent intent, final String resource) {
- if (intent == null) {
- throw new IllegalArgumentException("intent must not be null");
- }
- if (resource == null) {
- return;
- }
-
- intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT, GeckoPreferenceFragment.class.getName());
-
- Bundle fragmentArgs = new Bundle();
- fragmentArgs.putString("resource", resource);
- intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, fragmentArgs);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/LinkPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/LinkPreference.java
deleted file mode 100644
index 774f78c53..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/LinkPreference.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import org.mozilla.gecko.Tabs;
-
-import android.content.Context;
-import android.preference.Preference;
-import android.util.AttributeSet;
-
-class LinkPreference extends Preference {
- private String mUrl;
-
- public LinkPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- mUrl = attrs.getAttributeValue(null, "url");
- }
- public LinkPreference(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- mUrl = attrs.getAttributeValue(null, "url");
- }
-
- public void setUrl(String url) {
- mUrl = url;
- }
-
- @Override
- protected void onClick() {
- Tabs.getInstance().loadUrlInTab(mUrl);
- callChangeListener(mUrl);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/ListCheckboxPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/ListCheckboxPreference.java
deleted file mode 100644
index f56ea58b9..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/ListCheckboxPreference.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.Checkable;
-
-import org.mozilla.gecko.R;
-
-/**
- * This preference shows a checkbox on its left hand side, but will show a menu when clicked.
- * Its used for preferences like "Clear on Exit" that have a boolean on-off state, but that represent
- * multiple boolean options inside.
- **/
-class ListCheckboxPreference extends MultiChoicePreference implements Checkable {
- private static final String LOGTAG = "GeckoListCheckboxPreference";
- private boolean checked;
-
- public ListCheckboxPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- setWidgetLayoutResource(R.layout.preference_checkbox);
- }
-
- @Override
- public boolean isChecked() {
- return checked;
- }
-
- @Override
- protected void onBindView(View view) {
- super.onBindView(view);
-
- View checkboxView = view.findViewById(R.id.checkbox);
- if (checkboxView instanceof Checkable) {
- ((Checkable) checkboxView).setChecked(checked);
- }
- }
-
- @Override
- public void setChecked(boolean checked) {
- boolean changed = checked != this.checked;
- this.checked = checked;
- if (changed) {
- notifyDependencyChange(shouldDisableDependents());
- notifyChanged();
- }
- }
-
- @Override
- public void toggle() {
- checked = !checked;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/LocaleListPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/LocaleListPreference.java
deleted file mode 100644
index c962a3d19..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/LocaleListPreference.java
+++ /dev/null
@@ -1,316 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import java.nio.ByteBuffer;
-import java.text.Collator;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.Locale;
-import java.util.Set;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.BrowserLocaleManager;
-import org.mozilla.gecko.Locales;
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.preference.ListPreference;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.util.Log;
-
-public class LocaleListPreference extends ListPreference {
- private static final String LOG_TAG = "GeckoLocaleList";
-
- /**
- * With thanks to <http://stackoverflow.com/a/22679283/22003> for the
- * initial solution.
- *
- * This class encapsulates an approach to checking whether a script
- * is usable on a device. We attempt to draw a character from the
- * script (e.g., ব). If the fonts on the device don't have the correct
- * glyph, Android typically renders whitespace (rather than .notdef).
- *
- * Pass in part of the name of the locale in its local representation,
- * and a whitespace character; this class performs the graphical comparison.
- *
- * See Bug 1023451 Comment 24 for extensive explanation.
- */
- private static class CharacterValidator {
- private static final int BITMAP_WIDTH = 32;
- private static final int BITMAP_HEIGHT = 48;
-
- private final Paint paint = new Paint();
- private final byte[] missingCharacter;
-
- public CharacterValidator(String missing) {
- this.missingCharacter = getPixels(drawBitmap(missing));
- }
-
- private Bitmap drawBitmap(String text) {
- Bitmap b = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ALPHA_8);
- Canvas c = new Canvas(b);
- c.drawText(text, 0, BITMAP_HEIGHT / 2, this.paint);
- return b;
- }
-
- private static byte[] getPixels(final Bitmap b) {
- final int byteCount;
- if (Versions.feature19Plus) {
- byteCount = b.getAllocationByteCount();
- } else {
- // Close enough for government work.
- // Equivalent to getByteCount, but works on <12.
- byteCount = b.getRowBytes() * b.getHeight();
- }
-
- final ByteBuffer buffer = ByteBuffer.allocate(byteCount);
- try {
- b.copyPixelsToBuffer(buffer);
- } catch (RuntimeException e) {
- // Android throws this if there's not enough space in the buffer.
- // This should never occur, but if it does, we don't
- // really care -- we probably don't need the entire image.
- // This is awful. I apologize.
- if ("Buffer not large enough for pixels".equals(e.getMessage())) {
- return buffer.array();
- }
- throw e;
- }
-
- return buffer.array();
- }
-
- public boolean characterIsMissingInFont(String ch) {
- byte[] rendered = getPixels(drawBitmap(ch));
- return Arrays.equals(rendered, missingCharacter);
- }
- }
-
- private volatile Locale entriesLocale;
- private final CharacterValidator characterValidator;
-
- public LocaleListPreference(Context context) {
- this(context, null);
- }
-
- public LocaleListPreference(Context context, AttributeSet attributes) {
- super(context, attributes);
-
- // Thus far, missing glyphs are replaced by whitespace, not a box
- // or other Unicode codepoint.
- this.characterValidator = new CharacterValidator(" ");
- buildList();
- }
-
- private static final class LocaleDescriptor implements Comparable<LocaleDescriptor> {
- // We use Locale.US here to ensure a stable ordering of entries.
- private static final Collator COLLATOR = Collator.getInstance(Locale.US);
-
- public final String tag;
- private final String nativeName;
-
- public LocaleDescriptor(String tag) {
- this(Locales.parseLocaleCode(tag), tag);
- }
-
- public LocaleDescriptor(Locale locale, String tag) {
- this.tag = tag;
-
- final String displayName = locale.getDisplayName(locale);
- if (TextUtils.isEmpty(displayName)) {
- // There's nothing sane we can do.
- Log.w(LOG_TAG, "Display name is empty. Using " + locale.toString());
- this.nativeName = locale.toString();
- return;
- }
-
- // For now, uppercase the first character of LTR locale names.
- // This is pretty much what Android does. This is a reasonable hack
- // for Bug 1014602, but it won't generalize to all locales.
- final byte directionality = Character.getDirectionality(displayName.charAt(0));
- if (directionality == Character.DIRECTIONALITY_LEFT_TO_RIGHT) {
- this.nativeName = displayName.substring(0, 1).toUpperCase(locale) +
- displayName.substring(1);
- return;
- }
-
- this.nativeName = displayName;
- }
-
- public String getTag() {
- return this.tag;
- }
-
- public String getDisplayName() {
- return this.nativeName;
- }
-
- @Override
- public String toString() {
- return this.nativeName;
- }
-
-
- @Override
- public int compareTo(LocaleDescriptor another) {
- // We sort by name, so we use Collator.
- return COLLATOR.compare(this.nativeName, another.nativeName);
- }
-
- /**
- * See Bug 1023451 Comment 10 for the research that led to
- * this method.
- *
- * @return true if this locale can be used for displaying UI
- * on this device without known issues.
- */
- public boolean isUsable(CharacterValidator validator) {
- if (Versions.preLollipop && this.tag.matches("[a-zA-Z]{3}.*")) {
- // Earlier versions of Android can't load three-char locale code
- // resources.
- return false;
- }
-
- // Oh, for Java 7 switch statements.
- if (this.tag.equals("bn-IN")) {
- // Bengali sometimes has an English label if the Bengali script
- // is missing. This prevents us from simply checking character
- // rendering for bn-IN; we'll get a false positive for "B", not "ব".
- //
- // This doesn't seem to affect other Bengali-script locales
- // (below), which always have a label in native script.
- if (!this.nativeName.startsWith("বাংলা")) {
- // We're on an Android version that doesn't even have
- // characters to say বাংলা. Definite failure.
- return false;
- }
- }
-
- // These locales use a script that is often unavailable
- // on common Android devices. Make sure we can show them.
- // See documentation for CharacterValidator.
- // Note that bn-IN is checked here even if it passed above.
- if (this.tag.equals("or") ||
- this.tag.equals("my") ||
- this.tag.equals("pa-IN") ||
- this.tag.equals("gu-IN") ||
- this.tag.equals("bn-IN")) {
- if (validator.characterIsMissingInFont(this.nativeName.substring(0, 1))) {
- return false;
- }
- }
-
- return true;
- }
- }
-
- /**
- * Not every locale we ship can be used on every device, due to
- * font or rendering constraints.
- *
- * This method filters down the list before generating the descriptor array.
- */
- private LocaleDescriptor[] getUsableLocales() {
- Collection<String> shippingLocales = BrowserLocaleManager.getPackagedLocaleTags(getContext());
-
- // Future: single-locale builds should be specified, too.
- if (shippingLocales == null) {
- final String fallbackTag = BrowserLocaleManager.getInstance().getFallbackLocaleTag();
- return new LocaleDescriptor[] { new LocaleDescriptor(fallbackTag) };
- }
-
- final int initialCount = shippingLocales.size();
- final Set<LocaleDescriptor> locales = new HashSet<LocaleDescriptor>(initialCount);
- for (String tag : shippingLocales) {
- final LocaleDescriptor descriptor = new LocaleDescriptor(tag);
-
- if (!descriptor.isUsable(this.characterValidator)) {
- Log.w(LOG_TAG, "Skipping locale " + tag + " on this device.");
- continue;
- }
-
- locales.add(descriptor);
- }
-
- final int usableCount = locales.size();
- final LocaleDescriptor[] descriptors = locales.toArray(new LocaleDescriptor[usableCount]);
- Arrays.sort(descriptors, 0, usableCount);
- return descriptors;
- }
-
- @Override
- protected void onDialogClosed(boolean positiveResult) {
- // The superclass will take care of persistence.
- super.onDialogClosed(positiveResult);
-
- // Use this hook to try to fix up the environment ASAP.
- // Do this so that the redisplayed fragment is inflated
- // with the right locale.
- final Locale selectedLocale = getSelectedLocale();
- final Context context = getContext();
- BrowserLocaleManager.getInstance().updateConfiguration(context, selectedLocale);
- }
-
- private Locale getSelectedLocale() {
- final String tag = getValue();
- if (tag == null || tag.equals("")) {
- return Locale.getDefault();
- }
- return Locales.parseLocaleCode(tag);
- }
-
- @Override
- public CharSequence getSummary() {
- final String value = getValue();
-
- if (TextUtils.isEmpty(value)) {
- return getContext().getString(R.string.locale_system_default);
- }
-
- // We can't trust super.getSummary() across locale changes,
- // apparently, so let's do the same work.
- return new LocaleDescriptor(value).getDisplayName();
- }
-
- private void buildList() {
- final Locale currentLocale = Locale.getDefault();
- Log.d(LOG_TAG, "Building locales list. Current locale: " + currentLocale);
-
- if (currentLocale.equals(this.entriesLocale) &&
- getEntries() != null) {
- Log.v(LOG_TAG, "No need to build list.");
- return;
- }
-
- final LocaleDescriptor[] descriptors = getUsableLocales();
- final int count = descriptors.length;
-
- this.entriesLocale = currentLocale;
-
- // We leave room for "System default".
- final String[] entries = new String[count + 1];
- final String[] values = new String[count + 1];
-
- entries[0] = getContext().getString(R.string.locale_system_default);
- values[0] = "";
-
- for (int i = 0; i < count; ++i) {
- final String displayName = descriptors[i].getDisplayName();
- final String tag = descriptors[i].getTag();
- Log.v(LOG_TAG, displayName + " => " + tag);
- entries[i + 1] = displayName;
- values[i + 1] = tag;
- }
-
- setEntries(entries);
- setEntryValues(values);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/ModifiableHintPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/ModifiableHintPreference.java
deleted file mode 100644
index c545472e2..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/ModifiableHintPreference.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.preference.Preference;
-import android.text.Spanned;
-import android.text.SpannableStringBuilder;
-import android.text.style.ImageSpan;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-class ModifiableHintPreference extends Preference {
- private static final String LOGTAG = "ModifiableHintPref";
- private final Context mContext;
-
- private final String MATCH_STRING = "%I";
- private final int RESID_TEXT_VIEW = R.id.label_search_hint;
- private final int RESID_DRAWABLE = R.drawable.ab_add_search_engine;
- private final double SCALE_FACTOR = 0.5;
-
- public ModifiableHintPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- mContext = context;
- }
-
- public ModifiableHintPreference(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- mContext = context;
- }
-
- @Override
- protected View onCreateView(ViewGroup parent) {
- View thisView = super.onCreateView(parent);
- configurePreferenceView(thisView);
- return thisView;
- }
-
- private void configurePreferenceView(View view) {
- TextView textView = (TextView) view.findViewById(RESID_TEXT_VIEW);
- String searchHint = textView.getText().toString();
-
- // Use an ImageSpan to include the "add search" icon in the Tip.
- int imageSpanIndex = searchHint.indexOf(MATCH_STRING);
- if (imageSpanIndex != -1) {
- // Scale the resource.
- Drawable drawable = mContext.getResources().getDrawable(RESID_DRAWABLE);
- drawable.setBounds(0, 0, (int) (drawable.getIntrinsicWidth() * SCALE_FACTOR),
- (int) (drawable.getIntrinsicHeight() * SCALE_FACTOR));
-
- ImageSpan searchIcon = new ImageSpan(drawable);
- final SpannableStringBuilder hintBuilder = new SpannableStringBuilder(searchHint);
-
- // Insert the image.
- hintBuilder.setSpan(searchIcon, imageSpanIndex, imageSpanIndex + MATCH_STRING.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
- textView.setText(hintBuilder, TextView.BufferType.SPANNABLE);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/MultiChoicePreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/MultiChoicePreference.java
deleted file mode 100644
index 5749bf29d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/MultiChoicePreference.java
+++ /dev/null
@@ -1,271 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.util.PrefUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.app.AlertDialog.Builder;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.res.TypedArray;
-import android.content.SharedPreferences;
-import android.preference.DialogPreference;
-import android.util.AttributeSet;
-
-import java.util.HashSet;
-import java.util.Set;
-
-class MultiChoicePreference extends DialogPreference implements DialogInterface.OnMultiChoiceClickListener {
- private static final String LOGTAG = "GeckoMultiChoicePreference";
-
- private boolean mValues[];
- private boolean mPrevValues[];
- private CharSequence mEntryValues[];
- private CharSequence mEntries[];
- private CharSequence mInitialValues[];
-
- public MultiChoicePreference(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MultiChoicePreference);
- mEntries = a.getTextArray(R.styleable.MultiChoicePreference_entries);
- mEntryValues = a.getTextArray(R.styleable.MultiChoicePreference_entryValues);
- mInitialValues = a.getTextArray(R.styleable.MultiChoicePreference_initialValues);
- a.recycle();
-
- loadPersistedValues();
- }
-
- public MultiChoicePreference(Context context) {
- this(context, null);
- }
-
- /**
- * Sets the human-readable entries to be shown in the list. This will be
- * shown in subsequent dialogs.
- * <p>
- * Each entry must have a corresponding index in
- * {@link #setEntryValues(CharSequence[])} and
- * {@link #setInitialValues(CharSequence[])}.
- *
- * @param entries The entries.
- */
- public void setEntries(CharSequence[] entries) {
- mEntries = entries.clone();
- }
-
- /**
- * @param entriesResId The entries array as a resource.
- */
- public void setEntries(int entriesResId) {
- setEntries(getContext().getResources().getTextArray(entriesResId));
- }
-
- /**
- * Sets the preference values for preferences shown in the list.
- *
- * @param entryValues The entry values.
- */
- public void setEntryValues(CharSequence[] entryValues) {
- mEntryValues = entryValues.clone();
- loadPersistedValues();
- }
-
- /**
- * Entry values define a separate pref for each row in the dialog.
- *
- * @param entryValuesResId The entryValues array as a resource.
- */
- public void setEntryValues(int entryValuesResId) {
- setEntryValues(getContext().getResources().getTextArray(entryValuesResId));
- }
-
- /**
- * The array of initial entry values in this list. Each entryValue
- * corresponds to an entryKey. These values are used if a) the preference
- * isn't persisted, or b) the preference is persisted but hasn't yet been
- * set.
- *
- * @param initialValues The entry values
- */
- public void setInitialValues(CharSequence[] initialValues) {
- mInitialValues = initialValues.clone();
- loadPersistedValues();
- }
-
- /**
- * @param initialValuesResId The initialValues array as a resource.
- */
- public void setInitialValues(int initialValuesResId) {
- setInitialValues(getContext().getResources().getTextArray(initialValuesResId));
- }
-
- /**
- * The list of translated strings corresponding to each preference.
- *
- * @return The array of entries.
- */
- public CharSequence[] getEntries() {
- return mEntries.clone();
- }
-
- /**
- * The list of values corresponding to each preference.
- *
- * @return The array of values.
- */
- public CharSequence[] getEntryValues() {
- return mEntryValues.clone();
- }
-
- /**
- * The list of initial values for each preference. Each string in this list
- * should be either "true" or "false".
- *
- * @return The array of initial values.
- */
- public CharSequence[] getInitialValues() {
- return mInitialValues.clone();
- }
-
- public void setValue(final int i, final boolean value) {
- mValues[i] = value;
- mPrevValues = mValues.clone();
- }
-
- /**
- * The list of values for each preference. These values are updated after
- * the dialog has been displayed.
- *
- * @return The array of values.
- */
- public Set<String> getValues() {
- final Set<String> values = new HashSet<String>();
-
- if (mValues == null) {
- return values;
- }
-
- for (int i = 0; i < mValues.length; i++) {
- if (mValues[i]) {
- values.add(mEntryValues[i].toString());
- }
- }
-
- return values;
- }
-
- @Override
- public void onClick(DialogInterface dialog, int which, boolean val) {
- }
-
- @Override
- protected void onPrepareDialogBuilder(Builder builder) {
- if (mEntries == null || mInitialValues == null || mEntryValues == null) {
- throw new IllegalStateException(
- "MultiChoicePreference requires entries, entryValues, and initialValues arrays.");
- }
-
- if (mEntries.length != mEntryValues.length || mEntries.length != mInitialValues.length) {
- throw new IllegalStateException(
- "MultiChoicePreference entries, entryValues, and initialValues arrays must be the same length");
- }
-
- builder.setMultiChoiceItems(mEntries, mValues, this);
- }
-
- @Override
- protected void onDialogClosed(boolean positiveResult) {
- if (mPrevValues == null || mInitialValues == null) {
- // Initialization is done asynchronously, so these values may not
- // have been set before the dialog was closed.
- return;
- }
-
- if (!positiveResult) {
- // user cancelled; reset checkbox values to their previous state
- mValues = mPrevValues.clone();
- return;
- }
-
- mPrevValues = mValues.clone();
-
- if (!callChangeListener(getValues())) {
- return;
- }
-
- persist();
- }
-
- /* Persists the current data stored by this pref to SharedPreferences. */
- public boolean persist() {
- if (isPersistent()) {
- final SharedPreferences.Editor edit = GeckoSharedPrefs.forProfile(getContext()).edit();
- final boolean res = persist(edit);
- edit.apply();
- return res;
- }
-
- return false;
- }
-
- /* Internal persist method. Take an edit so that multiple prefs can be persisted in a single commit. */
- protected boolean persist(SharedPreferences.Editor edit) {
- if (isPersistent()) {
- Set<String> vals = getValues();
- PrefUtils.putStringSet(edit, getKey(), vals).apply();;
- return true;
- }
-
- return false;
- }
-
- /* Returns a list of EntryValues that are currently enabled. */
- public Set<String> getPersistedStrings(Set<String> defaultVal) {
- if (!isPersistent()) {
- return defaultVal;
- }
-
- final SharedPreferences prefs = GeckoSharedPrefs.forProfile(getContext());
- return PrefUtils.getStringSet(prefs, getKey(), defaultVal);
- }
-
- /**
- * Loads persistent prefs from shared preferences. If the preferences
- * aren't persistent or haven't yet been stored, they will be set to their
- * initial values.
- */
- protected void loadPersistedValues() {
- final int entryCount = mInitialValues.length;
- mValues = new boolean[entryCount];
-
- if (entryCount != mEntries.length || entryCount != mEntryValues.length) {
- throw new IllegalStateException(
- "MultiChoicePreference entryValues and initialValues arrays must be the same length");
- }
-
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- final Set<String> stringVals = getPersistedStrings(null);
-
- for (int i = 0; i < entryCount; i++) {
- if (stringVals != null) {
- mValues[i] = stringVals.contains(mEntryValues[i]);
- } else {
- final boolean defaultVal = mInitialValues[i].equals("true");
- mValues[i] = defaultVal;
- }
- }
-
- mPrevValues = mValues.clone();
- }
- });
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/MultiPrefMultiChoicePreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/MultiPrefMultiChoicePreference.java
deleted file mode 100644
index 580d613ca..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/MultiPrefMultiChoicePreference.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.res.TypedArray;
-import android.content.SharedPreferences;
-import android.widget.Button;
-import android.util.AttributeSet;
-import android.util.Log;
-
-import java.util.Set;
-
-/* Provides backwards compatibility for some old multi-choice pref types used by Gecko.
- * This will import the old data from the old prefs the first time it is run.
- */
-class MultiPrefMultiChoicePreference extends MultiChoicePreference {
- private static final String LOGTAG = "GeckoMultiPrefPreference";
- private static final String IMPORT_SUFFIX = "_imported_";
- private final CharSequence[] keys;
-
- public MultiPrefMultiChoicePreference(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MultiPrefMultiChoicePreference);
- keys = a.getTextArray(R.styleable.MultiPrefMultiChoicePreference_entryKeys);
- a.recycle();
-
- loadPersistedValues();
- }
-
- // Helper method for reading a boolean pref.
- private boolean getPersistedBoolean(SharedPreferences prefs, String key, boolean defaultReturnValue) {
- if (!isPersistent()) {
- return defaultReturnValue;
- }
-
- return prefs.getBoolean(key, defaultReturnValue);
- }
-
- // Overridden to do a one time import for the old preference type to the new one.
- @Override
- protected synchronized void loadPersistedValues() {
- // This will load the new pref if it exists.
- super.loadPersistedValues();
-
- // First check if we've already done the import the old data. If so, nothing to load.
- final SharedPreferences prefs = GeckoSharedPrefs.forApp(getContext());
- final boolean imported = getPersistedBoolean(prefs, getKey() + IMPORT_SUFFIX, false);
- if (imported) {
- return;
- }
-
- // Load the data we'll need to find the old style prefs
- final CharSequence[] init = getInitialValues();
- final CharSequence[] entries = getEntries();
- if (keys == null || init == null) {
- return;
- }
-
- final int entryCount = keys.length;
- if (entryCount != entries.length || entryCount != init.length) {
- throw new IllegalStateException("MultiChoicePreference entryKeys and initialValues arrays must be the same length");
- }
-
- // Now iterate through the entries on a background thread.
- final SharedPreferences.Editor edit = prefs.edit();
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- try {
- // Use one editor to batch as many changes as we can.
- for (int i = 0; i < entryCount; i++) {
- String key = keys[i].toString();
- boolean initialValue = "true".equals(init[i]);
- boolean val = getPersistedBoolean(prefs, key, initialValue);
-
- // Save the pref and remove the old preference.
- setValue(i, val);
- edit.remove(key);
- }
-
- persist(edit);
- edit.putBoolean(getKey() + IMPORT_SUFFIX, true);
- edit.apply();
- } catch (Exception ex) {
- Log.i(LOGTAG, "Err", ex);
- }
- }
- });
- }
-
-
- @Override
- public void onClick(DialogInterface dialog, int which, boolean val) {
- // enable positive button only if at least one item is checked
- boolean enabled = false;
- final Set<String> values = getValues();
-
- enabled = (values.size() > 0);
- final Button button = ((AlertDialog) dialog).getButton(DialogInterface.BUTTON_POSITIVE);
- if (button.isEnabled() != enabled) {
- button.setEnabled(enabled);
- }
- }
-
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/PanelsPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/PanelsPreference.java
deleted file mode 100644
index 337d9dd2f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/PanelsPreference.java
+++ /dev/null
@@ -1,255 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import org.mozilla.gecko.animation.PropertyAnimator;
-import org.mozilla.gecko.animation.PropertyAnimator.Property;
-import org.mozilla.gecko.animation.ViewHelper;
-import org.mozilla.gecko.R;
-
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnClickListener;
-import android.content.DialogInterface.OnShowListener;
-import android.content.res.Resources;
-import android.util.Log;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-public class PanelsPreference extends CustomListPreference {
- protected String LOGTAG = "PanelsPreference";
-
- // Position state of this Preference in enclosing category.
- private static final int STATE_IS_FIRST = 0;
- private static final int STATE_IS_LAST = 1;
-
- /**
- * Index of the context menu button for controlling display options.
- * For (removable) Dynamic panels, this button removes the panel.
- * For built-in panels, this button toggles showing or hiding the panel.
- */
- private static final int INDEX_DISPLAY_BUTTON = 1;
- private static final int INDEX_REORDER_BUTTON = 2;
-
- // Indices of buttons in context menu for reordering.
- private static final int INDEX_MOVE_UP_BUTTON = 0;
- private static final int INDEX_MOVE_DOWN_BUTTON = 1;
-
- private String LABEL_HIDE;
- private String LABEL_SHOW;
-
- private View preferenceView;
- protected boolean mIsHidden;
- private final boolean mIsRemovable;
-
- private boolean mAnimate;
- private static final int ANIMATION_DURATION_MS = 400;
-
- // State for reordering.
- private int mPositionState = -1;
- private final int mIndex;
-
- public PanelsPreference(Context context, CustomListCategory parentCategory, boolean isRemovable, int index, boolean animate) {
- super(context, parentCategory);
- mIsRemovable = isRemovable;
- mIndex = index;
- mAnimate = animate;
- }
-
- @Override
- protected int getPreferenceLayoutResource() {
- return R.layout.preference_panels;
- }
-
- @Override
- protected void onBindView(View view) {
- super.onBindView(view);
-
- // Override view handling so we can grey out "hidden" PanelPreferences.
- view.setEnabled(!mIsHidden);
-
- if (view instanceof ViewGroup) {
- final ViewGroup group = (ViewGroup) view;
- for (int i = 0; i < group.getChildCount(); i++) {
- group.getChildAt(i).setEnabled(!mIsHidden);
- }
- preferenceView = group;
- }
-
- if (mAnimate) {
- ViewHelper.setAlpha(preferenceView, 0);
-
- final PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION_MS);
- animator.attach(preferenceView, Property.ALPHA, 1);
- animator.start();
-
- // Clear animate flag.
- mAnimate = false;
- }
- }
-
- @Override
- protected String[] createDialogItems() {
- final Resources res = getContext().getResources();
- final String labelReorder = res.getString(R.string.pref_panels_reorder);
-
- if (mIsRemovable) {
- return new String[] { LABEL_SET_AS_DEFAULT, LABEL_REMOVE, labelReorder };
- }
-
- // Built-in panels can't be removed, so use show/hide options.
- LABEL_HIDE = res.getString(R.string.pref_panels_hide);
- LABEL_SHOW = res.getString(R.string.pref_panels_show);
-
- return new String[] { LABEL_SET_AS_DEFAULT, LABEL_HIDE, labelReorder };
- }
-
- @Override
- public void setIsDefault(boolean isDefault) {
- mIsDefault = isDefault;
- if (isDefault) {
- setSummary(LABEL_IS_DEFAULT);
- if (mIsHidden) {
- // Unhide the panel if it's being set as the default.
- setHidden(false);
- }
- } else {
- setSummary("");
- }
- }
-
- @Override
- protected void onDialogIndexClicked(int index) {
- switch (index) {
- case INDEX_SET_DEFAULT_BUTTON:
- mParentCategory.setDefault(this);
- break;
-
- case INDEX_DISPLAY_BUTTON:
- // Handle display options for the panel.
- if (mIsRemovable) {
- // For removable panels, the button displays text for removing the panel.
- mParentCategory.uninstall(this);
- } else {
- // Otherwise, the button toggles between text for showing or hiding the panel.
- ((PanelsPreferenceCategory) mParentCategory).setHidden(this, !mIsHidden);
- }
- break;
-
- case INDEX_REORDER_BUTTON:
- // Display dialog for changing preference order.
- final Dialog orderDialog = makeReorderDialog();
- orderDialog.show();
- break;
-
- default:
- Log.w(LOGTAG, "Selected index out of range: " + index);
- }
- }
-
- @Override
- protected void configureShownDialog() {
- super.configureShownDialog();
-
- // Handle Show/Hide buttons.
- if (!mIsRemovable) {
- final TextView hideButton = (TextView) mDialog.getListView().getChildAt(INDEX_DISPLAY_BUTTON);
- hideButton.setText(mIsHidden ? LABEL_SHOW : LABEL_HIDE);
- }
- }
-
-
- private Dialog makeReorderDialog() {
- final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
-
- final Resources res = getContext().getResources();
- final String labelUp = res.getString(R.string.pref_panels_move_up);
- final String labelDown = res.getString(R.string.pref_panels_move_down);
-
- builder.setTitle(getTitle());
- builder.setItems(new String[] { labelUp, labelDown }, new OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int index) {
- dialog.dismiss();
- switch (index) {
- case INDEX_MOVE_UP_BUTTON:
- ((PanelsPreferenceCategory) mParentCategory).moveUp(PanelsPreference.this);
- break;
-
- case INDEX_MOVE_DOWN_BUTTON:
- ((PanelsPreferenceCategory) mParentCategory).moveDown(PanelsPreference.this);
- break;
- }
- }
- });
-
- final Dialog dialog = builder.create();
- dialog.setOnShowListener(new OnShowListener() {
- @Override
- public void onShow(DialogInterface dialog) {
- setReorderItemsEnabled(dialog);
- }
- });
-
- return dialog;
- }
-
- public void setIsFirst() {
- mPositionState = STATE_IS_FIRST;
- }
-
- public void setIsLast() {
- mPositionState = STATE_IS_LAST;
- }
-
- /**
- * Configure enabled state of the reorder dialog, which must be done after the dialog is shown.
- * @param dialog Dialog to configure
- */
- private void setReorderItemsEnabled(DialogInterface dialog) {
- // Update button enabled-ness for reordering.
- switch (mPositionState) {
- case STATE_IS_FIRST:
- final TextView itemUp = (TextView) ((AlertDialog) dialog).getListView().getChildAt(INDEX_MOVE_UP_BUTTON);
- itemUp.setEnabled(false);
- // Disable clicks to this view.
- itemUp.setOnClickListener(null);
- break;
-
- case STATE_IS_LAST:
- final TextView itemDown = (TextView) ((AlertDialog) dialog).getListView().getChildAt(INDEX_MOVE_DOWN_BUTTON);
- itemDown.setEnabled(false);
- // Disable clicks to this view.
- itemDown.setOnClickListener(null);
- break;
-
- default:
- // Do nothing.
- break;
- }
- }
-
- public void setHidden(boolean toHide) {
- if (toHide) {
- setIsDefault(false);
- }
-
- if (mIsHidden != toHide) {
- mIsHidden = toHide;
- notifyChanged();
- }
- }
-
- public boolean isHidden() {
- return mIsHidden;
- }
-
- public int getIndex() {
- return mIndex;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/PanelsPreferenceCategory.java b/mobile/android/base/java/org/mozilla/gecko/preferences/PanelsPreferenceCategory.java
deleted file mode 100644
index d44b6eaa9..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/PanelsPreferenceCategory.java
+++ /dev/null
@@ -1,261 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.TelemetryContract.Method;
-import org.mozilla.gecko.home.HomeConfig;
-import org.mozilla.gecko.home.HomeConfig.PanelConfig;
-import org.mozilla.gecko.home.HomeConfig.State;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.UIAsyncTask;
-
-import android.content.Context;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-
-public class PanelsPreferenceCategory extends CustomListCategory {
- public static final String LOGTAG = "PanelsPrefCategory";
-
- protected HomeConfig mHomeConfig;
- protected HomeConfig.Editor mConfigEditor;
-
- protected UIAsyncTask.WithoutParams<State> mLoadTask;
-
- public PanelsPreferenceCategory(Context context) {
- super(context);
- initConfig(context);
- }
-
- public PanelsPreferenceCategory(Context context, AttributeSet attrs) {
- super(context, attrs);
- initConfig(context);
- }
-
- public PanelsPreferenceCategory(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- initConfig(context);
- }
-
- protected void initConfig(Context context) {
- mHomeConfig = HomeConfig.getDefault(context);
- }
-
- @Override
- public void onAttachedToActivity() {
- super.onAttachedToActivity();
-
- loadHomeConfig(null);
- }
-
- /**
- * Load the Home Panels config and populate the preferences screen and maintain local state.
- */
- private void loadHomeConfig(final String animatePanelId) {
- mLoadTask = new UIAsyncTask.WithoutParams<State>(ThreadUtils.getBackgroundHandler()) {
- @Override
- public HomeConfig.State doInBackground() {
- return mHomeConfig.load();
- }
-
- @Override
- public void onPostExecute(HomeConfig.State configState) {
- mConfigEditor = configState.edit();
- displayHomeConfig(configState, animatePanelId);
- }
- };
- mLoadTask.execute();
- }
-
- /**
- * Simplified refresh of Home Panels when there is no state to be persisted.
- */
- public void refresh() {
- refresh(null, null);
- }
-
- /**
- * Refresh the Home Panels list and animate a panel, if specified.
- * If null, load from HomeConfig.
- *
- * @param State HomeConfig.State to rebuild Home Panels list from.
- * @param String panelId of panel to be animated.
- */
- public void refresh(State state, String animatePanelId) {
- // Clear all the existing home panels.
- removeAll();
-
- if (state == null) {
- loadHomeConfig(animatePanelId);
- } else {
- displayHomeConfig(state, animatePanelId);
- }
- }
-
- private void displayHomeConfig(HomeConfig.State configState, String animatePanelId) {
- int index = 0;
- for (PanelConfig panelConfig : configState) {
- final boolean isRemovable = panelConfig.isDynamic();
-
- // Create and add the pref.
- final String panelId = panelConfig.getId();
- final boolean animate = TextUtils.equals(animatePanelId, panelId);
-
- final PanelsPreference pref = new PanelsPreference(getContext(), PanelsPreferenceCategory.this, isRemovable, index, animate);
- pref.setTitle(panelConfig.getTitle());
- pref.setKey(panelConfig.getId());
- // XXX: Pull icon from PanelInfo.
- addPreference(pref);
-
- if (panelConfig.isDisabled()) {
- pref.setHidden(true);
- }
-
- index++;
- }
-
- setPositionState();
- setDefaultFromConfig();
- }
-
- private void setPositionState() {
- final int prefCount = getPreferenceCount();
-
- // Pass in position state to first and last preference.
- final PanelsPreference firstPref = (PanelsPreference) getPreference(0);
- firstPref.setIsFirst();
-
- final PanelsPreference lastPref = (PanelsPreference) getPreference(prefCount - 1);
- lastPref.setIsLast();
- }
-
- private void setDefaultFromConfig() {
- final String defaultPanelId = mConfigEditor.getDefaultPanelId();
- if (defaultPanelId == null) {
- mDefaultReference = null;
- return;
- }
-
- final int prefCount = getPreferenceCount();
-
- for (int i = 0; i < prefCount; i++) {
- final PanelsPreference pref = (PanelsPreference) getPreference(i);
-
- if (defaultPanelId.equals(pref.getKey())) {
- super.setDefault(pref);
- break;
- }
- }
- }
-
- @Override
- public void setDefault(CustomListPreference pref) {
- super.setDefault(pref);
-
- final String id = pref.getKey();
-
- final String defaultPanelId = mConfigEditor.getDefaultPanelId();
- if (defaultPanelId != null && defaultPanelId.equals(id)) {
- return;
- }
-
- updateVisibilityPrefsForPanel(id, true);
-
- mConfigEditor.setDefault(id);
- mConfigEditor.apply();
-
- Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_SET_DEFAULT, Method.DIALOG, id);
- }
-
- @Override
- protected void onPrepareForRemoval() {
- super.onPrepareForRemoval();
- if (mLoadTask != null) {
- mLoadTask.cancel();
- }
- }
-
- @Override
- public void uninstall(CustomListPreference pref) {
- final String id = pref.getKey();
- mConfigEditor.uninstall(id);
- mConfigEditor.apply();
-
- Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_REMOVE, Method.DIALOG, id);
-
- super.uninstall(pref);
- }
-
- public void moveUp(PanelsPreference pref) {
- final int panelIndex = pref.getIndex();
- if (panelIndex > 0) {
- final String panelKey = pref.getKey();
- mConfigEditor.moveTo(panelKey, panelIndex - 1);
- final State state = mConfigEditor.apply();
-
- Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_MOVE, Method.DIALOG, panelKey);
-
- refresh(state, panelKey);
- }
- }
-
- public void moveDown(PanelsPreference pref) {
- final int panelIndex = pref.getIndex();
- if (panelIndex < getPreferenceCount() - 1) {
- final String panelKey = pref.getKey();
- mConfigEditor.moveTo(panelKey, panelIndex + 1);
- final State state = mConfigEditor.apply();
-
- Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_MOVE, Method.DIALOG, panelKey);
-
- refresh(state, panelKey);
- }
- }
-
- /**
- * Update the hide/show state of the preference and save the HomeConfig
- * changes.
- *
- * @param pref Preference to update
- * @param toHide New hidden state of the preference
- */
- protected void setHidden(PanelsPreference pref, boolean toHide) {
- final String id = pref.getKey();
- mConfigEditor.setDisabled(id, toHide);
- mConfigEditor.apply();
-
- if (toHide) {
- Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_HIDE, Method.DIALOG, id);
- } else {
- Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_SHOW, Method.DIALOG, id);
- }
-
- updateVisibilityPrefsForPanel(id, !toHide);
-
- pref.setHidden(toHide);
- setDefaultFromConfig();
- }
-
- /**
- * When the default panel is removed or disabled, find an enabled panel
- * if possible and set it as mDefaultReference.
- */
- @Override
- protected void setFallbackDefault() {
- setDefaultFromConfig();
- }
-
- private void updateVisibilityPrefsForPanel(String panelId, boolean toShow) {
- if (HomeConfig.getIdForBuiltinPanelType(HomeConfig.PanelType.BOOKMARKS).equals(panelId)) {
- GeckoSharedPrefs.forProfile(getContext()).edit().putBoolean(HomeConfig.PREF_KEY_BOOKMARKS_PANEL_ENABLED, toShow).apply();
- }
-
- if (HomeConfig.getIdForBuiltinPanelType(HomeConfig.PanelType.COMBINED_HISTORY).equals(panelId)) {
- GeckoSharedPrefs.forProfile(getContext()).edit().putBoolean(HomeConfig.PREF_KEY_HISTORY_PANEL_ENABLED, toShow).apply();
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/PrivateDataPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/PrivateDataPreference.java
deleted file mode 100644
index 61eff98e7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/PrivateDataPreference.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.icons.storage.DiskStorage;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Set;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.util.Log;
-
-class PrivateDataPreference extends MultiPrefMultiChoicePreference {
- private static final String LOGTAG = "GeckoPrivateDataPreference";
- private static final String PREF_KEY_PREFIX = "private.data.";
-
- public PrivateDataPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- protected void onDialogClosed(boolean positiveResult) {
- super.onDialogClosed(positiveResult);
-
- if (!positiveResult) {
- return;
- }
-
- Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.DIALOG, "settings");
-
- final Set<String> values = getValues();
- final JSONObject json = new JSONObject();
-
- for (String value : values) {
- // Privacy pref checkbox values are stored in Android prefs to
- // remember their check states. The key names are private.data.X,
- // where X is a string from Gecko sanitization. This prefix is
- // removed here so we can send the values to Gecko, which then does
- // the sanitization for each key.
- final String key = value.substring(PREF_KEY_PREFIX.length());
- try {
- json.put(key, true);
- } catch (JSONException e) {
- Log.e(LOGTAG, "JSON error", e);
- }
- }
-
- if (values.contains("private.data.offlineApps")) {
- // Remove all icons from storage if removing "Offline website data" was selected.
- DiskStorage.get(getContext()).evictAll();
- }
-
- // clear private data in gecko
- GeckoAppShell.notifyObservers("Sanitize:ClearData", json.toString());
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/SearchEnginePreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/SearchEnginePreference.java
deleted file mode 100644
index 3ba80b562..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/SearchEnginePreference.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.SnackbarBuilder;
-import org.mozilla.gecko.icons.IconCallback;
-import org.mozilla.gecko.icons.IconDescriptor;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.icons.Icons;
-import org.mozilla.gecko.widget.FaviconView;
-
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.support.design.widget.Snackbar;
-import android.text.SpannableString;
-import android.util.Log;
-import android.view.View;
-
-/**
- * Represents an element in the list of search engines on the preferences menu.
- */
-public class SearchEnginePreference extends CustomListPreference {
- protected String LOGTAG = "SearchEnginePreference";
-
- protected static final int INDEX_REMOVE_BUTTON = 1;
-
- // The icon to display in the prompt when clicked.
- private BitmapDrawable mPromptIcon;
-
- // The bitmap backing the drawable above - needed separately for the FaviconView.
- private Bitmap mIconBitmap;
- private final Object bitmapLock = new Object();
-
- private FaviconView mFaviconView;
-
- // Search engine identifier specified by the gecko search service. This will be "other"
- // for engines that are not shipped with the app.
- private String mIdentifier;
-
- public SearchEnginePreference(Context context, SearchPreferenceCategory parentCategory) {
- super(context, parentCategory);
- }
-
- /**
- * Called by Android when we're bound to the custom view. Allows us to set the custom properties
- * of our custom view elements as we desire (We can now use findViewById on them).
- *
- * @param view The view instance for this Preference object.
- */
- @Override
- protected void onBindView(View view) {
- super.onBindView(view);
-
- // We synchronise to avoid a race condition between this and the favicon loading callback in
- // setSearchEngineFromJSON.
- synchronized (bitmapLock) {
- // Set the icon in the FaviconView.
- mFaviconView = ((FaviconView) view.findViewById(R.id.search_engine_icon));
-
- if (mIconBitmap != null) {
- mFaviconView.updateAndScaleImage(IconResponse.create(mIconBitmap));
- }
- }
- }
-
- @Override
- protected int getPreferenceLayoutResource() {
- return R.layout.preference_search_engine;
- }
-
- /**
- * Returns the strings to be displayed in the dialog.
- */
- @Override
- protected String[] createDialogItems() {
- return new String[] { LABEL_SET_AS_DEFAULT,
- LABEL_REMOVE };
- }
-
- @Override
- public void showDialog() {
- // If this is the last engine, then we are the default, and none of the options
- // on this menu can do anything.
- if (mParentCategory.getPreferenceCount() == 1) {
- Activity activity = (Activity) getContext();
-
- SnackbarBuilder.builder(activity)
- .message(R.string.pref_search_last_toast)
- .duration(Snackbar.LENGTH_LONG)
- .buildAndShow();
-
- return;
- }
-
- super.showDialog();
- }
-
- @Override
- protected void configureDialogBuilder(AlertDialog.Builder builder) {
- // Copy the icon from this object to the prompt we produce. We lazily create the drawable,
- // as the user may not ever actually tap this object.
- if (mPromptIcon == null && mIconBitmap != null) {
- mPromptIcon = new BitmapDrawable(getContext().getResources(), mFaviconView.getBitmap());
- }
-
- builder.setIcon(mPromptIcon);
- }
-
- @Override
- protected void onDialogIndexClicked(int index) {
- switch (index) {
- case INDEX_SET_DEFAULT_BUTTON:
- mParentCategory.setDefault(this);
- break;
-
- case INDEX_REMOVE_BUTTON:
- mParentCategory.uninstall(this);
- break;
-
- default:
- Log.w(LOGTAG, "Selected index out of range.");
- break;
- }
- }
-
- /**
- * @return Identifier of built-in search engine, or "other" if engine is not built-in.
- */
- public String getIdentifier() {
- return mIdentifier;
- }
-
- /**
- * Configure this Preference object from the Gecko search engine JSON object.
- * @param geckoEngineJSON The Gecko-formatted JSON object representing the search engine.
- * @throws JSONException If the JSONObject is invalid.
- */
- public void setSearchEngineFromJSON(JSONObject geckoEngineJSON) throws JSONException {
- mIdentifier = geckoEngineJSON.getString("identifier");
-
- // A null JS value gets converted into a string.
- if (mIdentifier.equals("null")) {
- mIdentifier = "other";
- }
-
- final String engineName = geckoEngineJSON.getString("name");
- final SpannableString titleSpannable = new SpannableString(engineName);
-
- setTitle(titleSpannable);
-
- final String iconURI = geckoEngineJSON.getString("iconURI");
- // Keep a reference to the bitmap - we'll need it later in onBindView.
- try {
- Icons.with(getContext())
- .pageUrl(mIdentifier)
- .icon(IconDescriptor.createGenericIcon(iconURI))
- .privileged(true)
- .build()
- .execute(new IconCallback() {
- @Override
- public void onIconResponse(IconResponse response) {
- mIconBitmap = response.getBitmap();
-
- if (mFaviconView != null) {
- mFaviconView.updateAndScaleImage(response);
- }
- }
- });
- } catch (IllegalArgumentException e) {
- Log.e(LOGTAG, "IllegalArgumentException creating Bitmap. Most likely a zero-length bitmap.", e);
- } catch (NullPointerException e) {
- Log.e(LOGTAG, "NullPointerException creating Bitmap. Most likely a zero-length bitmap.", e);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/SearchPreferenceCategory.java b/mobile/android/base/java/org/mozilla/gecko/preferences/SearchPreferenceCategory.java
deleted file mode 100644
index 47db8b9b0..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/SearchPreferenceCategory.java
+++ /dev/null
@@ -1,145 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import android.content.Context;
-import android.preference.Preference;
-import android.util.AttributeSet;
-import android.util.Log;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.TelemetryContract.Method;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.ThreadUtils;
-
-public class SearchPreferenceCategory extends CustomListCategory implements GeckoEventListener {
- public static final String LOGTAG = "SearchPrefCategory";
-
- public SearchPreferenceCategory(Context context) {
- super(context);
- }
-
- public SearchPreferenceCategory(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public SearchPreferenceCategory(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-
- @Override
- protected void onAttachedToActivity() {
- super.onAttachedToActivity();
-
- // Register for SearchEngines messages and request list of search engines from Gecko.
- GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, "SearchEngines:Data");
- GeckoAppShell.notifyObservers("SearchEngines:GetVisible", null);
- }
-
- @Override
- protected void onPrepareForRemoval() {
- super.onPrepareForRemoval();
-
- GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, "SearchEngines:Data");
- }
-
- @Override
- public void setDefault(CustomListPreference item) {
- super.setDefault(item);
-
- sendGeckoEngineEvent("SearchEngines:SetDefault", item.getTitle().toString());
-
- final String identifier = ((SearchEnginePreference) item).getIdentifier();
- Telemetry.sendUIEvent(TelemetryContract.Event.SEARCH_SET_DEFAULT, Method.DIALOG, identifier);
- }
-
- @Override
- public void uninstall(CustomListPreference item) {
- super.uninstall(item);
-
- sendGeckoEngineEvent("SearchEngines:Remove", item.getTitle().toString());
-
- final String identifier = ((SearchEnginePreference) item).getIdentifier();
- Telemetry.sendUIEvent(TelemetryContract.Event.SEARCH_REMOVE, Method.DIALOG, identifier);
- }
-
- @Override
- public void handleMessage(String event, final JSONObject data) {
- if (event.equals("SearchEngines:Data")) {
- // Parse engines array from JSON.
- JSONArray engines;
- try {
- engines = data.getJSONArray("searchEngines");
- } catch (JSONException e) {
- Log.e(LOGTAG, "Unable to decode search engine data from Gecko.", e);
- return;
- }
-
- // Clear the preferences category from this thread.
- this.removeAll();
-
- // Create an element in this PreferenceCategory for each engine.
- for (int i = 0; i < engines.length(); i++) {
- try {
- final JSONObject engineJSON = engines.getJSONObject(i);
-
- final SearchEnginePreference enginePreference = new SearchEnginePreference(getContext(), this);
- enginePreference.setSearchEngineFromJSON(engineJSON);
- enginePreference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
- @Override
- public boolean onPreferenceClick(Preference preference) {
- SearchEnginePreference sPref = (SearchEnginePreference) preference;
- // Display the configuration dialog associated with the tapped engine.
- sPref.showDialog();
- return true;
- }
- });
-
- addPreference(enginePreference);
-
- // The first element in the array is the default engine.
- if (i == 0) {
- // We set this here, not in setSearchEngineFromJSON, because it allows us to
- // keep a reference to the default engine to use when the AlertDialog
- // callbacks are used.
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- enginePreference.setIsDefault(true);
- }
- });
- mDefaultReference = enginePreference;
- }
- } catch (JSONException e) {
- Log.e(LOGTAG, "JSONException parsing engine at index " + i, e);
- }
- }
- }
- }
-
- /**
- * Helper method to send a particular event string to Gecko with an associated engine name.
- * @param event The type of event to send.
- * @param engine The engine to which the event relates.
- */
- private void sendGeckoEngineEvent(String event, String engineName) {
- JSONObject json = new JSONObject();
- try {
- json.put("engine", engineName);
- } catch (JSONException e) {
- Log.e(LOGTAG, "JSONException creating search engine configuration change message for Gecko.", e);
- return;
- }
- GeckoAppShell.notifyObservers(event, json.toString());
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/SetHomepagePreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/SetHomepagePreference.java
deleted file mode 100644
index 55be702c4..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/SetHomepagePreference.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-package org.mozilla.gecko.preferences;
-
-import org.mozilla.gecko.AboutPages;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.R;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.preference.DialogPreference;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.EditText;
-import android.widget.RadioButton;
-import android.widget.RadioGroup;
-
-public class SetHomepagePreference extends DialogPreference {
- private static final String DEFAULT_HOMEPAGE = AboutPages.HOME;
-
- private final SharedPreferences prefs;
-
- private RadioGroup homepageLayout;
- private RadioButton defaultRadio;
- private RadioButton userAddressRadio;
- private EditText homepageEditText;
-
- // This is the url that 1) was loaded from prefs or, 2) stored
- // when the user pressed the "default homepage" checkbox.
- private String storedUrl;
-
- public SetHomepagePreference(final Context context, final AttributeSet attrs) {
- super(context, attrs);
- prefs = GeckoSharedPrefs.forProfile(context);
- }
-
- @Override
- protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
- // Without this GB devices have a black background to the dialog.
- builder.setInverseBackgroundForced(true);
- }
-
- @Override
- protected void onBindDialogView(final View view) {
- super.onBindDialogView(view);
-
- homepageLayout = (RadioGroup) view.findViewById(R.id.homepage_layout);
- defaultRadio = (RadioButton) view.findViewById(R.id.radio_default);
- userAddressRadio = (RadioButton) view.findViewById(R.id.radio_user_address);
- homepageEditText = (EditText) view.findViewById(R.id.edittext_user_address);
-
- storedUrl = prefs.getString(GeckoPreferences.PREFS_HOMEPAGE, DEFAULT_HOMEPAGE);
-
- homepageLayout.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(final RadioGroup radioGroup, final int checkedId) {
- if (checkedId == R.id.radio_user_address) {
- homepageEditText.setVisibility(View.VISIBLE);
- openKeyboardAndSelectAll(getContext(), homepageEditText);
- } else {
- homepageEditText.setVisibility(View.GONE);
- }
- }
- });
- setUIState(storedUrl);
- }
-
- private void setUIState(final String url) {
- if (isUrlDefaultHomepage(url)) {
- defaultRadio.setChecked(true);
- } else {
- userAddressRadio.setChecked(true);
- homepageEditText.setText(url);
- }
- }
-
- private boolean isUrlDefaultHomepage(final String url) {
- return TextUtils.isEmpty(url) || DEFAULT_HOMEPAGE.equals(url);
- }
-
- private static void openKeyboardAndSelectAll(final Context context, final View viewToFocus) {
- viewToFocus.requestFocus();
- viewToFocus.post(new Runnable() {
- @Override
- public void run() {
- InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
- imm.showSoftInput(viewToFocus, InputMethodManager.SHOW_IMPLICIT);
- // android:selectAllOnFocus doesn't work for the initial focus:
- // I'm not sure why. We manually selectAll instead.
- if (viewToFocus instanceof EditText) {
- ((EditText) viewToFocus).selectAll();
- }
- }
- });
- }
-
- @Override
- protected void onDialogClosed(final boolean positiveResult) {
- super.onDialogClosed(positiveResult);
- if (positiveResult) {
- final SharedPreferences.Editor editor = prefs.edit();
- final String homePageEditTextValue = homepageEditText.getText().toString();
- final String newPrefValue;
- if (homepageLayout.getCheckedRadioButtonId() == R.id.radio_default ||
- isUrlDefaultHomepage(homePageEditTextValue)) {
- newPrefValue = "";
- editor.remove(GeckoPreferences.PREFS_HOMEPAGE);
- } else {
- newPrefValue = homePageEditTextValue;
- editor.putString(GeckoPreferences.PREFS_HOMEPAGE, newPrefValue);
- }
- editor.apply();
-
- if (getOnPreferenceChangeListener() != null) {
- getOnPreferenceChangeListener().onPreferenceChange(this, newPrefValue);
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/preferences/SyncPreference.java b/mobile/android/base/java/org/mozilla/gecko/preferences/SyncPreference.java
deleted file mode 100644
index 350ac8fc0..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/SyncPreference.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.preferences;
-
-import android.content.Context;
-import android.content.Intent;
-import android.preference.Preference;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-
-import com.squareup.picasso.Picasso;
-import com.squareup.picasso.Target;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.TelemetryContract.Method;
-import org.mozilla.gecko.fxa.FxAccountConstants;
-import org.mozilla.gecko.fxa.activities.FxAccountWebFlowActivity;
-import org.mozilla.gecko.fxa.activities.PicassoPreferenceIconTarget;
-import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
-import org.mozilla.gecko.sync.ExtendedJSONObject;
-import org.mozilla.gecko.util.ThreadUtils;
-
-class SyncPreference extends Preference {
- private final Context mContext;
- private final Target profileAvatarTarget;
-
- public SyncPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- mContext = context;
- final float cornerRadius = mContext.getResources().getDimension(R.dimen.fxaccount_profile_image_width) / 2;
- profileAvatarTarget = new PicassoPreferenceIconTarget(mContext.getResources(), this, cornerRadius);
- }
-
- private void launchFxASetup() {
- final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
- intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
- mContext.startActivity(intent);
- }
-
- public void update(final AndroidFxAccount fxAccount) {
- if (fxAccount == null) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- setTitle(R.string.pref_sync);
- setSummary(R.string.pref_sync_summary);
- // Cancel any pending task.
- Picasso.with(mContext).cancelRequest(profileAvatarTarget);
- // Clear previously set icon.
- // Bug 1312719 - IconDrawable is prior to IconResId, drawable must be set null before setIcon(resId)
- // http://androidxref.com/5.1.1_r6/xref/frameworks/base/core/java/android/preference/Preference.java#562
- setIcon(null);
- setIcon(R.drawable.sync_avatar_default);
- }
- });
- return;
- }
-
- // Update title from account email.
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- setTitle(fxAccount.getEmail());
- setSummary("");
- }
- });
-
- final ExtendedJSONObject profileJSON = fxAccount.getProfileJSON();
- if (profileJSON == null) {
- return;
- }
-
- // Avatar URI empty, return early.
- final String avatarURI = profileJSON.getString(FxAccountConstants.KEY_PROFILE_JSON_AVATAR);
- if (TextUtils.isEmpty(avatarURI)) {
- return;
- }
-
- Picasso.with(mContext)
- .load(avatarURI)
- .centerInside()
- .resizeDimen(R.dimen.fxaccount_profile_image_width, R.dimen.fxaccount_profile_image_height)
- .placeholder(R.drawable.sync_avatar_default)
- .error(R.drawable.sync_avatar_default)
- .into(profileAvatarTarget);
- }
-
- @Override
- protected void onClick() {
- // Launch the FxA "Get started" activity, which will dispatch to the
- // right location.
- launchFxASetup();
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, Method.SETTINGS, "sync_setup");
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/promotion/AddToHomeScreenPromotion.java b/mobile/android/base/java/org/mozilla/gecko/promotion/AddToHomeScreenPromotion.java
deleted file mode 100644
index c1eeb6bd5..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/promotion/AddToHomeScreenPromotion.java
+++ /dev/null
@@ -1,237 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.promotion;
-
-import android.app.Activity;
-import android.content.Context;
-import android.database.Cursor;
-import android.os.Bundle;
-import android.support.annotation.CallSuper;
-import android.util.Log;
-
-import com.keepsafe.switchboard.SwitchBoard;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.AboutPages;
-import org.mozilla.gecko.BrowserApp;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.db.UrlAnnotations;
-import org.mozilla.gecko.delegates.TabsTrayVisibilityAwareDelegate;
-import org.mozilla.gecko.Experiments;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import java.lang.ref.WeakReference;
-
-import ch.boye.httpclientandroidlib.util.TextUtils;
-
-/**
- * Promote "Add to home screen" if user visits website often.
- */
-public class AddToHomeScreenPromotion extends TabsTrayVisibilityAwareDelegate implements Tabs.OnTabsChangedListener {
- private static class URLHistory {
- public final long visits;
- public final long lastVisit;
-
- private URLHistory(long visits, long lastVisit) {
- this.visits = visits;
- this.lastVisit = lastVisit;
- }
- }
-
- private static final String LOGTAG = "GeckoPromoteShortcut";
-
- private static final String EXPERIMENT_MINIMUM_TOTAL_VISITS = "minimumTotalVisits";
- private static final String EXPERIMENT_LAST_VISIT_MINIMUM_AGE = "lastVisitMinimumAgeMs";
- private static final String EXPERIMENT_LAST_VISIT_MAXIMUM_AGE = "lastVisitMaximumAgeMs";
-
- private WeakReference<Activity> activityReference;
- private boolean isEnabled;
- private int minimumVisits;
- private int lastVisitMinimumAgeMs;
- private int lastVisitMaximumAgeMs;
-
- @CallSuper
- @Override
- public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {
- super.onCreate(browserApp, savedInstanceState);
- activityReference = new WeakReference<Activity>(browserApp);
-
- initializeExperiment(browserApp);
- }
-
- @Override
- public void onResume(BrowserApp browserApp) {
- Tabs.registerOnTabsChangedListener(this);
- }
-
- @Override
- public void onPause(BrowserApp browserApp) {
- Tabs.unregisterOnTabsChangedListener(this);
- }
-
- private void initializeExperiment(Context context) {
- if (!SwitchBoard.isInExperiment(context, Experiments.PROMOTE_ADD_TO_HOMESCREEN)) {
- Log.v(LOGTAG, "Experiment not enabled");
- // Experiment is not enabled. No need to try to read values.
- return;
- }
-
- JSONObject values = SwitchBoard.getExperimentValuesFromJson(context, Experiments.PROMOTE_ADD_TO_HOMESCREEN);
- if (values == null) {
- // We didn't get any values for this experiment. Let's disable it instead of picking default
- // values that might be bad.
- return;
- }
-
- try {
- initializeWithValues(
- values.getInt(EXPERIMENT_MINIMUM_TOTAL_VISITS),
- values.getInt(EXPERIMENT_LAST_VISIT_MINIMUM_AGE),
- values.getInt(EXPERIMENT_LAST_VISIT_MAXIMUM_AGE));
- } catch (JSONException e) {
- Log.w(LOGTAG, "Could not read experiment values", e);
- }
- }
-
- private void initializeWithValues(int minimumVisits, int lastVisitMinimumAgeMs, int lastVisitMaximumAgeMs) {
- this.isEnabled = true;
-
- this.minimumVisits = minimumVisits;
- this.lastVisitMinimumAgeMs = lastVisitMinimumAgeMs;
- this.lastVisitMaximumAgeMs = lastVisitMaximumAgeMs;
- }
-
- @Override
- public void onTabChanged(final Tab tab, Tabs.TabEvents msg, String data) {
- if (tab == null) {
- return;
- }
-
- if (!Tabs.getInstance().isSelectedTab(tab)) {
- // We only ever want to show this promotion for the current tab.
- return;
- }
-
- if (Tabs.TabEvents.LOADED != msg) {
- return;
- }
-
- if (tab.isPrivate()) {
- // Never show the prompt for private browsing tabs.
- return;
- }
-
- if (isTabsTrayVisible()) {
- // We only want to show this prompt if this tab is in the foreground and not on top
- // of the tabs tray.
- return;
- }
-
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- maybeShowPromotionForUrl(tab.getURL(), tab.getTitle());
- }
- });
- }
-
- private void maybeShowPromotionForUrl(String url, String title) {
- if (!isEnabled) {
- return;
- }
-
- final Context context = activityReference.get();
- if (context == null) {
- return;
- }
-
- if (!shouldShowPromotion(context, url, title)) {
- return;
- }
-
- HomeScreenPrompt.show(context, url, title);
- }
-
- private boolean shouldShowPromotion(Context context, String url, String title) {
- if (TextUtils.isEmpty(url) || TextUtils.isEmpty(title)) {
- // We require an URL and a title for the shortcut.
- return false;
- }
-
- if (AboutPages.isAboutPage(url)) {
- // No promotion for our internal sites.
- return false;
- }
-
- if (!url.startsWith("https://")) {
- // Only promote websites that are served over HTTPS.
- return false;
- }
-
- URLHistory history = getHistoryForURL(context, url);
- if (history == null) {
- // There's no history for this URL yet or we can't read it right now. Just ignore.
- return false;
- }
-
- if (history.visits < minimumVisits) {
- // This URL has not been visited often enough.
- return false;
- }
-
- if (history.lastVisit > System.currentTimeMillis() - lastVisitMinimumAgeMs) {
- // The last visit is too new. Do not show promotion. This is mostly to avoid that the
- // promotion shows up for a quick refreshs and in the worst case the last visit could
- // be the current visit (race).
- return false;
- }
-
- if (history.lastVisit < System.currentTimeMillis() - lastVisitMaximumAgeMs) {
- // The last visit is to old. Do not show promotion.
- return false;
- }
-
- if (hasAcceptedOrDeclinedHomeScreenShortcut(context, url)) {
- // The user has already created a shortcut in the past or actively declined to create one.
- // Let's not ask again for this url - We do not want to be annoying.
- return false;
- }
-
- return true;
- }
-
- protected boolean hasAcceptedOrDeclinedHomeScreenShortcut(Context context, String url) {
- final UrlAnnotations urlAnnotations = BrowserDB.from(context).getUrlAnnotations();
- return urlAnnotations.hasAcceptedOrDeclinedHomeScreenShortcut(context.getContentResolver(), url);
- }
-
- protected URLHistory getHistoryForURL(Context context, String url) {
- final GeckoProfile profile = GeckoProfile.get(context);
- final BrowserDB browserDB = BrowserDB.from(profile);
-
- Cursor cursor = null;
- try {
- cursor = browserDB.getHistoryForURL(context.getContentResolver(), url);
-
- if (cursor.moveToFirst()) {
- return new URLHistory(
- cursor.getInt(cursor.getColumnIndex(BrowserContract.History.VISITS)),
- cursor.getLong(cursor.getColumnIndex(BrowserContract.History.DATE_LAST_VISITED)));
- }
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
-
- return null;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/promotion/HomeScreenPrompt.java b/mobile/android/base/java/org/mozilla/gecko/promotion/HomeScreenPrompt.java
deleted file mode 100644
index 0f2df8a2c..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/promotion/HomeScreenPrompt.java
+++ /dev/null
@@ -1,237 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.promotion;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Bundle;
-import android.view.MotionEvent;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.Locales;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.db.UrlAnnotations;
-import org.mozilla.gecko.icons.IconCallback;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.icons.Icons;
-import org.mozilla.gecko.Experiments;
-import org.mozilla.gecko.util.ActivityUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-
-/**
- * Prompt to promote adding the current website to the home screen.
- */
-public class HomeScreenPrompt extends Locales.LocaleAwareActivity implements IconCallback {
- private static final String EXTRA_TITLE = "title";
- private static final String EXTRA_URL = "url";
-
- private static final String TELEMETRY_EXTRA = "home_screen_promotion";
-
- private View containerView;
- private ImageView iconView;
- private String title;
- private String url;
- private boolean isAnimating;
- private boolean hasAccepted;
- private boolean hasDeclined;
-
- public static void show(Context context, String url, String title) {
- Intent intent = new Intent(context, HomeScreenPrompt.class);
- intent.putExtra(EXTRA_TITLE, title);
- intent.putExtra(EXTRA_URL, url);
- context.startActivity(intent);
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- fetchDataFromIntent();
- setupViews();
- loadShortcutIcon();
-
- slideIn();
-
- Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, Experiments.PROMOTE_ADD_TO_HOMESCREEN);
-
- // Technically this isn't triggered by a "service". But it's also triggered by a background task and without
- // actual user interaction.
- Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.SERVICE, TELEMETRY_EXTRA);
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
-
- Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, Experiments.PROMOTE_ADD_TO_HOMESCREEN);
- }
-
- private void fetchDataFromIntent() {
- final Bundle extras = getIntent().getExtras();
-
- title = extras.getString(EXTRA_TITLE);
- url = extras.getString(EXTRA_URL);
- }
-
- private void setupViews() {
- setContentView(R.layout.homescreen_prompt);
-
- ((TextView) findViewById(R.id.title)).setText(title);
-
- Uri uri = Uri.parse(url);
- ((TextView) findViewById(R.id.host)).setText(uri.getHost());
-
- containerView = findViewById(R.id.container);
- iconView = (ImageView) findViewById(R.id.icon);
-
- findViewById(R.id.add).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- hasAccepted = true;
-
- addToHomeScreen();
- }
- });
-
- findViewById(R.id.close).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- onDecline();
- }
- });
- }
-
- private void addToHomeScreen() {
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- GeckoAppShell.createShortcut(title, url);
-
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, TELEMETRY_EXTRA);
-
- ActivityUtils.goToHomeScreen(HomeScreenPrompt.this);
-
- finish();
- }
- });
- }
-
-
-
- private void loadShortcutIcon() {
- Icons.with(this)
- .pageUrl(url)
- .skipNetwork()
- .skipMemory()
- .forLauncherIcon()
- .build()
- .execute(this);
- }
-
- private void slideIn() {
- containerView.setTranslationY(500);
- containerView.setAlpha(0);
-
- final Animator translateAnimator = ObjectAnimator.ofFloat(containerView, "translationY", 0);
- translateAnimator.setDuration(400);
-
- final Animator alphaAnimator = ObjectAnimator.ofFloat(containerView, "alpha", 1);
- alphaAnimator.setStartDelay(200);
- alphaAnimator.setDuration(600);
-
- final AnimatorSet set = new AnimatorSet();
- set.playTogether(alphaAnimator, translateAnimator);
- set.setStartDelay(400);
-
- set.start();
- }
-
- /**
- * Remember that the user rejected creating a home screen shortcut for this URL.
- */
- private void rememberRejection() {
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- final UrlAnnotations urlAnnotations = BrowserDB.from(HomeScreenPrompt.this).getUrlAnnotations();
- urlAnnotations.insertHomeScreenShortcut(getContentResolver(), url, false);
- }
- });
- }
-
- private void slideOut() {
- if (isAnimating) {
- return;
- }
-
- isAnimating = true;
-
- ObjectAnimator animator = ObjectAnimator.ofFloat(containerView, "translationY", containerView.getHeight());
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- finish();
- }
-
- });
- animator.start();
- }
-
- @Override
- public void finish() {
- super.finish();
-
- // Don't perform an activity-dismiss animation.
- overridePendingTransition(0, 0);
- }
-
- @Override
- public void onBackPressed() {
- onDecline();
- }
-
- private void onDecline() {
- if (hasDeclined || hasAccepted) {
- return;
- }
-
- rememberRejection();
- slideOut();
-
- // Technically not always an action triggered by the "back" button but with the same effect: Finishing this
- // activity and going back to the previous one.
- Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.BACK, TELEMETRY_EXTRA);
-
- hasDeclined = true;
- }
-
- /**
- * User clicked outside of the prompt.
- */
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- onDecline();
-
- return true;
- }
-
- @Override
- public void onIconResponse(IconResponse response) {
- iconView.setImageBitmap(response.getBitmap());
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/promotion/ReaderViewBookmarkPromotion.java b/mobile/android/base/java/org/mozilla/gecko/promotion/ReaderViewBookmarkPromotion.java
deleted file mode 100644
index db5a531c6..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/promotion/ReaderViewBookmarkPromotion.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.promotion;
-
-import android.content.Intent;
-import android.content.SharedPreferences;
-
-import com.keepsafe.switchboard.SwitchBoard;
-
-import org.mozilla.gecko.BrowserApp;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.delegates.BrowserAppDelegateWithReference;
-import org.mozilla.gecko.reader.ReaderModeUtils;
-import org.mozilla.gecko.Experiments;
-
-public class ReaderViewBookmarkPromotion extends BrowserAppDelegateWithReference implements Tabs.OnTabsChangedListener {
- private static final String PREF_FIRST_RV_HINT_SHOWN = "first_reader_view_hint_shown";
- private static final String FIRST_READERVIEW_OPEN_TELEMETRYEXTRA = "first_readerview_open_prompt";
-
- private boolean hasEnteredReaderMode = false;
-
- @Override
- public void onResume(BrowserApp browserApp) {
- Tabs.registerOnTabsChangedListener(this);
- }
-
- @Override
- public void onPause(BrowserApp browserApp) {
- Tabs.unregisterOnTabsChangedListener(this);
- }
-
- @Override
- public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
- switch (msg) {
- case LOCATION_CHANGE:
- // old url: data
- // new url: tab.getURL()
- final boolean enteringReaderMode = ReaderModeUtils.isEnteringReaderMode(data, tab.getURL());
-
- if (!hasEnteredReaderMode && enteringReaderMode) {
- hasEnteredReaderMode = true;
- promoteBookmarking();
- }
-
- break;
- }
- }
-
- @Override
- public void onActivityResult(BrowserApp browserApp, int requestCode, int resultCode, Intent data) {
- switch (requestCode) {
- case BrowserApp.ACTIVITY_REQUEST_TRIPLE_READERVIEW:
- if (resultCode == BrowserApp.ACTIVITY_RESULT_TRIPLE_READERVIEW_ADD_BOOKMARK) {
- final Tab tab = Tabs.getInstance().getSelectedTab();
- if (tab != null) {
- tab.addBookmark();
- }
- } else if (resultCode == BrowserApp.ACTIVITY_RESULT_TRIPLE_READERVIEW_IGNORE) {
- // Nothing to do: we won't show this promotion again either way.
- }
- break;
- }
- }
-
- private void promoteBookmarking() {
- final BrowserApp browserApp = getBrowserApp();
- if (browserApp == null) {
- return;
- }
-
- final SharedPreferences prefs = GeckoSharedPrefs.forProfile(browserApp);
- final boolean isEnabled = SwitchBoard.isInExperiment(browserApp, Experiments.TRIPLE_READERVIEW_BOOKMARK_PROMPT);
-
- // We reuse the same preference as for the first offline reader view bookmark
- // as we only want to show one of the two UIs (they both explain the same
- // functionality).
- if (!isEnabled || prefs.getBoolean(PREF_FIRST_RV_HINT_SHOWN, false)) {
- return;
- }
-
- SimpleHelperUI.show(browserApp,
- FIRST_READERVIEW_OPEN_TELEMETRYEXTRA,
- BrowserApp.ACTIVITY_REQUEST_TRIPLE_READERVIEW,
- R.string.helper_triple_readerview_open_title,
- R.string.helper_triple_readerview_open_message,
- R.drawable.helper_readerview_bookmark, // We share the icon with the usual helper UI
- R.string.helper_triple_readerview_open_button,
- BrowserApp.ACTIVITY_RESULT_TRIPLE_READERVIEW_ADD_BOOKMARK,
- BrowserApp.ACTIVITY_RESULT_TRIPLE_READERVIEW_IGNORE);
-
- prefs
- .edit()
- .putBoolean(PREF_FIRST_RV_HINT_SHOWN, true)
- .apply();
- }
-
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/promotion/SimpleHelperUI.java b/mobile/android/base/java/org/mozilla/gecko/promotion/SimpleHelperUI.java
deleted file mode 100644
index b6b857fb9..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/promotion/SimpleHelperUI.java
+++ /dev/null
@@ -1,194 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.promotion;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.StringRes;
-import android.view.MotionEvent;
-import android.view.View;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import org.mozilla.gecko.Locales;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-
-/**
- * Generic HelperUI (prompt) that can be populated with an image, title, message and action button.
- * See show() for usage. This is run as an Activity, results must be handled in the parent Activities
- * onActivityResult().
- */
-public class SimpleHelperUI extends Locales.LocaleAwareActivity {
- public static final String PREF_FIRST_RVBP_SHOWN = "first_reader_view_bookmark_prompt_shown";
- public static final String FIRST_RVBP_SHOWN_TELEMETRYEXTRA = "first_readerview_bookmark_prompt";
-
- private View containerView;
-
- private boolean isAnimating;
-
- private String mTelemetryExtra;
-
- private static final String EXTRA_TELEMETRYEXTRA = "telemetryextra";
- private static final String EXTRA_TITLE = "title";
- private static final String EXTRA_MESSAGE = "message";
- private static final String EXTRA_IMAGE = "image";
- private static final String EXTRA_BUTTON = "button";
- private static final String EXTRA_RESULTCODE_POSITIVE = "positive";
- private static final String EXTRA_RESULTCODE_NEGATIVE = "negative";
-
-
- /**
- * Show a generic helper UI/prompt.
- *
- * @param owner The owning Activity, the result of this prompt will be delivered to its
- * onActivityResult().
- * @param requestCode The request code for the Activity that will be created, this is passed to
- * onActivityResult() to identify the prompt.
- *
- * @param positiveResultCode The result code passed to onActivityResult() when the button has
- * been pressed.
- * @param negativeResultCode The result code passed to onActivityResult() when the prompt was
- * dismissed, either by pressing outside the prompt or by pressing the
- * device back button.
- */
- public static void show(Activity owner, String telemetryExtra,
- int requestCode,
- @StringRes int title, @StringRes int message,
- @DrawableRes int image, @StringRes int buttonText,
- int positiveResultCode, int negativeResultCode) {
- Intent intent = new Intent(owner, SimpleHelperUI.class);
-
- intent.putExtra(EXTRA_TELEMETRYEXTRA, telemetryExtra);
-
- intent.putExtra(EXTRA_TITLE, title);
- intent.putExtra(EXTRA_MESSAGE, message);
-
- intent.putExtra(EXTRA_IMAGE, image);
- intent.putExtra(EXTRA_BUTTON, buttonText);
-
- intent.putExtra(EXTRA_RESULTCODE_POSITIVE, positiveResultCode);
- intent.putExtra(EXTRA_RESULTCODE_NEGATIVE, negativeResultCode);
-
- owner.startActivityForResult(intent, requestCode);
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- mTelemetryExtra = getIntent().getStringExtra(EXTRA_TELEMETRYEXTRA);
-
- setupViews();
-
- slideIn();
- }
-
- private void setupViews() {
- final Intent i = getIntent();
-
- setContentView(R.layout.simple_helper_ui);
-
- containerView = findViewById(R.id.container);
-
- ((ImageView) findViewById(R.id.image)).setImageResource(i.getIntExtra(EXTRA_IMAGE, 0));
-
- ((TextView) findViewById(R.id.title)).setText(i.getIntExtra(EXTRA_TITLE, 0));
-
- ((TextView) findViewById(R.id.message)).setText(i.getIntExtra(EXTRA_MESSAGE, 0));
-
- final Button button = ((Button) findViewById(R.id.button));
- button.setText(i.getIntExtra(EXTRA_BUTTON, 0));
- button.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- slideOut();
-
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, mTelemetryExtra);
-
- setResult(i.getIntExtra(EXTRA_RESULTCODE_POSITIVE, -1));
- }
- });
- }
-
- private void slideIn() {
- containerView.setTranslationY(500);
- containerView.setAlpha(0);
-
- final Animator translateAnimator = ObjectAnimator.ofFloat(containerView, "translationY", 0);
- translateAnimator.setDuration(400);
-
- final Animator alphaAnimator = ObjectAnimator.ofFloat(containerView, "alpha", 1);
- alphaAnimator.setStartDelay(200);
- alphaAnimator.setDuration(600);
-
- final AnimatorSet set = new AnimatorSet();
- set.playTogether(alphaAnimator, translateAnimator);
- set.setStartDelay(400);
-
- set.start();
- }
-
- private void slideOut() {
- if (isAnimating) {
- return;
- }
-
- isAnimating = true;
-
- ObjectAnimator animator = ObjectAnimator.ofFloat(containerView, "translationY", containerView.getHeight());
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- finish();
- }
-
- });
- animator.start();
- }
-
- @Override
- public void finish() {
- super.finish();
-
- // Don't perform an activity-dismiss animation.
- overridePendingTransition(0, 0);
- }
-
- @Override
- public void onBackPressed() {
- slideOut();
-
- Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.BACK, mTelemetryExtra);
-
- setResult(getIntent().getIntExtra(EXTRA_RESULTCODE_NEGATIVE, -1));
-
- }
-
- /**
- * User clicked outside of the prompt.
- */
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- slideOut();
-
- // Not really an action triggered by the "back" button but with the same effect: Finishing this
- // activity and going back to the previous one.
- Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.BACK, mTelemetryExtra);
-
- setResult(getIntent().getIntExtra(EXTRA_RESULTCODE_NEGATIVE, -1));
-
- return true;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/prompts/ColorPickerInput.java b/mobile/android/base/java/org/mozilla/gecko/prompts/ColorPickerInput.java
deleted file mode 100644
index 3d66eeea8..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/ColorPickerInput.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.prompts;
-
-import org.json.JSONObject;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.widget.BasicColorPicker;
-
-import android.content.Context;
-import android.graphics.Color;
-import android.view.LayoutInflater;
-import android.view.View;
-
-public class ColorPickerInput extends PromptInput {
- public static final String INPUT_TYPE = "color";
- public static final String LOGTAG = "GeckoColorPickerInput";
-
- private final boolean mShowAdvancedButton = true;
- private final int mInitialColor;
-
- public ColorPickerInput(JSONObject obj) {
- super(obj);
- String init = obj.optString("value");
- mInitialColor = Color.rgb(Integer.parseInt(init.substring(1, 3), 16),
- Integer.parseInt(init.substring(3, 5), 16),
- Integer.parseInt(init.substring(5, 7), 16));
- }
-
- @Override
- public View getView(Context context) throws UnsupportedOperationException {
- LayoutInflater inflater = LayoutInflater.from(context);
- mView = inflater.inflate(R.layout.basic_color_picker_dialog, null);
-
- BasicColorPicker cp = (BasicColorPicker) mView.findViewById(R.id.colorpicker);
- cp.setColor(mInitialColor);
-
- return mView;
- }
-
- @Override
- public Object getValue() {
- BasicColorPicker cp = (BasicColorPicker) mView.findViewById(R.id.colorpicker);
- int color = cp.getColor();
- return "#" + Integer.toHexString(color).substring(2);
- }
-
- @Override
- public boolean getScrollable() {
- return true;
- }
-
- @Override
- public boolean canApplyInputStyle() {
- return false;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/prompts/IconGridInput.java b/mobile/android/base/java/org/mozilla/gecko/prompts/IconGridInput.java
deleted file mode 100644
index bc7d7ac20..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/IconGridInput.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.prompts;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.json.JSONArray;
-import org.json.JSONObject;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.ResourceDrawableUtils;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.text.TextUtils;
-import android.view.Display;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
-import android.widget.ArrayAdapter;
-import android.widget.GridView;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-public class IconGridInput extends PromptInput implements OnItemClickListener {
- public static final String INPUT_TYPE = "icongrid";
- public static final String LOGTAG = "GeckoIconGridInput";
-
- private ArrayAdapter<IconGridItem> mAdapter; // An adapter holding a list of items to show in the grid
-
- private static int mColumnWidth = -1; // The maximum width of columns
- private static int mMaxColumns = -1; // The maximum number of columns to show
- private static int mIconSize = -1; // Size of icons in the grid
- private int mSelected; // Current selection
- private final JSONArray mArray;
-
- public IconGridInput(JSONObject obj) {
- super(obj);
- mArray = obj.optJSONArray("items");
- }
-
- @Override
- public View getView(Context context) throws UnsupportedOperationException {
- if (mColumnWidth < 0) {
- // getColumnWidth isn't available on pre-ICS, so we pull it out and assign it here
- mColumnWidth = context.getResources().getDimensionPixelSize(R.dimen.icongrid_columnwidth);
- }
-
- if (mIconSize < 0) {
- mIconSize = GeckoAppShell.getPreferredIconSize();
- }
-
- if (mMaxColumns < 0) {
- mMaxColumns = context.getResources().getInteger(R.integer.max_icon_grid_columns);
- }
-
- // TODO: Dynamically handle size changes
- final WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
- final Display display = wm.getDefaultDisplay();
- final int screenWidth = display.getWidth();
- int maxColumns = Math.min(mMaxColumns, screenWidth / mColumnWidth);
-
- final GridView view = (GridView) LayoutInflater.from(context).inflate(R.layout.icon_grid, null, false);
- view.setColumnWidth(mColumnWidth);
-
- final ArrayList<IconGridItem> items = new ArrayList<IconGridItem>(mArray.length());
- for (int i = 0; i < mArray.length(); i++) {
- IconGridItem item = new IconGridItem(context, mArray.optJSONObject(i));
- items.add(item);
- if (item.selected) {
- mSelected = i;
- }
- }
-
- view.setNumColumns(Math.min(items.size(), maxColumns));
- view.setOnItemClickListener(this);
- // Despite what the docs say, setItemChecked was not moved into the AbsListView class until sometime between
- // Android 2.3.7 and Android 4.0.3. For other versions the item won't be visually highlighted, BUT we really only
- // mSelected will still be set so that we default to its behavior.
- if (mSelected > -1) {
- view.setItemChecked(mSelected, true);
- }
-
- mAdapter = new IconGridAdapter(context, -1, items);
- view.setAdapter(mAdapter);
- mView = view;
- return mView;
- }
-
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- mSelected = position;
- notifyListeners(Integer.toString(position));
- }
-
- @Override
- public Object getValue() {
- return mSelected;
- }
-
- @Override
- public boolean getScrollable() {
- return true;
- }
-
- private class IconGridAdapter extends ArrayAdapter<IconGridItem> {
- public IconGridAdapter(Context context, int resource, List<IconGridItem> items) {
- super(context, resource, items);
- }
-
- @Override
- public View getView(int position, View convert, ViewGroup parent) {
- final Context context = parent.getContext();
- if (convert == null) {
- convert = LayoutInflater.from(context).inflate(R.layout.icon_grid_item, parent, false);
- }
- bindView(convert, context, position);
- return convert;
- }
-
- private void bindView(View v, Context c, int position) {
- final IconGridItem item = getItem(position);
- final TextView text1 = (TextView) v.findViewById(android.R.id.text1);
- text1.setText(item.label);
-
- final TextView text2 = (TextView) v.findViewById(android.R.id.text2);
- if (TextUtils.isEmpty(item.description)) {
- text2.setVisibility(View.GONE);
- } else {
- text2.setVisibility(View.VISIBLE);
- text2.setText(item.description);
- }
-
- final ImageView icon = (ImageView) v.findViewById(R.id.icon);
- icon.setImageDrawable(item.icon);
- ViewGroup.LayoutParams lp = icon.getLayoutParams();
- lp.width = lp.height = mIconSize;
- }
- }
-
- private class IconGridItem {
- final String label;
- final String description;
- final boolean selected;
- Drawable icon;
-
- public IconGridItem(final Context context, final JSONObject obj) {
- label = obj.optString("name");
- final String iconUrl = obj.optString("iconUri");
- description = obj.optString("description");
- selected = obj.optBoolean("selected");
-
- ResourceDrawableUtils.getDrawable(context, iconUrl, new ResourceDrawableUtils.BitmapLoader() {
- @Override
- public void onBitmapFound(Drawable d) {
- icon = d;
- if (mAdapter != null) {
- mAdapter.notifyDataSetChanged();
- }
- }
- });
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/prompts/IntentChooserPrompt.java b/mobile/android/base/java/org/mozilla/gecko/prompts/IntentChooserPrompt.java
deleted file mode 100644
index 502f1156d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/IntentChooserPrompt.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.prompts;
-
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.widget.GeckoActionProvider;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.widget.ListView;
-import android.util.Log;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Shows a prompt letting the user pick from a list of intent handlers for a set of Intents or
- * for a GeckoActionProvider. Basic usage:
- * IntentChooserPrompt prompt = new IntentChooserPrompt(context, new Intent[] {
- * ... // some intents
- * });
- * prompt.show("Title", context, new IntentHandler() {
- * public void onIntentSelected(Intent intent, int position) { }
- * public void onCancelled() { }
- * });
- **/
-public class IntentChooserPrompt {
- private static final String LOGTAG = "GeckoIntentChooser";
-
- private final ArrayList<PromptListItem> mItems;
-
- public IntentChooserPrompt(Context context, Intent[] intents) {
- mItems = getItems(context, intents);
- }
-
- public IntentChooserPrompt(Context context, GeckoActionProvider provider) {
- mItems = getItems(context, provider);
- }
-
- /* If an IntentHandler is passed in, will asynchronously call the handler when the dialog is closed
- * Otherwise, will return the Intent that was chosen by the user. Must be called on the UI thread.
- */
- public void show(final String title, final Context context, final IntentHandler handler) {
- ThreadUtils.assertOnUiThread();
-
- if (mItems.isEmpty()) {
- Log.i(LOGTAG, "No activities for the intent chooser!");
- handler.onCancelled();
- return;
- }
-
- // If there's only one item in the intent list, just return it
- if (mItems.size() == 1) {
- handler.onIntentSelected(mItems.get(0).getIntent(), 0);
- return;
- }
-
- final Prompt prompt = new Prompt(context, new Prompt.PromptCallback() {
- @Override
- public void onPromptFinished(String promptServiceResult) {
- if (handler == null) {
- return;
- }
-
- int itemId = -1;
- try {
- itemId = new JSONObject(promptServiceResult).getInt("button");
- } catch (JSONException e) {
- Log.e(LOGTAG, "result from promptservice was invalid: ", e);
- }
-
- if (itemId == -1) {
- handler.onCancelled();
- } else {
- handler.onIntentSelected(mItems.get(itemId).getIntent(), itemId);
- }
- }
- });
-
- PromptListItem[] arrays = new PromptListItem[mItems.size()];
- mItems.toArray(arrays);
- prompt.show(title, "", arrays, ListView.CHOICE_MODE_NONE);
-
- return;
- }
-
- // Whether or not any activities were found. Useful for checking if you should try a different Intent set
- public boolean hasActivities(Context context) {
- return mItems.isEmpty();
- }
-
- // Gets a list of PromptListItems for an Intent array
- private ArrayList<PromptListItem> getItems(final Context context, Intent[] intents) {
- final ArrayList<PromptListItem> items = new ArrayList<PromptListItem>();
-
- // If we have intents, use them to build the initial list
- for (final Intent intent : intents) {
- items.addAll(getItemsForIntent(context, intent));
- }
-
- return items;
- }
-
- // Gets a list of PromptListItems for a GeckoActionProvider
- private ArrayList<PromptListItem> getItems(final Context context, final GeckoActionProvider provider) {
- final ArrayList<PromptListItem> items = new ArrayList<PromptListItem>();
-
- // Add any intents from the provider.
- final PackageManager packageManager = context.getPackageManager();
- final ArrayList<ResolveInfo> infos = provider.getSortedActivities();
-
- for (final ResolveInfo info : infos) {
- items.add(getItemForResolveInfo(info, packageManager, provider.getIntent()));
- }
-
- return items;
- }
-
- private PromptListItem getItemForResolveInfo(ResolveInfo info, PackageManager pm, Intent intent) {
- PromptListItem item = new PromptListItem(info.loadLabel(pm).toString());
- item.setIcon(info.loadIcon(pm));
-
- Intent i = new Intent(intent);
- // These intents should be implicit.
- i.setComponent(new ComponentName(info.activityInfo.applicationInfo.packageName,
- info.activityInfo.name));
- item.setIntent(new Intent(i));
-
- return item;
- }
-
- private ArrayList<PromptListItem> getItemsForIntent(Context context, Intent intent) {
- ArrayList<PromptListItem> items = new ArrayList<PromptListItem>();
- PackageManager pm = context.getPackageManager();
- List<ResolveInfo> lri = pm.queryIntentActivityOptions(GeckoAppShell.getGeckoInterface().getActivity().getComponentName(), null, intent, 0);
-
- // If we didn't find any activities, just return the empty list
- if (lri == null) {
- return items;
- }
-
- // Otherwise, convert the ResolveInfo. Note we don't currently check for duplicates here.
- for (ResolveInfo ri : lri) {
- items.add(getItemForResolveInfo(ri, pm, intent));
- }
-
- return items;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/prompts/IntentHandler.java b/mobile/android/base/java/org/mozilla/gecko/prompts/IntentHandler.java
deleted file mode 100644
index 1509ab626..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/IntentHandler.java
+++ /dev/null
@@ -1,12 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.prompts;
-
-import android.content.Intent;
-
-public interface IntentHandler {
- public void onIntentSelected(Intent intent, int position);
- public void onCancelled();
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/prompts/Prompt.java b/mobile/android/base/java/org/mozilla/gecko/prompts/Prompt.java
deleted file mode 100644
index 11121b2cc..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/Prompt.java
+++ /dev/null
@@ -1,586 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.prompts;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.gfx.LayerView;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnCancelListener;
-import android.content.DialogInterface.OnClickListener;
-import android.content.res.Resources;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
-import android.widget.LinearLayout;
-import android.widget.ListView;
-import android.widget.ScrollView;
-
-import java.util.ArrayList;
-
-public class Prompt implements OnClickListener, OnCancelListener, OnItemClickListener,
- PromptInput.OnChangeListener, Tabs.OnTabsChangedListener {
- private static final String LOGTAG = "GeckoPromptService";
-
- private String[] mButtons;
- private PromptInput[] mInputs;
- private AlertDialog mDialog;
- private int mDoubleTapButtonType;
-
- private final LayoutInflater mInflater;
- private final Context mContext;
- private PromptCallback mCallback;
- private String mGuid;
- private PromptListAdapter mAdapter;
-
- private static boolean mInitialized;
- private static int mInputPaddingSize;
-
- private int mTabId = Tabs.INVALID_TAB_ID;
- private Object mPreviousInputValue = null;
-
- public Prompt(Context context, PromptCallback callback) {
- this(context);
- mCallback = callback;
- }
-
- private Prompt(Context context) {
- mContext = context;
- mInflater = LayoutInflater.from(mContext);
-
- if (!mInitialized) {
- Resources res = mContext.getResources();
- mInputPaddingSize = (int) (res.getDimension(R.dimen.prompt_service_inputs_padding));
- mInitialized = true;
- }
- }
-
- private View applyInputStyle(View view, PromptInput input) {
- // Don't add padding to color picker views
- if (input.canApplyInputStyle()) {
- view.setPadding(mInputPaddingSize, 0, mInputPaddingSize, 0);
- }
- return view;
- }
-
- public void show(JSONObject message) {
- String title = message.optString("title");
- String text = message.optString("text");
- mGuid = message.optString("guid");
-
- mButtons = getStringArray(message, "buttons");
- final int buttonCount = mButtons == null ? 0 : mButtons.length;
- mDoubleTapButtonType = convertIndexToButtonType(message.optInt("doubleTapButton", -1), buttonCount);
- mPreviousInputValue = null;
-
- JSONArray inputs = getSafeArray(message, "inputs");
- mInputs = new PromptInput[inputs.length()];
- for (int i = 0; i < mInputs.length; i++) {
- try {
- mInputs[i] = PromptInput.getInput(inputs.getJSONObject(i));
- mInputs[i].setListener(this);
- } catch (Exception ex) { }
- }
-
- PromptListItem[] menuitems = PromptListItem.getArray(message.optJSONArray("listitems"));
- String selected = message.optString("choiceMode");
-
- int choiceMode = ListView.CHOICE_MODE_NONE;
- if ("single".equals(selected)) {
- choiceMode = ListView.CHOICE_MODE_SINGLE;
- } else if ("multiple".equals(selected)) {
- choiceMode = ListView.CHOICE_MODE_MULTIPLE;
- }
-
- if (message.has("tabId")) {
- mTabId = message.optInt("tabId", Tabs.INVALID_TAB_ID);
- }
-
- show(title, text, menuitems, choiceMode);
- }
-
- private int convertIndexToButtonType(final int buttonIndex, final int buttonCount) {
- if (buttonIndex < 0 || buttonIndex >= buttonCount) {
- // All valid DialogInterface button values are < 0,
- // so we return 0 as an invalid value.
- return 0;
- }
-
- switch (buttonIndex) {
- case 0:
- return DialogInterface.BUTTON_POSITIVE;
- case 1:
- return DialogInterface.BUTTON_NEUTRAL;
- case 2:
- return DialogInterface.BUTTON_NEGATIVE;
- default:
- return 0;
- }
- }
-
- public void show(String title, String text, PromptListItem[] listItems, int choiceMode) {
- ThreadUtils.assertOnUiThread();
-
- try {
- create(title, text, listItems, choiceMode);
- } catch (IllegalStateException ex) {
- Log.i(LOGTAG, "Error building dialog", ex);
- return;
- }
-
- if (mTabId != Tabs.INVALID_TAB_ID) {
- Tabs.registerOnTabsChangedListener(this);
-
- final Tab tab = Tabs.getInstance().getTab(mTabId);
- if (Tabs.getInstance().getSelectedTab() == tab) {
- mDialog.show();
- }
- } else {
- mDialog.show();
- }
- }
-
- @Override
- public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final String data) {
- if (tab != Tabs.getInstance().getTab(mTabId)) {
- return;
- }
-
- switch (msg) {
- case SELECTED:
- Log.i(LOGTAG, "Selected");
- mDialog.show();
- break;
- case UNSELECTED:
- Log.i(LOGTAG, "Unselected");
- mDialog.hide();
- break;
- case LOCATION_CHANGE:
- Log.i(LOGTAG, "Location change");
- mDialog.cancel();
- break;
- }
- }
-
- private void create(String title, String text, PromptListItem[] listItems, int choiceMode)
- throws IllegalStateException {
-
- AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
- if (!TextUtils.isEmpty(title)) {
- // Long strings can delay showing the dialog, so we cap the number of characters shown to 256.
- builder.setTitle(title.substring(0, Math.min(title.length(), 256)));
- }
-
- if (!TextUtils.isEmpty(text)) {
- builder.setMessage(text);
- }
-
- // Because lists are currently added through the normal Android AlertBuilder interface, they're
- // incompatible with also adding additional input elements to a dialog.
- if (listItems != null && listItems.length > 0) {
- addListItems(builder, listItems, choiceMode);
- } else if (!addInputs(builder)) {
- throw new IllegalStateException("Could not add inputs to dialog");
- }
-
- int length = mButtons == null ? 0 : mButtons.length;
- if (length > 0) {
- builder.setPositiveButton(mButtons[0], this);
- if (length > 1) {
- builder.setNeutralButton(mButtons[1], this);
- if (length > 2) {
- builder.setNegativeButton(mButtons[2], this);
- }
- }
- }
-
- mDialog = builder.create();
- mDialog.setOnCancelListener(Prompt.this);
- }
-
- public void setButtons(String[] buttons) {
- mButtons = buttons;
- }
-
- public void setInputs(PromptInput[] inputs) {
- mInputs = inputs;
- }
-
- /* Adds to a result value from the lists that can be shown in dialogs.
- * Will set the selected value(s) to the button attribute of the
- * object that's passed in. If this is a multi-select dialog, sets a
- * selected attribute to an array of booleans.
- */
- private void addListResult(final JSONObject result, int which) {
- if (mAdapter == null) {
- return;
- }
-
- try {
- JSONArray selected = new JSONArray();
-
- // If the button has already been filled in
- ArrayList<Integer> selectedItems = mAdapter.getSelected();
- for (Integer item : selectedItems) {
- selected.put(item);
- }
-
- // If we haven't assigned a button yet, or we assigned it to -1, assign the which
- // parameter to both selected and the button.
- if (!result.has("button") || result.optInt("button") == -1) {
- if (!selectedItems.contains(which)) {
- selected.put(which);
- }
-
- result.put("button", which);
- }
-
- result.put("list", selected);
- } catch (JSONException ex) { }
- }
-
- /* Adds to a result value from the inputs that can be shown in dialogs.
- * Each input will set its own value in the result.
- */
- private void addInputValues(final JSONObject result) {
- try {
- if (mInputs != null) {
- for (int i = 0; i < mInputs.length; i++) {
- if (mInputs[i] != null) {
- result.put(mInputs[i].getId(), mInputs[i].getValue());
- }
- }
- }
- } catch (JSONException ex) { }
- }
-
- /* Adds the selected button to a result. This should only be called if there
- * are no lists shown on the dialog, since they also write their results to the button
- * attribute.
- */
- private void addButtonResult(final JSONObject result, int which) {
- int button = -1;
- switch (which) {
- case DialogInterface.BUTTON_POSITIVE : button = 0; break;
- case DialogInterface.BUTTON_NEUTRAL : button = 1; break;
- case DialogInterface.BUTTON_NEGATIVE : button = 2; break;
- }
- try {
- result.put("button", button);
- } catch (JSONException ex) { }
- }
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- ThreadUtils.assertOnUiThread();
- closeDialog(which);
- }
-
- /* Adds a set of list items to the prompt. This can be used for either context menu type dialogs, checked lists,
- * or multiple selection lists.
- *
- * @param builder
- * The alert builder currently building this dialog.
- * @param listItems
- * The items to add.
- * @param choiceMode
- * One of the ListView.CHOICE_MODE constants to designate whether this list shows checkmarks, radios buttons, or nothing.
- */
- private void addListItems(AlertDialog.Builder builder, PromptListItem[] listItems, int choiceMode) {
- switch (choiceMode) {
- case ListView.CHOICE_MODE_MULTIPLE_MODAL:
- case ListView.CHOICE_MODE_MULTIPLE:
- addMultiSelectList(builder, listItems);
- break;
- case ListView.CHOICE_MODE_SINGLE:
- addSingleSelectList(builder, listItems);
- break;
- case ListView.CHOICE_MODE_NONE:
- default:
- addMenuList(builder, listItems);
- }
- }
-
- /* Shows a multi-select list with checkmarks on the side. Android doesn't support using an adapter for
- * multi-choice lists by default so instead we insert our own custom list so that we can do fancy things
- * to the rows like disabling/indenting them.
- *
- * @param builder
- * The alert builder currently building this dialog.
- * @param listItems
- * The items to add.
- */
- private void addMultiSelectList(AlertDialog.Builder builder, PromptListItem[] listItems) {
- ListView listView = (ListView) mInflater.inflate(R.layout.select_dialog_list, null);
- listView.setOnItemClickListener(this);
- listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
-
- mAdapter = new PromptListAdapter(mContext, R.layout.select_dialog_multichoice, listItems);
- listView.setAdapter(mAdapter);
- builder.setView(listView);
- }
-
- /* Shows a single-select list with radio boxes on the side.
- *
- * @param builder
- * the alert builder currently building this dialog.
- * @param listItems
- * The items to add.
- */
- private void addSingleSelectList(AlertDialog.Builder builder, PromptListItem[] listItems) {
- mAdapter = new PromptListAdapter(mContext, R.layout.select_dialog_singlechoice, listItems);
- builder.setSingleChoiceItems(mAdapter, mAdapter.getSelectedIndex(), new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- // The adapter isn't aware of single vs. multi choice lists, so manually
- // clear any other selected items first.
- ArrayList<Integer> selected = mAdapter.getSelected();
- for (Integer sel : selected) {
- mAdapter.toggleSelected(sel);
- }
-
- // Now select this item.
- mAdapter.toggleSelected(which);
- closeIfNoButtons(which);
- }
- });
- }
-
- /* Shows a single-select list.
- *
- * @param builder
- * the alert builder currently building this dialog.
- * @param listItems
- * The items to add.
- */
- private void addMenuList(AlertDialog.Builder builder, PromptListItem[] listItems) {
- mAdapter = new PromptListAdapter(mContext, android.R.layout.simple_list_item_1, listItems);
- builder.setAdapter(mAdapter, this);
- }
-
- /* Wraps an input in a linearlayout. We do this so that we can set padding that appears outside the background
- * drawable for the view.
- */
- private View wrapInput(final PromptInput input) {
- final LinearLayout linearLayout = new LinearLayout(mContext);
- linearLayout.setOrientation(LinearLayout.VERTICAL);
- applyInputStyle(linearLayout, input);
-
- linearLayout.addView(input.getView(mContext));
-
- return linearLayout;
- }
-
- /* Add the requested input elements to the dialog.
- *
- * @param builder
- * the alert builder currently building this dialog.
- * @return
- * return true if the inputs were added successfully. This may fail
- * if the requested input is compatible with this Android version.
- */
- private boolean addInputs(AlertDialog.Builder builder) {
- int length = mInputs == null ? 0 : mInputs.length;
- if (length == 0) {
- return true;
- }
-
- try {
- View root = null;
- boolean scrollable = false; // If any of the inputs are scrollable, we won't wrap this in a ScrollView
-
- if (length == 1) {
- root = wrapInput(mInputs[0]);
- scrollable |= mInputs[0].getScrollable();
- } else if (length > 1) {
- LinearLayout linearLayout = new LinearLayout(mContext);
- linearLayout.setOrientation(LinearLayout.VERTICAL);
- for (int i = 0; i < length; i++) {
- View content = wrapInput(mInputs[i]);
- linearLayout.addView(content);
- scrollable |= mInputs[i].getScrollable();
- }
- root = linearLayout;
- }
-
- if (scrollable) {
- // If we're showing some sort of scrollable list, force an inverse background.
- builder.setInverseBackgroundForced(true);
- builder.setView(root);
- } else {
- ScrollView view = new ScrollView(mContext);
- view.addView(root);
- builder.setView(view);
- }
- } catch (Exception ex) {
- Log.e(LOGTAG, "Error showing prompt inputs", ex);
- // We cannot display these input widgets with this sdk version,
- // do not display any dialog and finish the prompt now.
- cancelDialog();
- return false;
- }
-
- return true;
- }
-
- /* AdapterView.OnItemClickListener
- * Called when a list item is clicked
- */
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- ThreadUtils.assertOnUiThread();
- mAdapter.toggleSelected(position);
-
- // If there are no buttons on this dialog, then we take selecting an item as a sign to close
- // the dialog. Note that means it will be hard to select multiple things in this list, but
- // given there is no way to confirm+close the dialog, it seems reasonable.
- closeIfNoButtons(position);
- }
-
- private boolean closeIfNoButtons(int selected) {
- ThreadUtils.assertOnUiThread();
- if (mButtons == null || mButtons.length == 0) {
- closeDialog(selected);
- return true;
- }
- return false;
- }
-
- /* @DialogInterface.OnCancelListener
- * Called when the user hits back to cancel a dialog. The dialog will close itself when this
- * ends. Setup the correct return values here.
- *
- * @param aDialog
- * A dialog interface for the dialog that's being closed.
- */
- @Override
- public void onCancel(DialogInterface aDialog) {
- ThreadUtils.assertOnUiThread();
- cancelDialog();
- }
-
- /* Called in situations where we want to cancel the dialog . This can happen if the user hits back,
- * or if the dialog can't be created because of invalid JSON.
- */
- private void cancelDialog() {
- JSONObject ret = new JSONObject();
- try {
- ret.put("button", -1);
- } catch (Exception ex) { }
- addInputValues(ret);
- notifyClosing(ret);
- }
-
- /* Called any time we're closing the dialog to cleanup and notify listeners that the dialog
- * is closing.
- */
- private void closeDialog(int which) {
- JSONObject ret = new JSONObject();
- mDialog.dismiss();
-
- addButtonResult(ret, which);
- addListResult(ret, which);
- addInputValues(ret);
-
- notifyClosing(ret);
- }
-
- /* Called any time we're closing the dialog to cleanup and notify listeners that the dialog
- * is closing.
- */
- private void notifyClosing(JSONObject aReturn) {
- try {
- aReturn.put("guid", mGuid);
- } catch (JSONException ex) { }
-
- if (mTabId != Tabs.INVALID_TAB_ID) {
- Tabs.unregisterOnTabsChangedListener(this);
- }
-
- if (mCallback != null) {
- mCallback.onPromptFinished(aReturn.toString());
- }
- }
-
- // Called when the prompt inputs on the dialog change
- @Override
- public void onChange(PromptInput input) {
- // If there are no buttons on this dialog, assuming that "changing" an input
- // means something was selected and we can close. This provides a way to tap
- // on a list item and close the dialog automatically.
- if (!closeIfNoButtons(-1)) {
- // Alternatively, if a default button has been specified for double tapping,
- // we want to close the dialog if the same input value has been transmitted
- // twice in a row.
- closeIfDoubleTapEnabled(input.getValue());
- }
- }
-
- private boolean closeIfDoubleTapEnabled(Object inputValue) {
- if (mDoubleTapButtonType != 0 && inputValue == mPreviousInputValue) {
- closeDialog(mDoubleTapButtonType);
- return true;
- }
- mPreviousInputValue = inputValue;
- return false;
- }
-
- private static JSONArray getSafeArray(JSONObject json, String key) {
- try {
- return json.getJSONArray(key);
- } catch (Exception e) {
- return new JSONArray();
- }
- }
-
- public static String[] getStringArray(JSONObject aObject, String aName) {
- JSONArray items = getSafeArray(aObject, aName);
- int length = items.length();
- String[] list = new String[length];
- for (int i = 0; i < length; i++) {
- try {
- list[i] = items.getString(i);
- } catch (Exception ex) { }
- }
- return list;
- }
-
- private static boolean[] getBooleanArray(JSONObject aObject, String aName) {
- JSONArray items = new JSONArray();
- try {
- items = aObject.getJSONArray(aName);
- } catch (Exception ex) { return null; }
- int length = items.length();
- boolean[] list = new boolean[length];
- for (int i = 0; i < length; i++) {
- try {
- list[i] = items.getBoolean(i);
- } catch (Exception ex) { }
- }
- return list;
- }
-
- public interface PromptCallback {
-
- /**
- * Called when the Prompt has been completed (i.e. when the user has selected an item or action in the Prompt).
- * This callback is run on the UI thread.
- */
- public void onPromptFinished(String jsonResult);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptInput.java b/mobile/android/base/java/org/mozilla/gecko/prompts/PromptInput.java
deleted file mode 100644
index 752f5c24c..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptInput.java
+++ /dev/null
@@ -1,398 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.prompts;
-
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.GregorianCalendar;
-
-import org.json.JSONObject;
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.widget.AllCapsTextView;
-import org.mozilla.gecko.widget.DateTimePicker;
-
-import android.content.Context;
-import android.content.res.Configuration;
-import android.support.design.widget.TextInputLayout;
-import android.support.v7.widget.AppCompatCheckBox;
-import android.text.Html;
-import android.text.InputType;
-import android.text.TextUtils;
-import android.text.format.DateFormat;
-import android.util.Log;
-import android.view.View;
-import android.view.ViewGroup.LayoutParams;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.ArrayAdapter;
-import android.widget.CheckBox;
-import android.widget.DatePicker;
-import android.widget.EditText;
-import android.widget.LinearLayout;
-import android.widget.Spinner;
-import android.widget.TextView;
-import android.widget.TimePicker;
-
-public abstract class PromptInput {
- protected final String mLabel;
- protected final String mType;
- protected final String mId;
- protected final String mValue;
- protected final String mMinValue;
- protected final String mMaxValue;
- protected OnChangeListener mListener;
- protected View mView;
- public static final String LOGTAG = "GeckoPromptInput";
-
- public interface OnChangeListener {
- void onChange(PromptInput input);
- }
-
- public void setListener(OnChangeListener listener) {
- mListener = listener;
- }
-
- public static class EditInput extends PromptInput {
- protected final String mHint;
- protected final boolean mAutofocus;
- public static final String INPUT_TYPE = "textbox";
-
- public EditInput(JSONObject object) {
- super(object);
- mHint = object.optString("hint");
- mAutofocus = object.optBoolean("autofocus");
- }
-
- @Override
- public View getView(final Context context) throws UnsupportedOperationException {
- EditText input = new EditText(context);
- input.setInputType(InputType.TYPE_CLASS_TEXT);
- input.setText(mValue);
-
- if (!TextUtils.isEmpty(mHint)) {
- input.setHint(mHint);
- }
-
- if (mAutofocus) {
- input.setOnFocusChangeListener(new View.OnFocusChangeListener() {
- @Override
- public void onFocusChange(View v, boolean hasFocus) {
- if (hasFocus) {
- ((InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE)).showSoftInput(v, 0);
- }
- }
- });
- input.requestFocus();
- }
-
- TextInputLayout inputLayout = new TextInputLayout(context);
- inputLayout.addView(input);
-
- mView = (View) inputLayout;
- return mView;
- }
-
- @Override
- public Object getValue() {
- final TextInputLayout inputLayout = (TextInputLayout) mView;
- return inputLayout.getEditText().getText();
- }
- }
-
- public static class NumberInput extends EditInput {
- public static final String INPUT_TYPE = "number";
- public NumberInput(JSONObject obj) {
- super(obj);
- }
-
- @Override
- public View getView(final Context context) throws UnsupportedOperationException {
- final TextInputLayout inputLayout = (TextInputLayout) super.getView(context);
- final EditText input = inputLayout.getEditText();
- input.setRawInputType(Configuration.KEYBOARD_12KEY);
- input.setInputType(InputType.TYPE_CLASS_NUMBER |
- InputType.TYPE_NUMBER_FLAG_SIGNED);
- return input;
- }
- }
-
- public static class PasswordInput extends EditInput {
- public static final String INPUT_TYPE = "password";
- public PasswordInput(JSONObject obj) {
- super(obj);
- }
-
- @Override
- public View getView(Context context) throws UnsupportedOperationException {
- final TextInputLayout inputLayout = (TextInputLayout) super.getView(context);
- inputLayout.getEditText().setInputType(InputType.TYPE_CLASS_TEXT |
- InputType.TYPE_TEXT_VARIATION_PASSWORD |
- InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
- return inputLayout;
- }
- }
-
- public static class CheckboxInput extends PromptInput {
- public static final String INPUT_TYPE = "checkbox";
- private final boolean mChecked;
-
- public CheckboxInput(JSONObject obj) {
- super(obj);
- mChecked = obj.optBoolean("checked");
- }
-
- @Override
- public View getView(Context context) throws UnsupportedOperationException {
- final CheckBox checkbox = new AppCompatCheckBox(context);
- checkbox.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
- checkbox.setText(mLabel);
- checkbox.setChecked(mChecked);
- mView = (View)checkbox;
- return mView;
- }
-
- @Override
- public Object getValue() {
- CheckBox checkbox = (CheckBox)mView;
- return checkbox.isChecked() ? Boolean.TRUE : Boolean.FALSE;
- }
- }
-
- public static class DateTimeInput extends PromptInput {
- public static final String[] INPUT_TYPES = new String[] {
- "date",
- "week",
- "time",
- "datetime-local",
- "datetime",
- "month"
- };
-
- public DateTimeInput(JSONObject obj) {
- super(obj);
- }
-
- @Override
- public View getView(Context context) throws UnsupportedOperationException {
- if (mType.equals("date")) {
- try {
- DateTimePicker input = new DateTimePicker(context, "yyyy-MM-dd", mValue,
- DateTimePicker.PickersState.DATE, mMinValue, mMaxValue);
- input.toggleCalendar(true);
- mView = (View)input;
- } catch (UnsupportedOperationException ex) {
- // We can't use our custom version of the DatePicker widget because the sdk is too old.
- // But we can fallback on the native one.
- DatePicker input = new DatePicker(context);
- try {
- if (!TextUtils.isEmpty(mValue)) {
- GregorianCalendar calendar = new GregorianCalendar();
- calendar.setTime(new SimpleDateFormat("yyyy-MM-dd").parse(mValue));
- input.updateDate(calendar.get(Calendar.YEAR),
- calendar.get(Calendar.MONTH),
- calendar.get(Calendar.DAY_OF_MONTH));
- }
- } catch (Exception e) {
- Log.e(LOGTAG, "error parsing format string: " + e);
- }
- mView = (View)input;
- }
- } else if (mType.equals("week")) {
- DateTimePicker input = new DateTimePicker(context, "yyyy-'W'ww", mValue,
- DateTimePicker.PickersState.WEEK, mMinValue, mMaxValue);
- mView = (View)input;
- } else if (mType.equals("time")) {
- TimePicker input = new TimePicker(context);
- input.setIs24HourView(DateFormat.is24HourFormat(context));
-
- GregorianCalendar calendar = new GregorianCalendar();
- if (!TextUtils.isEmpty(mValue)) {
- try {
- calendar.setTime(new SimpleDateFormat("HH:mm").parse(mValue));
- } catch (Exception e) { }
- }
- input.setCurrentHour(calendar.get(GregorianCalendar.HOUR_OF_DAY));
- input.setCurrentMinute(calendar.get(GregorianCalendar.MINUTE));
- mView = (View)input;
- } else if (mType.equals("datetime-local") || mType.equals("datetime")) {
- DateTimePicker input = new DateTimePicker(context, "yyyy-MM-dd HH:mm", mValue.replace("T", " ").replace("Z", ""),
- DateTimePicker.PickersState.DATETIME,
- mMinValue.replace("T", " ").replace("Z", ""), mMaxValue.replace("T", " ").replace("Z", ""));
- input.toggleCalendar(true);
- mView = (View)input;
- } else if (mType.equals("month")) {
- DateTimePicker input = new DateTimePicker(context, "yyyy-MM", mValue,
- DateTimePicker.PickersState.MONTH, mMinValue, mMaxValue);
- mView = (View)input;
- }
- return mView;
- }
-
- private static String formatDateString(String dateFormat, Calendar calendar) {
- return new SimpleDateFormat(dateFormat).format(calendar.getTime());
- }
-
- @Override
- public Object getValue() {
- if (mType.equals("time")) {
- TimePicker tp = (TimePicker)mView;
- GregorianCalendar calendar =
- new GregorianCalendar(0, 0, 0, tp.getCurrentHour(), tp.getCurrentMinute());
- return formatDateString("HH:mm", calendar);
- } else {
- DateTimePicker dp = (DateTimePicker)mView;
- GregorianCalendar calendar = new GregorianCalendar();
- calendar.setTimeInMillis(dp.getTimeInMillis());
- if (mType.equals("date")) {
- return formatDateString("yyyy-MM-dd", calendar);
- } else if (mType.equals("week")) {
- return formatDateString("yyyy-'W'ww", calendar);
- } else if (mType.equals("datetime-local")) {
- return formatDateString("yyyy-MM-dd'T'HH:mm", calendar);
- } else if (mType.equals("datetime")) {
- calendar.set(GregorianCalendar.ZONE_OFFSET, 0);
- calendar.setTimeInMillis(dp.getTimeInMillis());
- return formatDateString("yyyy-MM-dd'T'HH:mm'Z'", calendar);
- } else if (mType.equals("month")) {
- return formatDateString("yyyy-MM", calendar);
- }
- }
- return super.getValue();
- }
- }
-
- public static class MenulistInput extends PromptInput {
- public static final String INPUT_TYPE = "menulist";
- private static String[] mListitems;
- private static int mSelected;
-
- public Spinner spinner;
- public AllCapsTextView textView;
-
- public MenulistInput(JSONObject obj) {
- super(obj);
- mListitems = Prompt.getStringArray(obj, "values");
- mSelected = obj.optInt("selected");
- }
-
- @Override
- public View getView(final Context context) throws UnsupportedOperationException {
- spinner = new Spinner(context, Spinner.MODE_DIALOG);
- try {
- if (mListitems.length > 0) {
- ArrayAdapter<String> adapter = new ArrayAdapter<String>(context, android.R.layout.simple_spinner_item, mListitems);
- adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
-
- spinner.setAdapter(adapter);
- spinner.setSelection(mSelected);
- }
- } catch (Exception ex) {
- }
-
- if (!TextUtils.isEmpty(mLabel)) {
- LinearLayout container = new LinearLayout(context);
- container.setOrientation(LinearLayout.VERTICAL);
-
- textView = new AllCapsTextView(context, null);
- textView.setText(mLabel);
- container.addView(textView);
-
- container.addView(spinner);
- return container;
- }
-
- return spinner;
- }
-
- @Override
- public Object getValue() {
- return spinner.getSelectedItemPosition();
- }
- }
-
- public static class LabelInput extends PromptInput {
- public static final String INPUT_TYPE = "label";
- public LabelInput(JSONObject obj) {
- super(obj);
- }
-
- @Override
- public View getView(Context context) throws UnsupportedOperationException {
- // not really an input, but a way to add labels and such to the dialog
- TextView view = new TextView(context);
- view.setText(Html.fromHtml(mLabel));
- mView = view;
- return mView;
- }
- }
-
- public PromptInput(JSONObject obj) {
- mLabel = obj.optString("label");
- mType = obj.optString("type");
- String id = obj.optString("id");
- mId = TextUtils.isEmpty(id) ? mType : id;
- mValue = obj.optString("value");
- mMaxValue = obj.optString("max");
- mMinValue = obj.optString("min");
- }
-
- public static PromptInput getInput(JSONObject obj) {
- String type = obj.optString("type");
- switch (type) {
- case EditInput.INPUT_TYPE:
- return new EditInput(obj);
- case NumberInput.INPUT_TYPE:
- return new NumberInput(obj);
- case PasswordInput.INPUT_TYPE:
- return new PasswordInput(obj);
- case CheckboxInput.INPUT_TYPE:
- return new CheckboxInput(obj);
- case MenulistInput.INPUT_TYPE:
- return new MenulistInput(obj);
- case LabelInput.INPUT_TYPE:
- return new LabelInput(obj);
- case IconGridInput.INPUT_TYPE:
- return new IconGridInput(obj);
- case ColorPickerInput.INPUT_TYPE:
- return new ColorPickerInput(obj);
- case TabInput.INPUT_TYPE:
- return new TabInput(obj);
- default:
- for (String dtType : DateTimeInput.INPUT_TYPES) {
- if (dtType.equals(type)) {
- return new DateTimeInput(obj);
- }
- }
-
- break;
- }
-
- return null;
- }
-
- public abstract View getView(Context context) throws UnsupportedOperationException;
-
- public String getId() {
- return mId;
- }
-
- public Object getValue() {
- return null;
- }
-
- public boolean getScrollable() {
- return false;
- }
-
- public boolean canApplyInputStyle() {
- return true;
- }
-
- protected void notifyListeners(String val) {
- if (mListener != null) {
- mListener.onChange(this);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListAdapter.java b/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListAdapter.java
deleted file mode 100644
index 720086c92..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListAdapter.java
+++ /dev/null
@@ -1,281 +0,0 @@
-package org.mozilla.gecko.prompts;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.menu.MenuItemSwitcherLayout;
-import org.mozilla.gecko.widget.GeckoActionProvider;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CheckedTextView;
-import android.widget.TextView;
-import android.widget.ListView;
-import android.widget.ArrayAdapter;
-import android.util.TypedValue;
-
-import java.util.ArrayList;
-
-public class PromptListAdapter extends ArrayAdapter<PromptListItem> {
- private static final int VIEW_TYPE_ITEM = 0;
- private static final int VIEW_TYPE_GROUP = 1;
- private static final int VIEW_TYPE_ACTIONS = 2;
- private static final int VIEW_TYPE_COUNT = 3;
-
- private static final String LOGTAG = "GeckoPromptListAdapter";
-
- private final int mResourceId;
- private Drawable mBlankDrawable;
- private Drawable mMoreDrawable;
- private static int mGroupPaddingSize;
- private static int mLeftRightTextWithIconPadding;
- private static int mTopBottomTextWithIconPadding;
- private static int mIconSize;
- private static int mMinRowSize;
- private static int mIconTextPadding;
- private static float mTextSize;
- private static boolean mInitialized;
-
- PromptListAdapter(Context context, int textViewResourceId, PromptListItem[] objects) {
- super(context, textViewResourceId, objects);
- mResourceId = textViewResourceId;
- init();
- }
-
- private void init() {
- if (!mInitialized) {
- Resources res = getContext().getResources();
- mGroupPaddingSize = (int) (res.getDimension(R.dimen.prompt_service_group_padding_size));
- mLeftRightTextWithIconPadding = (int) (res.getDimension(R.dimen.prompt_service_left_right_text_with_icon_padding));
- mTopBottomTextWithIconPadding = (int) (res.getDimension(R.dimen.prompt_service_top_bottom_text_with_icon_padding));
- mIconTextPadding = (int) (res.getDimension(R.dimen.prompt_service_icon_text_padding));
- mIconSize = (int) (res.getDimension(R.dimen.prompt_service_icon_size));
- mMinRowSize = (int) (res.getDimension(R.dimen.menu_item_row_height));
- mTextSize = res.getDimension(R.dimen.menu_item_textsize);
-
- mInitialized = true;
- }
- }
-
- @Override
- public int getItemViewType(int position) {
- PromptListItem item = getItem(position);
- if (item.isGroup) {
- return VIEW_TYPE_GROUP;
- } else if (item.showAsActions) {
- return VIEW_TYPE_ACTIONS;
- } else {
- return VIEW_TYPE_ITEM;
- }
- }
-
- @Override
- public int getViewTypeCount() {
- return VIEW_TYPE_COUNT;
- }
-
- private Drawable getMoreDrawable(Resources res) {
- if (mMoreDrawable == null) {
- mMoreDrawable = res.getDrawable(R.drawable.menu_item_more);
- }
- return mMoreDrawable;
- }
-
- private Drawable getBlankDrawable(Resources res) {
- if (mBlankDrawable == null) {
- mBlankDrawable = res.getDrawable(R.drawable.blank);
- }
- return mBlankDrawable;
- }
-
- public void toggleSelected(int position) {
- PromptListItem item = getItem(position);
- item.setSelected(!item.getSelected());
- }
-
- private void maybeUpdateIcon(PromptListItem item, TextView t) {
- if (item.getIcon() == null && !item.inGroup && !item.isParent) {
- t.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
- return;
- }
-
- Drawable d = null;
- Resources res = getContext().getResources();
- // Set the padding between the icon and the text.
- t.setCompoundDrawablePadding(mIconTextPadding);
- if (item.getIcon() != null) {
- // We want the icon to be of a specific size. Some do not
- // follow this rule so we have to resize them.
- Bitmap bitmap = ((BitmapDrawable) item.getIcon()).getBitmap();
- d = new BitmapDrawable(res, Bitmap.createScaledBitmap(bitmap, mIconSize, mIconSize, true));
- } else if (item.inGroup) {
- // We don't currently support "indenting" items with icons
- d = getBlankDrawable(res);
- }
-
- Drawable moreDrawable = null;
- if (item.isParent) {
- moreDrawable = getMoreDrawable(res);
- }
-
- if (d != null || moreDrawable != null) {
- t.setCompoundDrawablesWithIntrinsicBounds(d, null, moreDrawable, null);
- }
- }
-
- private void maybeUpdateCheckedState(ListView list, int position, PromptListItem item, ViewHolder viewHolder) {
- viewHolder.textView.setEnabled(!item.disabled && !item.isGroup);
- viewHolder.textView.setClickable(item.isGroup || item.disabled);
- if (viewHolder.textView instanceof CheckedTextView) {
- // Apparently just using ct.setChecked(true) doesn't work, so this
- // is stolen from the android source code as a way to set the checked
- // state of these items
- list.setItemChecked(position, item.getSelected());
- }
- }
-
- boolean isSelected(int position) {
- return getItem(position).getSelected();
- }
-
- ArrayList<Integer> getSelected() {
- int length = getCount();
-
- ArrayList<Integer> selected = new ArrayList<Integer>();
- for (int i = 0; i < length; i++) {
- if (isSelected(i)) {
- selected.add(i);
- }
- }
-
- return selected;
- }
-
- int getSelectedIndex() {
- int length = getCount();
- for (int i = 0; i < length; i++) {
- if (isSelected(i)) {
- return i;
- }
- }
- return -1;
- }
-
- private View getActionView(PromptListItem item, final ListView list, final int position) {
- final GeckoActionProvider provider = GeckoActionProvider.getForType(item.getIntent().getType(), getContext());
- provider.setIntent(item.getIntent());
-
- final MenuItemSwitcherLayout view = (MenuItemSwitcherLayout) provider.onCreateActionView(
- GeckoActionProvider.ActionViewType.CONTEXT_MENU);
- // If a quickshare button is clicked, we need to close the dialog.
- view.addActionButtonClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- ListView.OnItemClickListener listener = list.getOnItemClickListener();
- if (listener != null) {
- listener.onItemClick(list, view, position, position);
- }
- }
- });
-
- return view;
- }
-
- private void updateActionView(final PromptListItem item, final MenuItemSwitcherLayout view, final ListView list, final int position) {
- view.setTitle(item.label);
- view.setIcon(item.getIcon());
- view.setSubMenuIndicator(item.isParent);
-
- // If the share button is clicked, we need to close the dialog and then show an intent chooser
- view.setMenuItemClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- ListView.OnItemClickListener listener = list.getOnItemClickListener();
- if (listener != null) {
- listener.onItemClick(list, view, position, position);
- }
-
- final GeckoActionProvider provider = GeckoActionProvider.getForType(item.getIntent().getType(), getContext());
- IntentChooserPrompt prompt = new IntentChooserPrompt(getContext(), provider);
- prompt.show(item.label, getContext(), new IntentHandler() {
- @Override
- public void onIntentSelected(final Intent intent, final int p) {
- provider.chooseActivity(p);
-
- // Context: Sharing via content contextmenu list (no explicit session is active)
- Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "promptlist");
- }
-
- @Override
- public void onCancelled() {
- // do nothing
- }
- });
- }
- });
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- PromptListItem item = getItem(position);
- int type = getItemViewType(position);
- ViewHolder viewHolder = null;
-
- if (convertView == null) {
- if (type == VIEW_TYPE_ACTIONS) {
- convertView = getActionView(item, (ListView) parent, position);
- } else {
- int resourceId = mResourceId;
- if (item.isGroup) {
- resourceId = R.layout.list_item_header;
- }
-
- LayoutInflater mInflater = LayoutInflater.from(getContext());
- convertView = mInflater.inflate(resourceId, null);
- convertView.setMinimumHeight(mMinRowSize);
-
- TextView tv = (TextView) convertView.findViewById(android.R.id.text1);
- tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
- viewHolder = new ViewHolder(tv, tv.getPaddingLeft(), tv.getPaddingRight(),
- tv.getPaddingTop(), tv.getPaddingBottom());
-
- convertView.setTag(viewHolder);
- }
- } else {
- viewHolder = (ViewHolder) convertView.getTag();
- }
-
- if (type == VIEW_TYPE_ACTIONS) {
- updateActionView(item, (MenuItemSwitcherLayout) convertView, (ListView) parent, position);
- } else {
- viewHolder.textView.setText(item.label);
- maybeUpdateCheckedState((ListView) parent, position, item, viewHolder);
- maybeUpdateIcon(item, viewHolder.textView);
- }
-
- return convertView;
- }
-
- private static class ViewHolder {
- public final TextView textView;
- public final int paddingLeft;
- public final int paddingRight;
- public final int paddingTop;
- public final int paddingBottom;
-
- ViewHolder(TextView aTextView, int aLeft, int aRight, int aTop, int aBottom) {
- textView = aTextView;
- paddingLeft = aLeft;
- paddingRight = aRight;
- paddingTop = aTop;
- paddingBottom = aBottom;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListItem.java b/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListItem.java
deleted file mode 100644
index 48ace735c..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListItem.java
+++ /dev/null
@@ -1,128 +0,0 @@
-package org.mozilla.gecko.prompts;
-
-import org.json.JSONException;
-import org.mozilla.gecko.IntentHelper;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.ThumbnailHelper;
-import org.mozilla.gecko.util.ResourceDrawableUtils;
-import org.mozilla.gecko.widget.GeckoActionProvider;
-
-import org.json.JSONArray;
-import org.json.JSONObject;
-
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.drawable.Drawable;
-
-import java.util.List;
-import java.util.ArrayList;
-
-// This class should die and be replaced with normal menu items
-public class PromptListItem {
- private static final String LOGTAG = "GeckoPromptListItem";
- public final String label;
- public final boolean isGroup;
- public final boolean inGroup;
- public final boolean disabled;
- public final int id;
- public final boolean showAsActions;
- public final boolean isParent;
-
- public Intent mIntent;
- public boolean mSelected;
- public Drawable mIcon;
-
- PromptListItem(JSONObject aObject) {
- Context context = GeckoAppShell.getContext();
- label = aObject.isNull("label") ? "" : aObject.optString("label");
- isGroup = aObject.optBoolean("isGroup");
- inGroup = aObject.optBoolean("inGroup");
- disabled = aObject.optBoolean("disabled");
- id = aObject.optInt("id");
- mSelected = aObject.optBoolean("selected");
-
- JSONObject obj = aObject.optJSONObject("showAsActions");
- if (obj != null) {
- showAsActions = true;
- String uri = obj.isNull("uri") ? "" : obj.optString("uri");
- String type = obj.isNull("type") ? GeckoActionProvider.DEFAULT_MIME_TYPE :
- obj.optString("type", GeckoActionProvider.DEFAULT_MIME_TYPE);
-
- mIntent = IntentHelper.getShareIntent(context, uri, type, "");
- isParent = true;
- } else {
- mIntent = null;
- showAsActions = false;
- // Support both "isParent" (backwards compat for older consumers), and "menu" for the new Tabbed prompt ui.
- isParent = aObject.optBoolean("isParent") || aObject.optBoolean("menu");
- }
-
- final String iconStr = aObject.optString("icon");
- if (iconStr != null) {
- final ResourceDrawableUtils.BitmapLoader loader = new ResourceDrawableUtils.BitmapLoader() {
- @Override
- public void onBitmapFound(Drawable d) {
- mIcon = d;
- }
- };
-
- if (iconStr.startsWith("thumbnail:")) {
- final int id = Integer.parseInt(iconStr.substring(10), 10);
- ThumbnailHelper.getInstance().getAndProcessThumbnailFor(id, loader);
- } else {
- ResourceDrawableUtils.getDrawable(context, iconStr, loader);
- }
- }
- }
-
- public void setIntent(Intent i) {
- mIntent = i;
- }
-
- public Intent getIntent() {
- return mIntent;
- }
-
- public void setIcon(Drawable icon) {
- mIcon = icon;
- }
-
- public Drawable getIcon() {
- return mIcon;
- }
-
- public void setSelected(boolean selected) {
- mSelected = selected;
- }
-
- public boolean getSelected() {
- return mSelected;
- }
-
- public PromptListItem(String aLabel) {
- label = aLabel;
- isGroup = false;
- inGroup = false;
- isParent = false;
- disabled = false;
- id = 0;
- showAsActions = false;
- }
-
- static PromptListItem[] getArray(JSONArray items) {
- if (items == null) {
- return new PromptListItem[0];
- }
-
- int length = items.length();
- List<PromptListItem> list = new ArrayList<>(length);
- for (int i = 0; i < length; i++) {
- try {
- PromptListItem item = new PromptListItem(items.getJSONObject(i));
- list.add(item);
- } catch (JSONException ex) { }
- }
-
- return list.toArray(new PromptListItem[length]);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptService.java b/mobile/android/base/java/org/mozilla/gecko/prompts/PromptService.java
deleted file mode 100644
index 8155cc1c6..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptService.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.prompts;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.content.Context;
-import android.util.Log;
-
-public class PromptService implements GeckoEventListener {
- private static final String LOGTAG = "GeckoPromptService";
-
- private final Context mContext;
-
- public PromptService(Context context) {
- GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
- "Prompt:Show",
- "Prompt:ShowTop");
- mContext = context;
- }
-
- public void destroy() {
- GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
- "Prompt:Show",
- "Prompt:ShowTop");
- }
-
- public void show(final String aTitle, final String aText, final PromptListItem[] aMenuList,
- final int aChoiceMode, final Prompt.PromptCallback callback) {
- // The dialog must be created on the UI thread.
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- Prompt p;
- p = new Prompt(mContext, callback);
- p.show(aTitle, aText, aMenuList, aChoiceMode);
- }
- });
- }
-
- // GeckoEventListener implementation
- @Override
- public void handleMessage(String event, final JSONObject message) {
- // The dialog must be created on the UI thread.
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- Prompt p;
- p = new Prompt(mContext, new Prompt.PromptCallback() {
- @Override
- public void onPromptFinished(String jsonResult) {
- try {
- EventDispatcher.sendResponse(message, new JSONObject(jsonResult));
- } catch (JSONException ex) {
- Log.i(LOGTAG, "Error building json response", ex);
- }
- }
- });
- p.show(message);
- }
- });
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/prompts/TabInput.java b/mobile/android/base/java/org/mozilla/gecko/prompts/TabInput.java
deleted file mode 100644
index ab490e79c..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/TabInput.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.prompts;
-
-import java.util.LinkedHashMap;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.content.Context;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.AdapterView;
-import android.widget.ListView;
-import android.widget.TabHost;
-import android.widget.TextView;
-
-public class TabInput extends PromptInput implements AdapterView.OnItemClickListener {
- public static final String INPUT_TYPE = "tabs";
- public static final String LOGTAG = "GeckoTabInput";
-
- /* Keeping the order of this in sync with the JSON is important. */
- final private LinkedHashMap<String, PromptListItem[]> mTabs;
-
- private TabHost mHost;
- private int mPosition;
-
- public TabInput(JSONObject obj) {
- super(obj);
- mTabs = new LinkedHashMap<String, PromptListItem[]>();
- try {
- JSONArray tabs = obj.getJSONArray("items");
- for (int i = 0; i < tabs.length(); i++) {
- JSONObject tab = tabs.getJSONObject(i);
- String title = tab.getString("label");
- JSONArray items = tab.getJSONArray("items");
- mTabs.put(title, PromptListItem.getArray(items));
- }
- } catch (JSONException ex) {
- Log.e(LOGTAG, "Exception", ex);
- }
- }
-
- @Override
- public View getView(final Context context) throws UnsupportedOperationException {
- final LayoutInflater inflater = LayoutInflater.from(context);
- mHost = (TabHost) inflater.inflate(R.layout.tab_prompt_input, null);
- mHost.setup();
-
- for (String title : mTabs.keySet()) {
- final TabHost.TabSpec spec = mHost.newTabSpec(title);
- spec.setContent(new TabHost.TabContentFactory() {
- @Override
- public View createTabContent(final String tag) {
- PromptListAdapter adapter = new PromptListAdapter(context, android.R.layout.simple_list_item_1, mTabs.get(tag));
- ListView listView = new ListView(context);
- listView.setCacheColorHint(0);
- listView.setOnItemClickListener(TabInput.this);
- listView.setAdapter(adapter);
- return listView;
- }
- });
-
- spec.setIndicator(title);
- mHost.addTab(spec);
- }
- mView = mHost;
- return mHost;
- }
-
- @Override
- public Object getValue() {
- JSONObject obj = new JSONObject();
- try {
- obj.put("tab", mHost.getCurrentTab());
- obj.put("item", mPosition);
- } catch (JSONException ex) { }
-
- return obj;
- }
-
- @Override
- public boolean getScrollable() {
- return true;
- }
-
- @Override
- public boolean canApplyInputStyle() {
- return false;
- }
-
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- ThreadUtils.assertOnUiThread();
- mPosition = position;
- notifyListeners(Integer.toString(position));
- }
-
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/push/Fetched.java b/mobile/android/base/java/org/mozilla/gecko/push/Fetched.java
deleted file mode 100644
index 42a7c6a90..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/push/Fetched.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.push;
-
-import android.support.annotation.NonNull;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-/**
- * Pair a (String) value with a timestamp. The timestamp is usually when the
- * value was fetched from a remote service or when the value was locally
- * generated.
- *
- * It's awkward to serialize generic values to JSON -- that requires lots of
- * factory classes -- so we specialize to String instances.
- */
-public class Fetched {
- public final String value;
- public final long timestamp;
-
- public Fetched(String value, long timestamp) {
- this.value = value;
- this.timestamp = timestamp;
- }
-
- public static Fetched now(String value) {
- return new Fetched(value, System.currentTimeMillis());
- }
-
- public static @NonNull Fetched fromJSONObject(@NonNull JSONObject json) {
- final String value = json.optString("value", null);
- final String timestampString = json.optString("timestamp", null);
- final long timestamp = timestampString != null ? Long.valueOf(timestampString) : 0L;
- return new Fetched(value, timestamp);
- }
-
- public JSONObject toJSONObject() throws JSONException {
- final JSONObject jsonObject = new JSONObject();
- if (value != null) {
- jsonObject.put("value", value);
- } else {
- jsonObject.remove("value");
- }
- jsonObject.put("timestamp", Long.toString(timestamp));
- return jsonObject;
- }
-
- @Override
- public boolean equals(Object o) {
- // Auto-generated.
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- Fetched fetched = (Fetched) o;
-
- if (timestamp != fetched.timestamp) return false;
- return !(value != null ? !value.equals(fetched.value) : fetched.value != null);
-
- }
-
- @Override
- public int hashCode() {
- // Auto-generated.
- int result = value != null ? value.hashCode() : 0;
- result = 31 * result + (int) (timestamp ^ (timestamp >>> 32));
- return result;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/push/PushClient.java b/mobile/android/base/java/org/mozilla/gecko/push/PushClient.java
deleted file mode 100644
index 9c1fab5f9..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/push/PushClient.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.push;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-import org.mozilla.gecko.push.RegisterUserAgentResponse;
-import org.mozilla.gecko.push.SubscribeChannelResponse;
-import org.mozilla.gecko.push.autopush.AutopushClient;
-import org.mozilla.gecko.push.autopush.AutopushClientException;
-import org.mozilla.gecko.sync.Utils;
-
-import java.util.concurrent.Executor;
-
-/**
- * This class bridges the autopush client, which is written in callback style, with the Fennec
- * push implementation, which is written in a linear style. It handles returning results and
- * re-throwing exceptions passed as messages.
- * <p/>
- * TODO: fold this into the autopush client directly.
- */
-public class PushClient {
- public static class LocalException extends Exception {
- private static final long serialVersionUID = 2387554736L;
-
- public LocalException(Throwable throwable) {
- super(throwable);
- }
- }
-
- private final AutopushClient autopushClient;
-
- public PushClient(String serverURI) {
- this.autopushClient = new AutopushClient(serverURI, Utils.newSynchronousExecutor());
- }
-
- /**
- * Each instance is <b>single-use</b>! Exactly one delegate method should be invoked once,
- * but we take care to handle multiple invocations (favoring the earliest), just to be safe.
- */
- protected static class Delegate<T> implements AutopushClient.RequestDelegate<T> {
- Object result; // Oh, for an algebraic data type when you need one!
-
- @SuppressWarnings("unchecked")
- public T responseOrThrow() throws LocalException, AutopushClientException {
- if (result instanceof LocalException) {
- throw (LocalException) result;
- }
- if (result instanceof AutopushClientException) {
- throw (AutopushClientException) result;
- }
- return (T) result;
- }
-
- @Override
- public void handleError(Exception e) {
- if (result == null) {
- result = new LocalException(e);
- }
- }
-
- @Override
- public void handleFailure(AutopushClientException e) {
- if (result == null) {
- result = e;
- }
- }
-
- @Override
- public void handleSuccess(T response) {
- if (result == null) {
- result = response;
- }
- }
- }
-
- public RegisterUserAgentResponse registerUserAgent(@NonNull String token) throws LocalException, AutopushClientException {
- final Delegate<RegisterUserAgentResponse> delegate = new Delegate<>();
- autopushClient.registerUserAgent(token, delegate);
- return delegate.responseOrThrow();
- }
-
- public void reregisterUserAgent(@NonNull String uaid, @NonNull String secret, @NonNull String token) throws LocalException, AutopushClientException {
- final Delegate<Void> delegate = new Delegate<>();
- autopushClient.reregisterUserAgent(uaid, secret, token, delegate);
- delegate.responseOrThrow(); // For side-effects only.
- }
-
- public void unregisterUserAgent(@NonNull String uaid, @NonNull String secret) throws LocalException, AutopushClientException {
- final Delegate<Void> delegate = new Delegate<>();
- autopushClient.unregisterUserAgent(uaid, secret, delegate);
- delegate.responseOrThrow(); // For side-effects only.
- }
-
- public SubscribeChannelResponse subscribeChannel(@NonNull String uaid, @NonNull String secret, @Nullable String appServerKey) throws LocalException, AutopushClientException {
- final Delegate<SubscribeChannelResponse> delegate = new Delegate<>();
- autopushClient.subscribeChannel(uaid, secret, appServerKey, delegate);
- return delegate.responseOrThrow();
- }
-
- public void unsubscribeChannel(@NonNull String uaid, @NonNull String secret, @NonNull String chid) throws LocalException, AutopushClientException {
- final Delegate<Void> delegate = new Delegate<>();
- autopushClient.unsubscribeChannel(uaid, secret, chid, delegate);
- delegate.responseOrThrow(); // For side-effects only.
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/push/PushManager.java b/mobile/android/base/java/org/mozilla/gecko/push/PushManager.java
deleted file mode 100644
index 42ef60b61..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/push/PushManager.java
+++ /dev/null
@@ -1,354 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.push;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.util.Log;
-
-import org.json.JSONObject;
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.gcm.GcmTokenClient;
-import org.mozilla.gecko.push.autopush.AutopushClientException;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import java.io.IOException;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-
-/**
- * The push manager advances push registrations, ensuring that the upstream autopush endpoint has
- * a fresh GCM token. It brokers channel subscription requests to the upstream and maintains
- * local state.
- * <p/>
- * This class is not thread safe. An individual instance should be accessed on a single
- * (background) thread.
- */
-public class PushManager {
- public static final long TIME_BETWEEN_AUTOPUSH_UAID_REGISTRATION_IN_MILLIS = 7 * 24 * 60 * 60 * 1000L; // One week.
-
- public static class ProfileNeedsConfigurationException extends Exception {
- private static final long serialVersionUID = 3326738888L;
-
- public ProfileNeedsConfigurationException() {
- super();
- }
- }
-
- private static final String LOG_TAG = "GeckoPushManager";
-
- protected final @NonNull PushState state;
- protected final @NonNull GcmTokenClient gcmClient;
- protected final @NonNull PushClientFactory pushClientFactory;
-
- // For testing only.
- public interface PushClientFactory {
- PushClient getPushClient(String autopushEndpoint, boolean debug);
- }
-
- public PushManager(@NonNull PushState state, @NonNull GcmTokenClient gcmClient, @NonNull PushClientFactory pushClientFactory) {
- this.state = state;
- this.gcmClient = gcmClient;
- this.pushClientFactory = pushClientFactory;
- }
-
- public PushRegistration registrationForSubscription(String chid) {
- // chids are globally unique, so we're not concerned about finding a chid associated to
- // any particular profile.
- for (Map.Entry<String, PushRegistration> entry : state.getRegistrations().entrySet()) {
- final PushSubscription subscription = entry.getValue().getSubscription(chid);
- if (subscription != null) {
- return entry.getValue();
- }
- }
- return null;
- }
-
- public Map<String, PushSubscription> allSubscriptionsForProfile(String profileName) {
- final PushRegistration registration = state.getRegistration(profileName);
- if (registration == null) {
- return Collections.emptyMap();
- }
- return Collections.unmodifiableMap(registration.subscriptions);
- }
-
- public PushRegistration registerUserAgent(final @NonNull String profileName, final long now) throws ProfileNeedsConfigurationException, AutopushClientException, PushClient.LocalException, GcmTokenClient.NeedsGooglePlayServicesException, IOException {
- Log.i(LOG_TAG, "Registering user agent for profile named: " + profileName);
- return advanceRegistration(profileName, now);
- }
-
- public PushRegistration unregisterUserAgent(final @NonNull String profileName, final long now) throws ProfileNeedsConfigurationException {
- Log.i(LOG_TAG, "Unregistering user agent for profile named: " + profileName);
-
- final PushRegistration registration = state.getRegistration(profileName);
- if (registration == null) {
- Log.w(LOG_TAG, "Cannot find registration corresponding to subscription; not unregistering remote uaid for profileName: " + profileName);
- return null;
- }
-
- final String uaid = registration.uaid.value;
- final String secret = registration.secret;
- if (uaid == null || secret == null) {
- Log.e(LOG_TAG, "Cannot unregisterUserAgent with null registration uaid or secret!");
- return null;
- }
-
- unregisterUserAgentOnBackgroundThread(registration);
- return registration;
- }
-
- public PushSubscription subscribeChannel(final @NonNull String profileName, final @NonNull String service, final @NonNull JSONObject serviceData, @Nullable String appServerKey, final long now) throws ProfileNeedsConfigurationException, AutopushClientException, PushClient.LocalException, GcmTokenClient.NeedsGooglePlayServicesException, IOException {
- Log.i(LOG_TAG, "Subscribing to channel for service: " + service + "; for profile named: " + profileName);
- final PushRegistration registration = advanceRegistration(profileName, now);
- final PushSubscription subscription = subscribeChannel(registration, profileName, service, serviceData, appServerKey, System.currentTimeMillis());
- return subscription;
- }
-
- protected PushSubscription subscribeChannel(final @NonNull PushRegistration registration, final @NonNull String profileName, final @NonNull String service, final @NonNull JSONObject serviceData, @Nullable String appServerKey, final long now) throws AutopushClientException, PushClient.LocalException {
- final String uaid = registration.uaid.value;
- final String secret = registration.secret;
- if (uaid == null || secret == null) {
- throw new IllegalStateException("Cannot subscribeChannel with null uaid or secret!");
- }
-
- // Verify endpoint is not null?
- final PushClient pushClient = pushClientFactory.getPushClient(registration.autopushEndpoint, registration.debug);
-
- final SubscribeChannelResponse result = pushClient.subscribeChannel(uaid, secret, appServerKey);
- if (registration.debug) {
- Log.i(LOG_TAG, "Got chid: " + result.channelID + " and endpoint: " + result.endpoint);
- } else {
- Log.i(LOG_TAG, "Got chid and endpoint.");
- }
-
- final PushSubscription subscription = new PushSubscription(result.channelID, profileName, result.endpoint, service, serviceData);
- registration.putSubscription(result.channelID, subscription);
- state.checkpoint();
-
- return subscription;
- }
-
- public PushSubscription unsubscribeChannel(final @NonNull String chid) {
- Log.i(LOG_TAG, "Unsubscribing from channel with chid: " + chid);
-
- final PushRegistration registration = registrationForSubscription(chid);
- if (registration == null) {
- Log.w(LOG_TAG, "Cannot find registration corresponding to subscription; not unregistering remote subscription: " + chid);
- return null;
- }
-
- // We remove the local subscription before the remote subscription: without the local
- // subscription we'll ignoring incoming messages, and after some amount of time the
- // server will expire the channel due to non-activity. This is also Desktop's approach.
- final PushSubscription subscription = registration.removeSubscription(chid);
- state.checkpoint();
-
- if (subscription == null) {
- // This should never happen.
- Log.e(LOG_TAG, "Subscription did not exist: " + chid);
- return null;
- }
-
- final String uaid = registration.uaid.value;
- final String secret = registration.secret;
- if (uaid == null || secret == null) {
- Log.e(LOG_TAG, "Cannot unsubscribeChannel with null registration uaid or secret!");
- return null;
- }
-
- final PushClient pushClient = pushClientFactory.getPushClient(registration.autopushEndpoint, registration.debug);
- // Fire and forget.
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- try {
- pushClient.unsubscribeChannel(registration.uaid.value, registration.secret, chid);
- Log.i(LOG_TAG, "Unsubscribed from channel with chid: " + chid);
- } catch (PushClient.LocalException | AutopushClientException e) {
- Log.w(LOG_TAG, "Failed to unsubscribe from channel with chid; ignoring: " + chid, e);
- }
- }
- });
-
- return subscription;
- }
-
- public PushRegistration configure(final @NonNull String profileName, final @NonNull String endpoint, final boolean debug, final long now) {
- Log.i(LOG_TAG, "Updating configuration.");
- final PushRegistration registration = state.getRegistration(profileName);
- final PushRegistration newRegistration;
- if (registration != null) {
- if (!endpoint.equals(registration.autopushEndpoint)) {
- if (debug) {
- Log.i(LOG_TAG, "Push configuration autopushEndpoint changed! Was: " + registration.autopushEndpoint + "; now: " + endpoint);
- } else {
- Log.i(LOG_TAG, "Push configuration autopushEndpoint changed!");
- }
-
- newRegistration = new PushRegistration(endpoint, debug, Fetched.now(null), null);
-
- if (registration.uaid.value != null) {
- // New endpoint! All registrations and subscriptions have been dropped, and
- // should be removed remotely.
- unregisterUserAgentOnBackgroundThread(registration);
- }
- } else if (debug != registration.debug) {
- Log.i(LOG_TAG, "Push configuration debug changed: " + debug);
- newRegistration = registration.withDebug(debug);
- } else {
- newRegistration = registration;
- }
- } else {
- if (debug) {
- Log.i(LOG_TAG, "Push configuration set: " + endpoint + "; debug: " + debug);
- } else {
- Log.i(LOG_TAG, "Push configuration set!");
- }
- newRegistration = new PushRegistration(endpoint, debug, new Fetched(null, now), null);
- }
-
- if (newRegistration != registration) {
- state.putRegistration(profileName, newRegistration);
- state.checkpoint();
- }
-
- return newRegistration;
- }
-
- private void unregisterUserAgentOnBackgroundThread(final PushRegistration registration) {
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- try {
- pushClientFactory.getPushClient(registration.autopushEndpoint, registration.debug).unregisterUserAgent(registration.uaid.value, registration.secret);
- Log.i(LOG_TAG, "Unregistered user agent with uaid: " + registration.uaid.value);
- } catch (PushClient.LocalException | AutopushClientException e) {
- Log.w(LOG_TAG, "Failed to unregister user agent with uaid; ignoring: " + registration.uaid.value, e);
- }
- }
- });
- }
-
- protected @NonNull PushRegistration advanceRegistration(final @NonNull String profileName, final long now) throws ProfileNeedsConfigurationException, AutopushClientException, PushClient.LocalException, GcmTokenClient.NeedsGooglePlayServicesException, IOException {
- final PushRegistration registration = state.getRegistration(profileName);
- if (registration == null || registration.autopushEndpoint == null) {
- Log.i(LOG_TAG, "Cannot advance to registered: registration needs configuration.");
- throw new ProfileNeedsConfigurationException();
- }
- return advanceRegistration(registration, profileName, now);
- }
-
- protected @NonNull PushRegistration advanceRegistration(final PushRegistration registration, final @NonNull String profileName, final long now) throws AutopushClientException, PushClient.LocalException, GcmTokenClient.NeedsGooglePlayServicesException, IOException {
- final Fetched gcmToken = gcmClient.getToken(AppConstants.MOZ_ANDROID_GCM_SENDERID, registration.debug);
-
- final PushClient pushClient = pushClientFactory.getPushClient(registration.autopushEndpoint, registration.debug);
-
- if (registration.uaid.value == null) {
- if (registration.debug) {
- Log.i(LOG_TAG, "No uaid; requesting from autopush endpoint: " + registration.autopushEndpoint);
- } else {
- Log.i(LOG_TAG, "No uaid: requesting from autopush endpoint.");
- }
- final RegisterUserAgentResponse result = pushClient.registerUserAgent(gcmToken.value);
- if (registration.debug) {
- Log.i(LOG_TAG, "Got uaid: " + result.uaid + " and secret: " + result.secret);
- } else {
- Log.i(LOG_TAG, "Got uaid and secret.");
- }
- final long nextNow = System.currentTimeMillis();
- final PushRegistration nextRegistration = registration.withUserAgentID(result.uaid, result.secret, nextNow);
- state.putRegistration(profileName, nextRegistration);
- state.checkpoint();
- return advanceRegistration(nextRegistration, profileName, nextNow);
- }
-
- if (registration.uaid.timestamp + TIME_BETWEEN_AUTOPUSH_UAID_REGISTRATION_IN_MILLIS < now
- || registration.uaid.timestamp < gcmToken.timestamp) {
- if (registration.debug) {
- Log.i(LOG_TAG, "Stale uaid; re-registering with autopush endpoint: " + registration.autopushEndpoint);
- } else {
- Log.i(LOG_TAG, "Stale uaid: re-registering with autopush endpoint.");
- }
-
- pushClient.reregisterUserAgent(registration.uaid.value, registration.secret, gcmToken.value);
-
- Log.i(LOG_TAG, "Re-registered uaid and secret.");
- final long nextNow = System.currentTimeMillis();
- final PushRegistration nextRegistration = registration.withUserAgentID(registration.uaid.value, registration.secret, nextNow);
- state.putRegistration(profileName, nextRegistration);
- state.checkpoint();
- return advanceRegistration(nextRegistration, profileName, nextNow);
- }
-
- Log.d(LOG_TAG, "Existing uaid is fresh; no need to request from autopush endpoint.");
- return registration;
- }
-
- public void invalidateGcmToken() {
- gcmClient.invalidateToken();
- }
-
- public void startup(long now) {
- try {
- Log.i(LOG_TAG, "Startup: requesting GCM token.");
- gcmClient.getToken(AppConstants.MOZ_ANDROID_GCM_SENDERID, false); // For side-effects.
- } catch (GcmTokenClient.NeedsGooglePlayServicesException e) {
- // Requires user intervention. At App startup, we don't want to address this. In
- // response to user activity, we do want to try to have the user address this.
- Log.w(LOG_TAG, "Startup: needs Google Play Services. Ignoring until GCM is requested in response to user activity.");
- return;
- } catch (IOException e) {
- // We're temporarily unable to get a GCM token. There's nothing to be done; we'll
- // try to advance the App's state in response to user activity or at next startup.
- Log.w(LOG_TAG, "Startup: Google Play Services is available, but we can't get a token; ignoring.", e);
- return;
- }
-
- Log.i(LOG_TAG, "Startup: advancing all registrations.");
- final Map<String, PushRegistration> registrations = state.getRegistrations();
-
- // Now advance all registrations.
- try {
- final Iterator<Map.Entry<String, PushRegistration>> it = registrations.entrySet().iterator();
- while (it.hasNext()) {
- final Map.Entry<String, PushRegistration> entry = it.next();
- final String profileName = entry.getKey();
- final PushRegistration registration = entry.getValue();
- if (registration.subscriptions.isEmpty()) {
- Log.i(LOG_TAG, "Startup: no subscriptions for profileName; not advancing registration: " + profileName);
- continue;
- }
-
- try {
- advanceRegistration(profileName, now); // For side-effects.
- Log.i(LOG_TAG, "Startup: advanced registration for profileName: " + profileName);
- } catch (ProfileNeedsConfigurationException e) {
- Log.i(LOG_TAG, "Startup: cannot advance registration for profileName: " + profileName + "; profile needs configuration from Gecko.");
- } catch (AutopushClientException e) {
- if (e.isTransientError()) {
- Log.w(LOG_TAG, "Startup: cannot advance registration for profileName: " + profileName + "; got transient autopush error. Ignoring; will advance on demand.", e);
- } else {
- Log.w(LOG_TAG, "Startup: cannot advance registration for profileName: " + profileName + "; got permanent autopush error. Removing registration entirely.", e);
- it.remove();
- }
- } catch (PushClient.LocalException e) {
- Log.w(LOG_TAG, "Startup: cannot advance registration for profileName: " + profileName + "; got local exception. Ignoring; will advance on demand.", e);
- }
- }
- } catch (GcmTokenClient.NeedsGooglePlayServicesException e) {
- Log.w(LOG_TAG, "Startup: cannot advance any registrations; need Google Play Services!", e);
- return;
- } catch (IOException e) {
- Log.w(LOG_TAG, "Startup: cannot advance any registrations; intermittent Google Play Services exception; ignoring, will advance on demand.", e);
- return;
- }
-
- // We may have removed registrations above. Checkpoint just to be safe!
- state.checkpoint();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/push/PushRegistration.java b/mobile/android/base/java/org/mozilla/gecko/push/PushRegistration.java
deleted file mode 100644
index a991774ff..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/push/PushRegistration.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.push;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-
-/**
- * Represent an autopush User Agent registration.
- * <p/>
- * Such a registration associates an endpoint, optional debug flag, some Google
- * Cloud Messaging data, and the returned uaid and secret.
- * <p/>
- * Each registration is associated to a single Gecko profile, although we don't
- * enforce that here. This class is immutable, so it is by definition
- * thread-safe.
- */
-public class PushRegistration {
- public final String autopushEndpoint;
- public final boolean debug;
- // TODO: fold (timestamp, {uaid, secret}) into this class.
- public final @NonNull Fetched uaid;
- public final String secret;
-
- protected final @NonNull Map<String, PushSubscription> subscriptions;
-
- public PushRegistration(String autopushEndpoint, boolean debug, @NonNull Fetched uaid, @Nullable String secret, @NonNull Map<String, PushSubscription> subscriptions) {
- this.autopushEndpoint = autopushEndpoint;
- this.debug = debug;
- this.uaid = uaid;
- this.secret = secret;
- this.subscriptions = subscriptions;
- }
-
- public PushRegistration(String autopushEndpoint, boolean debug, @NonNull Fetched uaid, @Nullable String secret) {
- this(autopushEndpoint, debug, uaid, secret, new HashMap<String, PushSubscription>());
- }
-
- public JSONObject toJSONObject() throws JSONException {
- final JSONObject subscriptions = new JSONObject();
- for (Map.Entry<String, PushSubscription> entry : this.subscriptions.entrySet()) {
- subscriptions.put(entry.getKey(), entry.getValue().toJSONObject());
- }
-
- final JSONObject jsonObject = new JSONObject();
- jsonObject.put("autopushEndpoint", autopushEndpoint);
- jsonObject.put("debug", debug);
- jsonObject.put("uaid", uaid.toJSONObject());
- jsonObject.put("secret", secret);
- jsonObject.put("subscriptions", subscriptions);
- return jsonObject;
- }
-
- public static PushRegistration fromJSONObject(@NonNull JSONObject registration) throws JSONException {
- final String endpoint = registration.optString("autopushEndpoint", null);
- final boolean debug = registration.getBoolean("debug");
- final Fetched uaid = Fetched.fromJSONObject(registration.getJSONObject("uaid"));
- final String secret = registration.optString("secret", null);
-
- final JSONObject subscriptionsObject = registration.getJSONObject("subscriptions");
- final Map<String, PushSubscription> subscriptions = new HashMap<>();
- final Iterator<String> it = subscriptionsObject.keys();
- while (it.hasNext()) {
- final String chid = it.next();
- subscriptions.put(chid, PushSubscription.fromJSONObject(subscriptionsObject.getJSONObject(chid)));
- }
-
- return new PushRegistration(endpoint, debug, uaid, secret, subscriptions);
- }
-
- @Override
- public boolean equals(Object o) {
- // Auto-generated.
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- PushRegistration that = (PushRegistration) o;
-
- if (autopushEndpoint != null ? !autopushEndpoint.equals(that.autopushEndpoint) : that.autopushEndpoint != null)
- return false;
- if (!uaid.equals(that.uaid)) return false;
- if (secret != null ? !secret.equals(that.secret) : that.secret != null) return false;
- if (subscriptions != null ? !subscriptions.equals(that.subscriptions) : that.subscriptions != null) return false;
- return (debug == that.debug);
- }
-
- @Override
- public int hashCode() {
- // Auto-generated.
- int result = autopushEndpoint != null ? autopushEndpoint.hashCode() : 0;
- result = 31 * result + (debug ? 1 : 0);
- result = 31 * result + uaid.hashCode();
- result = 31 * result + (secret != null ? secret.hashCode() : 0);
- result = 31 * result + (subscriptions != null ? subscriptions.hashCode() : 0);
- return result;
- }
-
- public PushRegistration withDebug(boolean debug) {
- return new PushRegistration(this.autopushEndpoint, debug, this.uaid, this.secret, this.subscriptions);
- }
-
- public PushRegistration withUserAgentID(String uaid, String secret, long nextNow) {
- return new PushRegistration(this.autopushEndpoint, this.debug, new Fetched(uaid, nextNow), secret, this.subscriptions);
- }
-
- public PushSubscription getSubscription(@NonNull String chid) {
- return subscriptions.get(chid);
- }
-
- public PushSubscription putSubscription(@NonNull String chid, @NonNull PushSubscription subscription) {
- return subscriptions.put(chid, subscription);
- }
-
- public PushSubscription removeSubscription(@NonNull String chid) {
- return subscriptions.remove(chid);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/push/PushService.java b/mobile/android/base/java/org/mozilla/gecko/push/PushService.java
deleted file mode 100644
index 7c3a9434f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/push/PushService.java
+++ /dev/null
@@ -1,440 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.push;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.util.Log;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.GeckoService;
-import org.mozilla.gecko.GeckoThread;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.annotation.ReflectionTarget;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.gcm.GcmTokenClient;
-import org.mozilla.gecko.push.autopush.AutopushClientException;
-import org.mozilla.gecko.util.BundleEventListener;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Class that handles messages used in the Google Cloud Messaging and DOM push API integration.
- * <p/>
- * This singleton services Gecko messages from dom/push/PushServiceAndroidGCM.jsm and Google Cloud
- * Messaging requests.
- * <p/>
- * It is expected that Gecko is started (if not already running) soon after receiving GCM messages
- * otherwise there is a greater risk that pending messages that have not been handle by Gecko will
- * be lost if this service is killed.
- * <p/>
- * It's worth noting that we allow the DOM push API in restricted profiles.
- */
-@ReflectionTarget
-public class PushService implements BundleEventListener {
- private static final String LOG_TAG = "GeckoPushService";
-
- public static final String SERVICE_WEBPUSH = "webpush";
- public static final String SERVICE_FXA = "fxa";
-
- private static PushService sInstance;
-
- private static final String[] GECKO_EVENTS = new String[] {
- "PushServiceAndroidGCM:Configure",
- "PushServiceAndroidGCM:DumpRegistration",
- "PushServiceAndroidGCM:DumpSubscriptions",
- "PushServiceAndroidGCM:Initialized",
- "PushServiceAndroidGCM:Uninitialized",
- "PushServiceAndroidGCM:RegisterUserAgent",
- "PushServiceAndroidGCM:UnregisterUserAgent",
- "PushServiceAndroidGCM:SubscribeChannel",
- "PushServiceAndroidGCM:UnsubscribeChannel",
- "History:GetPrePathLastVisitedTimeMilliseconds",
- };
-
- private enum GeckoComponent {
- PushServiceAndroidGCM
- }
-
- public static synchronized PushService getInstance(Context context) {
- if (sInstance == null) {
- onCreate(context);
- }
- return sInstance;
- }
-
- @ReflectionTarget
- public static synchronized void onCreate(Context context) {
- if (sInstance != null) {
- return;
- }
- sInstance = new PushService(context);
-
- sInstance.registerGeckoEventListener();
- sInstance.onStartup();
- }
-
- protected final PushManager pushManager;
-
- // NB: These are not thread-safe, we're depending on these being access from the same background thread.
- private boolean isReadyPushServiceAndroidGCM = false;
- private final List<JSONObject> pendingPushMessages;
-
- public PushService(Context context) {
- pushManager = new PushManager(new PushState(context, "GeckoPushState.json"), new GcmTokenClient(context), new PushManager.PushClientFactory() {
- @Override
- public PushClient getPushClient(String autopushEndpoint, boolean debug) {
- return new PushClient(autopushEndpoint);
- }
- });
-
- pendingPushMessages = new LinkedList<>();
- }
-
- public void onStartup() {
- Log.i(LOG_TAG, "Starting up.");
- ThreadUtils.assertOnBackgroundThread();
-
- try {
- pushManager.startup(System.currentTimeMillis());
- } catch (Exception e) {
- Log.e(LOG_TAG, "Got exception during startup; ignoring.", e);
- return;
- }
- }
-
- public void onRefresh() {
- Log.i(LOG_TAG, "Google Play Services requested GCM token refresh; invalidating GCM token and running startup again.");
- ThreadUtils.assertOnBackgroundThread();
-
- pushManager.invalidateGcmToken();
- try {
- pushManager.startup(System.currentTimeMillis());
- } catch (Exception e) {
- Log.e(LOG_TAG, "Got exception during refresh; ignoring.", e);
- return;
- }
- }
-
- public void onMessageReceived(final @NonNull Context context, final @NonNull Bundle bundle) {
- Log.i(LOG_TAG, "Google Play Services GCM message received; delivering.");
- ThreadUtils.assertOnBackgroundThread();
-
- final String chid = bundle.getString("chid");
- if (chid == null) {
- Log.w(LOG_TAG, "No chid found; ignoring message.");
- return;
- }
-
- final PushRegistration registration = pushManager.registrationForSubscription(chid);
- if (registration == null) {
- Log.w(LOG_TAG, "Cannot find registration corresponding to subscription for chid: " + chid + "; ignoring message.");
- return;
- }
-
- final PushSubscription subscription = registration.getSubscription(chid);
- if (subscription == null) {
- // This should never happen. There's not much to be done; in the future, perhaps we
- // could try to drop the remote subscription?
- Log.e(LOG_TAG, "No subscription found for chid: " + chid + "; ignoring message.");
- return;
- }
-
- boolean isWebPush = SERVICE_WEBPUSH.equals(subscription.service);
- boolean isFxAPush = SERVICE_FXA.equals(subscription.service);
- if (!isWebPush && !isFxAPush) {
- Log.e(LOG_TAG, "Message directed to unknown service; dropping: " + subscription.service);
- return;
- }
-
- Log.i(LOG_TAG, "Message directed to service: " + subscription.service);
-
- if (subscription.serviceData == null) {
- Log.e(LOG_TAG, "No serviceData found for chid: " + chid + "; ignoring dom/push message.");
- return;
- }
-
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.SERVICE, "dom-push-api");
-
- final String profileName = subscription.serviceData.optString("profileName", null);
- final String profilePath = subscription.serviceData.optString("profilePath", null);
- if (profileName == null || profilePath == null) {
- Log.e(LOG_TAG, "Corrupt serviceData found for chid: " + chid + "; ignoring dom/push message.");
- return;
- }
-
- if (canSendPushMessagesToGecko()) {
- if (!GeckoThread.canUseProfile(profileName, new File(profilePath))) {
- Log.e(LOG_TAG, "Mismatched profile for chid: " + chid + "; ignoring dom/push message.");
- return;
- }
- } else {
- final Intent intent = GeckoService.getIntentToCreateServices(context, "android-push-service");
- GeckoService.setIntentProfile(intent, profileName, profilePath);
- context.startService(intent);
- }
-
- final JSONObject data = new JSONObject();
- try {
- data.put("channelID", chid);
- data.put("con", bundle.getString("con"));
- data.put("enc", bundle.getString("enc"));
- // Only one of cryptokey (newer) and enckey (deprecated) should be set, but the
- // Gecko handler will verify this.
- data.put("cryptokey", bundle.getString("cryptokey"));
- data.put("enckey", bundle.getString("enckey"));
- data.put("message", bundle.getString("body"));
-
- if (!canSendPushMessagesToGecko()) {
- data.put("profileName", profileName);
- data.put("profilePath", profilePath);
- data.put("service", subscription.service);
- }
- } catch (JSONException e) {
- Log.e(LOG_TAG, "Got exception delivering dom/push message to Gecko!", e);
- return;
- }
-
- if (!canSendPushMessagesToGecko()) {
- Log.i(LOG_TAG, "Required service not initialized, adding message to queue.");
- pendingPushMessages.add(data);
- return;
- }
-
- if (isWebPush) {
- sendMessageToGeckoService(data);
- } else {
- sendMessageToDecodeToGeckoService(data);
- }
- }
-
- protected static void sendMessageToGeckoService(final @NonNull JSONObject message) {
- Log.i(LOG_TAG, "Delivering dom/push message to Gecko!");
- GeckoAppShell.notifyObservers("PushServiceAndroidGCM:ReceivedPushMessage",
- message.toString(),
- GeckoThread.State.PROFILE_READY);
- }
-
- protected static void sendMessageToDecodeToGeckoService(final @NonNull JSONObject message) {
- Log.i(LOG_TAG, "Delivering dom/push message to decode to Gecko!");
- }
-
- protected void registerGeckoEventListener() {
- Log.d(LOG_TAG, "Registered Gecko event listener.");
- EventDispatcher.getInstance().registerBackgroundThreadListener(this, GECKO_EVENTS);
- }
-
- protected void unregisterGeckoEventListener() {
- Log.d(LOG_TAG, "Unregistered Gecko event listener.");
- EventDispatcher.getInstance().unregisterBackgroundThreadListener(this, GECKO_EVENTS);
- }
-
- @Override
- public void handleMessage(final String event, final Bundle message, final EventCallback callback) {
- Log.i(LOG_TAG, "Handling event: " + event);
- ThreadUtils.assertOnBackgroundThread();
-
- final Context context = GeckoAppShell.getApplicationContext();
- // We're invoked in response to a Gecko message on a background thread. We should always
- // be able to safely retrieve the current Gecko profile.
- final GeckoProfile geckoProfile = GeckoProfile.get(context);
-
- if (callback == null) {
- Log.e(LOG_TAG, "callback must not be null in " + event);
- return;
- }
-
- try {
- if ("PushServiceAndroidGCM:Initialized".equals(event)) {
- processComponentState(GeckoComponent.PushServiceAndroidGCM, true);
- callback.sendSuccess(null);
- return;
- }
- if ("PushServiceAndroidGCM:Uninitialized".equals(event)) {
- processComponentState(GeckoComponent.PushServiceAndroidGCM, false);
- callback.sendSuccess(null);
- return;
- }
- if ("PushServiceAndroidGCM:Configure".equals(event)) {
- final String endpoint = message.getString("endpoint");
- if (endpoint == null) {
- callback.sendError("endpoint must not be null in " + event);
- return;
- }
- final boolean debug = message.getBoolean("debug", false);
- pushManager.configure(geckoProfile.getName(), endpoint, debug, System.currentTimeMillis()); // For side effects.
- callback.sendSuccess(null);
- return;
- }
- if ("PushServiceAndroidGCM:DumpRegistration".equals(event)) {
- // In the future, this might be used to interrogate the Java Push Manager
- // registration state from JavaScript.
- callback.sendError("Not yet implemented!");
- return;
- }
- if ("PushServiceAndroidGCM:DumpSubscriptions".equals(event)) {
- try {
- final Map<String, PushSubscription> result = pushManager.allSubscriptionsForProfile(geckoProfile.getName());
-
- final JSONObject json = new JSONObject();
- for (Map.Entry<String, PushSubscription> entry : result.entrySet()) {
- json.put(entry.getKey(), entry.getValue().toJSONObject());
- }
- callback.sendSuccess(json);
- } catch (JSONException e) {
- callback.sendError("Got exception handling message [" + event + "]: " + e.toString());
- }
- return;
- }
- if ("PushServiceAndroidGCM:RegisterUserAgent".equals(event)) {
- try {
- pushManager.registerUserAgent(geckoProfile.getName(), System.currentTimeMillis()); // For side-effects.
- callback.sendSuccess(null);
- } catch (PushManager.ProfileNeedsConfigurationException | AutopushClientException | PushClient.LocalException | IOException e) {
- Log.e(LOG_TAG, "Got exception in " + event, e);
- callback.sendError("Got exception handling message [" + event + "]: " + e.toString());
- }
- return;
- }
- if ("PushServiceAndroidGCM:UnregisterUserAgent".equals(event)) {
- // In the future, this might be used to tell the Java Push Manager to unregister
- // a User Agent entirely from JavaScript. Right now, however, everything is
- // subscription based; there's no concept of unregistering all subscriptions
- // simultaneously.
- callback.sendError("Not yet implemented!");
- return;
- }
- if ("PushServiceAndroidGCM:SubscribeChannel".equals(event)) {
- final String service = SERVICE_FXA.equals(message.getString("service")) ?
- SERVICE_FXA :
- SERVICE_WEBPUSH;
- final JSONObject serviceData;
- final String appServerKey = message.getString("appServerKey");
- try {
- serviceData = new JSONObject();
- serviceData.put("profileName", geckoProfile.getName());
- serviceData.put("profilePath", geckoProfile.getDir().getAbsolutePath());
- } catch (JSONException e) {
- Log.e(LOG_TAG, "Got exception in " + event, e);
- callback.sendError("Got exception handling message [" + event + "]: " + e.toString());
- return;
- }
-
- final PushSubscription subscription;
- try {
- subscription = pushManager.subscribeChannel(geckoProfile.getName(), service, serviceData, appServerKey, System.currentTimeMillis());
- } catch (PushManager.ProfileNeedsConfigurationException | AutopushClientException | PushClient.LocalException | IOException e) {
- Log.e(LOG_TAG, "Got exception in " + event, e);
- callback.sendError("Got exception handling message [" + event + "]: " + e.toString());
- return;
- }
-
- final JSONObject json = new JSONObject();
- try {
- json.put("channelID", subscription.chid);
- json.put("endpoint", subscription.webpushEndpoint);
- } catch (JSONException e) {
- Log.e(LOG_TAG, "Got exception in " + event, e);
- callback.sendError("Got exception handling message [" + event + "]: " + e.toString());
- return;
- }
-
- Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.SERVICE, "dom-push-api");
- callback.sendSuccess(json);
- return;
- }
- if ("PushServiceAndroidGCM:UnsubscribeChannel".equals(event)) {
- final String channelID = message.getString("channelID");
- if (channelID == null) {
- callback.sendError("channelID must not be null in " + event);
- return;
- }
-
- // Fire and forget. See comments in the function itself.
- final PushSubscription pushSubscription = pushManager.unsubscribeChannel(channelID);
- if (pushSubscription != null) {
- Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.SERVICE, "dom-push-api");
- callback.sendSuccess(null);
- return;
- }
-
- callback.sendError("Could not unsubscribe from channel: " + channelID);
- return;
- }
- if ("History:GetPrePathLastVisitedTimeMilliseconds".equals(event)) {
- if (callback == null) {
- Log.e(LOG_TAG, "callback must not be null in " + event);
- return;
- }
- final String prePath = message.getString("prePath");
- if (prePath == null) {
- callback.sendError("prePath must not be null in " + event);
- return;
- }
- // We're on a background thread, so we can be synchronous.
- final long millis = BrowserDB.from(geckoProfile).getPrePathLastVisitedTimeMilliseconds(
- context.getContentResolver(), prePath);
- callback.sendSuccess(millis);
- return;
- }
- } catch (GcmTokenClient.NeedsGooglePlayServicesException e) {
- // TODO: improve this. Can we find a point where the user is *definitely* interacting
- // with the WebPush? Perhaps we can show a dialog when interacting with the Push
- // permissions, and then be more aggressive showing this notification when we have
- // registrations and subscriptions that can't be advanced.
- callback.sendError("To handle event [" + event + "], user interaction is needed to enable Google Play Services.");
- }
- }
-
- private void processComponentState(@NonNull GeckoComponent component, boolean isReady) {
- if (component == GeckoComponent.PushServiceAndroidGCM) {
- isReadyPushServiceAndroidGCM = isReady;
- }
-
- // Send all pending messages to Gecko.
- if (canSendPushMessagesToGecko()) {
- sendPushMessagesToGecko(pendingPushMessages);
- pendingPushMessages.clear();
- }
- }
-
- private boolean canSendPushMessagesToGecko() {
- return isReadyPushServiceAndroidGCM;
- }
-
- private static void sendPushMessagesToGecko(@NonNull List<JSONObject> messages) {
- for (JSONObject pushMessage : messages) {
- final String profileName = pushMessage.optString("profileName", null);
- final String profilePath = pushMessage.optString("profilePath", null);
- final String service = pushMessage.optString("service", null);
- if (profileName == null || profilePath == null ||
- !GeckoThread.canUseProfile(profileName, new File(profilePath))) {
- Log.e(LOG_TAG, "Mismatched profile for chid: " +
- pushMessage.optString("channelID") +
- "; ignoring dom/push message.");
- continue;
- }
- if (SERVICE_WEBPUSH.equals(service)) {
- sendMessageToGeckoService(pushMessage);
- } else if (SERVICE_FXA.equals(service)) {
- sendMessageToDecodeToGeckoService(pushMessage);
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/push/PushState.java b/mobile/android/base/java/org/mozilla/gecko/push/PushState.java
deleted file mode 100644
index 686bf5a0d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/push/PushState.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.push;
-
-import android.content.Context;
-import android.support.annotation.NonNull;
-import android.support.annotation.WorkerThread;
-import android.support.v4.util.AtomicFile;
-import android.util.Log;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-
-/**
- * Firefox for Android maintains an App-wide mapping associating
- * profile names to push registrations. Each push registration in turn associates channels to
- * push subscriptions.
- * <p/>
- * We use a simple storage model of JSON backed by an atomic file. It is assumed that instances
- * of this class will reference distinct files on disk; and that all accesses will be happen on a
- * single (worker thread).
- */
-public class PushState {
- private static final String LOG_TAG = "GeckoPushState";
-
- private static final long VERSION = 1L;
-
- protected final @NonNull AtomicFile file;
-
- protected final @NonNull Map<String, PushRegistration> registrations;
-
- public PushState(Context context, @NonNull String fileName) {
- this.registrations = new HashMap<>();
-
- file = new AtomicFile(new File(context.getApplicationInfo().dataDir, fileName));
- synchronized (file) {
- try {
- final String s = new String(file.readFully(), "UTF-8");
- final JSONObject temp = new JSONObject(s);
- if (temp.optLong("version", 0L) != VERSION) {
- throw new JSONException("Unknown version!");
- }
-
- final JSONObject registrationsObject = temp.getJSONObject("registrations");
- final Iterator<String> it = registrationsObject.keys();
- while (it.hasNext()) {
- final String profileName = it.next();
- final PushRegistration registration = PushRegistration.fromJSONObject(registrationsObject.getJSONObject(profileName));
- this.registrations.put(profileName, registration);
- }
- } catch (FileNotFoundException e) {
- Log.i(LOG_TAG, "No storage found; starting fresh.");
- this.registrations.clear();
- } catch (IOException | JSONException e) {
- Log.w(LOG_TAG, "Got exception reading storage; dropping storage and starting fresh.", e);
- this.registrations.clear();
- }
- }
- }
-
- public JSONObject toJSONObject() throws JSONException {
- final JSONObject registrations = new JSONObject();
- for (Map.Entry<String, PushRegistration> entry : this.registrations.entrySet()) {
- registrations.put(entry.getKey(), entry.getValue().toJSONObject());
- }
-
- final JSONObject jsonObject = new JSONObject();
- jsonObject.put("version", 1L);
- jsonObject.put("registrations", registrations);
- return jsonObject;
- }
-
- /**
- * Synchronously persist the cache to disk.
- * @return whether the cache was persisted successfully.
- */
- @WorkerThread
- public boolean checkpoint() {
- synchronized (file) {
- FileOutputStream fileOutputStream = null;
- try {
- fileOutputStream = file.startWrite();
- fileOutputStream.write(toJSONObject().toString().getBytes("UTF-8"));
- file.finishWrite(fileOutputStream);
- return true;
- } catch (JSONException | IOException e) {
- Log.e(LOG_TAG, "Got exception writing JSON storage; ignoring.", e);
- if (fileOutputStream != null) {
- file.failWrite(fileOutputStream);
- }
- return false;
- }
- }
- }
-
- public PushRegistration putRegistration(@NonNull String profileName, @NonNull PushRegistration registration) {
- return registrations.put(profileName, registration);
- }
-
- /**
- * Return the existing push registration for the given profile name.
- * @return the push registration, if one is registered; null otherwise.
- */
- public PushRegistration getRegistration(@NonNull String profileName) {
- return registrations.get(profileName);
- }
-
- /**
- * Return all push registrations, keyed by profile names.
- * @return a map of all push registrations. <b>The map is intentionally mutable - be careful!</b>
- */
- public @NonNull Map<String, PushRegistration> getRegistrations() {
- return registrations;
- }
-
- /**
- * Remove any existing push registration for the given profile name.
- * </p>
- * Most registration removals are during iteration, which should use an iterator that is
- * aware of removals.
- * @return the removed push registration, if one was removed; null otherwise.
- */
- public PushRegistration removeRegistration(@NonNull String profileName) {
- return registrations.remove(profileName);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/push/PushSubscription.java b/mobile/android/base/java/org/mozilla/gecko/push/PushSubscription.java
deleted file mode 100644
index ecf752591..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/push/PushSubscription.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.push;
-
-import android.support.annotation.NonNull;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-/**
- * Represent an autopush Channel subscription.
- * <p/>
- * Such a subscription associates a user agent and autopush data with a channel
- * ID, a WebPush endpoint, and some service-specific data.
- * <p/>
- * Cloud Messaging data, and the returned uaid and secret.
- * <p/>
- * Each registration is associated to a single Gecko profile, although we don't
- * enforce that here. This class is immutable, so it is by definition
- * thread-safe.
- */
-public class PushSubscription {
- public final @NonNull String chid;
- public final @NonNull String profileName;
- public final @NonNull String webpushEndpoint;
- public final @NonNull String service;
- public final JSONObject serviceData;
-
- public PushSubscription(@NonNull String chid, @NonNull String profileName, @NonNull String webpushEndpoint, @NonNull String service, JSONObject serviceData) {
- this.chid = chid;
- this.profileName = profileName;
- this.webpushEndpoint = webpushEndpoint;
- this.service = service;
- this.serviceData = serviceData;
- }
-
- public JSONObject toJSONObject() throws JSONException {
- final JSONObject jsonObject = new JSONObject();
- jsonObject.put("chid", chid);
- jsonObject.put("profileName", profileName);
- jsonObject.put("webpushEndpoint", webpushEndpoint);
- jsonObject.put("service", service);
- jsonObject.put("serviceData", serviceData);
- return jsonObject;
- }
-
- public static PushSubscription fromJSONObject(@NonNull JSONObject subscription) throws JSONException {
- final String chid = subscription.getString("chid");
- final String profileName = subscription.getString("profileName");
- final String webpushEndpoint = subscription.getString("webpushEndpoint");
- final String service = subscription.getString("service");
- final JSONObject serviceData = subscription.optJSONObject("serviceData");
- return new PushSubscription(chid, profileName, webpushEndpoint, service, serviceData);
- }
-
- @Override
- public boolean equals(Object o) {
- // Auto-generated.
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- PushSubscription that = (PushSubscription) o;
-
- if (!chid.equals(that.chid)) return false;
- if (!profileName.equals(that.profileName)) return false;
- if (!webpushEndpoint.equals(that.webpushEndpoint)) return false;
- return service.equals(that.service);
- }
-
- @Override
- public int hashCode() {
- // Auto-generated.
- int result = profileName.hashCode();
- result = 31 * result + chid.hashCode();
- result = 31 * result + webpushEndpoint.hashCode();
- result = 31 * result + service.hashCode();
- return result;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/reader/ReaderModeUtils.java b/mobile/android/base/java/org/mozilla/gecko/reader/ReaderModeUtils.java
deleted file mode 100644
index e70aac5b5..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/reader/ReaderModeUtils.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.reader;
-
-import org.mozilla.gecko.AboutPages;
-import org.mozilla.gecko.util.StringUtils;
-
-import android.net.Uri;
-
-public class ReaderModeUtils {
- private static final String LOGTAG = "ReaderModeUtils";
-
- /**
- * Extract the URL from a valid about:reader URL. You may want to use stripAboutReaderUrl
- * instead to always obtain a valid String.
- *
- * @see #stripAboutReaderUrl(String) for a safer version that returns the original URL for malformed/invalid
- * URLs.
- * @return <code>null</code> if the URL is malformed or doesn't contain a URL parameter.
- */
- private static String getUrlFromAboutReader(String aboutReaderUrl) {
- return StringUtils.getQueryParameter(aboutReaderUrl, "url");
- }
-
- public static boolean isEnteringReaderMode(String oldURL, String newURL) {
- if (oldURL == null || newURL == null) {
- return false;
- }
-
- if (!AboutPages.isAboutReader(newURL)) {
- return false;
- }
-
- String urlFromAboutReader = getUrlFromAboutReader(newURL);
- if (urlFromAboutReader == null) {
- return false;
- }
-
- return urlFromAboutReader.equals(oldURL);
- }
-
- public static String getAboutReaderForUrl(String url) {
- return getAboutReaderForUrl(url, -1);
- }
-
- /**
- * Obtain the underlying URL from an about:reader URL.
- * This will return the input URL if either of the following is true:
- * 1. the input URL is a non about:reader URL
- * 2. the input URL is an invalid/unparseable about:reader URL
- */
- public static String stripAboutReaderUrl(String url) {
- if (!AboutPages.isAboutReader(url)) {
- return url;
- }
-
- final String strippedUrl = getUrlFromAboutReader(url);
- return strippedUrl != null ? strippedUrl : url;
- }
-
- public static String getAboutReaderForUrl(String url, int tabId) {
- String aboutReaderUrl = AboutPages.READER + "?url=" + Uri.encode(url);
-
- if (tabId >= 0) {
- aboutReaderUrl += "&tabId=" + tabId;
- }
-
- return aboutReaderUrl;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/reader/ReadingListHelper.java b/mobile/android/base/java/org/mozilla/gecko/reader/ReadingListHelper.java
deleted file mode 100644
index e01ff79ac..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/reader/ReadingListHelper.java
+++ /dev/null
@@ -1,154 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.reader;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import org.mozilla.gecko.AboutPages;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.icons.IconRequest;
-import org.mozilla.gecko.icons.Icons;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.UIAsyncTask;
-
-import android.content.Context;
-import android.util.Log;
-
-import java.util.concurrent.ExecutionException;
-
-public final class ReadingListHelper implements NativeEventListener {
- private static final String LOGTAG = "GeckoReadingListHelper";
-
- protected final Context context;
- private final BrowserDB db;
-
- public ReadingListHelper(Context context, GeckoProfile profile) {
- this.context = context;
- this.db = BrowserDB.from(profile);
-
- GeckoApp.getEventDispatcher().registerGeckoThreadListener((NativeEventListener) this,
- "Reader:FaviconRequest", "Reader:AddedToCache");
- }
-
- public void uninit() {
- GeckoApp.getEventDispatcher().unregisterGeckoThreadListener((NativeEventListener) this,
- "Reader:FaviconRequest", "Reader:AddedToCache");
- }
-
- @Override
- public void handleMessage(final String event, final NativeJSObject message,
- final EventCallback callback) {
- switch (event) {
- case "Reader:FaviconRequest": {
- handleReaderModeFaviconRequest(callback, message.getString("url"));
- break;
- }
- case "Reader:AddedToCache": {
- // AddedToCache is a one way message: callback will be null, and we therefore shouldn't
- // attempt to handle it.
- handleAddedToCache(message.getString("url"), message.getString("path"), message.getInt("size"));
- break;
- }
- }
- }
-
- /**
- * Gecko (ReaderMode) requests the page favicon to append to the
- * document head for display.
- */
- private void handleReaderModeFaviconRequest(final EventCallback callback, final String url) {
- (new UIAsyncTask.WithoutParams<String>(ThreadUtils.getBackgroundHandler()) {
- @Override
- public String doInBackground() {
- // This is a bit ridiculous if you look at the bigger picture: Reader mode extracts
- // the article content. We insert the content into a new document (about:reader).
- // Some events are exchanged to lookup the icon URL for the actual website. This
- // URL is then added to the markup which will then trigger our icon loading code in
- // the Tab class.
- //
- // The Tab class could just lookup and load the icon itself. All it needs to do is
- // to strip the about:reader URL and perform a normal icon load from cache.
- //
- // A more global solution (looking at desktop and iOS) would be to copy the <link>
- // markup from the original page to the about:reader page and then rely on our normal
- // icon loading code. This would work even if we do not have anything in the cache
- // for some kind of reason.
-
- final IconRequest request = Icons.with(context)
- .pageUrl(url)
- .prepareOnly()
- .build();
-
- try {
- request.execute(null).get();
- if (request.getIconCount() > 0) {
- return request.getBestIcon().getUrl();
- }
- } catch (InterruptedException | ExecutionException e) {
- // Ignore
- }
-
- return null;
- }
-
- @Override
- public void onPostExecute(String faviconUrl) {
- JSONObject args = new JSONObject();
- if (faviconUrl != null) {
- try {
- args.put("url", url);
- args.put("faviconUrl", faviconUrl);
- } catch (JSONException e) {
- Log.w(LOGTAG, "Error building JSON favicon arguments.", e);
- }
- }
- callback.sendSuccess(args.toString());
- }
- }).execute();
- }
-
- private void handleAddedToCache(final String url, final String path, final int size) {
- final SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(context);
-
- rch.put(url, path, size);
- }
-
- public static void cacheReaderItem(final String url, final int tabID, Context context) {
- if (AboutPages.isAboutReader(url)) {
- throw new IllegalArgumentException("Page url must be original (not about:reader) url");
- }
-
- SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(context);
-
- if (!rch.isURLCached(url)) {
- GeckoAppShell.notifyObservers("Reader:AddToCache", Integer.toString(tabID));
- }
- }
-
- public static void removeCachedReaderItem(final String url, Context context) {
- if (AboutPages.isAboutReader(url)) {
- throw new IllegalArgumentException("Page url must be original (not about:reader) url");
- }
-
- SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(context);
-
- if (rch.isURLCached(url)) {
- GeckoAppShell.notifyObservers("Reader:RemoveFromCache", url);
- }
-
- // When removing items from the cache we can probably spare ourselves the async callback
- // that we use when adding cached items. We know the cached item will be gone, hence
- // we no longer need to track it in the SavedReaderViewHelper
- rch.remove(url);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/reader/SavedReaderViewHelper.java b/mobile/android/base/java/org/mozilla/gecko/reader/SavedReaderViewHelper.java
deleted file mode 100644
index e60abac71..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/reader/SavedReaderViewHelper.java
+++ /dev/null
@@ -1,247 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.reader;
-
-import android.content.Context;
-import android.support.annotation.NonNull;
-import android.util.Log;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.db.UrlAnnotations;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Iterator;
-
-/**
- * Helper to keep track of items that are stored in the reader view cache. This is an in-memory list
- * of the reader view items that are cached on disk. It is intended to allow quickly determining whether
- * a given URL is in the cache, and also how many cached items there are.
- *
- * Currently we have 1:1 correspondence of reader view items (in the URL-annotations table)
- * to cached items. This is _not_ a true cache, we never purge/cleanup items here - we only remove
- * items when we un-reader-view/bookmark them. This is an acceptable model while we can guarantee the
- * 1:1 correspondence.
- *
- * It isn't strictly necessary to mirror cached items in SQL at this stage, however it seems sensible
- * to maintain URL anotations to avoid additional DB migrations in future.
- * It is also simpler to implement the reading list smart-folder using the annotations (even if we do
- * all other decoration from our in-memory cache record), as that is what we will need when
- * we move away from the 1:1 correspondence.
- *
- * Bookmarks can be in one of two states - plain bookmark, or reader view bookmark that is also saved
- * offline. We're hoping to introduce real cache management / cleanup in future, in which case a
- * third user-visible state (reader view bookmark without a cache entry) will be added. However that logic is
- * much more complicated and requires substantial changes in how we decorate reader view bookmarks.
- * With the current 1:1 correspondence we can use this in-memory helper to quickly decorate
- * bookmarks (in all the various lists and panels that are used), whereas supporting
- * the third state requires significant changes in order to allow joining with the
- * URL-annotations table wherever bookmarks might be retrieved (i.e. multiple homepanels, each with
- * their own loaders and adapter).
- *
- * If/when cache cleanup and sync are implemented, URL annotations will be the canonical record of
- * user intent, and the cache will no longer represent all reader view bookmarks. We will have (A)
- * cached items that are not a bookmark, or bookmarks without the reader view annotation (both of
- * these would need purging), and (B) bookmarks with a reader view annotation, but not stored in
- * the cache (which we might want to download in the background). Supporting (B) is currently difficult,
- * see previous paragraph.
- */
-public class SavedReaderViewHelper {
- private static final String LOG_TAG = "SavedReaderViewHelper";
-
- private static final String PATH = "path";
- private static final String SIZE = "size";
-
- private static final String DIRECTORY = "readercache";
- private static final String FILE_NAME = "items.json";
- private static final String FILE_PATH = DIRECTORY + "/" + FILE_NAME;
-
- // We use null to indicate that the cache hasn't yet been loaded. Loading has to be explicitly
- // requested by client code, and must happen on the background thread. Attempting to access
- // items (which happens mainly on the UI thread) before explicitly loading them is not permitted.
- private JSONObject mItems = null;
-
- private final Context mContext;
-
- private static SavedReaderViewHelper instance = null;
-
- private SavedReaderViewHelper(Context context) {
- mContext = context;
- }
-
- public static synchronized SavedReaderViewHelper getSavedReaderViewHelper(final Context context) {
- if (instance == null) {
- instance = new SavedReaderViewHelper(context);
- }
-
- return instance;
- }
-
- /**
- * Load the reader view cache list from our JSON file.
- *
- * Must not be run on the UI thread due to file access.
- */
- public synchronized void loadItems() {
- // TODO bug 1264489
- // This is a band aid fix for Bug 1264134. We need to figure out the root cause and reenable this
- // assertion.
- // ThreadUtils.assertNotOnUiThread();
-
- if (mItems != null) {
- return;
- }
-
- try {
- mItems = GeckoProfile.get(mContext).readJSONObjectFromFile(FILE_PATH);
- } catch (IOException e) {
- mItems = new JSONObject();
- }
- }
-
- private synchronized void assertItemsLoaded() {
- if (mItems == null) {
- throw new IllegalStateException("SavedReaderView items must be explicitly loaded using loadItems() before access.");
- }
- }
-
- private JSONObject makeItem(@NonNull String path, long size) throws JSONException {
- final JSONObject item = new JSONObject();
-
- item.put(PATH, path);
- item.put(SIZE, size);
-
- return item;
- }
-
- public synchronized boolean isURLCached(@NonNull final String URL) {
- assertItemsLoaded();
- return mItems.has(URL);
- }
-
- /**
- * Insert an item into the list of cached items.
- *
- * This may be called from any thread.
- */
- public synchronized void put(@NonNull final String pageURL, @NonNull final String path, final long size) {
- assertItemsLoaded();
-
- try {
- mItems.put(pageURL, makeItem(path, size));
- } catch (JSONException e) {
- Log.w(LOG_TAG, "Item insertion failed:", e);
- // This should never happen, absent any errors in our own implementation
- throw new IllegalStateException("Failure inserting into SavedReaderViewHelper json");
- }
-
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- UrlAnnotations annotations = BrowserDB.from(mContext).getUrlAnnotations();
- annotations.insertReaderViewUrl(mContext.getContentResolver(), pageURL);
-
- commit();
- }
- });
- }
-
- protected synchronized void remove(@NonNull final String pageURL) {
- assertItemsLoaded();
-
- mItems.remove(pageURL);
-
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- UrlAnnotations annotations = BrowserDB.from(mContext).getUrlAnnotations();
- annotations.deleteReaderViewUrl(mContext.getContentResolver(), pageURL);
-
- commit();
- }
- });
- }
-
- @RobocopTarget
- public synchronized int size() {
- assertItemsLoaded();
- return mItems.length();
- }
-
- private synchronized void commit() {
- ThreadUtils.assertOnBackgroundThread();
-
- GeckoProfile profile = GeckoProfile.get(mContext);
- File cacheDir = new File(profile.getDir(), DIRECTORY);
-
- if (!cacheDir.exists()) {
- Log.i(LOG_TAG, "No preexisting cache directory, creating now");
-
- boolean cacheDirCreated = cacheDir.mkdir();
- if (!cacheDirCreated) {
- throw new IllegalStateException("Couldn't create cache directory, unable to track reader view cache");
- }
- }
-
- profile.writeFile(FILE_PATH, mItems.toString());
- }
-
- /**
- * Return the Reader View URL for a given URL if it is contained in the cache. Returns the
- * plain URL if the page is not cached.
- */
- public static String getReaderURLIfCached(final Context context, @NonNull final String pageURL) {
- SavedReaderViewHelper rvh = getSavedReaderViewHelper(context);
-
- if (rvh.isURLCached(pageURL)) {
- return ReaderModeUtils.getAboutReaderForUrl(pageURL);
- } else {
- return pageURL;
- }
- }
-
- /**
- * Obtain the total disk space used for saved reader view items, in KB.
- *
- * @return Total disk space used (KB), or Integer.MAX_VALUE on overflow.
- */
- public synchronized int getDiskSpacedUsedKB() {
- // JSONObject is not thread safe - we need to be synchronized to avoid issues (most likely to
- // occur if items are removed during iteration).
- final Iterator<String> keys = mItems.keys();
- long bytes = 0;
-
- while (keys.hasNext()) {
- final String pageURL = keys.next();
- try {
- final JSONObject item = mItems.getJSONObject(pageURL);
- bytes += item.getLong(SIZE);
-
- // Overflow is highly unlikely (we will hit device storage limits before we hit integer limits),
- // but we should still handle this for correctness.
- // We definitely can't store our output in an int if we overflow the long here.
- if (bytes < 0) {
- return Integer.MAX_VALUE;
- }
- } catch (JSONException e) {
- // This shouldn't ever happen:
- throw new IllegalStateException("Must be able to access items in saved reader view list", e);
- }
- }
-
- long kb = bytes / 1024;
- if (kb > Integer.MAX_VALUE) {
- return Integer.MAX_VALUE;
- } else {
- return (int) kb;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/restrictions/DefaultConfiguration.java b/mobile/android/base/java/org/mozilla/gecko/restrictions/DefaultConfiguration.java
deleted file mode 100644
index 480078a98..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/DefaultConfiguration.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.restrictions;
-
-/**
- * Default implementation of RestrictionConfiguration interface. Used whenever no restrictions are enforced for the
- * current profile.
- */
-public class DefaultConfiguration implements RestrictionConfiguration {
- @Override
- public boolean isAllowed(Restrictable restrictable) {
- if (restrictable == Restrictable.BLOCK_LIST) {
- return false;
- }
-
- return true;
- }
-
- @Override
- public boolean canLoadUrl(String url) {
- return true;
- }
-
- @Override
- public boolean isRestricted() {
- return false;
- }
-
- @Override
- public void update() {}
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/restrictions/GuestProfileConfiguration.java b/mobile/android/base/java/org/mozilla/gecko/restrictions/GuestProfileConfiguration.java
deleted file mode 100644
index f9663ccf7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/GuestProfileConfiguration.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.restrictions;
-
-import android.net.Uri;
-
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * RestrictionConfiguration implementation for guest profiles.
- */
-public class GuestProfileConfiguration implements RestrictionConfiguration {
- static List<Restrictable> DISABLED_FEATURES = Arrays.asList(
- Restrictable.DOWNLOAD,
- Restrictable.INSTALL_EXTENSION,
- Restrictable.INSTALL_APPS,
- Restrictable.BROWSE,
- Restrictable.SHARE,
- Restrictable.BOOKMARK,
- Restrictable.ADD_CONTACT,
- Restrictable.SET_IMAGE,
- Restrictable.MODIFY_ACCOUNTS,
- Restrictable.REMOTE_DEBUGGING,
- Restrictable.IMPORT_SETTINGS,
- Restrictable.BLOCK_LIST,
- Restrictable.DATA_CHOICES,
- Restrictable.DEFAULT_THEME
- );
-
- @SuppressWarnings("serial")
- private static final List<String> BANNED_SCHEMES = Arrays.asList(
- "file",
- "chrome",
- "resource",
- "jar",
- "wyciwyg"
- );
-
- private static final List<String> BANNED_URLS = Arrays.asList(
- "about:config",
- "about:addons"
- );
-
- @Override
- public boolean isAllowed(Restrictable restrictable) {
- return !DISABLED_FEATURES.contains(restrictable);
- }
-
- @Override
- public boolean canLoadUrl(String url) {
- // Null URLs are always permitted.
- if (url == null) {
- return true;
- }
-
- final Uri u = Uri.parse(url);
- final String scheme = u.getScheme();
- if (BANNED_SCHEMES.contains(scheme)) {
- return false;
- }
-
- url = url.toLowerCase();
- for (String banned : BANNED_URLS) {
- if (url.startsWith(banned)) {
- return false;
- }
- }
-
- return true;
- }
-
- @Override
- public boolean isRestricted() {
- return true;
- }
-
- @Override
- public void update() {}
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictable.java b/mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictable.java
deleted file mode 100644
index f794c5782..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictable.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.restrictions;
-
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.support.annotation.StringRes;
-
-/**
- * This is a list of things we can restrict you from doing. Some of these are reflected in Android UserManager constants.
- * Others are specific to us.
- * These constants should be in sync with the ones from toolkit/components/parentalcontrols/nsIParentalControlsService.idl
- */
-public enum Restrictable {
- DOWNLOAD(1, "downloads", 0, 0),
-
- INSTALL_EXTENSION(
- 2, "no_install_extensions",
- R.string.restrictable_feature_addons_installation,
- R.string.restrictable_feature_addons_installation_description),
-
- // UserManager.DISALLOW_INSTALL_APPS
- INSTALL_APPS(3, "no_install_apps", 0 , 0),
-
- BROWSE(4, "browse", 0, 0),
-
- SHARE(5, "share", 0, 0),
-
- BOOKMARK(6, "bookmark", 0, 0),
-
- ADD_CONTACT(7, "add_contact", 0, 0),
-
- SET_IMAGE(8, "set_image", 0, 0),
-
- // UserManager.DISALLOW_MODIFY_ACCOUNTS
- MODIFY_ACCOUNTS(9, "no_modify_accounts", 0, 0),
-
- REMOTE_DEBUGGING(10, "remote_debugging", 0, 0),
-
- IMPORT_SETTINGS(11, "import_settings", 0, 0),
-
- PRIVATE_BROWSING(
- 12, "private_browsing",
- R.string.restrictable_feature_private_browsing,
- R.string.restrictable_feature_private_browsing_description),
-
- DATA_CHOICES(13, "data_coices", 0, 0),
-
- CLEAR_HISTORY(14, "clear_history",
- R.string.restrictable_feature_clear_history,
- R.string.restrictable_feature_clear_history_description),
-
- MASTER_PASSWORD(15, "master_password", 0, 0),
-
- GUEST_BROWSING(16, "guest_browsing", 0, 0),
-
- ADVANCED_SETTINGS(17, "advanced_settings",
- R.string.restrictable_feature_advanced_settings,
- R.string.restrictable_feature_advanced_settings_description),
-
- CAMERA_MICROPHONE(18, "camera_microphone",
- R.string.restrictable_feature_camera_microphone,
- R.string.restrictable_feature_camera_microphone_description),
-
- BLOCK_LIST(19, "block_list",
- R.string.restrictable_feature_block_list,
- R.string.restrictable_feature_block_list_description),
-
- TELEMETRY(20, "telemetry",
- R.string.datareporting_telemetry_title,
- R.string.datareporting_telemetry_summary),
-
- HEALTH_REPORT(21, "health_report",
- R.string.datareporting_fhr_title,
- R.string.datareporting_fhr_summary2),
-
- DEFAULT_THEME(22, "default_theme", 0, 0);
-
- public final int id;
- public final String name;
-
- @StringRes
- public final int title;
-
- @StringRes
- public final int description;
-
- Restrictable(final int id, final String name, @StringRes int title, @StringRes int description) {
- this.id = id;
- this.name = name;
- this.title = title;
- this.description = description;
- }
-
- public String getTitle(Context context) {
- if (title == 0) {
- return toString();
- }
- return context.getResources().getString(title);
- }
-
- public String getDescription(Context context) {
- if (description == 0) {
- return null;
- }
- return context.getResources().getString(description);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.java b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.java
deleted file mode 100644
index 15a0b97f4..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.restrictions;
-
-import org.mozilla.gecko.AboutPages;
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.StrictMode;
-import android.os.UserManager;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
-public class RestrictedProfileConfiguration implements RestrictionConfiguration {
- // Mapping from restrictable feature to default state (on/off)
- private static Map<Restrictable, Boolean> configuration = new LinkedHashMap<>();
- static {
- configuration.put(Restrictable.INSTALL_EXTENSION, false);
- configuration.put(Restrictable.PRIVATE_BROWSING, false);
- configuration.put(Restrictable.CLEAR_HISTORY, false);
- configuration.put(Restrictable.MASTER_PASSWORD, false);
- configuration.put(Restrictable.GUEST_BROWSING, false);
- configuration.put(Restrictable.ADVANCED_SETTINGS, false);
- configuration.put(Restrictable.CAMERA_MICROPHONE, false);
- configuration.put(Restrictable.DATA_CHOICES, false);
- configuration.put(Restrictable.BLOCK_LIST, false);
- configuration.put(Restrictable.TELEMETRY, false);
- configuration.put(Restrictable.HEALTH_REPORT, true);
- configuration.put(Restrictable.DEFAULT_THEME, true);
- }
-
- /**
- * These restrictions are hidden from the admin configuration UI.
- */
- private static List<Restrictable> hiddenRestrictions = new ArrayList<>();
- static {
- hiddenRestrictions.add(Restrictable.MASTER_PASSWORD);
- hiddenRestrictions.add(Restrictable.GUEST_BROWSING);
- hiddenRestrictions.add(Restrictable.DATA_CHOICES);
- hiddenRestrictions.add(Restrictable.DEFAULT_THEME);
-
- // Hold behind Nightly flag until we have an actual block list deployed.
- if (!AppConstants.NIGHTLY_BUILD) {
- hiddenRestrictions.add(Restrictable.BLOCK_LIST);
- }
- }
-
- /* package-private */ static boolean shouldHide(Restrictable restrictable) {
- return hiddenRestrictions.contains(restrictable);
- }
-
- /* package-private */ static Map<Restrictable, Boolean> getConfiguration() {
- return configuration;
- }
-
- private Context context;
-
- public RestrictedProfileConfiguration(Context context) {
- this.context = context.getApplicationContext();
- }
-
- @Override
- public synchronized boolean isAllowed(Restrictable restrictable) {
- // Special casing system/user restrictions
- if (restrictable == Restrictable.INSTALL_APPS || restrictable == Restrictable.MODIFY_ACCOUNTS) {
- return RestrictionCache.getUserRestriction(context, restrictable.name);
- }
-
- if (!RestrictionCache.hasApplicationRestriction(context, restrictable.name) && !configuration.containsKey(restrictable)) {
- // Always allow features that are not in the configuration
- return true;
- }
-
- return RestrictionCache.getApplicationRestriction(context, restrictable.name, configuration.get(restrictable));
- }
-
- @Override
- public boolean canLoadUrl(String url) {
- if (!isAllowed(Restrictable.INSTALL_EXTENSION) && AboutPages.isAboutAddons(url)) {
- return false;
- }
-
- if (!isAllowed(Restrictable.PRIVATE_BROWSING) && AboutPages.isAboutPrivateBrowsing(url)) {
- return false;
- }
-
- if (AboutPages.isAboutConfig(url)) {
- // Always block access to about:config to prevent circumventing restrictions (Bug 1189233)
- return false;
- }
-
- return true;
- }
-
- @Override
- public boolean isRestricted() {
- return true;
- }
-
- @Override
- public synchronized void update() {
- RestrictionCache.invalidate();
- }
-
- public static List<Restrictable> getVisibleRestrictions() {
- final List<Restrictable> visibleList = new ArrayList<>();
-
- for (Restrictable restrictable : configuration.keySet()) {
- if (hiddenRestrictions.contains(restrictable)) {
- continue;
- }
- visibleList.add(restrictable);
- }
-
- return visibleList;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionCache.java b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionCache.java
deleted file mode 100644
index 523cc113b..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionCache.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.restrictions;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.StrictMode;
-import android.os.UserManager;
-
-import org.mozilla.gecko.util.ThreadUtils;
-
-/**
- * Cache for user and application restrictions.
- */
-public class RestrictionCache {
- private static Bundle cachedAppRestrictions;
- private static Bundle cachedUserRestrictions;
- private static boolean isCacheInvalid = true;
-
- private RestrictionCache() {}
-
- public static synchronized boolean getUserRestriction(Context context, String restriction) {
- updateCacheIfNeeded(context);
- return cachedUserRestrictions.getBoolean(restriction);
- }
-
- public static synchronized boolean hasApplicationRestriction(Context context, String restriction) {
- updateCacheIfNeeded(context);
- return cachedAppRestrictions.containsKey(restriction);
- }
-
- public static synchronized boolean getApplicationRestriction(Context context, String restriction, boolean defaultValue) {
- updateCacheIfNeeded(context);
- return cachedAppRestrictions.getBoolean(restriction, defaultValue);
- }
-
- public static synchronized boolean hasApplicationRestrictions(Context context) {
- updateCacheIfNeeded(context);
- return !cachedAppRestrictions.isEmpty();
- }
-
- public static synchronized void invalidate() {
- isCacheInvalid = true;
- }
-
- private static void updateCacheIfNeeded(Context context) {
- // If we are not on the UI thread then we can just go ahead and read the values (Bug 1189347).
- // Otherwise we read from the cache to avoid blocking the UI thread. If the cache is invalid
- // then we hazard the consequences and just do the read.
- if (isCacheInvalid || !ThreadUtils.isOnUiThread()) {
- readRestrictions(context);
- isCacheInvalid = false;
- }
- }
-
- @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
- private static void readRestrictions(Context context) {
- final UserManager mgr = (UserManager) context.getSystemService(Context.USER_SERVICE);
-
- // If we do not have anything in the cache yet then this read might happen on the UI thread (Bug 1189347).
- final StrictMode.ThreadPolicy policy = StrictMode.allowThreadDiskReads();
-
- try {
- Bundle appRestrictions = mgr.getApplicationRestrictions(context.getPackageName());
- migrateRestrictionsIfNeeded(appRestrictions);
-
- cachedAppRestrictions = appRestrictions;
- cachedUserRestrictions = mgr.getUserRestrictions(); // Always implies disk read
- } finally {
- StrictMode.setThreadPolicy(policy);
- }
- }
-
- /**
- * This method migrates the old set of DISALLOW_ restrictions to the new restrictable feature ones (Bug 1189336).
- */
- /* package-private */ static void migrateRestrictionsIfNeeded(Bundle bundle) {
- if (!bundle.containsKey(Restrictable.INSTALL_EXTENSION.name) && bundle.containsKey("no_install_extensions")) {
- bundle.putBoolean(Restrictable.INSTALL_EXTENSION.name, !bundle.getBoolean("no_install_extensions"));
- }
-
- if (!bundle.containsKey(Restrictable.PRIVATE_BROWSING.name) && bundle.containsKey("no_private_browsing")) {
- bundle.putBoolean(Restrictable.PRIVATE_BROWSING.name, !bundle.getBoolean("no_private_browsing"));
- }
-
- if (!bundle.containsKey(Restrictable.CLEAR_HISTORY.name) && bundle.containsKey("no_clear_history")) {
- bundle.putBoolean(Restrictable.CLEAR_HISTORY.name, !bundle.getBoolean("no_clear_history"));
- }
-
- if (!bundle.containsKey(Restrictable.ADVANCED_SETTINGS.name) && bundle.containsKey("no_advanced_settings")) {
- bundle.putBoolean(Restrictable.ADVANCED_SETTINGS.name, !bundle.getBoolean("no_advanced_settings"));
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionConfiguration.java b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionConfiguration.java
deleted file mode 100644
index 7c40da734..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionConfiguration.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.restrictions;
-
-/**
- * Interface for classes that Restrictions will delegate to for making decisions.
- */
-public interface RestrictionConfiguration {
- /**
- * Is the user allowed to perform this action?
- */
- boolean isAllowed(Restrictable restrictable);
-
- /**
- * Is the user allowed to load the given URL?
- */
- boolean canLoadUrl(String url);
-
- /**
- * Is this user restricted in any way?
- */
- boolean isRestricted();
-
- /**
- * Update restrictions if needed.
- */
- void update();
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionProvider.java b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionProvider.java
deleted file mode 100644
index 26b9a446f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionProvider.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.restrictions;
-
-import org.mozilla.gecko.AppConstants;
-
-import android.annotation.TargetApi;
-import android.app.Activity;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.RestrictionEntry;
-import android.os.Build;
-import android.os.Bundle;
-import android.text.TextUtils;
-
-import java.util.ArrayList;
-import java.util.Map;
-
-/**
- * Broadcast receiver providing supported restrictions to the system.
- */
-@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
-public class RestrictionProvider extends BroadcastReceiver {
- @Override
- public void onReceive(final Context context, final Intent intent) {
- if (AppConstants.Versions.preJBMR2) {
- // This broadcast does not make any sense prior to Jelly Bean MR2.
- return;
- }
-
- final PendingResult result = goAsync();
-
- new Thread() {
- @Override
- public void run() {
- final Bundle oldRestrictions = intent.getBundleExtra(Intent.EXTRA_RESTRICTIONS_BUNDLE);
- RestrictionCache.migrateRestrictionsIfNeeded(oldRestrictions);
-
- final Bundle extras = new Bundle();
-
- ArrayList<RestrictionEntry> entries = initRestrictions(context, oldRestrictions);
- extras.putParcelableArrayList(Intent.EXTRA_RESTRICTIONS_LIST, entries);
-
- result.setResult(Activity.RESULT_OK, null, extras);
- result.finish();
- }
- }.start();
- }
-
- private ArrayList<RestrictionEntry> initRestrictions(Context context, Bundle oldRestrictions) {
- ArrayList<RestrictionEntry> entries = new ArrayList<RestrictionEntry>();
-
- final Map<Restrictable, Boolean> configuration = RestrictedProfileConfiguration.getConfiguration();
-
- for (Restrictable restrictable : configuration.keySet()) {
- if (RestrictedProfileConfiguration.shouldHide(restrictable)) {
- continue;
- }
-
- RestrictionEntry entry = createRestrictionEntryWithDefaultValue(context, restrictable,
- oldRestrictions.getBoolean(restrictable.name, configuration.get(restrictable)));
- entries.add(entry);
- }
-
- return entries;
- }
-
- private RestrictionEntry createRestrictionEntryWithDefaultValue(Context context, Restrictable restrictable, boolean defaultValue) {
- RestrictionEntry entry = new RestrictionEntry(restrictable.name, defaultValue);
-
- entry.setTitle(restrictable.getTitle(context));
-
- final String description = restrictable.getDescription(context);
- if (!TextUtils.isEmpty(description)) {
- entry.setDescription(description);
- }
-
- return entry;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictions.java b/mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictions.java
deleted file mode 100644
index 0cf680810..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictions.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.restrictions;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Build;
-import android.util.Log;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.annotation.WrapForJNI;
-
-@RobocopTarget
-public class Restrictions {
- private static final String LOGTAG = "GeckoRestrictedProfiles";
-
- private static RestrictionConfiguration configuration;
-
- private static RestrictionConfiguration getConfiguration(Context context) {
- if (configuration == null) {
- configuration = createConfiguration(context);
- }
-
- return configuration;
- }
-
- public static synchronized RestrictionConfiguration createConfiguration(Context context) {
- if (configuration != null) {
- // This method is synchronized and another thread might already have created the configuration.
- return configuration;
- }
-
- if (isGuestProfile(context)) {
- return new GuestProfileConfiguration();
- } else if (isRestrictedProfile(context)) {
- return new RestrictedProfileConfiguration(context);
- } else {
- return new DefaultConfiguration();
- }
- }
-
- private static boolean isGuestProfile(Context context) {
- if (configuration != null) {
- return configuration instanceof GuestProfileConfiguration;
- }
-
- GeckoAppShell.GeckoInterface geckoInterface = GeckoAppShell.getGeckoInterface();
- if (geckoInterface != null) {
- return geckoInterface.getProfile().inGuestMode();
- }
-
- return GeckoProfile.get(context).inGuestMode();
- }
-
- @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
- public static boolean isRestrictedProfile(Context context) {
- if (configuration != null) {
- return configuration instanceof RestrictedProfileConfiguration;
- }
-
- if (Versions.preJBMR2) {
- // Early versions don't support restrictions at all
- return false;
- }
-
- // The user is on a restricted profile if, and only if, we injected application restrictions during account setup.
- return RestrictionCache.hasApplicationRestrictions(context);
- }
-
- public static void update(Context context) {
- getConfiguration(context).update();
- }
-
- private static Restrictable geckoActionToRestriction(int action) {
- for (Restrictable rest : Restrictable.values()) {
- if (rest.id == action) {
- return rest;
- }
- }
-
- throw new IllegalArgumentException("Unknown action " + action);
- }
-
- private static boolean canLoadUrl(final Context context, final String url) {
- return getConfiguration(context).canLoadUrl(url);
- }
-
- @WrapForJNI(calledFrom = "gecko")
- public static boolean isUserRestricted() {
- return isUserRestricted(GeckoAppShell.getApplicationContext());
- }
-
- public static boolean isUserRestricted(final Context context) {
- return getConfiguration(context).isRestricted();
- }
-
- public static boolean isAllowed(final Context context, final Restrictable restrictable) {
- return getConfiguration(context).isAllowed(restrictable);
- }
-
- @WrapForJNI(calledFrom = "gecko")
- public static boolean isAllowed(int action, String url) {
- final Restrictable restrictable;
- try {
- restrictable = geckoActionToRestriction(action);
- } catch (IllegalArgumentException ex) {
- // Unknown actions represent a coding error, so we
- // refuse the action and log.
- Log.e(LOGTAG, "Unknown action " + action + "; check calling code.");
- return false;
- }
-
- final Context context = GeckoAppShell.getApplicationContext();
-
- if (Restrictable.BROWSE == restrictable) {
- return canLoadUrl(context, url);
- } else {
- return isAllowed(context, restrictable);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/search/SearchEngine.java b/mobile/android/base/java/org/mozilla/gecko/search/SearchEngine.java
deleted file mode 100644
index d4d9938e2..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/search/SearchEngine.java
+++ /dev/null
@@ -1,304 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.search;
-
-import android.net.Uri;
-import android.util.Log;
-import android.util.Xml;
-
-import org.mozilla.gecko.util.StringUtils;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-
-/**
- * Extend this class to add a new search engine to
- * the search activity.
- */
-public class SearchEngine {
- private static final String LOG_TAG = "SearchEngine";
-
- private static final String URLTYPE_SUGGEST_JSON = "application/x-suggestions+json";
- private static final String URLTYPE_SEARCH_HTML = "text/html";
-
- private static final String URL_REL_MOBILE = "mobile";
-
- // Parameters copied from nsSearchService.js
- private static final String MOZ_PARAM_LOCALE = "\\{moz:locale\\}";
- private static final String MOZ_PARAM_DIST_ID = "\\{moz:distributionID\\}";
- private static final String MOZ_PARAM_OFFICIAL = "\\{moz:official\\}";
-
- // Supported OpenSearch parameters
- // See http://opensearch.a9.com/spec/1.1/querysyntax/#core
- private static final String OS_PARAM_USER_DEFINED = "\\{searchTerms\\??\\}";
- private static final String OS_PARAM_INPUT_ENCODING = "\\{inputEncoding\\??\\}";
- private static final String OS_PARAM_LANGUAGE = "\\{language\\??\\}";
- private static final String OS_PARAM_OUTPUT_ENCODING = "\\{outputEncoding\\??\\}";
- private static final String OS_PARAM_OPTIONAL = "\\{(?:\\w+:)?\\w+\\?\\}";
-
- // Boilerplate bookmarklet-style JS for injecting CSS into the
- // head of a web page. The actual CSS is inserted at `%s`.
- private static final String STYLE_INJECTION_SCRIPT =
- "javascript:(function(){" +
- "var tag=document.createElement('style');" +
- "tag.type='text/css';" +
- "document.getElementsByTagName('head')[0].appendChild(tag);" +
- "tag.innerText='%s'})();";
-
- // The Gecko search identifier. This will be null for engines that don't ship with the locale.
- private final String identifier;
-
- private String shortName;
- private String iconURL;
-
- // Ordered list of preferred results URIs.
- private final List<Uri> resultsUris = new ArrayList<Uri>();
- private Uri suggestUri;
-
- /**
- *
- * @param in InputStream of open search plugin XML
- */
- public SearchEngine(String identifier, InputStream in) throws IOException, XmlPullParserException {
- this.identifier = identifier;
-
- final XmlPullParser parser = Xml.newPullParser();
- parser.setInput(in, null);
- parser.nextTag();
- readSearchPlugin(parser);
- }
-
- private void readSearchPlugin(XmlPullParser parser) throws XmlPullParserException, IOException {
- if (XmlPullParser.START_TAG != parser.getEventType()) {
- throw new XmlPullParserException("Expected start tag: " + parser.getPositionDescription());
- }
-
- final String name = parser.getName();
- if (!"SearchPlugin".equals(name) && !"OpenSearchDescription".equals(name)) {
- throw new XmlPullParserException("Expected <SearchPlugin> or <OpenSearchDescription> as root tag: "
- + parser.getPositionDescription());
- }
-
- while (parser.next() != XmlPullParser.END_TAG) {
- if (parser.getEventType() != XmlPullParser.START_TAG) {
- continue;
- }
-
- final String tag = parser.getName();
- if (tag.equals("ShortName")) {
- readShortName(parser);
- } else if (tag.equals("Url")) {
- readUrl(parser);
- } else if (tag.equals("Image")) {
- readImage(parser);
- } else {
- skip(parser);
- }
- }
- }
-
- private void readShortName(XmlPullParser parser) throws IOException, XmlPullParserException {
- parser.require(XmlPullParser.START_TAG, null, "ShortName");
- if (parser.next() == XmlPullParser.TEXT) {
- shortName = parser.getText();
- parser.nextTag();
- }
- }
-
- private void readUrl(XmlPullParser parser) throws XmlPullParserException, IOException {
- parser.require(XmlPullParser.START_TAG, null, "Url");
-
- final String type = parser.getAttributeValue(null, "type");
- final String template = parser.getAttributeValue(null, "template");
- final String rel = parser.getAttributeValue(null, "rel");
-
- Uri uri = Uri.parse(template);
-
- while (parser.next() != XmlPullParser.END_TAG) {
- if (parser.getEventType() != XmlPullParser.START_TAG) {
- continue;
- }
-
- final String tag = parser.getName();
-
- if (tag.equals("Param")) {
- final String name = parser.getAttributeValue(null, "name");
- final String value = parser.getAttributeValue(null, "value");
- uri = uri.buildUpon().appendQueryParameter(name, value).build();
- parser.nextTag();
- // TODO: Support for other tags
- //} else if (tag.equals("MozParam")) {
- } else {
- skip(parser);
- }
- }
-
- if (type.equals(URLTYPE_SEARCH_HTML)) {
- // Prefer mobile URIs.
- if (rel != null && rel.equals(URL_REL_MOBILE)) {
- resultsUris.add(0, uri);
- } else {
- resultsUris.add(uri);
- }
- } else if (type.equals(URLTYPE_SUGGEST_JSON)) {
- suggestUri = uri;
- }
- }
-
- private void readImage(XmlPullParser parser) throws XmlPullParserException, IOException {
- parser.require(XmlPullParser.START_TAG, null, "Image");
-
- // TODO: Use width and height to get a preferred icon URL.
- //final int width = Integer.parseInt(parser.getAttributeValue(null, "width"));
- //final int height = Integer.parseInt(parser.getAttributeValue(null, "height"));
-
- if (parser.next() == XmlPullParser.TEXT) {
- iconURL = parser.getText();
- parser.nextTag();
- }
- }
-
- private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
- if (parser.getEventType() != XmlPullParser.START_TAG) {
- throw new IllegalStateException();
- }
- int depth = 1;
- while (depth != 0) {
- switch (parser.next()) {
- case XmlPullParser.END_TAG:
- depth--;
- break;
- case XmlPullParser.START_TAG:
- depth++;
- break;
- }
- }
- }
-
- /**
- * HACKS! We'll need to replace this with endpoints that return the correct content.
- *
- * Retrieve a JS snippet, in bookmarklet style, that can be used
- * to modify the results page.
- */
- public String getInjectableJs() {
- final String css;
-
- if (identifier == null) {
- css = "";
- } else if (identifier.equals("bing")) {
- css = "#mHeader{display:none}#contentWrapper{margin-top:0}";
- } else if (identifier.equals("google")) {
- css = "#sfcnt,#top_nav{display:none}";
- } else if (identifier.equals("yahoo")) {
- css = "#nav,#header{display:none}";
- } else {
- css = "";
- }
-
- return String.format(STYLE_INJECTION_SCRIPT, css);
- }
-
- public String getIdentifier() {
- return identifier;
- }
-
- public String getName() {
- return shortName;
- }
-
- public String getIconURL() {
- return iconURL;
- }
-
- /**
- * Finds the search query encoded in a given results URL.
- *
- * @param url Current results URL.
- * @return The search query, or an empty string if a query couldn't be found.
- */
- public String queryForResultsUrl(String url) {
- final Uri resultsUri = getResultsUri();
- final Set<String> names = StringUtils.getQueryParameterNames(resultsUri);
- for (String name : names) {
- if (resultsUri.getQueryParameter(name).matches(OS_PARAM_USER_DEFINED)) {
- return Uri.parse(url).getQueryParameter(name);
- }
- }
- return "";
- }
-
- /**
- * Create a uri string that can be used to fetch the results page.
- *
- * @param query The user's query. This method will escape and encode the query.
- */
- public String resultsUriForQuery(String query) {
- final Uri resultsUri = getResultsUri();
- if (resultsUri == null) {
- Log.e(LOG_TAG, "No results URL for search engine: " + shortName);
- return "";
- }
- final String template = Uri.decode(resultsUri.toString());
- return paramSubstitution(template, Uri.encode(query));
- }
-
- /**
- * Create a uri string to fetch autocomplete suggestions.
- *
- * @param query The user's query. This method will escape and encode the query.
- */
- public String getSuggestionTemplate(String query) {
- if (suggestUri == null) {
- Log.e(LOG_TAG, "No suggestions template for search engine: " + shortName);
- return "";
- }
- final String template = Uri.decode(suggestUri.toString());
- return paramSubstitution(template, Uri.encode(query));
- }
-
- /**
- * @return Preferred results URI.
- */
- private Uri getResultsUri() {
- if (resultsUris.isEmpty()) {
- return null;
- }
- return resultsUris.get(0);
- }
-
- /**
- * Formats template string with proper parameters. Modeled after
- * ParamSubstitution in nsSearchService.js
- *
- * @param template
- * @param query
- * @return
- */
- private String paramSubstitution(String template, String query) {
- final String locale = Locale.getDefault().toString();
-
- template = template.replaceAll(MOZ_PARAM_LOCALE, locale);
- template = template.replaceAll(MOZ_PARAM_DIST_ID, "");
- template = template.replaceAll(MOZ_PARAM_OFFICIAL, "unofficial");
-
- template = template.replaceAll(OS_PARAM_USER_DEFINED, query);
- template = template.replaceAll(OS_PARAM_INPUT_ENCODING, "UTF-8");
-
- template = template.replaceAll(OS_PARAM_LANGUAGE, locale);
- template = template.replaceAll(OS_PARAM_OUTPUT_ENCODING, "UTF-8");
-
- // Replace any optional parameters
- template = template.replaceAll(OS_PARAM_OPTIONAL, "");
-
- return template;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/search/SearchEngineManager.java b/mobile/android/base/java/org/mozilla/gecko/search/SearchEngineManager.java
deleted file mode 100644
index 4b33db40a..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/search/SearchEngineManager.java
+++ /dev/null
@@ -1,764 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.search;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.support.annotation.Nullable;
-import android.support.annotation.UiThread;
-import android.support.annotation.WorkerThread;
-import android.text.TextUtils;
-import android.util.Log;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.Locales;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.distribution.Distribution;
-import org.mozilla.gecko.util.FileUtils;
-import org.mozilla.gecko.util.GeckoJarReader;
-import org.mozilla.gecko.util.HardwareUtils;
-import org.mozilla.gecko.util.RawResource;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
-import java.lang.ref.WeakReference;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Locale;
-
-/**
- * This class is not thread-safe, except where otherwise noted.
- *
- * This class contains a reference to {@link Context} - DO NOT LEAK!
- */
-public class SearchEngineManager implements SharedPreferences.OnSharedPreferenceChangeListener {
- private static final String LOG_TAG = "GeckoSearchEngineManager";
-
- // Gecko pref that defines the name of the default search engine.
- private static final String PREF_GECKO_DEFAULT_ENGINE = "browser.search.defaultenginename";
-
- // Gecko pref that defines the name of the default searchplugin locale.
- private static final String PREF_GECKO_DEFAULT_LOCALE = "distribution.searchplugins.defaultLocale";
-
- // Key for shared preference that stores default engine name.
- private static final String PREF_DEFAULT_ENGINE_KEY = "search.engines.defaultname";
-
- // Key for shared preference that stores search region.
- private static final String PREF_REGION_KEY = "search.region";
-
- // URL for the geo-ip location service. Keep in sync with "browser.search.geoip.url" perference in Gecko.
- private static final String GEOIP_LOCATION_URL = "https://location.services.mozilla.com/v1/country?key=" + AppConstants.MOZ_MOZILLA_API_KEY;
-
- // This should go through GeckoInterface to get the UA, but the search activity
- // doesn't use a GeckoView yet. Until it does, get the UA directly.
- private static final String USER_AGENT = HardwareUtils.isTablet() ?
- AppConstants.USER_AGENT_FENNEC_TABLET : AppConstants.USER_AGENT_FENNEC_MOBILE;
-
- private final Context context;
- private final Distribution distribution;
- @Nullable private volatile SearchEngineCallback changeCallback;
- @Nullable private volatile SearchEngine engine;
-
- // Cached version of default locale included in Gecko chrome manifest.
- // This should only be accessed from the background thread.
- private String fallbackLocale;
-
- // Cached version of default locale included in Distribution preferences.
- // This should only be accessed from the background thread.
- private String distributionLocale;
-
- public static interface SearchEngineCallback {
- public void execute(@Nullable SearchEngine engine);
- }
-
- public SearchEngineManager(Context context, Distribution distribution) {
- this.context = context;
- this.distribution = distribution;
- GeckoSharedPrefs.forApp(context).registerOnSharedPreferenceChangeListener(this);
- }
-
- /**
- * Sets a callback to be called when the default engine changes. This can be called from any thread.
- *
- * @param changeCallback SearchEngineCallback to be called after the search engine
- * changed. This will run on the UI thread.
- * Note: callback may be called with null engine.
- */
- public void setChangeCallback(SearchEngineCallback changeCallback) {
- this.changeCallback = changeCallback;
- }
-
- /**
- * Perform an action with the user's default search engine. This can be called from any thread.
- *
- * @param callback The callback to be used with the user's default search engine. The call
- * may be sync or async; if the call is async, it will be called on the
- * ui thread.
- */
- public void getEngine(SearchEngineCallback callback) {
- if (engine != null) {
- callback.execute(engine);
- } else {
- getDefaultEngine(callback);
- }
- }
-
- /**
- * Should be called when the object goes out of scope.
- */
- public void unregisterListeners() {
- GeckoSharedPrefs.forApp(context).unregisterOnSharedPreferenceChangeListener(this);
- }
-
- private volatile int ignorePreferenceChange = 0;
-
- @UiThread // according to the docs.
- @Override
- public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
- if (!TextUtils.equals(PREF_DEFAULT_ENGINE_KEY, key)) {
- return;
- }
-
- if (ignorePreferenceChange > 0) {
- ignorePreferenceChange--;
- return;
- }
-
- getDefaultEngine(changeCallback);
- }
-
- /**
- * Runs a SearchEngineCallback on the main thread.
- */
- private void runCallback(final SearchEngine engine, @Nullable final SearchEngineCallback callback) {
- ThreadUtils.postToUiThread(new RunCallbackUiThreadRunnable(this, engine, callback));
- }
-
- // Static is not strictly necessary but the outer class has a reference to Context so we should GC ASAP.
- private static class RunCallbackUiThreadRunnable implements Runnable {
- private final WeakReference<SearchEngineManager> searchEngineManagerWeakReference;
- private final SearchEngine searchEngine;
- private final SearchEngineCallback callback;
-
- public RunCallbackUiThreadRunnable(final SearchEngineManager searchEngineManager, final SearchEngine searchEngine,
- final SearchEngineCallback callback) {
- this.searchEngineManagerWeakReference = new WeakReference<>(searchEngineManager);
- this.searchEngine = searchEngine;
- this.callback = callback;
- }
-
- @UiThread
- @Override
- public void run() {
- final SearchEngineManager searchEngineManager = searchEngineManagerWeakReference.get();
- if (searchEngineManager == null) {
- return;
- }
-
- // Cache engine for future calls to getEngine.
- searchEngineManager.engine = searchEngine;
- if (callback != null) {
- callback.execute(searchEngine);
- }
-
- }
- }
-
- /**
- * This method finds and creates the default search engine. It will first look for
- * the default engine name, then create the engine from that name.
- *
- * To find the default engine name, we first look in shared preferences, then
- * the distribution (if one exists), and finally fall back to the localized default.
- *
- * @param callback SearchEngineCallback to be called after successfully looking
- * up the search engine. This will run on the UI thread.
- * Note: callback may be called with null engine.
- */
- private void getDefaultEngine(final SearchEngineCallback callback) {
- // This runnable is posted to the background thread.
- distribution.addOnDistributionReadyCallback(new GetDefaultEngineDistributionCallbacks(this, callback));
- }
-
- // Static is not strictly necessary but the outer class contains a reference to Context so we should GC ASAP.
- private static class GetDefaultEngineDistributionCallbacks implements Distribution.ReadyCallback {
- private final WeakReference<SearchEngineManager> searchEngineManagerWeakReference;
- private final SearchEngineCallback callback;
-
- public GetDefaultEngineDistributionCallbacks(final SearchEngineManager searchEngineManager,
- final SearchEngineCallback callback) {
- this.searchEngineManagerWeakReference = new WeakReference<>(searchEngineManager);
- this.callback = callback;
- }
-
- @Override
- public void distributionNotFound() {
- defaultBehavior();
- }
-
- @Override
- public void distributionFound(Distribution distribution) {
- defaultBehavior();
- }
-
- @Override
- public void distributionArrivedLate(Distribution distribution) {
- final SearchEngineManager searchEngineManager = searchEngineManagerWeakReference.get();
- if (searchEngineManager == null) {
- return;
- }
-
- // Let's see if there's a name in the distro.
- // If so, just this once we'll override the saved value.
- final String name = searchEngineManager.getDefaultEngineNameFromDistribution();
-
- if (name == null) {
- return;
- }
-
- // Store the default engine name for the future.
- // Increment an 'ignore' counter so that this preference change
- // won't cause getDefaultEngine to be called again.
- searchEngineManager.ignorePreferenceChange++;
- GeckoSharedPrefs.forApp(searchEngineManager.context)
- .edit()
- .putString(PREF_DEFAULT_ENGINE_KEY, name)
- .apply();
-
- final SearchEngine engine = searchEngineManager.createEngineFromName(name);
- searchEngineManager.runCallback(engine, callback);
- }
-
- @WorkerThread // calling methods are @WorkerThread
- private void defaultBehavior() {
- final SearchEngineManager searchEngineManager = searchEngineManagerWeakReference.get();
- if (searchEngineManager == null) {
- return;
- }
-
- // First look for a default name stored in shared preferences.
- String name = GeckoSharedPrefs.forApp(searchEngineManager.context).getString(PREF_DEFAULT_ENGINE_KEY, null);
-
- // Check for a region stored in shared preferences. If we don't have a region,
- // we should force a recheck of the default engine.
- String region = GeckoSharedPrefs.forApp(searchEngineManager.context).getString(PREF_REGION_KEY, null);
-
- if (name != null && region != null) {
- Log.d(LOG_TAG, "Found default engine name in SharedPreferences: " + name);
- } else {
- // First, look for the default search engine in a distribution.
- name = searchEngineManager.getDefaultEngineNameFromDistribution();
- if (name == null) {
- // Otherwise, get the default engine that we ship.
- name = searchEngineManager.getDefaultEngineNameFromLocale();
- }
-
- // Store the default engine name for the future.
- // Increment an 'ignore' counter so that this preference change
- // won't cause getDefaultEngine to be called again.
- searchEngineManager.ignorePreferenceChange++;
- GeckoSharedPrefs.forApp(searchEngineManager.context)
- .edit()
- .putString(PREF_DEFAULT_ENGINE_KEY, name)
- .apply();
- }
-
- final SearchEngine engine = searchEngineManager.createEngineFromName(name);
- searchEngineManager.runCallback(engine, callback);
- }
- }
-
- /**
- * Looks for a default search engine included in a distribution.
- * This method must be called after the distribution is ready.
- *
- * @return search engine name.
- */
- private String getDefaultEngineNameFromDistribution() {
- if (!distribution.exists()) {
- return null;
- }
-
- final File prefFile = distribution.getDistributionFile("preferences.json");
- if (prefFile == null) {
- return null;
- }
-
- try {
- final JSONObject all = FileUtils.readJSONObjectFromFile(prefFile);
-
- // First, look for a default locale specified by the distribution.
- if (all.has("Preferences")) {
- final JSONObject prefs = all.getJSONObject("Preferences");
- if (prefs.has(PREF_GECKO_DEFAULT_LOCALE)) {
- Log.d(LOG_TAG, "Found default searchplugin locale in distribution Preferences.");
- distributionLocale = prefs.getString(PREF_GECKO_DEFAULT_LOCALE);
- }
- }
-
- // Then, check to see if there's a locale-specific default engine override.
- final String languageTag = Locales.getLanguageTag(Locale.getDefault());
- final String overridesKey = "LocalizablePreferences." + languageTag;
- if (all.has(overridesKey)) {
- final JSONObject overridePrefs = all.getJSONObject(overridesKey);
- if (overridePrefs.has(PREF_GECKO_DEFAULT_ENGINE)) {
- Log.d(LOG_TAG, "Found default engine name in distribution LocalizablePreferences override.");
- return overridePrefs.getString(PREF_GECKO_DEFAULT_ENGINE);
- }
- }
-
- // Next, check to see if there's a non-override default engine pref.
- if (all.has("LocalizablePreferences")) {
- final JSONObject localizablePrefs = all.getJSONObject("LocalizablePreferences");
- if (localizablePrefs.has(PREF_GECKO_DEFAULT_ENGINE)) {
- Log.d(LOG_TAG, "Found default engine name in distribution LocalizablePreferences.");
- return localizablePrefs.getString(PREF_GECKO_DEFAULT_ENGINE);
- }
- }
- } catch (IOException e) {
- Log.e(LOG_TAG, "Error getting search engine name from preferences.json", e);
- } catch (JSONException e) {
- Log.e(LOG_TAG, "Error parsing preferences.json", e);
- }
- return null;
- }
-
- /**
- * Helper function for converting an InputStream to a String.
- * @param is InputStream you want to convert to a String
- *
- * @return String containing the data
- */
- private String getHttpResponse(HttpURLConnection conn) {
- InputStream is = null;
- try {
- is = new BufferedInputStream(conn.getInputStream());
- return new java.util.Scanner(is).useDelimiter("\\A").next();
- } catch (Exception e) {
- return "";
- } finally {
- if (is != null) {
- try {
- is.close();
- } catch (IOException e) {
- Log.e(LOG_TAG, "Error closing InputStream", e);
- }
- }
- }
- }
-
- /**
- * Gets the country code based on the current IP, using the Mozilla Location Service.
- * We cache the country code in a shared preference, so we only fetch from the network
- * once.
- *
- * @return String containing the country code
- */
- private String fetchCountryCode() {
- // First, we look to see if we have a cached code.
- final String region = GeckoSharedPrefs.forApp(context).getString(PREF_REGION_KEY, null);
- if (region != null) {
- return region;
- }
-
- // Since we didn't have a cached code, we need to fetch a code from the service.
- try {
- String responseText = null;
-
- URL url = new URL(GEOIP_LOCATION_URL);
- HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
- try {
- // POST an empty JSON object.
- final String message = "{}";
-
- urlConnection.setDoOutput(true);
- urlConnection.setConnectTimeout(10000);
- urlConnection.setReadTimeout(10000);
- urlConnection.setRequestMethod("POST");
- urlConnection.setRequestProperty("User-Agent", USER_AGENT);
- urlConnection.setRequestProperty("Content-Type", "application/json");
- urlConnection.setFixedLengthStreamingMode(message.getBytes().length);
-
- final OutputStream out = urlConnection.getOutputStream();
- out.write(message.getBytes());
- out.close();
-
- responseText = getHttpResponse(urlConnection);
- } finally {
- urlConnection.disconnect();
- }
-
- if (responseText == null) {
- Log.e(LOG_TAG, "Country code fetch failed");
- return null;
- }
-
- // Extract the country code and save it for later in a cache.
- final JSONObject response = new JSONObject(responseText);
- return response.optString("country_code", null);
- } catch (Exception e) {
- Log.e(LOG_TAG, "Country code fetch failed", e);
- }
-
- return null;
- }
-
- /**
- * Looks for the default search engine shipped in the locale.
- *
- * @return search engine name.
- */
- private String getDefaultEngineNameFromLocale() {
- try {
- final JSONObject browsersearch = new JSONObject(RawResource.getAsString(context, R.raw.browsersearch));
-
- // Get the region used to fence search engines.
- String region = fetchCountryCode();
-
- // Store the result, even if it's empty. If we fail to get a region, we never
- // try to get it again, and we will always fallback to the non-region engine.
- GeckoSharedPrefs.forApp(context)
- .edit()
- .putString(PREF_REGION_KEY, (region == null ? "" : region))
- .apply();
-
- if (region != null) {
- if (browsersearch.has("regions")) {
- final JSONObject regions = browsersearch.getJSONObject("regions");
- if (regions.has(region)) {
- final JSONObject regionData = regions.getJSONObject(region);
- Log.d(LOG_TAG, "Found region-specific default engine name in browsersearch.json.");
- return regionData.getString("default");
- }
- }
- }
-
- // Either we have no geoip region, or we didn't find the right region and we are falling back to the default.
- if (browsersearch.has("default")) {
- Log.d(LOG_TAG, "Found default engine name in browsersearch.json.");
- return browsersearch.getString("default");
- }
- } catch (IOException e) {
- Log.e(LOG_TAG, "Error getting search engine name from browsersearch.json", e);
- } catch (JSONException e) {
- Log.e(LOG_TAG, "Error parsing browsersearch.json", e);
- }
- return null;
- }
-
- /**
- * Creates a SearchEngine instance from an engine name.
- *
- * To create the engine, we first try to find the search plugin in the distribution
- * (if one exists), followed by the localized plugins we ship with the browser, and
- * then finally third-party plugins that are installed in the profile directory.
- *
- * This method must be called after the distribution is ready.
- *
- * @param name The search engine name (e.g. "Google" or "Amazon.com")
- * @return SearchEngine instance for name.
- */
- private SearchEngine createEngineFromName(String name) {
- // First, look in the distribution.
- SearchEngine engine = createEngineFromDistribution(name);
-
- // Second, look in the jar for plugins shipped with the locale.
- if (engine == null) {
- engine = createEngineFromLocale(name);
- }
-
- // Finally, look in the profile for third-party plugins.
- if (engine == null) {
- engine = createEngineFromProfile(name);
- }
-
- if (engine == null) {
- Log.e(LOG_TAG, "Could not create search engine from name: " + name);
- }
-
- return engine;
- }
-
- /**
- * Creates a SearchEngine instance for a distribution search plugin.
- *
- * This method iterates through the distribution searchplugins directory,
- * creating SearchEngine instances until it finds one with the right name.
- *
- * This method must be called after the distribution is ready.
- *
- * @param name Search engine name.
- * @return SearchEngine instance for name.
- */
- private SearchEngine createEngineFromDistribution(String name) {
- if (!distribution.exists()) {
- return null;
- }
-
- final File pluginsDir = distribution.getDistributionFile("searchplugins");
- if (pluginsDir == null) {
- return null;
- }
-
- // Collect an array of files to scan using the same approach as
- // DirectoryService._appendDistroSearchDirs which states:
- // Common engines are loaded for all locales. If there is no locale directory for
- // the current locale, there is a pref: "distribution.searchplugins.defaultLocale",
- // which specifies a default locale to use.
- ArrayList<File> files = new ArrayList<>();
-
- // Load files from the common folder first
- final File[] commonFiles = (new File(pluginsDir, "common")).listFiles();
- if (commonFiles != null) {
- Collections.addAll(files, commonFiles);
- }
-
- // Next, check to see if there's a locale-specific override.
- final File localeDir = new File(pluginsDir, "locale");
- if (localeDir != null) {
- final String languageTag = Locales.getLanguageTag(Locale.getDefault());
- final File[] localeFiles = (new File(localeDir, languageTag)).listFiles();
- if (localeFiles != null) {
- Collections.addAll(files, localeFiles);
- } else {
- // We didn't append the locale dir - try the default one.
- if (distributionLocale != null) {
- final File[] defaultLocaleFiles = (new File(localeDir, distributionLocale)).listFiles();
- if (defaultLocaleFiles != null) {
- Collections.addAll(files, defaultLocaleFiles);
- }
- }
- }
- }
-
- if (files.isEmpty()) {
- Log.e(LOG_TAG, "Could not find search plugin files in distribution directory");
- return null;
- }
-
- return createEngineFromFileList(files.toArray(new File[files.size()]), name);
- }
-
- /**
- * Creates a SearchEngine instance for a search plugin shipped in the locale.
- *
- * This method reads the list of search plugin file names from list.txt, then
- * iterates through the files, creating SearchEngine instances until it finds one
- * with the right name. Unfortunately, we need to do this because there is no
- * other way to map the search engine "name" to the file for the search plugin.
- *
- * @param name Search engine name.
- * @return SearchEngine instance for name.
- */
- private SearchEngine createEngineFromLocale(String name) {
- final InputStream in = getInputStreamFromSearchPluginsJar("list.txt");
- if (in == null) {
- return null;
- }
- final BufferedReader br = getBufferedReader(in);
-
- try {
- String identifier;
- while ((identifier = br.readLine()) != null) {
- final InputStream pluginIn = getInputStreamFromSearchPluginsJar(identifier + ".xml");
- // pluginIn can be null if the xml file doesn't exist which
- // can happen with :hidden plugins
- if (pluginIn != null) {
- final SearchEngine engine = createEngineFromInputStream(identifier, pluginIn);
- if (engine != null && engine.getName().equals(name)) {
- return engine;
- }
- }
- }
- } catch (Exception e) {
- Log.e(LOG_TAG, "Error creating shipped search engine from name: " + name, e);
- } finally {
- try {
- br.close();
- } catch (IOException e) {
- // Ignore.
- }
- }
- return null;
- }
-
- /**
- * Creates a SearchEngine instance for a search plugin in the profile directory.
- *
- * This method iterates through the profile searchplugins directory, creating
- * SearchEngine instances until it finds one with the right name.
- *
- * @param name Search engine name.
- * @return SearchEngine instance for name.
- */
- private SearchEngine createEngineFromProfile(String name) {
- final File pluginsDir = GeckoProfile.get(context).getFile("searchplugins");
- if (pluginsDir == null) {
- return null;
- }
-
- final File[] files = pluginsDir.listFiles();
- if (files == null) {
- Log.e(LOG_TAG, "Could not find search plugin files in profile directory");
- return null;
- }
- return createEngineFromFileList(files, name);
- }
-
- /**
- * This method iterates through an array of search plugin files, creating
- * SearchEngine instances until it finds one with the right name.
- *
- * @param files Array of search plugin files. Should not be null.
- * @param name Search engine name.
- * @return SearchEngine instance for name.
- */
- private SearchEngine createEngineFromFileList(File[] files, String name) {
- for (int i = 0; i < files.length; i++) {
- try {
- final FileInputStream fis = new FileInputStream(files[i]);
- final SearchEngine engine = createEngineFromInputStream(null, fis);
- if (engine != null && engine.getName().equals(name)) {
- return engine;
- }
- } catch (IOException e) {
- Log.e(LOG_TAG, "Error creating search engine from name: " + name, e);
- }
- }
- return null;
- }
-
- /**
- * Creates a SearchEngine instance from an InputStream.
- *
- * This method closes the stream after it is done reading it.
- *
- * @param identifier Seach engine identifier. This only exists for search engines that
- * ship with the default set of engines in the locale.
- * @param in InputStream for search plugin XML file.
- * @return SearchEngine instance.
- */
- private SearchEngine createEngineFromInputStream(String identifier, InputStream in) {
- try {
- try {
- return new SearchEngine(identifier, in);
- } finally {
- in.close();
- }
- } catch (Exception e) {
- Log.e(LOG_TAG, "Exception creating search engine", e);
- }
-
- return null;
- }
-
- /**
- * Reads a file from the searchplugins directory in the Gecko jar.
- *
- * @param fileName name of the file to read.
- * @return InputStream for file.
- */
- private InputStream getInputStreamFromSearchPluginsJar(String fileName) {
- final Locale locale = Locale.getDefault();
-
- // First, try a file path for the full locale.
- final String languageTag = Locales.getLanguageTag(locale);
- String url = getSearchPluginsJarURL(context, languageTag, fileName);
-
- InputStream in = GeckoJarReader.getStream(context, url);
- if (in != null) {
- return in;
- }
-
- // If that doesn't work, try a file path for just the language.
- final String language = Locales.getLanguage(locale);
- if (!languageTag.equals(language)) {
- url = getSearchPluginsJarURL(context, language, fileName);
- in = GeckoJarReader.getStream(context, url);
- if (in != null) {
- return in;
- }
- }
-
- // Finally, fall back to default locale defined in chrome registry.
- url = getSearchPluginsJarURL(context, getFallbackLocale(), fileName);
- return GeckoJarReader.getStream(context, url);
- }
-
- /**
- * Finds a fallback locale in the Gecko chrome registry. If a locale is declared
- * here, we should be guaranteed to find a searchplugins directory for it.
- *
- * This method should only be accessed from the background thread.
- */
- private String getFallbackLocale() {
- if (fallbackLocale != null) {
- return fallbackLocale;
- }
-
- final InputStream in = GeckoJarReader.getStream(
- context, GeckoJarReader.getJarURL(context, "chrome/chrome.manifest"));
- if (in == null) {
- return null;
- }
- final BufferedReader br = getBufferedReader(in);
-
- try {
- String line;
- while ((line = br.readLine()) != null) {
- // We're looking for a line like "locale global en-US en-US/locale/en-US/global/"
- // https://developer.mozilla.org/en/docs/Chrome_Registration#locale
- if (line.startsWith("locale global ")) {
- fallbackLocale = line.split(" ", 4)[2];
- break;
- }
- }
- } catch (IOException e) {
- Log.e(LOG_TAG, "Error reading fallback locale from chrome registry", e);
- } finally {
- try {
- br.close();
- } catch (IOException e) {
- // Ignore.
- }
- }
- return fallbackLocale;
- }
-
- /**
- * Gets the jar URL for a file in the searchplugins directory.
- *
- * @param locale String representing the Gecko locale (e.g. "en-US").
- * @param fileName The name of the file to read.
- * @return URL for jar file.
- */
- private static String getSearchPluginsJarURL(Context context, String locale, String fileName) {
- final String path = "chrome/" + locale + "/locale/" + locale + "/browser/searchplugins/" + fileName;
- return GeckoJarReader.getJarURL(context, path);
- }
-
- private BufferedReader getBufferedReader(InputStream in) {
- try {
- return new BufferedReader(new InputStreamReader(in, "UTF-8"));
- } catch (UnsupportedEncodingException e) {
- // Cannot happen.
- return null;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueueHelper.java b/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueueHelper.java
deleted file mode 100644
index 667eb8f6c..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueueHelper.java
+++ /dev/null
@@ -1,357 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabqueue;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.res.Resources;
-import android.graphics.PixelFormat;
-import android.support.v4.app.NotificationCompat;
-import android.support.v4.content.ContextCompat;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.View;
-import android.view.WindowManager;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class TabQueueHelper {
- private static final String LOGTAG = "Gecko" + TabQueueHelper.class.getSimpleName();
-
- // Disable Tab Queue for API level 10 (GB) - Bug 1206055
- public static final boolean TAB_QUEUE_ENABLED = true;
-
- public static final String FILE_NAME = "tab_queue_url_list.json";
- public static final String LOAD_URLS_ACTION = "TAB_QUEUE_LOAD_URLS_ACTION";
- public static final int TAB_QUEUE_NOTIFICATION_ID = R.id.tabQueueNotification;
-
- public static final String PREF_TAB_QUEUE_COUNT = "tab_queue_count";
- public static final String PREF_TAB_QUEUE_LAUNCHES = "tab_queue_launches";
- public static final String PREF_TAB_QUEUE_TIMES_PROMPT_SHOWN = "tab_queue_times_prompt_shown";
-
- public static final int MAX_TIMES_TO_SHOW_PROMPT = 3;
- public static final int EXTERNAL_LAUNCHES_BEFORE_SHOWING_PROMPT = 3;
-
- // result codes for returning from the prompt
- public static final int TAB_QUEUE_YES = 201;
- public static final int TAB_QUEUE_NO = 202;
-
- /**
- * Checks if the specified context can draw on top of other apps. As of API level 23, an app
- * cannot draw on top of other apps unless it declares the SYSTEM_ALERT_WINDOW permission in
- * its manifest, AND the user specifically grants the app this capability.
- *
- * @return true if the specified context can draw on top of other apps, false otherwise.
- */
- public static boolean canDrawOverlays(Context context) {
- if (AppConstants.Versions.preMarshmallow) {
- return true; // We got the permission at install time.
- }
-
- // It would be nice to just use Settings.canDrawOverlays() - but this helper is buggy for
- // apps using sharedUserId (See bug 1244722).
- // Instead we'll add and remove an invisible view. If this is successful then we seem to
- // have permission to draw overlays.
-
- View view = new View(context);
- view.setVisibility(View.INVISIBLE);
-
- WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
- 1, 1,
- WindowManager.LayoutParams.TYPE_PHONE,
- WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
- PixelFormat.TRANSLUCENT);
-
- WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
-
- try {
- windowManager.addView(view, layoutParams);
- windowManager.removeView(view);
- return true;
- } catch (final SecurityException | WindowManager.BadTokenException e) {
- return false;
- }
- }
-
- /**
- * Check if we should show the tab queue prompt
- *
- * @param context
- * @return true if we should display the prompt, false if not.
- */
- public static boolean shouldShowTabQueuePrompt(Context context) {
- final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
-
- int numberOfTimesTabQueuePromptSeen = prefs.getInt(PREF_TAB_QUEUE_TIMES_PROMPT_SHOWN, 0);
-
- // Exit early if the feature is already enabled or the user has seen the
- // prompt more than MAX_TIMES_TO_SHOW_PROMPT times.
- if (isTabQueueEnabled(prefs) || numberOfTimesTabQueuePromptSeen >= MAX_TIMES_TO_SHOW_PROMPT) {
- return false;
- }
-
- final int viewActionIntentLaunches = prefs.getInt(PREF_TAB_QUEUE_LAUNCHES, 0) + 1;
- if (viewActionIntentLaunches < EXTERNAL_LAUNCHES_BEFORE_SHOWING_PROMPT) {
- // Allow a few external links to open before we prompt the user.
- prefs.edit().putInt(PREF_TAB_QUEUE_LAUNCHES, viewActionIntentLaunches).apply();
- } else if (viewActionIntentLaunches == EXTERNAL_LAUNCHES_BEFORE_SHOWING_PROMPT) {
- // Reset to avoid repeatedly showing the prompt if the user doesn't interact with it and
- // we get more external VIEW action intents in.
- final SharedPreferences.Editor editor = prefs.edit();
- editor.remove(TabQueueHelper.PREF_TAB_QUEUE_LAUNCHES);
-
- int timesPromptShown = prefs.getInt(TabQueueHelper.PREF_TAB_QUEUE_TIMES_PROMPT_SHOWN, 0) + 1;
- editor.putInt(TabQueueHelper.PREF_TAB_QUEUE_TIMES_PROMPT_SHOWN, timesPromptShown);
- editor.apply();
-
- // Show the prompt
- return true;
- }
-
- return false;
- }
-
- /**
- * Reads file and converts any content to JSON, adds passed in URL to the data and writes back to the file,
- * creating the file if it doesn't already exist. This should not be run on the UI thread.
- *
- * @param profile
- * @param url URL to add
- * @param filename filename to add URL to
- * @return the number of tabs currently queued
- */
- public static int queueURL(final GeckoProfile profile, final String url, final String filename) {
- ThreadUtils.assertNotOnUiThread();
-
- JSONArray jsonArray = profile.readJSONArrayFromFile(filename);
-
- jsonArray.put(url);
-
- profile.writeFile(filename, jsonArray.toString());
-
- return jsonArray.length();
- }
-
- /**
- * Remove a url from the file, if it exists.
- * If the url exists multiple times, all instances of it will be removed.
- * This should not be run on the UI thread.
- *
- * @param context
- * @param urlToRemove URL to remove
- * @param filename filename to remove URL from
- * @return the number of queued urls
- */
- public static int removeURLFromFile(final Context context, final String urlToRemove, final String filename) {
- ThreadUtils.assertNotOnUiThread();
-
- final GeckoProfile profile = GeckoProfile.get(context);
-
- JSONArray jsonArray = profile.readJSONArrayFromFile(filename);
- JSONArray newArray = new JSONArray();
- String url;
-
- // Since JSONArray.remove was only added in API 19, we have to use two arrays in order to remove.
- for (int i = 0; i < jsonArray.length(); i++) {
- try {
- url = jsonArray.getString(i);
- } catch (JSONException e) {
- url = "";
- }
- if (!TextUtils.isEmpty(url) && !urlToRemove.equals(url)) {
- newArray.put(url);
- }
- }
-
- profile.writeFile(filename, newArray.toString());
-
- final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
- prefs.edit().putInt(PREF_TAB_QUEUE_COUNT, newArray.length()).apply();
-
- return newArray.length();
- }
-
- /**
- * Get up to eight of the last queued URLs for displaying in the notification.
- */
- public static List<String> getLastURLs(final Context context, final String filename) {
- final GeckoProfile profile = GeckoProfile.get(context);
- final JSONArray jsonArray = profile.readJSONArrayFromFile(filename);
- final List<String> urls = new ArrayList<>(8);
-
- for (int i = 0; i < 8; i++) {
- try {
- urls.add(jsonArray.getString(i));
- } catch (JSONException e) {
- Log.w(LOGTAG, "Unable to parse URL from tab queue array", e);
- }
- }
-
- return urls;
- }
-
- /**
- * Displays a notification showing the total number of tabs queue. If there is already a notification displayed, it
- * will be replaced.
- *
- * @param context
- * @param tabsQueued
- */
- public static void showNotification(final Context context, final int tabsQueued, final List<String> urls) {
- ThreadUtils.assertNotOnUiThread();
-
- Intent resultIntent = new Intent();
- resultIntent.setClassName(context, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
- resultIntent.setAction(TabQueueHelper.LOAD_URLS_ACTION);
-
- PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, resultIntent, PendingIntent.FLAG_CANCEL_CURRENT);
-
- final String text;
- final Resources resources = context.getResources();
- if (tabsQueued == 1) {
- text = resources.getString(R.string.tab_queue_notification_text_singular);
- } else {
- text = resources.getString(R.string.tab_queue_notification_text_plural, tabsQueued);
- }
-
- NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
- inboxStyle.setBigContentTitle(text);
- for (String url : urls) {
- inboxStyle.addLine(url);
- }
- inboxStyle.setSummaryText(resources.getString(R.string.tab_queue_notification_title));
-
- NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
- .setSmallIcon(R.drawable.ic_status_logo)
- .setContentTitle(text)
- .setContentText(resources.getString(R.string.tab_queue_notification_title))
- .setStyle(inboxStyle)
- .setColor(ContextCompat.getColor(context, R.color.fennec_ui_orange))
- .setNumber(tabsQueued)
- .setContentIntent(pendingIntent);
-
- NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.notify(TabQueueHelper.TAB_QUEUE_NOTIFICATION_ID, builder.build());
- }
-
- public static boolean shouldOpenTabQueueUrls(final Context context) {
- ThreadUtils.assertNotOnUiThread();
-
- // TODO: Use profile shared prefs when bug 1147925 gets fixed.
- final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
-
- int tabsQueued = prefs.getInt(PREF_TAB_QUEUE_COUNT, 0);
-
- return isTabQueueEnabled(prefs) && tabsQueued > 0;
- }
-
- public static int getTabQueueLength(final Context context) {
- ThreadUtils.assertNotOnUiThread();
-
- // TODO: Use profile shared prefs when bug 1147925 gets fixed.
- final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
- return prefs.getInt(PREF_TAB_QUEUE_COUNT, 0);
- }
-
- public static void openQueuedUrls(final Context context, final GeckoProfile profile, final String filename, boolean shouldPerformJavaScriptCallback) {
- ThreadUtils.assertNotOnUiThread();
-
- removeNotification(context);
-
- // exit early if we don't have any tabs queued
- if (getTabQueueLength(context) < 1) {
- return;
- }
-
- JSONArray jsonArray = profile.readJSONArrayFromFile(filename);
-
- if (jsonArray.length() > 0) {
- JSONObject data = new JSONObject();
- try {
- data.put("urls", jsonArray);
- data.put("shouldNotifyTabsOpenedToJava", shouldPerformJavaScriptCallback);
- GeckoAppShell.notifyObservers("Tabs:OpenMultiple", data.toString());
- } catch (JSONException e) {
- // Don't exit early as we perform cleanup at the end of this function.
- Log.e(LOGTAG, "Error sending tab queue data", e);
- }
- }
-
- try {
- profile.deleteFileFromProfileDir(filename);
- } catch (IllegalArgumentException e) {
- Log.e(LOGTAG, "Error deleting Tab Queue data file.", e);
- }
-
- // TODO: Use profile shared prefs when bug 1147925 gets fixed.
- final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
- prefs.edit().remove(PREF_TAB_QUEUE_COUNT).apply();
- }
-
- protected static void removeNotification(Context context) {
- NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.cancel(TAB_QUEUE_NOTIFICATION_ID);
- }
-
- public static boolean processTabQueuePromptResponse(int resultCode, Context context) {
- final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
- final SharedPreferences.Editor editor = prefs.edit();
-
- switch (resultCode) {
- case TAB_QUEUE_YES:
- editor.putBoolean(GeckoPreferences.PREFS_TAB_QUEUE, true);
-
- // By making this one more than EXTERNAL_LAUNCHES_BEFORE_SHOWING_PROMPT we ensure the prompt
- // will never show again without having to keep track of an extra pref.
- editor.putInt(TabQueueHelper.PREF_TAB_QUEUE_LAUNCHES,
- TabQueueHelper.EXTERNAL_LAUNCHES_BEFORE_SHOWING_PROMPT + 1);
- break;
-
- case TAB_QUEUE_NO:
- // The user clicked the 'no' button, so let's make sure the user never sees the prompt again by
- // maxing out the pref used to count the VIEW action intents received and times they've seen the prompt.
-
- editor.putInt(TabQueueHelper.PREF_TAB_QUEUE_LAUNCHES,
- TabQueueHelper.EXTERNAL_LAUNCHES_BEFORE_SHOWING_PROMPT + 1);
-
- editor.putInt(TabQueueHelper.PREF_TAB_QUEUE_TIMES_PROMPT_SHOWN,
- TabQueueHelper.MAX_TIMES_TO_SHOW_PROMPT + 1);
- break;
-
- default:
- // We shouldn't ever get here.
- Log.w(LOGTAG, "Unrecognized result code received from the tab queue prompt: " + resultCode);
- }
-
- editor.apply();
-
- return resultCode == TAB_QUEUE_YES;
- }
-
- public static boolean isTabQueueEnabled(Context context) {
- return isTabQueueEnabled(GeckoSharedPrefs.forApp(context));
- }
-
- public static boolean isTabQueueEnabled(SharedPreferences prefs) {
- return prefs.getBoolean(GeckoPreferences.PREFS_TAB_QUEUE, false);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueuePrompt.java b/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueuePrompt.java
deleted file mode 100644
index ead16ccba..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueuePrompt.java
+++ /dev/null
@@ -1,215 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabqueue;
-
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.Locales;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-
-import android.annotation.TargetApi;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.provider.Settings;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.View;
-import android.widget.Toast;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-
-public class TabQueuePrompt extends Locales.LocaleAwareActivity {
- public static final String LOGTAG = "Gecko" + TabQueuePrompt.class.getSimpleName();
-
- private static final int SETTINGS_REQUEST_CODE = 1;
-
- // Flag set during animation to prevent animation multiple-start.
- private boolean isAnimating;
-
- private View containerView;
- private View buttonContainer;
- private View enabledConfirmation;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- showTabQueueEnablePrompt();
- }
-
- private void showTabQueueEnablePrompt() {
- setContentView(R.layout.tab_queue_prompt);
-
- final View okButton = findViewById(R.id.ok_button);
- okButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- onConfirmButtonPressed();
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "tabqueue_prompt_yes");
- }
- });
- findViewById(R.id.cancel_button).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "tabqueue_prompt_no");
- setResult(TabQueueHelper.TAB_QUEUE_NO);
- finish();
- }
- });
- final View settingsButton = findViewById(R.id.settings_button);
- settingsButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- onSettingsButtonPressed();
- }
- });
-
- final View tipView = findViewById(R.id.tip_text);
- final View settingsPermitView = findViewById(R.id.settings_permit_text);
-
- if (TabQueueHelper.canDrawOverlays(this)) {
- okButton.setVisibility(View.VISIBLE);
- settingsButton.setVisibility(View.GONE);
- tipView.setVisibility(View.VISIBLE);
- settingsPermitView.setVisibility(View.GONE);
- } else {
- okButton.setVisibility(View.GONE);
- settingsButton.setVisibility(View.VISIBLE);
- tipView.setVisibility(View.GONE);
- settingsPermitView.setVisibility(View.VISIBLE);
- }
-
- containerView = findViewById(R.id.tab_queue_container);
- buttonContainer = findViewById(R.id.button_container);
- enabledConfirmation = findViewById(R.id.enabled_confirmation);
-
- containerView.setTranslationY(500);
- containerView.setAlpha(0);
-
- final Animator translateAnimator = ObjectAnimator.ofFloat(containerView, "translationY", 0);
- translateAnimator.setDuration(400);
-
- final Animator alphaAnimator = ObjectAnimator.ofFloat(containerView, "alpha", 1);
- alphaAnimator.setStartDelay(200);
- alphaAnimator.setDuration(600);
-
- final AnimatorSet set = new AnimatorSet();
- set.playTogether(alphaAnimator, translateAnimator);
- set.setStartDelay(400);
-
- set.start();
- }
-
- @Override
- public void finish() {
- super.finish();
-
- // Don't perform an activity-dismiss animation.
- overridePendingTransition(0, 0);
- }
-
- private void onConfirmButtonPressed() {
- enabledConfirmation.setVisibility(View.VISIBLE);
- enabledConfirmation.setAlpha(0);
-
- final Animator buttonsAlphaAnimator = ObjectAnimator.ofFloat(buttonContainer, "alpha", 0);
- buttonsAlphaAnimator.setDuration(300);
-
- final Animator messagesAlphaAnimator = ObjectAnimator.ofFloat(enabledConfirmation, "alpha", 1);
- messagesAlphaAnimator.setDuration(300);
- messagesAlphaAnimator.setStartDelay(200);
-
- final AnimatorSet set = new AnimatorSet();
- set.playTogether(buttonsAlphaAnimator, messagesAlphaAnimator);
-
- set.addListener(new AnimatorListenerAdapter() {
-
- @Override
- public void onAnimationEnd(Animator animation) {
-
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- slideOut();
- setResult(TabQueueHelper.TAB_QUEUE_YES);
- }
- }, 1000);
- }
- });
-
- set.start();
- }
-
- @TargetApi(Build.VERSION_CODES.M)
- private void onSettingsButtonPressed() {
- Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
- intent.setData(Uri.parse("package:" + getPackageName()));
- startActivityForResult(intent, SETTINGS_REQUEST_CODE);
-
- Toast.makeText(this, R.string.tab_queue_prompt_permit_drawing_over_apps, Toast.LENGTH_LONG).show();
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (requestCode != SETTINGS_REQUEST_CODE) {
- return;
- }
-
- if (TabQueueHelper.canDrawOverlays(this)) {
- // User granted the permission in Android's settings.
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "tabqueue_prompt_yes");
-
- setResult(TabQueueHelper.TAB_QUEUE_YES);
- finish();
- }
- }
-
- /**
- * Slide the overlay down off the screen and destroy it.
- */
- private void slideOut() {
- if (isAnimating) {
- return;
- }
-
- isAnimating = true;
-
- ObjectAnimator animator = ObjectAnimator.ofFloat(containerView, "translationY", containerView.getHeight());
- animator.addListener(new AnimatorListenerAdapter() {
-
- @Override
- public void onAnimationEnd(Animator animation) {
- finish();
- }
-
- });
- animator.start();
- }
-
- /**
- * Close the dialog if back is pressed.
- */
- @Override
- public void onBackPressed() {
- slideOut();
- }
-
- /**
- * Close the dialog if the anything that isn't a button is tapped.
- */
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- slideOut();
- return true;
- }
-} \ No newline at end of file
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueueService.java b/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueueService.java
deleted file mode 100644
index ebb1bd761..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueueService.java
+++ /dev/null
@@ -1,342 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabqueue;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-
-import android.annotation.TargetApi;
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.res.Resources;
-import android.graphics.PixelFormat;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.provider.Settings;
-import android.support.v4.app.NotificationCompat;
-import android.support.v4.app.NotificationManagerCompat;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.WindowManager;
-import android.widget.Button;
-import android.widget.TextView;
-import android.widget.Toast;
-import org.mozilla.gecko.mozglue.SafeIntent;
-
-import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-
-/**
- * On launch this Service displays a View over the currently running process with an action to open the url in Fennec
- * immediately. If the user takes no action, allowing the runnable to be processed after the specified
- * timeout (TOAST_TIMEOUT), the url is added to a file which is then read in Fennec on next launch, this allows the
- * user to quickly queue urls to open without having to open Fennec each time. If the Service receives an Intent whilst
- * the created View is still active, the old url is immediately processed and the View is re-purposed with the new
- * Intent data.
- * <p/>
- * The SYSTEM_ALERT_WINDOW permission is used to allow us to insert a View from this Service which responds to user
- * interaction, whilst still allowing whatever is in the background to be seen and interacted with.
- * <p/>
- * Using an Activity to do this doesn't seem to work as there's an issue to do with the native android intent resolver
- * dialog not being hidden when the toast is shown. Using an IntentService instead of a Service doesn't work as
- * each new Intent received kicks off the IntentService lifecycle anew which means that a new View is created each time,
- * meaning that we can't quickly queue the current data and re-purpose the View. The asynchronous nature of the
- * IntentService is another prohibitive factor.
- * <p/>
- * General approach taken is similar to the FB chat heads functionality:
- * http://stackoverflow.com/questions/15975988/what-apis-in-android-is-facebook-using-to-create-chat-heads
- */
-public class TabQueueService extends Service {
- private static final String LOGTAG = "Gecko" + TabQueueService.class.getSimpleName();
-
- private static final long TOAST_TIMEOUT = 3000;
- private static final long TOAST_DOUBLE_TAP_TIMEOUT_MILLIS = 6000;
-
- private WindowManager windowManager;
- private View toastLayout;
- private Button openNowButton;
- private Handler tabQueueHandler;
- private WindowManager.LayoutParams toastLayoutParams;
- private volatile StopServiceRunnable stopServiceRunnable;
- private HandlerThread handlerThread;
- private ExecutorService executorService;
-
- @Override
- public IBinder onBind(Intent intent) {
- // Not used
- return null;
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
- executorService = Executors.newSingleThreadExecutor();
-
- handlerThread = new HandlerThread("TabQueueHandlerThread");
- handlerThread.start();
- tabQueueHandler = new Handler(handlerThread.getLooper());
-
- windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
-
- LayoutInflater layoutInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
- toastLayout = layoutInflater.inflate(R.layout.tab_queue_toast, null);
-
- final Resources resources = getResources();
-
- TextView messageView = (TextView) toastLayout.findViewById(R.id.toast_message);
- messageView.setText(resources.getText(R.string.tab_queue_toast_message));
-
- openNowButton = (Button) toastLayout.findViewById(R.id.toast_button);
- openNowButton.setText(resources.getText(R.string.tab_queue_toast_action));
-
- toastLayoutParams = new WindowManager.LayoutParams(
- WindowManager.LayoutParams.MATCH_PARENT,
- WindowManager.LayoutParams.WRAP_CONTENT,
- WindowManager.LayoutParams.TYPE_PHONE,
- WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
- WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH |
- WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
- PixelFormat.TRANSLUCENT);
-
- toastLayoutParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
- }
-
- @Override
- public int onStartCommand(final Intent intent, final int flags, final int startId) {
- // If this is a redelivery then lets bypass the entire double tap to open now code as that's a big can of worms,
- // we also don't expect redeliveries because of the short time window associated with this feature.
- if (flags != START_FLAG_REDELIVERY) {
- final Context applicationContext = getApplicationContext();
- final SharedPreferences sharedPreferences = GeckoSharedPrefs.forApp(applicationContext);
-
- final String lastUrl = sharedPreferences.getString(GeckoPreferences.PREFS_TAB_QUEUE_LAST_SITE, "");
-
- final SafeIntent safeIntent = new SafeIntent(intent);
- final String intentUrl = safeIntent.getDataString();
-
- final long lastRunTime = sharedPreferences.getLong(GeckoPreferences.PREFS_TAB_QUEUE_LAST_TIME, 0);
- final boolean isWithinDoubleTapTimeLimit = System.currentTimeMillis() - lastRunTime < TOAST_DOUBLE_TAP_TIMEOUT_MILLIS;
-
- if (!TextUtils.isEmpty(lastUrl) && lastUrl.equals(intentUrl) && isWithinDoubleTapTimeLimit) {
- // Background thread because we could do some file IO if we have to remove a url from the list.
- tabQueueHandler.post(new Runnable() {
- @Override
- public void run() {
- // If there is a runnable around, that means that the previous process hasn't yet completed, so
- // we will need to prevent it from running and remove the view from the window manager.
- // If there is no runnable around then the url has already been added to the list, so we'll
- // need to remove it before proceeding or that url will open multiple times.
- if (stopServiceRunnable != null) {
- tabQueueHandler.removeCallbacks(stopServiceRunnable);
- stopSelfResult(stopServiceRunnable.getStartId());
- stopServiceRunnable = null;
- removeView();
- } else {
- TabQueueHelper.removeURLFromFile(applicationContext, intentUrl, TabQueueHelper.FILE_NAME);
- }
- openNow(safeIntent.getUnsafe());
-
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "tabqueue-doubletap");
- stopSelfResult(startId);
- }
- });
-
- return START_REDELIVER_INTENT;
- }
-
- sharedPreferences.edit().putString(GeckoPreferences.PREFS_TAB_QUEUE_LAST_SITE, intentUrl)
- .putLong(GeckoPreferences.PREFS_TAB_QUEUE_LAST_TIME, System.currentTimeMillis())
- .apply();
- }
-
- if (stopServiceRunnable != null) {
- // If we're already displaying a toast, keep displaying it but store the previous url.
- // The open button will refer to the most recently opened link.
- tabQueueHandler.removeCallbacks(stopServiceRunnable);
- stopServiceRunnable.run(false);
- } else {
- try {
- windowManager.addView(toastLayout, toastLayoutParams);
- } catch (final SecurityException | WindowManager.BadTokenException e) {
- Toast.makeText(this, getText(R.string.tab_queue_toast_message), Toast.LENGTH_SHORT).show();
- showSettingsNotification();
- }
- }
-
- stopServiceRunnable = new StopServiceRunnable(startId) {
- @Override
- public void onRun() {
- addURLToTabQueue(intent, TabQueueHelper.FILE_NAME);
- stopServiceRunnable = null;
- }
- };
-
- openNowButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(final View view) {
- tabQueueHandler.removeCallbacks(stopServiceRunnable);
- stopServiceRunnable = null;
- removeView();
- openNow(intent);
-
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "tabqueue-now");
- stopSelfResult(startId);
- }
- });
-
- tabQueueHandler.postDelayed(stopServiceRunnable, TOAST_TIMEOUT);
-
- return START_REDELIVER_INTENT;
- }
-
- private void openNow(Intent intent) {
- Intent forwardIntent = new Intent(intent);
- forwardIntent.setClassName(getApplicationContext(), AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
- forwardIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(forwardIntent);
-
- TabQueueHelper.removeNotification(getApplicationContext());
-
- GeckoSharedPrefs.forApp(getApplicationContext()).edit().remove(GeckoPreferences.PREFS_TAB_QUEUE_LAST_SITE)
- .remove(GeckoPreferences.PREFS_TAB_QUEUE_LAST_TIME)
- .apply();
-
- executorService.submit(new Runnable() {
- @Override
- public void run() {
- int queuedTabCount = TabQueueHelper.getTabQueueLength(TabQueueService.this);
- Telemetry.addToHistogram("FENNEC_TABQUEUE_QUEUESIZE", queuedTabCount);
- }
- });
-
- }
-
- @TargetApi(Build.VERSION_CODES.M)
- private void showSettingsNotification() {
- if (AppConstants.Versions.preMarshmallow) {
- return;
- }
-
- final Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
- intent.setData(Uri.parse("package:" + getPackageName()));
- PendingIntent pendingIntent = PendingIntent.getActivity(this, intent.hashCode(), intent, 0);
-
- final String text = getString(R.string.tab_queue_notification_settings);
-
- final NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle()
- .bigText(text);
-
- final Notification notification = new NotificationCompat.Builder(this)
- .setContentTitle(getString(R.string.pref_tab_queue_title))
- .setContentText(text)
- .setCategory(NotificationCompat.CATEGORY_ERROR)
- .setStyle(style)
- .setSmallIcon(R.drawable.ic_status_logo)
- .setContentIntent(pendingIntent)
- .setPriority(NotificationCompat.PRIORITY_MAX)
- .setAutoCancel(true)
- .addAction(R.drawable.ic_action_settings, getString(R.string.tab_queue_prompt_settings_button), pendingIntent)
- .build();
-
- NotificationManagerCompat.from(this).notify(R.id.tabQueueSettingsNotification, notification);
- }
-
- private void removeView() {
- try {
- windowManager.removeView(toastLayout);
- } catch (IllegalArgumentException | IllegalStateException e) {
- // This can happen if the Service is killed by the system. If this happens the View will have already
- // been removed but the runnable will have been kept alive.
- Log.e(LOGTAG, "Error removing Tab Queue toast from service", e);
- }
- }
-
- private void addURLToTabQueue(final Intent intent, final String filename) {
- if (intent == null) {
- // This should never happen, but let's return silently instead of crashing if it does.
- Log.w(LOGTAG, "Error adding URL to tab queue - invalid intent passed in.");
- return;
- }
- final SafeIntent safeIntent = new SafeIntent(intent);
- final String intentData = safeIntent.getDataString();
-
- // As we're doing disk IO, let's run this stuff in a separate thread.
- executorService.submit(new Runnable() {
- @Override
- public void run() {
- Context applicationContext = getApplicationContext();
- final GeckoProfile profile = GeckoProfile.get(applicationContext);
- int tabsQueued = TabQueueHelper.queueURL(profile, intentData, filename);
- List<String> urls = TabQueueHelper.getLastURLs(applicationContext, filename);
-
- TabQueueHelper.showNotification(applicationContext, tabsQueued, urls);
-
- // Store the number of URLs queued so that we don't have to read and process the file to see if we have
- // any urls to open.
- // TODO: Use profile shared prefs when bug 1147925 gets fixed.
- final SharedPreferences prefs = GeckoSharedPrefs.forApp(applicationContext);
-
- prefs.edit().putInt(TabQueueHelper.PREF_TAB_QUEUE_COUNT, tabsQueued).apply();
- }
- });
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- handlerThread.quit();
- }
-
- /**
- * A modified Runnable which additionally removes the view from the window view hierarchy and stops the service
- * when run, unless explicitly instructed not to.
- */
- private abstract class StopServiceRunnable implements Runnable {
-
- private final int startId;
-
- public StopServiceRunnable(final int startId) {
- this.startId = startId;
- }
-
- public void run() {
- run(true);
- }
-
- public void run(final boolean shouldRemoveView) {
- onRun();
-
- if (shouldRemoveView) {
- removeView();
- }
-
- stopSelfResult(startId);
- }
-
- public int getStartId() {
- return startId;
- }
-
- public abstract void onRun();
- }
-} \ No newline at end of file
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabReceivedService.java b/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabReceivedService.java
deleted file mode 100644
index 4f5baacdb..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabReceivedService.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabqueue;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.BrowserLocaleManager;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.BrowserContract;
-
-import android.app.IntentService;
-import android.app.PendingIntent;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.support.annotation.Nullable;
-import android.support.annotation.WorkerThread;
-import android.support.v4.app.NotificationCompat;
-import android.support.v4.app.NotificationManagerCompat;
-import android.util.Log;
-
-/**
- * An IntentService that displays a notification for a tab sent to this device.
- *
- * The expected Intent should contain:
- * * Data: URI to open in the notification
- * * EXTRA_TITLE: Page title of the URI to open
- */
-public class TabReceivedService extends IntentService {
- private static final String LOGTAG = "Gecko" + TabReceivedService.class.getSimpleName();
-
- private static final String PREF_NOTIFICATION_ID = "tab_received_notification_id";
-
- private static final int MAX_NOTIFICATION_COUNT = 1000;
-
- public TabReceivedService() {
- super(LOGTAG);
- setIntentRedelivery(true);
- }
-
- @Override
- protected void onHandleIntent(final Intent intent) {
- // IntentServices don't keep the process alive so
- // we need to do this every time. Ideally, we wouldn't.
- final Resources res = getResources();
- BrowserLocaleManager.getInstance().correctLocale(this, res, res.getConfiguration());
-
- final String uri = intent.getDataString();
- if (uri == null) {
- Log.d(LOGTAG, "Received null uri – ignoring");
- return;
- }
-
- final Intent notificationIntent = new Intent(Intent.ACTION_VIEW, intent.getData());
- notificationIntent.putExtra(BrowserContract.SKIP_TAB_QUEUE_FLAG, true);
- final PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
-
- final String notificationTitle = getNotificationTitle(intent.getStringExtra(BrowserContract.EXTRA_CLIENT_GUID));
- final NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
- builder.setSmallIcon(R.drawable.flat_icon);
- builder.setContentTitle(notificationTitle);
- builder.setWhen(System.currentTimeMillis());
- builder.setAutoCancel(true);
- builder.setContentText(uri);
- builder.setContentIntent(contentIntent);
-
- // Trigger "heads-up" notification mode on supported Android versions.
- builder.setPriority(NotificationCompat.PRIORITY_HIGH);
- final Uri notificationSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
- if (notificationSoundUri != null) {
- builder.setSound(notificationSoundUri);
- }
-
- final SharedPreferences prefs = GeckoSharedPrefs.forApp(this);
- final int notificationId = getNextNotificationId(prefs.getInt(PREF_NOTIFICATION_ID, 0));
- final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
- notificationManager.notify(notificationId, builder.build());
-
- // Save the ID last so if the Service is killed and the Intent is redelivered,
- // the ID is unlikely to have been updated and we would re-use the the old one.
- // This would prevent two identical notifications from appearing if the
- // notification was shown during the previous Intent processing attempt.
- prefs.edit().putInt(PREF_NOTIFICATION_ID, notificationId).apply();
- }
-
- /**
- * @param clientGUID the guid of the client in the clients table
- * @return the client's name from the clients table, if possible, else the brand name.
- */
- @WorkerThread
- private String getNotificationTitle(@Nullable final String clientGUID) {
- if (clientGUID == null) {
- Log.w(LOGTAG, "Received null guid, using brand name.");
- return AppConstants.MOZ_APP_DISPLAYNAME;
- }
-
- final Cursor c = getContentResolver().query(BrowserContract.Clients.CONTENT_URI,
- new String[] { BrowserContract.Clients.NAME },
- BrowserContract.Clients.GUID + "=?", new String[] { clientGUID }, null);
- try {
- if (c != null && c.moveToFirst()) {
- return c.getString(c.getColumnIndex(BrowserContract.Clients.NAME));
- } else {
- Log.w(LOGTAG, "Device not found, using brand name.");
- return AppConstants.MOZ_APP_DISPLAYNAME;
- }
- } finally {
- if (c != null) {
- c.close();
- }
- }
- }
-
- /**
- * Notification IDs must be unique else a notification
- * will be overwritten so we cycle them.
- */
- private int getNextNotificationId(final int currentId) {
- if (currentId > MAX_NOTIFICATION_COUNT) {
- return 0;
- } else {
- return currentId + 1;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/PrivateTabsPanel.java b/mobile/android/base/java/org/mozilla/gecko/tabs/PrivateTabsPanel.java
deleted file mode 100644
index b7bd83376..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/PrivateTabsPanel.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabs;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.tabs.TabsPanel.CloseAllPanelView;
-import org.mozilla.gecko.tabs.TabsPanel.TabsLayout;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.FrameLayout;
-
-/**
- * A container that wraps the private tabs {@link android.widget.AdapterView} and empty
- * {@link android.view.View} to manage both of their visibility states by changing the visibility of
- * this container as calling {@link android.widget.AdapterView#setVisibility} does not affect the
- * empty View's visibility.
- */
-class PrivateTabsPanel extends FrameLayout implements CloseAllPanelView {
- private final TabsLayout tabsLayout;
-
- public PrivateTabsPanel(final Context context, final AttributeSet attrs) {
- super(context, attrs);
-
- LayoutInflater.from(context).inflate(R.layout.private_tabs_panel, this);
- tabsLayout = (TabsLayout) findViewById(R.id.private_tabs_layout);
-
- final View emptyTabsFrame = findViewById(R.id.private_tabs_empty);
- tabsLayout.setEmptyView(emptyTabsFrame);
- }
-
- @Override
- public void setTabsPanel(final TabsPanel panel) {
- tabsLayout.setTabsPanel(panel);
- }
-
- @Override
- public void show() {
- tabsLayout.show();
- setVisibility(View.VISIBLE);
- }
-
- @Override
- public void hide() {
- setVisibility(View.GONE);
- tabsLayout.hide();
- }
-
- @Override
- public boolean shouldExpand() {
- return tabsLayout.shouldExpand();
- }
-
- @Override
- public void closeAll() {
- tabsLayout.closeAll();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabCurve.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabCurve.java
deleted file mode 100644
index 0b6a30d7a..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabCurve.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabs;
-
-import android.graphics.Path;
-
-/**
- * Utility methods to draws Firefox's tab curve shape.
- */
-public class TabCurve {
-
- public enum Direction {
- LEFT(-1),
- RIGHT(1);
-
- private final int value;
-
- private Direction(int value) {
- this.value = value;
- }
- }
-
- // Curve's aspect ratio
- private static final float ASPECT_RATIO = 0.729f;
-
- // Width multipliers
- private static final float W_M1 = 0.343f;
- private static final float W_M2 = 0.514f;
- private static final float W_M3 = 0.49f;
- private static final float W_M4 = 0.545f;
- private static final float W_M5 = 0.723f;
-
- // Height multipliers
- private static final float H_M1 = 0.25f;
- private static final float H_M2 = 0.5f;
- private static final float H_M3 = 0.72f;
- private static final float H_M4 = 0.961f;
-
- private TabCurve() {
- }
-
- public static float getWidthForHeight(float height) {
- return (int) (height * ASPECT_RATIO);
- }
-
- public static void drawFromTop(Path path, float from, float height, Direction dir) {
- final float width = getWidthForHeight(height);
-
- path.cubicTo(from + width * W_M1 * dir.value, 0.0f,
- from + width * W_M3 * dir.value, height * H_M1,
- from + width * W_M2 * dir.value, height * H_M2);
- path.cubicTo(from + width * W_M4 * dir.value, height * H_M3,
- from + width * W_M5 * dir.value, height * H_M4,
- from + width * dir.value, height);
- }
-
- public static void drawFromBottom(Path path, float from, float height, Direction dir) {
- final float width = getWidthForHeight(height);
-
- path.cubicTo(from + width * (1f - W_M5) * dir.value, height * H_M4,
- from + width * (1f - W_M4) * dir.value, height * H_M3,
- from + width * (1f - W_M2) * dir.value, height * H_M2);
- path.cubicTo(from + width * (1f - W_M3) * dir.value, height * H_M1,
- from + width * (1f - W_M1) * dir.value, 0.0f,
- from + width * dir.value, 0.0f);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryController.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryController.java
deleted file mode 100644
index 7b06c994c..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryController.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabs;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.util.GeckoRequest;
-import org.mozilla.gecko.util.NativeJSObject;
-
-import android.util.Log;
-
-public class TabHistoryController {
- private static final String LOGTAG = "TabHistoryController";
- private final OnShowTabHistory showTabHistoryListener;
-
- public static enum HistoryAction {
- ALL,
- BACK,
- FORWARD
- };
-
- public interface OnShowTabHistory {
- void onShowHistory(List<TabHistoryPage> historyPageList, int toIndex);
- }
-
- public TabHistoryController(OnShowTabHistory showTabHistoryListener) {
- this.showTabHistoryListener = showTabHistoryListener;
- }
-
- /**
- * This method will show the history for the current tab.
- */
- public boolean showTabHistory(final Tab tab, final HistoryAction action) {
- JSONObject json = new JSONObject();
- try {
- json.put("action", action.name());
- json.put("tabId", tab.getId());
- } catch (JSONException e) {
- Log.e(LOGTAG, "JSON error", e);
- }
-
- GeckoAppShell.sendRequestToGecko(new GeckoRequest("Session:GetHistory", json) {
- @Override
- public void onResponse(NativeJSObject nativeJSObject) {
- /*
- * The response from gecko request is of the form
- * {
- * "historyItems" : [
- * {
- * "title": "google",
- * "url": "google.com",
- * "selected": false
- * }
- * ],
- * toIndex = 1
- * }
- */
-
- final NativeJSObject[] historyItems = nativeJSObject.getObjectArray("historyItems");
- if (historyItems.length == 0) {
- // Empty history, return without showing the popup.
- return;
- }
-
- final List<TabHistoryPage> historyPageList = new ArrayList<>(historyItems.length);
- final int toIndex = nativeJSObject.getInt("toIndex");
-
- for (NativeJSObject obj : historyItems) {
- final String title = obj.getString("title");
- final String url = obj.getString("url");
- final boolean selected = obj.getBoolean("selected");
- historyPageList.add(new TabHistoryPage(title, url, selected));
- }
-
- showTabHistoryListener.onShowHistory(historyPageList, toIndex);
- }
- });
- return true;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryFragment.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryFragment.java
deleted file mode 100644
index e6deabdcf..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryFragment.java
+++ /dev/null
@@ -1,172 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabs;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoApplication;
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.content.DialogInterface;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentTransaction;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
-import android.widget.ArrayAdapter;
-import android.widget.ListView;
-
-public class TabHistoryFragment extends Fragment implements OnItemClickListener, OnClickListener {
- private static final String ARG_LIST = "historyPageList";
- private static final String ARG_INDEX = "index";
- private static final String BACK_STACK_ID = "backStateId";
-
- private List<TabHistoryPage> historyPageList;
- private int toIndex;
- private ListView dialogList;
- private int backStackId = -1;
- private ViewGroup parent;
- private boolean dismissed;
-
- public TabHistoryFragment() {
-
- }
-
- public static TabHistoryFragment newInstance(List<TabHistoryPage> historyPageList, int toIndex) {
- final TabHistoryFragment fragment = new TabHistoryFragment();
- final Bundle args = new Bundle();
- args.putParcelableArrayList(ARG_LIST, (ArrayList<? extends Parcelable>) historyPageList);
- args.putInt(ARG_INDEX, toIndex);
- fragment.setArguments(args);
- return fragment;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (savedInstanceState != null) {
- backStackId = savedInstanceState.getInt(BACK_STACK_ID, -1);
- }
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- this.parent = container;
- parent.setVisibility(View.VISIBLE);
- View view = inflater.inflate(R.layout.tab_history_layout, container, false);
- view.setOnClickListener(this);
- dialogList = (ListView) view.findViewById(R.id.tab_history_list);
- dialogList.setDivider(null);
- return view;
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- Bundle bundle = getArguments();
- historyPageList = bundle.getParcelableArrayList(ARG_LIST);
- toIndex = bundle.getInt(ARG_INDEX);
- final ArrayAdapter<TabHistoryPage> urlAdapter = new TabHistoryAdapter(getActivity(), historyPageList);
- dialogList.setAdapter(urlAdapter);
- dialogList.setOnItemClickListener(this);
- }
-
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- String index = String.valueOf(toIndex - position);
- GeckoAppShell.notifyObservers("Session:Navigate", index);
- dismiss();
- }
-
- @Override
- public void onClick(View v) {
- // Since the fragment view fills the entire screen, any clicks outside of the history
- // ListView will end up here.
- dismiss();
- }
-
- @Override
- public void onPause() {
- super.onPause();
- dismiss();
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- dismiss();
-
- GeckoApplication.watchReference(getActivity(), this);
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- if (backStackId >= 0) {
- outState.putInt(BACK_STACK_ID, backStackId);
- }
- }
-
- // Function to add this fragment to activity state with containerViewId as parent.
- // This similar in functionality to DialogFragment.show() except that containerId is provided here.
- public void show(final int containerViewId, final FragmentTransaction transaction, final String tag) {
- dismissed = false;
- transaction.add(containerViewId, this, tag);
- transaction.addToBackStack(tag);
- // Populating the tab history requires a gecko call (which can be slow) - therefore the app
- // state by the time we try to show this fragment is unknown, and we could be in the
- // middle of shutting down:
- backStackId = transaction.commitAllowingStateLoss();
- }
-
- // Pop the fragment from backstack if it exists.
- public void dismiss() {
- if (dismissed) {
- return;
- }
-
- dismissed = true;
-
- if (backStackId >= 0) {
- getFragmentManager().popBackStackImmediate(backStackId, FragmentManager.POP_BACK_STACK_INCLUSIVE);
- backStackId = -1;
- }
-
- if (parent != null) {
- parent.setVisibility(View.GONE);
- }
- }
-
- private static class TabHistoryAdapter extends ArrayAdapter<TabHistoryPage> {
- private final List<TabHistoryPage> pages;
- private final Context context;
-
- public TabHistoryAdapter(Context context, List<TabHistoryPage> pages) {
- super(context, R.layout.tab_history_item_row, pages);
- this.context = context;
- this.pages = pages;
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- TabHistoryItemRow row = (TabHistoryItemRow) convertView;
- if (row == null) {
- row = new TabHistoryItemRow(context, null);
- }
-
- row.update(pages.get(position), position == 0, position == pages.size() - 1);
- return row;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryItemRow.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryItemRow.java
deleted file mode 100644
index 112dbc07d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryItemRow.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabs;
-
-import android.content.Context;
-import android.graphics.Typeface;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.RelativeLayout;
-import android.widget.TextView;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.icons.IconResponse;
-import org.mozilla.gecko.icons.Icons;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.widget.FaviconView;
-
-import java.util.concurrent.Future;
-
-public class TabHistoryItemRow extends RelativeLayout {
- private final FaviconView favicon;
- private final TextView title;
- private final ImageView timeLineTop;
- private final ImageView timeLineBottom;
- private Future<IconResponse> ongoingIconLoad;
-
- public TabHistoryItemRow(Context context, AttributeSet attrs) {
- super(context, attrs);
- LayoutInflater.from(context).inflate(R.layout.tab_history_item_row, this);
- favicon = (FaviconView) findViewById(R.id.tab_history_icon);
- title = (TextView) findViewById(R.id.tab_history_title);
- timeLineTop = (ImageView) findViewById(R.id.tab_history_timeline_top);
- timeLineBottom = (ImageView) findViewById(R.id.tab_history_timeline_bottom);
- }
-
- // Update the views with historic page detail.
- public void update(final TabHistoryPage historyPage, boolean isFirstElement, boolean isLastElement) {
- ThreadUtils.assertOnUiThread();
-
- timeLineTop.setVisibility(isFirstElement ? View.INVISIBLE : View.VISIBLE);
- timeLineBottom.setVisibility(isLastElement ? View.INVISIBLE : View.VISIBLE);
- title.setText(historyPage.getTitle());
-
- if (historyPage.isSelected()) {
- // Highlight title with bold font.
- title.setTypeface(null, Typeface.BOLD);
- } else {
- // Clear previously set bold font.
- title.setTypeface(null, Typeface.NORMAL);
- }
-
- favicon.setEnabled(historyPage.isSelected());
- favicon.clearImage();
-
- if (ongoingIconLoad != null) {
- ongoingIconLoad.cancel(true);
- }
-
- ongoingIconLoad = Icons.with(getContext())
- .pageUrl(historyPage.getUrl())
- .skipNetwork()
- .build()
- .execute(favicon.createIconCallback());
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryPage.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryPage.java
deleted file mode 100644
index 6c608b2ac..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryPage.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabs;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-
-public class TabHistoryPage implements Parcelable {
- private final String title;
- private final String url;
- private final boolean selected;
-
- public TabHistoryPage(String title, String url, boolean selected) {
- this.title = title;
- this.url = url;
- this.selected = selected;
- }
-
- public String getTitle() {
- return title;
- }
-
- public String getUrl() {
- return url;
- }
-
- public boolean isSelected() {
- return selected;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeString(title);
- dest.writeString(url);
- dest.writeInt(selected ? 1 : 0);
- }
-
- public static final Parcelable.Creator<TabHistoryPage> CREATOR = new Parcelable.Creator<TabHistoryPage>() {
- @Override
- public TabHistoryPage createFromParcel(final Parcel source) {
- final String title = source.readString();
- final String url = source.readString();
- final boolean selected = source.readByte() != 0;
-
- final TabHistoryPage page = new TabHistoryPage(title, url, selected);
- return page;
- }
-
- @Override
- public TabHistoryPage[] newArray(int size) {
- return new TabHistoryPage[size];
- }
- };
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabPanelBackButton.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabPanelBackButton.java
deleted file mode 100644
index 7ea02407e..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabPanelBackButton.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabs;
-
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.ViewGroup;
-import android.widget.ImageButton;
-
-public class TabPanelBackButton extends ImageButton {
-
- private int dividerWidth = 0;
-
- private final Drawable divider;
- private final int dividerPadding;
-
- public TabPanelBackButton(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabPanelBackButton);
- divider = a.getDrawable(R.styleable.TabPanelBackButton_rightDivider);
- dividerPadding = (int) a.getDimension(R.styleable.TabPanelBackButton_dividerVerticalPadding, 0);
- a.recycle();
-
- if (divider != null) {
- dividerWidth = divider.getIntrinsicWidth();
- }
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- setMeasuredDimension(getMeasuredWidth() + dividerWidth, getMeasuredHeight());
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- if (divider != null) {
- final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) getLayoutParams();
- final int left = getRight() - lp.rightMargin - dividerWidth;
-
- divider.setBounds(left, getPaddingTop() + dividerPadding,
- left + dividerWidth, getHeight() - getPaddingBottom() - dividerPadding);
- divider.draw(canvas);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStrip.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStrip.java
deleted file mode 100644
index 5d3719343..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStrip.java
+++ /dev/null
@@ -1,170 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabs;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.StateListDrawable;
-import android.graphics.Rect;
-import android.support.v4.content.ContextCompat;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.TouchDelegate;
-import android.view.View;
-import android.view.ViewTreeObserver;
-
-import org.mozilla.gecko.BrowserApp.TabStripInterface;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.widget.themed.ThemedImageButton;
-import org.mozilla.gecko.widget.themed.ThemedLinearLayout;
-
-public class TabStrip extends ThemedLinearLayout
- implements TabStripInterface {
- private static final String LOGTAG = "GeckoTabStrip";
-
- private final TabStripView tabStripView;
- private final ThemedImageButton addTabButton;
-
- private final TabsListener tabsListener;
- private OnTabAddedOrRemovedListener tabChangedListener;
-
- public TabStrip(Context context) {
- this(context, null);
- }
-
- public TabStrip(Context context, AttributeSet attrs) {
- super(context, attrs);
- setOrientation(HORIZONTAL);
-
- LayoutInflater.from(context).inflate(R.layout.tab_strip_inner, this);
- tabStripView = (TabStripView) findViewById(R.id.tab_strip);
-
- addTabButton = (ThemedImageButton) findViewById(R.id.add_tab);
- addTabButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- final Tabs tabs = Tabs.getInstance();
- if (isPrivateMode()) {
- tabs.addPrivateTab();
- } else {
- tabs.addTab();
- }
- }
- });
-
- getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- getViewTreeObserver().removeOnPreDrawListener(this);
-
- final Rect r = new Rect();
- r.left = addTabButton.getRight();
- r.right = getWidth();
- r.top = 0;
- r.bottom = getHeight();
-
- // Redirect touch events between the 'new tab' button and the edge
- // of the screen to the 'new tab' button.
- setTouchDelegate(new TouchDelegate(r, addTabButton));
-
- return true;
- }
- });
-
- tabsListener = new TabsListener();
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- Tabs.registerOnTabsChangedListener(tabsListener);
- tabStripView.refreshTabs();
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- Tabs.unregisterOnTabsChangedListener(tabsListener);
- tabStripView.clearTabs();
- }
-
- @Override
- public void setPrivateMode(boolean isPrivate) {
- super.setPrivateMode(isPrivate);
- addTabButton.setPrivateMode(isPrivate);
- }
-
- public void setOnTabChangedListener(OnTabAddedOrRemovedListener listener) {
- tabChangedListener = listener;
- }
-
- private class TabsListener implements Tabs.OnTabsChangedListener {
- @Override
- public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
- switch (msg) {
- case RESTORED:
- tabStripView.restoreTabs();
- break;
-
- case ADDED:
- tabStripView.addTab(tab);
- if (tabChangedListener != null) {
- tabChangedListener.onTabChanged();
- }
- break;
-
- case CLOSED:
- tabStripView.removeTab(tab);
- if (tabChangedListener != null) {
- tabChangedListener.onTabChanged();
- }
- break;
-
- case SELECTED:
- // Update the selected position, then fall through...
- tabStripView.selectTab(tab);
- setPrivateMode(tab.isPrivate());
- case UNSELECTED:
- // We just need to update the style for the unselected tab...
- case TITLE:
- case FAVICON:
- case RECORDING_CHANGE:
- case AUDIO_PLAYING_CHANGE:
- tabStripView.updateTab(tab);
- break;
- }
- }
- }
-
- @Override
- public void refresh() {
- tabStripView.refresh();
- }
-
- @Override
- public void onLightweightThemeChanged() {
- final Drawable drawable = getTheme().getDrawable(this);
- if (drawable == null) {
- return;
- }
-
- final StateListDrawable stateList = new StateListDrawable();
- stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.text_and_tabs_tray_grey));
- stateList.addState(EMPTY_STATE_SET, drawable);
-
- setBackgroundDrawable(stateList);
- }
-
- @Override
- public void onLightweightThemeReset() {
- final int defaultBackgroundColor = ContextCompat.getColor(getContext(), R.color.text_and_tabs_tray_grey);
- setBackgroundColor(defaultBackgroundColor);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripAdapter.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripAdapter.java
deleted file mode 100644
index 8778aac31..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripAdapter.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabs;
-
-import android.content.Context;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-
-class TabStripAdapter extends BaseAdapter {
- private static final String LOGTAG = "GeckoTabStripAdapter";
-
- private final Context context;
- private List<Tab> tabs;
-
- public TabStripAdapter(Context context) {
- this.context = context;
- }
-
- @Override
- public Tab getItem(int position) {
- return (tabs != null &&
- position >= 0 &&
- position < tabs.size() ? tabs.get(position) : null);
- }
-
- @Override
- public long getItemId(int position) {
- final Tab tab = getItem(position);
- return (tab != null ? tab.getId() : -1);
- }
-
- @Override
- public boolean hasStableIds() {
- return true;
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- final TabStripItemView item;
- if (convertView == null) {
- item = (TabStripItemView)
- LayoutInflater.from(context).inflate(R.layout.tab_strip_item, parent, false);
- } else {
- item = (TabStripItemView) convertView;
- }
-
- final Tab tab = tabs.get(position);
- item.updateFromTab(tab);
-
- return item;
- }
-
- @Override
- public int getCount() {
- return (tabs != null ? tabs.size() : 0);
- }
-
- int getPositionForTab(Tab tab) {
- if (tabs == null || tab == null) {
- return -1;
- }
-
- return tabs.indexOf(tab);
- }
-
- void removeTab(Tab tab) {
- if (tabs == null) {
- return;
- }
-
- tabs.remove(tab);
- notifyDataSetChanged();
- }
-
- void refresh(List<Tab> tabs) {
- // The list of tabs is guaranteed to be non-null.
- // See TabStripView.refreshTabs().
- this.tabs = tabs;
- notifyDataSetChanged();
- }
-
- void clear() {
- tabs = null;
- notifyDataSetInvalidated();
- }
-} \ No newline at end of file
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripItemView.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripItemView.java
deleted file mode 100644
index 27eaed125..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripItemView.java
+++ /dev/null
@@ -1,254 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabs;
-
-import org.mozilla.gecko.AboutPages;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.widget.ResizablePathDrawable;
-import org.mozilla.gecko.widget.ResizablePathDrawable.NonScaledPathShape;
-import org.mozilla.gecko.widget.themed.ThemedImageButton;
-import org.mozilla.gecko.widget.themed.ThemedLinearLayout;
-import org.mozilla.gecko.widget.themed.ThemedTextView;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Path;
-import android.graphics.Region;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.widget.Checkable;
-import android.widget.ImageView;
-
-public class TabStripItemView extends ThemedLinearLayout
- implements Checkable {
- private static final String LOGTAG = "GeckoTabStripItem";
-
- private static final int[] STATE_CHECKED = {
- android.R.attr.state_checked
- };
-
- private int id = -1;
- private boolean checked;
-
- private final ImageView faviconView;
- private final ThemedTextView titleView;
- private final ThemedImageButton closeView;
-
- private final ResizablePathDrawable backgroundDrawable;
- private final Region tabRegion;
- private final Region tabClipRegion;
- private boolean tabRegionNeedsUpdate;
-
- private final int faviconSize;
- private Bitmap lastFavicon;
-
- public TabStripItemView(Context context) {
- this(context, null);
- }
-
- public TabStripItemView(Context context, AttributeSet attrs) {
- super(context, attrs);
- setOrientation(HORIZONTAL);
-
- tabRegion = new Region();
- tabClipRegion = new Region();
-
- final Resources res = context.getResources();
-
- final ColorStateList tabColors =
- res.getColorStateList(R.color.tab_strip_item_bg);
- backgroundDrawable = new ResizablePathDrawable(new TabCurveShape(), tabColors);
- setBackgroundDrawable(backgroundDrawable);
-
- faviconSize = res.getDimensionPixelSize(R.dimen.browser_toolbar_favicon_size);
-
- LayoutInflater.from(context).inflate(R.layout.tab_strip_item_view, this);
- setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- if (id < 0) {
- throw new IllegalStateException("Invalid tab id:" + id);
- }
-
- Tabs.getInstance().selectTab(id);
- }
- });
-
- faviconView = (ImageView) findViewById(R.id.favicon);
- titleView = (ThemedTextView) findViewById(R.id.title);
-
- closeView = (ThemedImageButton) findViewById(R.id.close);
- closeView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- if (id < 0) {
- throw new IllegalStateException("Invalid tab id:" + id);
- }
-
- final Tabs tabs = Tabs.getInstance();
- tabs.closeTab(tabs.getTab(id), true);
- }
- });
- }
-
- @Override
- protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
- super.onSizeChanged(width, height, oldWidth, oldHeight);
-
- // Queue a tab region update in the next draw() call. We don't
- // update it immediately here because we need the new path from
- // the background drawable to be updated first.
- tabRegionNeedsUpdate = true;
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- final int action = event.getActionMasked();
- final int x = (int) event.getX();
- final int y = (int) event.getY();
-
- // Let motion events through if they're off the tab shape bounds.
- if (action == MotionEvent.ACTION_DOWN && !tabRegion.contains(x, y)) {
- return false;
- }
-
- return super.onTouchEvent(event);
- }
-
- @Override
- public void draw(Canvas canvas) {
- super.draw(canvas);
-
- if (tabRegionNeedsUpdate) {
- final Path path = backgroundDrawable.getPath();
- tabClipRegion.set(0, 0, getWidth(), getHeight());
- tabRegion.setPath(path, tabClipRegion);
- tabRegionNeedsUpdate = false;
- }
- }
-
- @Override
- public int[] onCreateDrawableState(int extraSpace) {
- final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
-
- if (checked) {
- mergeDrawableStates(drawableState, STATE_CHECKED);
- }
-
- return drawableState;
- }
-
- @Override
- public boolean isChecked() {
- return checked;
- }
-
- @Override
- public void setChecked(boolean checked) {
- if (this.checked == checked) {
- return;
- }
-
- this.checked = checked;
- refreshDrawableState();
- }
-
- @Override
- public void toggle() {
- setChecked(!checked);
- }
-
- @Override
- public void setPressed(boolean pressed) {
- super.setPressed(pressed);
-
- // The surrounding tab strip dividers need to be hidden
- // when a tab item enters pressed state.
- View parent = (View) getParent();
- if (parent != null) {
- parent.invalidate();
- }
- }
-
- void updateFromTab(Tab tab) {
- if (tab == null) {
- return;
- }
-
- id = tab.getId();
-
- updateTitle(tab);
- updateFavicon(tab.getFavicon());
- setPrivateMode(tab.isPrivate());
- }
-
- private void updateTitle(Tab tab) {
- final String title;
-
- // Avoid flickering the about:home URL on every load given how often
- // this page is used in the UI.
- if (AboutPages.isAboutHome(tab.getURL())) {
- titleView.setText(R.string.home_title);
- } else {
- titleView.setText(tab.getDisplayTitle());
- }
-
- // TODO: Set content description to indicate audio is playing.
- if (tab.isAudioPlaying()) {
- titleView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.tab_audio_playing, 0, 0, 0);
- } else {
- titleView.setCompoundDrawables(null, null, null, null);
- }
- }
-
- private void updateFavicon(final Bitmap favicon) {
- if (favicon == null) {
- lastFavicon = null;
- faviconView.setImageResource(R.drawable.toolbar_favicon_default);
- return;
- }
- if (favicon == lastFavicon) {
- return;
- }
-
- // Cache the original so we can debounce without scaling.
- lastFavicon = favicon;
-
- final Bitmap scaledFavicon =
- Bitmap.createScaledBitmap(favicon, faviconSize, faviconSize, false);
- faviconView.setImageBitmap(scaledFavicon);
- }
-
- private static class TabCurveShape extends NonScaledPathShape {
- @Override
- protected void onResize(float width, float height) {
- final Path path = getPath();
-
- path.reset();
-
- final float curveWidth = TabCurve.getWidthForHeight(height);
-
- path.moveTo(0, height);
- TabCurve.drawFromBottom(path, 0, height, TabCurve.Direction.RIGHT);
- path.lineTo(width - curveWidth, 0);
-
- TabCurve.drawFromTop(path, width - curveWidth, height, TabCurve.Direction.RIGHT);
- path.lineTo(0, height);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripView.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripView.java
deleted file mode 100644
index f3ec19cef..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripView.java
+++ /dev/null
@@ -1,449 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabs;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.LinearGradient;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.Shader;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.animation.DecelerateInterpolator;
-import android.view.View;
-import android.view.ViewTreeObserver.OnPreDrawListener;
-
-import android.animation.Animator;
-import android.animation.Animator.AnimatorListener;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.widget.TwoWayView;
-
-public class TabStripView extends TwoWayView {
- private static final String LOGTAG = "GeckoTabStrip";
-
- private static final int ANIM_TIME_MS = 200;
- private static final DecelerateInterpolator ANIM_INTERPOLATOR =
- new DecelerateInterpolator();
-
- private final TabStripAdapter adapter;
- private final Drawable divider;
-
- private final TabAnimatorListener animatorListener;
-
- private boolean isRestoringTabs;
-
- // Filled by calls to ShapeDrawable.getPadding();
- // saved to prevent allocation in draw().
- private final Rect dividerPadding = new Rect();
-
- private boolean isPrivate;
-
- private final Paint fadingEdgePaint;
- private final int fadingEdgeSize;
-
- public TabStripView(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- setOrientation(Orientation.HORIZONTAL);
- setChoiceMode(ChoiceMode.SINGLE);
- setItemsCanFocus(true);
- setChildrenDrawingOrderEnabled(true);
- setWillNotDraw(false);
-
- final Resources resources = getResources();
-
- divider = resources.getDrawable(R.drawable.tab_strip_divider);
- divider.getPadding(dividerPadding);
-
- final int itemMargin =
- resources.getDimensionPixelSize(R.dimen.tablet_tab_strip_item_margin);
- setItemMargin(itemMargin);
-
- animatorListener = new TabAnimatorListener();
-
- fadingEdgePaint = new Paint();
- fadingEdgeSize =
- resources.getDimensionPixelOffset(R.dimen.tablet_tab_strip_fading_edge_size);
-
- adapter = new TabStripAdapter(context);
- setAdapter(adapter);
- }
-
- private View getViewForTab(Tab tab) {
- final int position = adapter.getPositionForTab(tab);
- return getChildAt(position - getFirstVisiblePosition());
- }
-
- private int getPositionForSelectedTab() {
- return adapter.getPositionForTab(Tabs.getInstance().getSelectedTab());
- }
-
- private void updateSelectedStyle(int selected) {
- setItemChecked(selected, true);
- }
-
- private void updateSelectedPosition(boolean ensureVisible) {
- final int selected = getPositionForSelectedTab();
- if (selected != -1) {
- updateSelectedStyle(selected);
-
- if (ensureVisible) {
- ensurePositionIsVisible(selected, true);
- }
- }
- }
-
- private void animateRemoveTab(Tab removedTab) {
- final int removedPosition = adapter.getPositionForTab(removedTab);
-
- final View removedView = getViewForTab(removedTab);
-
- // The removed position might not have a matching child view
- // when it's not within the visible range of positions in the strip.
- if (removedView == null) {
- return;
- }
-
- // We don't animate the removed child view (it just disappears)
- // but we still need its size of animate all affected children
- // within the visible viewport.
- final int removedSize = removedView.getWidth() + getItemMargin();
-
- getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- getViewTreeObserver().removeOnPreDrawListener(this);
-
- final int firstPosition = getFirstVisiblePosition();
- final List<Animator> childAnimators = new ArrayList<Animator>();
-
- final int childCount = getChildCount();
- for (int i = removedPosition - firstPosition; i < childCount; i++) {
- final View child = getChildAt(i);
-
- final ObjectAnimator animator =
- ObjectAnimator.ofFloat(child, "translationX", removedSize, 0);
- childAnimators.add(animator);
- }
-
- final AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(childAnimators);
- animatorSet.setDuration(ANIM_TIME_MS);
- animatorSet.setInterpolator(ANIM_INTERPOLATOR);
- animatorSet.addListener(animatorListener);
-
- animatorSet.start();
-
- return true;
- }
- });
- }
-
- private void animateNewTab(Tab newTab) {
- final int newPosition = adapter.getPositionForTab(newTab);
- if (newPosition < 0) {
- return;
- }
-
- getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- getViewTreeObserver().removeOnPreDrawListener(this);
-
- final int firstPosition = getFirstVisiblePosition();
-
- final View newChild = getChildAt(newPosition - firstPosition);
- if (newChild == null) {
- return true;
- }
-
- final List<Animator> childAnimators = new ArrayList<Animator>();
- childAnimators.add(
- ObjectAnimator.ofFloat(newChild, "translationY", newChild.getHeight(), 0));
-
- // This will momentaneously add a gap on the right side
- // because TwoWayView doesn't provide APIs to control
- // view recycling programatically to handle these transitory
- // states in the container during animations.
-
- final int tabSize = newChild.getWidth();
- final int newIndex = newPosition - firstPosition;
- final int childCount = getChildCount();
- for (int i = newIndex + 1; i < childCount; i++) {
- final View child = getChildAt(i);
-
- childAnimators.add(
- ObjectAnimator.ofFloat(child, "translationX", -tabSize, 0));
- }
-
- final AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(childAnimators);
- animatorSet.setDuration(ANIM_TIME_MS);
- animatorSet.setInterpolator(ANIM_INTERPOLATOR);
- animatorSet.addListener(animatorListener);
-
- animatorSet.start();
-
- return true;
- }
- });
- }
-
- private void animateRestoredTabs() {
- getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- getViewTreeObserver().removeOnPreDrawListener(this);
-
- final List<Animator> childAnimators = new ArrayList<Animator>();
-
- final int tabHeight = getHeight() - getPaddingTop() - getPaddingBottom();
- final int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- final View child = getChildAt(i);
-
- childAnimators.add(
- ObjectAnimator.ofFloat(child, "translationY", tabHeight, 0));
- }
-
- final AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(childAnimators);
- animatorSet.setDuration(ANIM_TIME_MS);
- animatorSet.setInterpolator(ANIM_INTERPOLATOR);
- animatorSet.addListener(animatorListener);
-
- animatorSet.start();
-
- return true;
- }
- });
- }
-
- /**
- * Ensures the tab at the given position is visible. If we are not restoring tabs and
- * shouldAnimate == true, the tab will animate to be visible, if it is not already visible.
- */
- private void ensurePositionIsVisible(final int position, final boolean shouldAnimate) {
- // We just want to move the strip to the right position
- // when restoring tabs on startup.
- if (isRestoringTabs || !shouldAnimate) {
- setSelection(position);
- return;
- }
-
- getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- getViewTreeObserver().removeOnPreDrawListener(this);
- smoothScrollToPosition(position);
- return true;
- }
- });
- }
-
- private int getCheckedIndex(int childCount) {
- final int checkedIndex = getCheckedItemPosition() - getFirstVisiblePosition();
- if (checkedIndex < 0 || checkedIndex > childCount - 1) {
- return INVALID_POSITION;
- }
-
- return checkedIndex;
- }
-
- void refreshTabs() {
- // Store a different copy of the tabs, so that we don't have
- // to worry about accidentally updating it on the wrong thread.
- final List<Tab> tabs = new ArrayList<Tab>();
-
- for (Tab tab : Tabs.getInstance().getTabsInOrder()) {
- if (tab.isPrivate() == isPrivate) {
- tabs.add(tab);
- }
- }
-
- adapter.refresh(tabs);
- updateSelectedPosition(true);
- }
-
- void clearTabs() {
- adapter.clear();
- }
-
- void restoreTabs() {
- isRestoringTabs = true;
- refreshTabs();
- animateRestoredTabs();
- isRestoringTabs = false;
- }
-
- void addTab(Tab tab) {
- // Refresh the list to make sure the new tab is
- // added in the right position.
- refreshTabs();
- animateNewTab(tab);
- }
-
- void removeTab(Tab tab) {
- animateRemoveTab(tab);
- adapter.removeTab(tab);
- updateSelectedPosition(false);
- }
-
- void selectTab(Tab tab) {
- if (tab.isPrivate() != isPrivate) {
- isPrivate = tab.isPrivate();
- refreshTabs();
- } else {
- updateSelectedPosition(true);
- }
- }
-
- void updateTab(Tab tab) {
- final TabStripItemView item = (TabStripItemView) getViewForTab(tab);
- if (item != null) {
- item.updateFromTab(tab);
- }
- }
-
- private float getFadingEdgeStrength() {
- final int childCount = getChildCount();
- if (childCount == 0) {
- return 0.0f;
- } else {
- if (getFirstVisiblePosition() + childCount - 1 < adapter.getCount() - 1) {
- return 1.0f;
- }
-
- final int right = getChildAt(childCount - 1).getRight();
- final int paddingRight = getPaddingRight();
- final int width = getWidth();
-
- final float strength = (right > width - paddingRight ?
- (float) (right - width + paddingRight) / fadingEdgeSize : 0.0f);
-
- return Math.max(0.0f, Math.min(strength, 1.0f));
- }
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
-
- fadingEdgePaint.setShader(new LinearGradient(w - fadingEdgeSize, 0, w, 0,
- new int[] { 0x0, 0x11292C29, 0xDD292C29 },
- new float[] { 0, 0.4f, 1.0f }, Shader.TileMode.CLAMP));
- }
-
- @Override
- protected int getChildDrawingOrder(int childCount, int i) {
- final int checkedIndex = getCheckedIndex(childCount);
- if (checkedIndex == INVALID_POSITION) {
- return i;
- }
-
- // Always draw the currently selected tab on top of all
- // other child views so that its curve is fully visible.
- if (i == childCount - 1) {
- return checkedIndex;
- } else if (checkedIndex <= i) {
- return i + 1;
- } else {
- return i;
- }
- }
-
- private void drawDividers(Canvas canvas) {
- final int bottom = getHeight() - getPaddingBottom() - dividerPadding.bottom;
- final int top = bottom - divider.getIntrinsicHeight();
-
- final int dividerWidth = divider.getIntrinsicWidth();
- final int itemMargin = getItemMargin();
-
- final int childCount = getChildCount();
- final int checkedIndex = getCheckedIndex(childCount);
-
- for (int i = 1; i < childCount; i++) {
- final View child = getChildAt(i);
-
- final boolean pressed = (child.isPressed() || getChildAt(i - 1).isPressed());
- final boolean checked = (i == checkedIndex || i == checkedIndex + 1);
-
- // Don't draw dividers for around checked or pressed items
- // so that they are not drawn on top of the tab curves.
- if (pressed || checked) {
- continue;
- }
-
- final int left = child.getLeft() - (itemMargin / 2) - dividerWidth;
- final int right = left + dividerWidth;
-
- divider.setBounds(left, top, right, bottom);
- divider.draw(canvas);
- }
- }
-
- private void drawFadingEdge(Canvas canvas) {
- final float strength = getFadingEdgeStrength();
- if (strength > 0.0f) {
- final int r = getRight();
- canvas.drawRect(r - fadingEdgeSize, getTop(), r, getBottom(), fadingEdgePaint);
- fadingEdgePaint.setAlpha((int) (strength * 255));
- }
- }
-
- @Override
- public void draw(Canvas canvas) {
- super.draw(canvas);
- drawDividers(canvas);
- drawFadingEdge(canvas);
- }
-
- public void refresh() {
- final int selectedPosition = getPositionForSelectedTab();
- if (selectedPosition != -1) {
- ensurePositionIsVisible(selectedPosition, false);
- }
- }
-
- private class TabAnimatorListener implements AnimatorListener {
- private void setLayerType(int layerType) {
- final int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- getChildAt(i).setLayerType(layerType, null);
- }
- }
-
- @Override
- public void onAnimationStart(Animator animation) {
- setLayerType(View.LAYER_TYPE_HARDWARE);
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- // This method is called even if the animator is canceled.
- setLayerType(View.LAYER_TYPE_NONE);
- }
-
- @Override
- public void onAnimationRepeat(Animator animation) {
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- }
-
- }
-} \ No newline at end of file
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java
deleted file mode 100644
index ead7db9fe..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java
+++ /dev/null
@@ -1,712 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabs;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.animation.PropertyAnimator;
-import org.mozilla.gecko.tabs.TabsPanel.TabsLayout;
-import org.mozilla.gecko.widget.themed.ThemedRelativeLayout;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.os.Build;
-import android.util.AttributeSet;
-import android.util.SparseArray;
-import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
-import android.view.animation.DecelerateInterpolator;
-import android.widget.AbsListView;
-import android.widget.AdapterView;
-import android.widget.Button;
-import android.widget.GridView;
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import android.animation.ValueAnimator;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A tabs layout implementation for the tablet redesign (bug 1014156) and later ported to mobile (bug 1193745).
- */
-
-class TabsGridLayout extends GridView
- implements TabsLayout,
- Tabs.OnTabsChangedListener {
-
- private static final String LOGTAG = "Gecko" + TabsGridLayout.class.getSimpleName();
-
- public static final int ANIM_DELAY_MULTIPLE_MS = 20;
- private static final int ANIM_TIME_MS = 200;
- private static final DecelerateInterpolator ANIM_INTERPOLATOR = new DecelerateInterpolator();
-
- private final SparseArray<PointF> tabLocations = new SparseArray<PointF>();
- private final boolean isPrivate;
- private final TabsLayoutAdapter tabsAdapter;
- private final int columnWidth;
- private TabsPanel tabsPanel;
- private int lastSelectedTabId;
-
- public TabsGridLayout(final Context context, final AttributeSet attrs) {
- super(context, attrs, R.attr.tabGridLayoutViewStyle);
-
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabsLayout);
- isPrivate = (a.getInt(R.styleable.TabsLayout_tabs, 0x0) == 1);
- a.recycle();
-
- tabsAdapter = new TabsGridLayoutAdapter(context);
- setAdapter(tabsAdapter);
-
- setRecyclerListener(new RecyclerListener() {
- @Override
- public void onMovedToScrapHeap(View view) {
- TabsLayoutItemView item = (TabsLayoutItemView) view;
- item.setThumbnail(null);
- }
- });
-
- // The clipToPadding setting in the styles.xml doesn't seem to be working (bug 1101784)
- // so lets set it manually in code for the moment as it's needed for the padding animation
- setClipToPadding(false);
-
- setVerticalFadingEdgeEnabled(false);
-
- final Resources resources = getResources();
- columnWidth = resources.getDimensionPixelSize(R.dimen.tab_panel_column_width);
-
- final int padding = resources.getDimensionPixelSize(R.dimen.tab_panel_grid_padding);
- final int paddingTop = resources.getDimensionPixelSize(R.dimen.tab_panel_grid_padding_top);
-
- // Lets set double the top padding on the bottom so that the last row shows up properly!
- // Your demise, GridView, cannot come fast enough.
- final int paddingBottom = paddingTop * 2;
-
- setPadding(padding, paddingTop, padding, paddingBottom);
-
- setOnItemClickListener(new OnItemClickListener() {
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- final TabsLayoutItemView tabView = (TabsLayoutItemView) view;
- final int tabId = tabView.getTabId();
- final Tab tab = Tabs.getInstance().selectTab(tabId);
- if (tab == null) {
- return;
- }
- autoHidePanel();
- Tabs.getInstance().notifyListeners(tab, Tabs.TabEvents.OPENED_FROM_TABS_TRAY);
- }
- });
-
- TabSwipeGestureListener mSwipeListener = new TabSwipeGestureListener();
- setOnTouchListener(mSwipeListener);
- setOnScrollListener(mSwipeListener.makeScrollListener());
- }
-
- private void populateTabLocations(final Tab removedTab) {
- tabLocations.clear();
-
- final int firstPosition = getFirstVisiblePosition();
- final int lastPosition = getLastVisiblePosition();
- final int numberOfColumns = getNumColumns();
- final int childCount = getChildCount();
- final int removedPosition = tabsAdapter.getPositionForTab(removedTab);
-
- for (int x = 1, i = (removedPosition - firstPosition) + 1; i < childCount; i++, x++) {
- final View child = getChildAt(i);
- if (child != null) {
- // Reset the transformations here in case the user is swiping tabs away fast and they swipe a tab
- // before the last animation has finished (bug 1179195).
- resetTransforms(child);
-
- tabLocations.append(x, new PointF(child.getX(), child.getY()));
- }
- }
-
- final boolean firstChildOffScreen = ((firstPosition > 0) || getChildAt(0).getY() < 0);
- final boolean lastChildVisible = (lastPosition - childCount == firstPosition - 1);
- final boolean oneItemOnLastRow = (lastPosition % numberOfColumns == 0);
- if (firstChildOffScreen && lastChildVisible && oneItemOnLastRow) {
- // We need to set the view's bottom padding to prevent a sudden jump as the
- // last item in the row is being removed. We then need to remove the padding
- // via a sweet animation
-
- final int removedHeight = getChildAt(0).getMeasuredHeight();
- final int verticalSpacing =
- getResources().getDimensionPixelOffset(R.dimen.tab_panel_grid_vspacing);
-
- ValueAnimator paddingAnimator = ValueAnimator.ofInt(getPaddingBottom() + removedHeight + verticalSpacing, getPaddingBottom());
- paddingAnimator.setDuration(ANIM_TIME_MS * 2);
-
- paddingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
-
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), (Integer) animation.getAnimatedValue());
- }
- });
- paddingAnimator.start();
- }
- }
-
- @Override
- public void setTabsPanel(TabsPanel panel) {
- tabsPanel = panel;
- }
-
- @Override
- public void show() {
- setVisibility(View.VISIBLE);
- Tabs.getInstance().refreshThumbnails();
- Tabs.registerOnTabsChangedListener(this);
- refreshTabsData();
-
- final Tab currentlySelectedTab = Tabs.getInstance().getSelectedTab();
- final int position = currentlySelectedTab != null ? tabsAdapter.getPositionForTab(currentlySelectedTab) : -1;
- if (position != -1) {
- final boolean selectionChanged = lastSelectedTabId != currentlySelectedTab.getId();
- final boolean positionIsVisible = position >= getFirstVisiblePosition() && position <= getLastVisiblePosition();
-
- if (selectionChanged || !positionIsVisible) {
- smoothScrollToPosition(position);
- }
- }
- }
-
- @Override
- public void hide() {
- lastSelectedTabId = Tabs.getInstance().getSelectedTab().getId();
- setVisibility(View.GONE);
- Tabs.unregisterOnTabsChangedListener(this);
- GeckoAppShell.notifyObservers("Tab:Screenshot:Cancel", "");
- tabsAdapter.clear();
- }
-
- @Override
- public boolean shouldExpand() {
- return true;
- }
-
- private void autoHidePanel() {
- tabsPanel.autoHidePanel();
- }
-
- @Override
- public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
- switch (msg) {
- case ADDED:
- // Refresh only if panel is shown. show() will call refreshTabsData() later again.
- if (tabsPanel.isShown()) {
- // Refresh the list to make sure the new tab is added in the right position.
- refreshTabsData();
- }
- break;
-
- case CLOSED:
-
- // This is limited to >= ICS as animations on GB devices are generally pants
- if (Build.VERSION.SDK_INT >= 11 && tabsAdapter.getCount() > 0) {
- animateRemoveTab(tab);
- }
-
- final Tabs tabsInstance = Tabs.getInstance();
-
- if (tabsAdapter.removeTab(tab)) {
- if (tab.isPrivate() == isPrivate && tabsAdapter.getCount() > 0) {
- int selected = tabsAdapter.getPositionForTab(tabsInstance.getSelectedTab());
- updateSelectedStyle(selected);
- }
- if (!tab.isPrivate()) {
- // Make sure we always have at least one normal tab
- final Iterable<Tab> tabs = tabsInstance.getTabsInOrder();
- boolean removedTabIsLastNormalTab = true;
- for (Tab singleTab : tabs) {
- if (!singleTab.isPrivate()) {
- removedTabIsLastNormalTab = false;
- break;
- }
- }
- if (removedTabIsLastNormalTab) {
- tabsInstance.addTab();
- }
- }
- }
- break;
-
- case SELECTED:
- // Update the selected position, then fall through...
- updateSelectedPosition();
- case UNSELECTED:
- // We just need to update the style for the unselected tab...
- case THUMBNAIL:
- case TITLE:
- case RECORDING_CHANGE:
- case AUDIO_PLAYING_CHANGE:
- View view = getChildAt(tabsAdapter.getPositionForTab(tab) - getFirstVisiblePosition());
- if (view == null)
- return;
-
- ((TabsLayoutItemView) view).assignValues(tab);
- break;
- }
- }
-
- // Updates the selected position in the list so that it will be scrolled to the right place.
- private void updateSelectedPosition() {
- int selected = tabsAdapter.getPositionForTab(Tabs.getInstance().getSelectedTab());
- updateSelectedStyle(selected);
-
- if (selected != -1) {
- setSelection(selected);
- }
- }
-
- /**
- * Updates the selected/unselected style for the tabs.
- *
- * @param selected position of the selected tab
- */
- private void updateSelectedStyle(final int selected) {
- post(new Runnable() {
- @Override
- public void run() {
- final int displayCount = tabsAdapter.getCount();
-
- for (int i = 0; i < displayCount; i++) {
- final Tab tab = tabsAdapter.getItem(i);
- final boolean checked = displayCount == 1 || i == selected;
- final View tabView = getViewForTab(tab);
- if (tabView != null) {
- ((TabsLayoutItemView) tabView).setChecked(checked);
- }
- // setItemChecked doesn't exist until API 11, despite what the API docs say!
- setItemChecked(i, checked);
- }
- }
- });
- }
-
- private void refreshTabsData() {
- // Store a different copy of the tabs, so that we don't have to worry about
- // accidentally updating it on the wrong thread.
- ArrayList<Tab> tabData = new ArrayList<>();
-
- Iterable<Tab> allTabs = Tabs.getInstance().getTabsInOrder();
- for (Tab tab : allTabs) {
- if (tab.isPrivate() == isPrivate)
- tabData.add(tab);
- }
-
- tabsAdapter.setTabs(tabData);
- updateSelectedPosition();
- }
-
- private void resetTransforms(View view) {
- view.setAlpha(1);
- view.setTranslationX(0);
- view.setTranslationY(0);
-
- ((TabsLayoutItemView) view).setCloseVisible(true);
- }
-
- @Override
- public void closeAll() {
-
- autoHidePanel();
-
- if (getChildCount() == 0) {
- return;
- }
-
- final Iterable<Tab> tabs = Tabs.getInstance().getTabsInOrder();
- for (Tab tab : tabs) {
- // In the normal panel we want to close all tabs (both private and normal),
- // but in the private panel we only want to close private tabs.
- if (!isPrivate || tab.isPrivate()) {
- Tabs.getInstance().closeTab(tab, false);
- }
- }
- }
-
- private View getViewForTab(Tab tab) {
- final int position = tabsAdapter.getPositionForTab(tab);
- return getChildAt(position - getFirstVisiblePosition());
- }
-
- void closeTab(View v) {
- if (tabsAdapter.getCount() == 1) {
- autoHidePanel();
- }
-
- TabsLayoutItemView itemView = (TabsLayoutItemView) v.getTag();
- Tab tab = Tabs.getInstance().getTab(itemView.getTabId());
-
- Tabs.getInstance().closeTab(tab, true);
- }
-
- private void animateRemoveTab(final Tab removedTab) {
- final int removedPosition = tabsAdapter.getPositionForTab(removedTab);
-
- final View removedView = getViewForTab(removedTab);
-
- // The removed position might not have a matching child view
- // when it's not within the visible range of positions in the strip.
- if (removedView == null) {
- return;
- }
- final int removedHeight = removedView.getMeasuredHeight();
-
- populateTabLocations(removedTab);
-
- getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- getViewTreeObserver().removeOnPreDrawListener(this);
- // We don't animate the removed child view (it just disappears)
- // but we still need its size to animate all affected children
- // within the visible viewport.
- final int childCount = getChildCount();
- final int firstPosition = getFirstVisiblePosition();
- final int numberOfColumns = getNumColumns();
-
- final List<Animator> childAnimators = new ArrayList<>();
-
- PropertyValuesHolder translateX, translateY;
- for (int x = 0, i = removedPosition - firstPosition; i < childCount; i++, x++) {
- final View child = getChildAt(i);
- ObjectAnimator animator;
-
- if (i % numberOfColumns == numberOfColumns - 1) {
- // Animate X & Y
- translateX = PropertyValuesHolder.ofFloat("translationX", -(columnWidth * numberOfColumns), 0);
- translateY = PropertyValuesHolder.ofFloat("translationY", removedHeight, 0);
- animator = ObjectAnimator.ofPropertyValuesHolder(child, translateX, translateY);
- } else {
- // Just animate X
- translateX = PropertyValuesHolder.ofFloat("translationX", columnWidth, 0);
- animator = ObjectAnimator.ofPropertyValuesHolder(child, translateX);
- }
- animator.setStartDelay(x * ANIM_DELAY_MULTIPLE_MS);
- childAnimators.add(animator);
- }
-
- final AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(childAnimators);
- animatorSet.setDuration(ANIM_TIME_MS);
- animatorSet.setInterpolator(ANIM_INTERPOLATOR);
- animatorSet.start();
-
- // Set the starting position of the child views - because we are delaying the start
- // of the animation, we need to prevent the items being drawn in their final position
- // prior to the animation starting
- for (int x = 1, i = (removedPosition - firstPosition) + 1; i < childCount; i++, x++) {
- final View child = getChildAt(i);
-
- final PointF targetLocation = tabLocations.get(x + 1);
- if (targetLocation == null) {
- continue;
- }
-
- child.setX(targetLocation.x);
- child.setY(targetLocation.y);
- }
-
- return true;
- }
- });
- }
-
-
- private void animateCancel(final View view) {
- PropertyAnimator animator = new PropertyAnimator(ANIM_TIME_MS);
- animator.attach(view, PropertyAnimator.Property.ALPHA, 1);
- animator.attach(view, PropertyAnimator.Property.TRANSLATION_X, 0);
-
- animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
- @Override
- public void onPropertyAnimationStart() {
- }
-
- @Override
- public void onPropertyAnimationEnd() {
- TabsLayoutItemView tab = (TabsLayoutItemView) view;
- tab.setCloseVisible(true);
- }
- });
-
- animator.start();
- }
-
- private class TabsGridLayoutAdapter extends TabsLayoutAdapter {
-
- final private Button.OnClickListener mCloseClickListener;
-
- public TabsGridLayoutAdapter(Context context) {
- super(context, R.layout.tabs_layout_item_view);
-
- mCloseClickListener = new Button.OnClickListener() {
- @Override
- public void onClick(View v) {
- closeTab(v);
- }
- };
- }
-
- @Override
- TabsLayoutItemView newView(int position, ViewGroup parent) {
- final TabsLayoutItemView item = super.newView(position, parent);
-
- item.setCloseOnClickListener(mCloseClickListener);
- ((ThemedRelativeLayout) item.findViewById(R.id.wrapper)).setPrivateMode(isPrivate);
-
- return item;
- }
-
- @Override
- public void bindView(TabsLayoutItemView view, Tab tab) {
- super.bindView(view, tab);
-
- // If we're recycling this view, there's a chance it was transformed during
- // the close animation. Remove any of those properties.
- resetTransforms(view);
- }
- }
-
- private class TabSwipeGestureListener implements View.OnTouchListener {
- // same value the stock browser uses for after drag animation velocity in pixels/sec
- // http://androidxref.com/4.0.4/xref/packages/apps/Browser/src/com/android/browser/NavTabScroller.java#61
- private static final float MIN_VELOCITY = 750;
-
- private final int mSwipeThreshold;
- private final int mMinFlingVelocity;
-
- private final int mMaxFlingVelocity;
- private VelocityTracker mVelocityTracker;
-
- private int mTabWidth = 1;
-
- private View mSwipeView;
- private Runnable mPendingCheckForTap;
-
- private float mSwipeStartX;
- private boolean mSwiping;
- private boolean mEnabled;
-
- public TabSwipeGestureListener() {
- mEnabled = true;
-
- ViewConfiguration vc = ViewConfiguration.get(TabsGridLayout.this.getContext());
- mSwipeThreshold = vc.getScaledTouchSlop();
- mMinFlingVelocity = (int) (TabsGridLayout.this.getContext().getResources().getDisplayMetrics().density * MIN_VELOCITY);
- mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
- }
-
- public void setEnabled(boolean enabled) {
- mEnabled = enabled;
- }
-
- public OnScrollListener makeScrollListener() {
- return new OnScrollListener() {
- @Override
- public void onScrollStateChanged(AbsListView view, int scrollState) {
- setEnabled(scrollState != GridView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
- }
-
- @Override
- public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
-
- }
- };
- }
-
- @Override
- public boolean onTouch(View view, MotionEvent e) {
- if (!mEnabled) {
- return false;
- }
-
- switch (e.getActionMasked()) {
- case MotionEvent.ACTION_DOWN: {
- // Check if we should set pressed state on the
- // touched view after a standard delay.
- triggerCheckForTap();
-
- final float x = e.getRawX();
- final float y = e.getRawY();
-
- // Find out which view is being touched
- mSwipeView = findViewAt(x, y);
-
- if (mSwipeView != null) {
- if (mTabWidth < 2) {
- mTabWidth = mSwipeView.getWidth();
- }
-
- mSwipeStartX = e.getRawX();
-
- mVelocityTracker = VelocityTracker.obtain();
- mVelocityTracker.addMovement(e);
- }
-
- view.onTouchEvent(e);
- return true;
- }
-
- case MotionEvent.ACTION_UP: {
- if (mSwipeView == null) {
- break;
- }
-
- cancelCheckForTap();
- mSwipeView.setPressed(false);
-
- if (!mSwiping) {
- final TabsLayoutItemView item = (TabsLayoutItemView) mSwipeView;
- final int tabId = item.getTabId();
- final Tab tab = Tabs.getInstance().selectTab(tabId);
- if (tab != null) {
- autoHidePanel();
- Tabs.getInstance().notifyListeners(tab, Tabs.TabEvents.OPENED_FROM_TABS_TRAY);
- }
-
- mVelocityTracker.recycle();
- mVelocityTracker = null;
- break;
- }
-
- mVelocityTracker.addMovement(e);
- mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
-
- float velocityX = Math.abs(mVelocityTracker.getXVelocity());
-
- boolean dismiss = false;
-
- float deltaX = mSwipeView.getTranslationX();
-
- if (Math.abs(deltaX) > mTabWidth / 2) {
- dismiss = true;
- } else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity) {
- dismiss = mSwiping && (deltaX * mVelocityTracker.getYVelocity() > 0);
- }
- if (dismiss) {
- closeTab(mSwipeView.findViewById(R.id.close));
- } else {
- animateCancel(mSwipeView);
- }
- mVelocityTracker.recycle();
- mVelocityTracker = null;
- mSwipeView = null;
-
- mSwipeStartX = 0;
- mSwiping = false;
- }
-
- case MotionEvent.ACTION_MOVE: {
- if (mSwipeView == null || mVelocityTracker == null) {
- break;
- }
-
- mVelocityTracker.addMovement(e);
-
- float delta = e.getRawX() - mSwipeStartX;
-
- boolean isScrollingX = Math.abs(delta) > mSwipeThreshold;
- boolean isSwipingToClose = isScrollingX;
-
- // If we're actually swiping, make sure we don't
- // set pressed state on the swiped view.
- if (isScrollingX) {
- cancelCheckForTap();
- }
-
- if (isSwipingToClose) {
- mSwiping = true;
- TabsGridLayout.this.requestDisallowInterceptTouchEvent(true);
-
- ((TabsLayoutItemView) mSwipeView).setCloseVisible(false);
-
- // Stops listview from highlighting the touched item
- // in the list when swiping.
- MotionEvent cancelEvent = MotionEvent.obtain(e);
- cancelEvent.setAction(MotionEvent.ACTION_CANCEL |
- (e.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
- TabsGridLayout.this.onTouchEvent(cancelEvent);
- cancelEvent.recycle();
- }
-
- if (mSwiping) {
- mSwipeView.setTranslationX(delta);
-
- mSwipeView.setAlpha(Math.min(1f, 1f - 2f * Math.abs(delta) / mTabWidth));
-
- return true;
- }
-
- break;
- }
- }
- return false;
- }
-
- private View findViewAt(float rawX, float rawY) {
- Rect rect = new Rect();
-
- int[] listViewCoords = new int[2];
- TabsGridLayout.this.getLocationOnScreen(listViewCoords);
-
- int x = (int) rawX - listViewCoords[0];
- int y = (int) rawY - listViewCoords[1];
-
- for (int i = 0; i < TabsGridLayout.this.getChildCount(); i++) {
- View child = TabsGridLayout.this.getChildAt(i);
- child.getHitRect(rect);
-
- if (rect.contains(x, y)) {
- return child;
- }
- }
-
- return null;
- }
-
- private void triggerCheckForTap() {
- if (mPendingCheckForTap == null) {
- mPendingCheckForTap = new CheckForTap();
- }
-
- TabsGridLayout.this.postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
- }
-
- private void cancelCheckForTap() {
- if (mPendingCheckForTap == null) {
- return;
- }
-
- TabsGridLayout.this.removeCallbacks(mPendingCheckForTap);
- }
-
- private class CheckForTap implements Runnable {
- @Override
- public void run() {
- if (!mSwiping && mSwipeView != null && mEnabled) {
- mSwipeView.setPressed(true);
- }
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java
deleted file mode 100644
index d5362f1f1..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java
+++ /dev/null
@@ -1,216 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabs;
-
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.widget.RecyclerViewClickSupport;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.support.v7.widget.RecyclerView;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.Button;
-
-import java.util.ArrayList;
-
-public abstract class TabsLayout extends RecyclerView
- implements TabsPanel.TabsLayout,
- Tabs.OnTabsChangedListener,
- RecyclerViewClickSupport.OnItemClickListener,
- TabsTouchHelperCallback.DismissListener {
-
- private static final String LOGTAG = "Gecko" + TabsLayout.class.getSimpleName();
-
- private final boolean isPrivate;
- private TabsPanel tabsPanel;
- private final TabsLayoutRecyclerAdapter tabsAdapter;
-
- public TabsLayout(Context context, AttributeSet attrs, int itemViewLayoutResId) {
- super(context, attrs);
-
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabsLayout);
- isPrivate = (a.getInt(R.styleable.TabsLayout_tabs, 0x0) == 1);
- a.recycle();
-
- tabsAdapter = new TabsLayoutRecyclerAdapter(context, itemViewLayoutResId, isPrivate,
- /* close on click listener */
- new Button.OnClickListener() {
- @Override
- public void onClick(View v) {
- // The view here is the close button, which has a reference
- // to the parent TabsLayoutItemView in its tag, hence the getTag() call.
- TabsLayoutItemView itemView = (TabsLayoutItemView) v.getTag();
- closeTab(itemView);
- }
- });
- setAdapter(tabsAdapter);
-
- RecyclerViewClickSupport.addTo(this).setOnItemClickListener(this);
-
- setRecyclerListener(new RecyclerListener() {
- @Override
- public void onViewRecycled(RecyclerView.ViewHolder holder) {
- final TabsLayoutItemView itemView = (TabsLayoutItemView) holder.itemView;
- itemView.setThumbnail(null);
- itemView.setCloseVisible(true);
- }
- });
- }
-
- @Override
- public void setTabsPanel(TabsPanel panel) {
- tabsPanel = panel;
- }
-
- @Override
- public void show() {
- setVisibility(View.VISIBLE);
- Tabs.getInstance().refreshThumbnails();
- Tabs.registerOnTabsChangedListener(this);
- refreshTabsData();
- }
-
- @Override
- public void hide() {
- setVisibility(View.GONE);
- Tabs.unregisterOnTabsChangedListener(this);
- GeckoAppShell.notifyObservers("Tab:Screenshot:Cancel", "");
- tabsAdapter.clear();
- }
-
- @Override
- public boolean shouldExpand() {
- return true;
- }
-
- protected void autoHidePanel() {
- tabsPanel.autoHidePanel();
- }
-
- @Override
- public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
- switch (msg) {
- case ADDED:
- final int tabIndex = Integer.parseInt(data);
- tabsAdapter.notifyTabInserted(tab, tabIndex);
- if (addAtIndexRequiresScroll(tabIndex)) {
- // (The current Tabs implementation updates the SELECTED tab *after* this
- // call to ADDED, so don't just call updateSelectedPosition().)
- scrollToPosition(tabIndex);
- }
- break;
-
- case CLOSED:
- if (tab.isPrivate() == isPrivate && tabsAdapter.getItemCount() > 0) {
- tabsAdapter.removeTab(tab);
- }
- break;
-
- case SELECTED:
- case UNSELECTED:
- case THUMBNAIL:
- case TITLE:
- case RECORDING_CHANGE:
- case AUDIO_PLAYING_CHANGE:
- tabsAdapter.notifyTabChanged(tab);
- break;
- }
- }
-
- // Addition of a tab at selected positions (dependent on LayoutManager) will result in a tab
- // being added out of view - return true if index is such a position.
- abstract protected boolean addAtIndexRequiresScroll(int index);
-
- @Override
- public void onItemClicked(RecyclerView recyclerView, int position, View v) {
- final TabsLayoutItemView item = (TabsLayoutItemView) v;
- final int tabId = item.getTabId();
- final Tab tab = Tabs.getInstance().selectTab(tabId);
- if (tab == null) {
- // The tab that was clicked no longer exists in the tabs list (which can happen if you
- // tap on a tab while its remove animation is running), so ignore the click.
- return;
- }
-
- autoHidePanel();
- Tabs.getInstance().notifyListeners(tab, Tabs.TabEvents.OPENED_FROM_TABS_TRAY);
- }
-
- // Updates the selected position in the list so that it will be scrolled to the right place.
- private void updateSelectedPosition() {
- final int selected = tabsAdapter.getPositionForTab(Tabs.getInstance().getSelectedTab());
- if (selected != NO_POSITION) {
- scrollToPosition(selected);
- }
- }
-
- private void refreshTabsData() {
- // Store a different copy of the tabs, so that we don't have to worry about
- // accidentally updating it on the wrong thread.
- final ArrayList<Tab> tabData = new ArrayList<>();
- final Iterable<Tab> allTabs = Tabs.getInstance().getTabsInOrder();
-
- for (final Tab tab : allTabs) {
- if (tab.isPrivate() == isPrivate) {
- tabData.add(tab);
- }
- }
-
- tabsAdapter.setTabs(tabData);
- updateSelectedPosition();
- }
-
- private void closeTab(View view) {
- final TabsLayoutItemView itemView = (TabsLayoutItemView) view;
- final Tab tab = getTabForView(itemView);
- if (tab == null) {
- // We can be null here if this is the second closeTab call resulting from a sufficiently
- // fast double tap on the close tab button.
- return;
- }
-
- final boolean closingLastTab = tabsAdapter.getItemCount() == 1;
- Tabs.getInstance().closeTab(tab, true);
- if (closingLastTab) {
- autoHidePanel();
- }
- }
-
- protected void closeAllTabs() {
- final Iterable<Tab> tabs = Tabs.getInstance().getTabsInOrder();
- for (final Tab tab : tabs) {
- // In the normal panel we want to close all tabs (both private and normal),
- // but in the private panel we only want to close private tabs.
- if (!isPrivate || tab.isPrivate()) {
- Tabs.getInstance().closeTab(tab, false);
- }
- }
- }
-
- @Override
- public void onItemDismiss(View view) {
- closeTab(view);
- }
-
- private Tab getTabForView(View view) {
- if (view == null) {
- return null;
- }
- return Tabs.getInstance().getTab(((TabsLayoutItemView) view).getTabId());
- }
-
- @Override
- public void setEmptyView(View emptyView) {
- // We never display an empty view.
- }
-
- @Override
- abstract public void closeAll();
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutAdapter.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutAdapter.java
deleted file mode 100644
index 367da640f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutAdapter.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabs;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Tab;
-
-import android.content.Context;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-
-import java.util.ArrayList;
-
-// Adapter to bind tabs into a list
-public class TabsLayoutAdapter extends BaseAdapter {
- public static final String LOGTAG = "Gecko" + TabsLayoutAdapter.class.getSimpleName();
-
- private final Context mContext;
- private final int mTabLayoutId;
- private ArrayList<Tab> mTabs;
- private final LayoutInflater mInflater;
-
- public TabsLayoutAdapter (Context context, int tabLayoutId) {
- mContext = context;
- mInflater = LayoutInflater.from(mContext);
- mTabLayoutId = tabLayoutId;
- }
-
- final void setTabs (ArrayList<Tab> tabs) {
- mTabs = tabs;
- notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
- }
-
- final boolean removeTab (Tab tab) {
- boolean tabRemoved = mTabs.remove(tab);
- if (tabRemoved) {
- notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
- }
- return tabRemoved;
- }
-
- final void clear() {
- mTabs = null;
-
- notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
- }
-
- @Override
- public int getCount() {
- return (mTabs == null ? 0 : mTabs.size());
- }
-
- @Override
- public Tab getItem(int position) {
- return mTabs.get(position);
- }
-
- @Override
- public long getItemId(int position) {
- return position;
- }
-
- final int getPositionForTab(Tab tab) {
- if (mTabs == null || tab == null)
- return -1;
-
- return mTabs.indexOf(tab);
- }
-
- @Override
- public boolean isEnabled(int position) {
- return true;
- }
-
- @Override
- final public TabsLayoutItemView getView(int position, View convertView, ViewGroup parent) {
- final TabsLayoutItemView view;
- if (convertView == null) {
- view = newView(position, parent);
- } else {
- view = (TabsLayoutItemView) convertView;
- }
- final Tab tab = mTabs.get(position);
- bindView(view, tab);
- return view;
- }
-
- TabsLayoutItemView newView(int position, ViewGroup parent) {
- return (TabsLayoutItemView) mInflater.inflate(mTabLayoutId, parent, false);
- }
-
- void bindView(TabsLayoutItemView view, Tab tab) {
- view.assignValues(tab);
- }
-} \ No newline at end of file
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutItemView.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutItemView.java
deleted file mode 100644
index 975e779d6..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutItemView.java
+++ /dev/null
@@ -1,172 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabs;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.widget.TabThumbnailWrapper;
-import org.mozilla.gecko.widget.TouchDelegateWithReset;
-import org.mozilla.gecko.widget.themed.ThemedRelativeLayout;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.util.TypedValue;
-import android.view.View;
-import android.view.ViewTreeObserver;
-import android.widget.Checkable;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-public class TabsLayoutItemView extends LinearLayout
- implements Checkable {
- private static final String LOGTAG = "Gecko" + TabsLayoutItemView.class.getSimpleName();
- private static final int[] STATE_CHECKED = { android.R.attr.state_checked };
- private boolean mChecked;
-
- private int mTabId;
- private TextView mTitle;
- private TabsPanelThumbnailView mThumbnail;
- private ImageView mCloseButton;
- private TabThumbnailWrapper mThumbnailWrapper;
-
- public TabsLayoutItemView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public int[] onCreateDrawableState(int extraSpace) {
- final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
-
- if (mChecked) {
- mergeDrawableStates(drawableState, STATE_CHECKED);
- }
-
- return drawableState;
- }
-
- @Override
- public boolean isEnabled() {
- return true;
- }
-
- @Override
- public boolean isChecked() {
- return mChecked;
- }
-
- @Override
- public void setChecked(boolean checked) {
- if (mChecked == checked) {
- return;
- }
-
- mChecked = checked;
- refreshDrawableState();
-
- int count = getChildCount();
- for (int i = 0; i < count; i++) {
- final View child = getChildAt(i);
- if (child instanceof Checkable) {
- ((Checkable) child).setChecked(checked);
- }
- }
- }
-
- @Override
- public void toggle() {
- mChecked = !mChecked;
- }
-
- public void setCloseOnClickListener(OnClickListener mOnClickListener) {
- mCloseButton.setOnClickListener(mOnClickListener);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- mTitle = (TextView) findViewById(R.id.title);
- mThumbnail = (TabsPanelThumbnailView) findViewById(R.id.thumbnail);
- mCloseButton = (ImageView) findViewById(R.id.close);
- mThumbnailWrapper = (TabThumbnailWrapper) findViewById(R.id.wrapper);
-
- growCloseButtonHitArea();
- }
-
- private void growCloseButtonHitArea() {
- getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- getViewTreeObserver().removeOnPreDrawListener(this);
-
- // Ideally we want the close button hit area to be 40x40dp but we are constrained by the height of the parent, so
- // we make it as tall as the parent view and 40dp across.
- final int targetHitArea = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 40, getResources().getDisplayMetrics());;
-
- final Rect hitRect = new Rect();
- hitRect.top = 0;
- hitRect.right = getWidth();
- hitRect.left = getWidth() - targetHitArea;
- hitRect.bottom = targetHitArea;
-
- setTouchDelegate(new TouchDelegateWithReset(hitRect, mCloseButton));
-
- return true;
- }
- });
- }
-
- protected void assignValues(Tab tab) {
- if (tab == null) {
- return;
- }
-
- mTabId = tab.getId();
-
- setChecked(Tabs.getInstance().isSelectedTab(tab));
-
- Drawable thumbnailImage = tab.getThumbnail();
- mThumbnail.setImageDrawable(thumbnailImage);
-
- mThumbnail.setPrivateMode(tab.isPrivate());
-
- if (mThumbnailWrapper != null) {
- mThumbnailWrapper.setRecording(tab.isRecording());
- }
-
- final String tabTitle = tab.getDisplayTitle();
- mTitle.setText(tabTitle);
- mCloseButton.setTag(this);
-
- if (tab.isAudioPlaying()) {
- mTitle.setCompoundDrawablesWithIntrinsicBounds(R.drawable.tab_audio_playing, 0, 0, 0);
- final String tabTitleWithAudio =
- getResources().getString(R.string.tab_title_prefix_is_playing_audio, tabTitle);
- mTitle.setContentDescription(tabTitleWithAudio);
- } else {
- mTitle.setCompoundDrawables(null, null, null, null);
- mTitle.setContentDescription(tabTitle);
- }
- }
-
- public int getTabId() {
- return mTabId;
- }
-
- public void setThumbnail(Drawable thumbnail) {
- mThumbnail.setImageDrawable(thumbnail);
- }
-
- public void setCloseVisible(boolean visible) {
- mCloseButton.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
- }
-
- public void setPrivateMode(boolean isPrivate) {
- ((ThemedRelativeLayout) findViewById(R.id.wrapper)).setPrivateMode(isPrivate);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutRecyclerAdapter.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutRecyclerAdapter.java
deleted file mode 100644
index 090d74f9d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutRecyclerAdapter.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabs;
-
-import org.mozilla.gecko.Tab;
-
-import android.content.Context;
-import android.support.annotation.NonNull;
-import android.support.v7.widget.RecyclerView;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-
-import java.util.ArrayList;
-
-public class TabsLayoutRecyclerAdapter
- extends RecyclerView.Adapter<TabsLayoutRecyclerAdapter.TabsListViewHolder> {
-
- private static final String LOGTAG = "Gecko" + TabsLayoutRecyclerAdapter.class.getSimpleName();
-
- private final int tabLayoutId;
- private @NonNull ArrayList<Tab> tabs;
- private final LayoutInflater inflater;
- private final boolean isPrivate;
- // Click listener for the close button on itemViews.
- private final Button.OnClickListener closeOnClickListener;
-
- // The TabsLayoutItemView takes care of caching its own Views, so we don't need to do anything
- // here except not be abstract.
- public static class TabsListViewHolder extends RecyclerView.ViewHolder {
- public TabsListViewHolder(View itemView) {
- super(itemView);
- }
- }
-
- public TabsLayoutRecyclerAdapter(Context context, int tabLayoutId, boolean isPrivate,
- Button.OnClickListener closeOnClickListener) {
- inflater = LayoutInflater.from(context);
- this.tabLayoutId = tabLayoutId;
- this.isPrivate = isPrivate;
- this.closeOnClickListener = closeOnClickListener;
- tabs = new ArrayList<>(0);
- }
-
- /* package */ final void setTabs(@NonNull ArrayList<Tab> tabs) {
- this.tabs = tabs;
- notifyDataSetChanged();
- }
-
- /* package */ final void clear() {
- tabs = new ArrayList<>(0);
- notifyDataSetChanged();
- }
-
- /* package */ final boolean removeTab(Tab tab) {
- final int position = getPositionForTab(tab);
- if (position == -1) {
- return false;
- }
- tabs.remove(position);
- notifyItemRemoved(position);
- return true;
- }
-
- /* package */ final int getPositionForTab(Tab tab) {
- if (tab == null) {
- return -1;
- }
-
- return tabs.indexOf(tab);
- }
-
- /* package */ void notifyTabChanged(Tab tab) {
- notifyItemChanged(getPositionForTab(tab));
- }
-
- /* package */ void notifyTabInserted(Tab tab, int index) {
- if (index >= 0 && index <= tabs.size()) {
- tabs.add(index, tab);
- notifyItemInserted(index);
- } else {
- // Add to the end.
- tabs.add(tab);
- notifyItemInserted(tabs.size() - 1);
- // index == -1 is a valid way to add to the end, the other cases are errors.
- if (index != -1) {
- Log.e(LOGTAG, "Tab was inserted at an invalid position: " + Integer.toString(index));
- }
- }
- }
-
- @Override
- public int getItemCount() {
- return tabs.size();
- }
-
- private Tab getItem(int position) {
- return tabs.get(position);
- }
-
- @Override
- public void onBindViewHolder(TabsListViewHolder viewHolder, int position) {
- final Tab tab = getItem(position);
- final TabsLayoutItemView itemView = (TabsLayoutItemView) viewHolder.itemView;
- itemView.assignValues(tab);
- // Be careful (re)setting position values here: bind is called on each notifyItemChanged,
- // so you could be stomping on values that have been set in support of other animations
- // that are already underway.
- }
-
- @Override
- public TabsListViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- final TabsLayoutItemView viewItem = (TabsLayoutItemView) inflater.inflate(tabLayoutId, parent, false);
- viewItem.setPrivateMode(isPrivate);
- viewItem.setCloseOnClickListener(closeOnClickListener);
-
- return new TabsListViewHolder(viewItem);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayout.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayout.java
deleted file mode 100644
index 8cf2f8ede..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayout.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabs;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.animation.PropertyAnimator;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.content.Context;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.helper.ItemTouchHelper;
-import android.util.AttributeSet;
-import android.view.View;
-
-public class TabsListLayout extends TabsLayout {
- // Time to animate non-flinged tabs of screen, in milliseconds
- private static final int ANIMATION_DURATION = 250;
-
- // Time between starting successive tab animations in closeAllTabs.
- private static final int ANIMATION_CASCADE_DELAY = 75;
-
- private int closeAllAnimationCount;
-
- public TabsListLayout(Context context, AttributeSet attrs) {
- super(context, attrs, R.layout.tabs_list_item_view);
-
- setHasFixedSize(true);
-
- setLayoutManager(new LinearLayoutManager(context));
-
- // A TouchHelper handler for swipe to close.
- final TabsTouchHelperCallback callback = new TabsTouchHelperCallback(this);
- final ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
- touchHelper.attachToRecyclerView(this);
-
- setItemAnimator(new TabsListLayoutAnimator(ANIMATION_DURATION));
- }
-
- @Override
- public void closeAll() {
- final int childCount = getChildCount();
-
- // Just close the panel if there are no tabs to close.
- if (childCount == 0) {
- autoHidePanel();
- return;
- }
-
- // Disable the view so that gestures won't interfere wth the tab close animation.
- setEnabled(false);
-
- // Delay starting each successive animation to create a cascade effect.
- int cascadeDelay = 0;
- closeAllAnimationCount = 0;
- for (int i = childCount - 1; i >= 0; i--) {
- final View view = getChildAt(i);
- if (view == null) {
- continue;
- }
-
- final PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION);
- animator.attach(view, PropertyAnimator.Property.ALPHA, 0);
-
- animator.attach(view, PropertyAnimator.Property.TRANSLATION_X, view.getWidth());
-
- closeAllAnimationCount++;
-
- animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
- @Override
- public void onPropertyAnimationStart() {
- }
-
- @Override
- public void onPropertyAnimationEnd() {
- closeAllAnimationCount--;
- if (closeAllAnimationCount > 0) {
- return;
- }
-
- // Hide the panel after the animation is done.
- autoHidePanel();
-
- // Re-enable the view after the animation is done.
- TabsListLayout.this.setEnabled(true);
-
- // Then actually close all the tabs.
- closeAllTabs();
- }
- });
-
- ThreadUtils.postDelayedToUiThread(new Runnable() {
- @Override
- public void run() {
- animator.start();
- }
- }, cascadeDelay);
-
- cascadeDelay += ANIMATION_CASCADE_DELAY;
- }
- }
-
- @Override
- protected boolean addAtIndexRequiresScroll(int index) {
- return index == 0 || index == getAdapter().getItemCount() - 1;
- }
-
- @Override
- public void onChildAttachedToWindow(View child) {
- // Make sure we reset any attributes that may have been animated in this child's previous
- // incarnation.
- child.setTranslationX(0);
- child.setTranslationY(0);
- child.setAlpha(1);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayoutAnimator.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayoutAnimator.java
deleted file mode 100644
index 471abf883..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayoutAnimator.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabs;
-
-import org.mozilla.gecko.widget.DefaultItemAnimatorBase;
-
-import android.support.v4.view.ViewCompat;
-import android.support.v7.widget.RecyclerView;
-import android.view.View;
-
-class TabsListLayoutAnimator extends DefaultItemAnimatorBase {
- public TabsListLayoutAnimator(int animationDuration) {
- setRemoveDuration(animationDuration);
- setAddDuration(animationDuration);
- // A fade in/out each time the title/thumbnail/etc. gets updated isn't helpful, so disable
- // the change animation.
- setSupportsChangeAnimations(false);
- }
-
- @Override
- protected boolean preAnimateRemoveImpl(final RecyclerView.ViewHolder holder) {
- // If the view isn't at full alpha then we were closed by a swipe which an
- // ItemTouchHelper is animating for us, so just return without animating the remove and
- // let runPendingAnimations pick up the rest.
- if (holder.itemView.getAlpha() < 1) {
- return false;
- }
- resetAnimation(holder);
- return true;
- }
-
- @Override
- protected void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
- final View itemView = holder.itemView;
- ViewCompat.animate(itemView)
- .setDuration(getRemoveDuration())
- .translationX(itemView.getWidth())
- .alpha(0)
- .setListener(new DefaultRemoveVpaListener(holder))
- .start();
- }
-
- @Override
- protected boolean preAnimateAddImpl(RecyclerView.ViewHolder holder) {
- resetAnimation(holder);
- final View itemView = holder.itemView;
- itemView.setTranslationX(itemView.getWidth());
- itemView.setAlpha(0);
- return true;
- }
-
- @Override
- protected void animateAddImpl(final RecyclerView.ViewHolder holder) {
- final View itemView = holder.itemView;
- ViewCompat.animate(itemView)
- .setDuration(getAddDuration())
- .translationX(0)
- .alpha(1)
- .setListener(new DefaultAddVpaListener(holder))
- .start();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanel.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanel.java
deleted file mode 100644
index 2be127010..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanel.java
+++ /dev/null
@@ -1,456 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabs;
-
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.GeckoApplication;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.animation.PropertyAnimator;
-import org.mozilla.gecko.animation.ViewHelper;
-import org.mozilla.gecko.lwt.LightweightTheme;
-import org.mozilla.gecko.lwt.LightweightThemeDrawable;
-import org.mozilla.gecko.restrictions.Restrictable;
-import org.mozilla.gecko.restrictions.Restrictions;
-import org.mozilla.gecko.util.HardwareUtils;
-import org.mozilla.gecko.widget.GeckoPopupMenu;
-import org.mozilla.gecko.widget.IconTabWidget;
-
-import android.content.Context;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.widget.Button;
-import android.widget.FrameLayout;
-import android.widget.ImageButton;
-import android.widget.LinearLayout;
-import android.widget.RelativeLayout;
-import org.mozilla.gecko.widget.themed.ThemedImageButton;
-
-public class TabsPanel extends LinearLayout
- implements GeckoPopupMenu.OnMenuItemClickListener,
- LightweightTheme.OnChangeListener,
- IconTabWidget.OnTabChangedListener {
- private static final String LOGTAG = "Gecko" + TabsPanel.class.getSimpleName();
-
- public enum Panel {
- NORMAL_TABS,
- PRIVATE_TABS,
- }
-
- public interface PanelView {
- void setTabsPanel(TabsPanel panel);
- void show();
- void hide();
- boolean shouldExpand();
- }
-
- public interface CloseAllPanelView extends PanelView {
- void closeAll();
- }
-
- public interface TabsLayout extends CloseAllPanelView {
- void setEmptyView(View view);
- }
-
- public interface TabsLayoutChangeListener {
- void onTabsLayoutChange(int width, int height);
- }
-
- public static View createTabsLayout(final Context context, final AttributeSet attrs) {
- final boolean isLandscape = context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
-
- if (HardwareUtils.isTablet() || isLandscape) {
- return new TabsGridLayout(context, attrs);
- } else {
- return new TabsListLayout(context, attrs);
- }
- }
-
- private final Context mContext;
- private final GeckoApp mActivity;
- private final LightweightTheme mTheme;
- private RelativeLayout mHeader;
- private FrameLayout mTabsContainer;
- private PanelView mPanel;
- private PanelView mPanelNormal;
- private PanelView mPanelPrivate;
- private TabsLayoutChangeListener mLayoutChangeListener;
-
- private IconTabWidget mTabWidget;
- private View mMenuButton;
- private ImageButton mAddTab;
- private ImageButton mNavBackButton;
-
- private Panel mCurrentPanel;
- private boolean mVisible;
- private boolean mHeaderVisible;
-
- private final GeckoPopupMenu mPopupMenu;
-
- public TabsPanel(Context context, AttributeSet attrs) {
- super(context, attrs);
- mContext = context;
- mActivity = (GeckoApp) context;
- mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
-
- mCurrentPanel = Panel.NORMAL_TABS;
-
- mPopupMenu = new GeckoPopupMenu(context);
- mPopupMenu.inflate(R.menu.tabs_menu);
- mPopupMenu.setOnMenuItemClickListener(this);
-
- inflateLayout(context);
- initialize();
- }
-
- private void inflateLayout(Context context) {
- LayoutInflater.from(context).inflate(R.layout.tabs_panel_default, this);
- }
-
- private void initialize() {
- mHeader = (RelativeLayout) findViewById(R.id.tabs_panel_header);
- mTabsContainer = (FrameLayout) findViewById(R.id.tabs_container);
-
- mPanelNormal = (PanelView) findViewById(R.id.normal_tabs);
- mPanelNormal.setTabsPanel(this);
-
- mPanelPrivate = (PanelView) findViewById(R.id.private_tabs_panel);
- mPanelPrivate.setTabsPanel(this);
-
- mAddTab = (ImageButton) findViewById(R.id.add_tab);
- mAddTab.setOnClickListener(new Button.OnClickListener() {
- @Override
- public void onClick(View v) {
- TabsPanel.this.addTab();
- }
- });
-
- mTabWidget = (IconTabWidget) findViewById(R.id.tab_widget);
-
- mTabWidget.addTab(R.drawable.tabs_normal, R.string.tabs_normal);
- final ThemedImageButton privateTabsPanel =
- (ThemedImageButton) mTabWidget.addTab(R.drawable.tabs_private, R.string.tabs_private);
- privateTabsPanel.setPrivateMode(true);
-
- if (!Restrictions.isAllowed(mContext, Restrictable.PRIVATE_BROWSING)) {
- mTabWidget.setVisibility(View.GONE);
- }
-
- mTabWidget.setTabSelectionListener(this);
-
- mMenuButton = findViewById(R.id.menu);
- mMenuButton.setOnClickListener(new Button.OnClickListener() {
- @Override
- public void onClick(View view) {
- showMenu();
- }
- });
-
- mNavBackButton = (ImageButton) findViewById(R.id.nav_back);
- mNavBackButton.setOnClickListener(new Button.OnClickListener() {
- @Override
- public void onClick(View view) {
- mActivity.onBackPressed();
- }
- });
- }
-
- public void showMenu() {
- final Menu menu = mPopupMenu.getMenu();
-
- // Each panel has a "+" shortcut button, so don't show it for that panel.
- menu.findItem(R.id.new_tab).setVisible(mCurrentPanel != Panel.NORMAL_TABS);
- menu.findItem(R.id.new_private_tab).setVisible(mCurrentPanel != Panel.PRIVATE_TABS
- && Restrictions.isAllowed(mContext, Restrictable.PRIVATE_BROWSING));
-
- // Only show "Clear * tabs" for current panel.
- menu.findItem(R.id.close_all_tabs).setVisible(mCurrentPanel == Panel.NORMAL_TABS);
- menu.findItem(R.id.close_private_tabs).setVisible(mCurrentPanel == Panel.PRIVATE_TABS);
-
- mPopupMenu.show();
- }
-
- private void addTab() {
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "new_tab");
-
- if (mCurrentPanel == Panel.NORMAL_TABS) {
- mActivity.addTab();
- } else {
- mActivity.addPrivateTab();
- }
-
- mActivity.autoHideTabs();
- }
-
- @Override
- public void onTabChanged(int index) {
- if (index == 0) {
- show(Panel.NORMAL_TABS);
- } else {
- show(Panel.PRIVATE_TABS);
- }
- }
-
- @Override
- public boolean onMenuItemClick(MenuItem item) {
- final int itemId = item.getItemId();
-
- if (itemId == R.id.close_all_tabs) {
- if (mCurrentPanel == Panel.NORMAL_TABS) {
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, "close_all_tabs");
-
- // Disable the menu button so that the menu won't interfere with the tab close animation.
- mMenuButton.setEnabled(false);
- ((CloseAllPanelView) mPanelNormal).closeAll();
- } else {
- Log.e(LOGTAG, "Close all tabs menu item should only be visible for normal tabs panel");
- }
- return true;
- }
-
- if (itemId == R.id.close_private_tabs) {
- if (mCurrentPanel == Panel.PRIVATE_TABS) {
- // Mask private browsing
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, "close_all_tabs");
-
- ((CloseAllPanelView) mPanelPrivate).closeAll();
- } else {
- Log.e(LOGTAG, "Close private tabs menu item should only be visible for private tabs panel");
- }
- return true;
- }
-
- if (itemId == R.id.new_tab || itemId == R.id.new_private_tab) {
- hide();
- }
-
- return mActivity.onOptionsItemSelected(item);
- }
-
- private static int getTabContainerHeight(FrameLayout tabsContainer) {
- final Resources resources = tabsContainer.getContext().getResources();
-
- final int screenHeight = resources.getDisplayMetrics().heightPixels;
- final int actionBarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height);
-
- return screenHeight - actionBarHeight;
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
- mTheme.addListener(this);
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- mTheme.removeListener(this);
- }
-
- @Override
- @SuppressWarnings("deprecation") // setBackgroundDrawable deprecated by API level 16
- public void onLightweightThemeChanged() {
- final int background = ContextCompat.getColor(getContext(), R.color.text_and_tabs_tray_grey);
- final LightweightThemeDrawable drawable = mTheme.getColorDrawable(this, background, true);
- if (drawable == null)
- return;
-
- drawable.setAlpha(34, 0);
- setBackgroundDrawable(drawable);
- }
-
- @Override
- public void onLightweightThemeReset() {
- setBackgroundColor(ContextCompat.getColor(getContext(), R.color.text_and_tabs_tray_grey));
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- onLightweightThemeChanged();
- }
-
- // Tabs Panel Toolbar contains the Buttons
- static class TabsPanelToolbar extends LinearLayout
- implements LightweightTheme.OnChangeListener {
- private final LightweightTheme mTheme;
-
- public TabsPanelToolbar(Context context, AttributeSet attrs) {
- super(context, attrs);
- mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
- mTheme.addListener(this);
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- mTheme.removeListener(this);
- }
-
- @Override
- @SuppressWarnings("deprecation") // setBackgroundDrawable deprecated by API level 16
- public void onLightweightThemeChanged() {
- final int background = ContextCompat.getColor(getContext(), R.color.text_and_tabs_tray_grey);
- final LightweightThemeDrawable drawable = mTheme.getColorDrawable(this, background);
- if (drawable == null)
- return;
-
- drawable.setAlpha(34, 34);
- setBackgroundDrawable(drawable);
- }
-
- @Override
- public void onLightweightThemeReset() {
- setBackgroundColor(ContextCompat.getColor(getContext(), R.color.text_and_tabs_tray_grey));
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- onLightweightThemeChanged();
- }
- }
-
- public void show(Panel panelToShow) {
- prepareToShow(panelToShow);
- int height = getVerticalPanelHeight();
- dispatchLayoutChange(getWidth(), height);
- mHeaderVisible = true;
- }
-
- public void prepareToShow(Panel panelToShow) {
- if (!isShown()) {
- setVisibility(View.VISIBLE);
- }
-
- if (mPanel != null) {
- // Hide the old panel.
- mPanel.hide();
- }
-
- mVisible = true;
- mCurrentPanel = panelToShow;
-
- int index = panelToShow.ordinal();
- mTabWidget.setCurrentTab(index);
-
- switch (panelToShow) {
- case NORMAL_TABS:
- mPanel = mPanelNormal;
- break;
- case PRIVATE_TABS:
- mPanel = mPanelPrivate;
- break;
-
- default:
- throw new IllegalArgumentException("Unknown panel type " + panelToShow);
- }
- mPanel.show();
-
- mAddTab.setVisibility(View.VISIBLE);
-
- mMenuButton.setEnabled(true);
- mPopupMenu.setAnchor(mMenuButton);
- }
-
- public int getVerticalPanelHeight() {
- final int actionBarHeight = mContext.getResources().getDimensionPixelSize(R.dimen.browser_toolbar_height);
- final int height = actionBarHeight + getTabContainerHeight(mTabsContainer);
- return height;
- }
-
- public void hide() {
- mHeaderVisible = false;
-
- if (mVisible) {
- mVisible = false;
- mPopupMenu.dismiss();
- dispatchLayoutChange(0, 0);
- }
- }
-
- public void refresh() {
- removeAllViews();
-
- inflateLayout(mContext);
- initialize();
-
- if (mVisible)
- show(mCurrentPanel);
- }
-
- public void autoHidePanel() {
- mActivity.autoHideTabs();
- }
-
- @Override
- public boolean isShown() {
- return mVisible;
- }
-
- public void setHWLayerEnabled(boolean enabled) {
- if (enabled) {
- mHeader.setLayerType(View.LAYER_TYPE_HARDWARE, null);
- mTabsContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
- } else {
- mHeader.setLayerType(View.LAYER_TYPE_NONE, null);
- mTabsContainer.setLayerType(View.LAYER_TYPE_NONE, null);
- }
- }
-
- public void prepareTabsAnimation(PropertyAnimator animator) {
- if (!mHeaderVisible) {
- final Resources resources = getContext().getResources();
- final int toolbarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height);
- final int translationY = (mVisible ? 0 : -toolbarHeight);
- if (mVisible) {
- ViewHelper.setTranslationY(mHeader, -toolbarHeight);
- ViewHelper.setTranslationY(mTabsContainer, -toolbarHeight);
- ViewHelper.setAlpha(mTabsContainer, 0.0f);
- }
- animator.attach(mTabsContainer, PropertyAnimator.Property.ALPHA, mVisible ? 1.0f : 0.0f);
- animator.attach(mTabsContainer, PropertyAnimator.Property.TRANSLATION_Y, translationY);
- animator.attach(mHeader, PropertyAnimator.Property.TRANSLATION_Y, translationY);
- }
-
- setHWLayerEnabled(true);
- }
-
- public void finishTabsAnimation() {
- setHWLayerEnabled(false);
-
- // If the tray is now hidden, call hide() on current panel and unset it as the current panel
- // to avoid hide() being called again when the layout is opened next.
- if (!mVisible && mPanel != null) {
- mPanel.hide();
- mPanel = null;
- }
- }
-
- public void setTabsLayoutChangeListener(TabsLayoutChangeListener listener) {
- mLayoutChangeListener = listener;
- }
-
- private void dispatchLayoutChange(int width, int height) {
- if (mLayoutChangeListener != null)
- mLayoutChangeListener.onTabsLayoutChange(width, height);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanelThumbnailView.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanelThumbnailView.java
deleted file mode 100644
index 09254bf76..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanelThumbnailView.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabs;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.ThumbnailHelper;
-import org.mozilla.gecko.widget.CropImageView;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-
-/**
- * A width constrained ImageView to show thumbnails of open tabs in the tabs panel.
- */
-public class TabsPanelThumbnailView extends CropImageView {
- public static final String LOGTAG = "Gecko" + TabsPanelThumbnailView.class.getSimpleName();
-
-
- public TabsPanelThumbnailView(final Context context) {
- this(context, null);
- }
-
- public TabsPanelThumbnailView(final Context context, final AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public TabsPanelThumbnailView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-
- @Override
- protected float getAspectRatio() {
- return ThumbnailHelper.TABS_PANEL_THUMBNAIL_ASPECT_RATIO;
- }
-
- @Override
- public void setImageDrawable(Drawable drawable) {
- boolean resize = true;
-
- if (drawable == null) {
- drawable = getResources().getDrawable(R.drawable.tab_panel_tab_background);
- resize = false;
- setScaleType(ScaleType.FIT_XY);
- }
-
- super.setImageDrawable(drawable, resize);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsTouchHelperCallback.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsTouchHelperCallback.java
deleted file mode 100644
index 36e9e4739..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsTouchHelperCallback.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.tabs;
-
-import android.graphics.Canvas;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.helper.ItemTouchHelper;
-import android.view.View;
-
-class TabsTouchHelperCallback extends ItemTouchHelper.Callback {
- private final DismissListener dismissListener;
-
- interface DismissListener {
- void onItemDismiss(View view);
- }
-
- public TabsTouchHelperCallback(DismissListener dismissListener) {
- this.dismissListener = dismissListener;
- }
-
- @Override
- public boolean isItemViewSwipeEnabled() {
- return true;
- }
-
- @Override
- public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
- return makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
- }
-
- @Override
- public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {
- dismissListener.onItemDismiss(viewHolder.itemView);
- }
-
- @Override
- public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
- RecyclerView.ViewHolder target) {
- return false;
- }
-
- // Alpha on an itemView being swiped should decrease to a min over a distance equal to the
- // width of the item being swiped.
- @Override
- public void onChildDraw(Canvas c,
- RecyclerView recyclerView,
- RecyclerView.ViewHolder viewHolder,
- float dX,
- float dY,
- int actionState,
- boolean isCurrentlyActive) {
- if (actionState != ItemTouchHelper.ACTION_STATE_SWIPE) {
- return;
- }
-
- super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
-
- viewHolder.itemView.setAlpha(Math.max(0.1f,
- Math.min(1f, 1f - 2f * Math.abs(dX) / viewHolder.itemView.getWidth())));
- }
-
- public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
- super.clearView(recyclerView, viewHolder);
- viewHolder.itemView.setAlpha(1);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java
deleted file mode 100644
index 6ed4bb0d4..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java
+++ /dev/null
@@ -1,16 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.telemetry;
-
-import org.mozilla.gecko.AppConstants;
-
-public class TelemetryConstants {
- // To test, set this to true & change "toolkit.telemetry.server" in about:config.
- public static final boolean UPLOAD_ENABLED = AppConstants.MOZILLA_OFFICIAL; // Disabled for developer builds.
-
- public static final String USER_AGENT =
- "Firefox-Android-Telemetry/" + AppConstants.MOZ_APP_VERSION + " (" + AppConstants.MOZ_APP_UA_NAME + ")";
-
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryCorePingDelegate.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryCorePingDelegate.java
deleted file mode 100644
index fae674b2d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryCorePingDelegate.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, you can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-package org.mozilla.gecko.telemetry;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.support.annotation.Nullable;
-import android.support.annotation.WorkerThread;
-import android.util.Log;
-import org.mozilla.gecko.BrowserApp;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.adjust.AttributionHelperListener;
-import org.mozilla.gecko.telemetry.measurements.CampaignIdMeasurements;
-import org.mozilla.gecko.delegates.BrowserAppDelegateWithReference;
-import org.mozilla.gecko.distribution.DistributionStoreCallback;
-import org.mozilla.gecko.search.SearchEngineManager;
-import org.mozilla.gecko.sync.ExtendedJSONObject;
-import org.mozilla.gecko.telemetry.measurements.SearchCountMeasurements;
-import org.mozilla.gecko.telemetry.measurements.SessionMeasurements;
-import org.mozilla.gecko.telemetry.pingbuilders.TelemetryCorePingBuilder;
-import org.mozilla.gecko.util.StringUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import java.io.IOException;
-
-/**
- * An activity-lifecycle delegate for uploading the core ping.
- */
-public class TelemetryCorePingDelegate extends BrowserAppDelegateWithReference
- implements SearchEngineManager.SearchEngineCallback, AttributionHelperListener {
- private static final String LOGTAG = StringUtils.safeSubstring(
- "Gecko" + TelemetryCorePingDelegate.class.getSimpleName(), 0, 23);
-
- private static final String PREF_IS_FIRST_RUN = "telemetry-isFirstRun";
-
- private TelemetryDispatcher telemetryDispatcher; // lazy
- private final SessionMeasurements sessionMeasurements = new SessionMeasurements();
-
- @Override
- public void onStart(final BrowserApp browserApp) {
- TelemetryPreferences.initPreferenceObserver(browserApp, browserApp.getProfile().getName());
-
- // We don't upload in onCreate because that's only called when the Activity needs to be instantiated
- // and it's possible the system will never free the Activity from memory.
- //
- // We don't upload in onResume/onPause because that will be called each time the Activity is obscured,
- // including by our own Activities/dialogs, and there is no reason to upload each time we're unobscured.
- //
- // We're left with onStart/onStop and we upload in onStart because onStop is not guaranteed to be called
- // and we want to upload the first run ASAP (e.g. to get install data before the app may crash).
- uploadPing(browserApp);
- }
-
- @Override
- public void onStop(final BrowserApp browserApp) {
- // We've decided to upload primarily in onStart (see note there). However, if it's the first run,
- // it's possible a user used fennec and decided never to return to it again - it'd be great to get
- // their session information before they decided to give it up so we upload here on first run.
- //
- // Caveats:
- // * onStop is not guaranteed to be called in low memory conditions so it's possible we won't upload,
- // but it's better than it was before.
- // * Besides first run (because of this call), we can never get the user's *last* session data.
- //
- // If we are really interested in the user's last session data, we could consider uploading in onStop
- // but it's less robust (see discussion in bug 1277091).
- final SharedPreferences sharedPrefs = getSharedPreferences(browserApp);
- if (sharedPrefs.getBoolean(PREF_IS_FIRST_RUN, true)) {
- sharedPrefs.edit()
- .putBoolean(PREF_IS_FIRST_RUN, false)
- .apply();
- uploadPing(browserApp);
- }
- }
-
- private void uploadPing(final BrowserApp browserApp) {
- final SearchEngineManager searchEngineManager = browserApp.getSearchEngineManager();
- searchEngineManager.getEngine(this);
- }
-
- @Override
- public void onResume(BrowserApp browserApp) {
- sessionMeasurements.recordSessionStart();
- }
-
- @Override
- public void onPause(BrowserApp browserApp) {
- // onStart/onStop is ideal over onResume/onPause. However, onStop is not guaranteed to be called and
- // dealing with that possibility adds a lot of complexity that we don't want to handle at this point.
- sessionMeasurements.recordSessionEnd(browserApp);
- }
-
- @WorkerThread // via constructor
- private TelemetryDispatcher getTelemetryDispatcher(final BrowserApp browserApp) {
- if (telemetryDispatcher == null) {
- final GeckoProfile profile = browserApp.getProfile();
- final String profilePath = profile.getDir().getAbsolutePath();
- final String profileName = profile.getName();
- telemetryDispatcher = new TelemetryDispatcher(profilePath, profileName);
- }
- return telemetryDispatcher;
- }
-
- private SharedPreferences getSharedPreferences(final BrowserApp activity) {
- return GeckoSharedPrefs.forProfileName(activity, activity.getProfile().getName());
- }
-
- // via SearchEngineCallback - may be called from any thread.
- @Override
- public void execute(@Nullable final org.mozilla.gecko.search.SearchEngine engine) {
- // Don't waste resources queueing to the background thread if we don't have a reference.
- if (getBrowserApp() == null) {
- return;
- }
-
- // The containing method can be called from onStart: queue this work so that
- // the first launch of the activity doesn't trigger profile init too early.
- //
- // Additionally, getAndIncrementSequenceNumber must be called from a worker thread.
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @WorkerThread
- @Override
- public void run() {
- final BrowserApp activity = getBrowserApp();
- if (activity == null) {
- return;
- }
-
- final GeckoProfile profile = activity.getProfile();
- if (!TelemetryUploadService.isUploadEnabledByProfileConfig(activity, profile)) {
- Log.d(LOGTAG, "Core ping upload disabled by profile config. Returning.");
- return;
- }
-
- final String clientID;
- try {
- clientID = profile.getClientId();
- } catch (final IOException e) {
- Log.w(LOGTAG, "Unable to get client ID to generate core ping: " + e);
- return;
- }
-
- // Each profile can have different telemetry data so we intentionally grab the shared prefs for the profile.
- final SharedPreferences sharedPrefs = getSharedPreferences(activity);
- final SessionMeasurements.SessionMeasurementsContainer sessionMeasurementsContainer =
- sessionMeasurements.getAndResetSessionMeasurements(activity);
- final TelemetryCorePingBuilder pingBuilder = new TelemetryCorePingBuilder(activity)
- .setClientID(clientID)
- .setDefaultSearchEngine(TelemetryCorePingBuilder.getEngineIdentifier(engine))
- .setProfileCreationDate(TelemetryCorePingBuilder.getProfileCreationDate(activity, profile))
- .setSequenceNumber(TelemetryCorePingBuilder.getAndIncrementSequenceNumber(sharedPrefs))
- .setSessionCount(sessionMeasurementsContainer.sessionCount)
- .setSessionDuration(sessionMeasurementsContainer.elapsedSeconds);
- maybeSetOptionalMeasurements(activity, sharedPrefs, pingBuilder);
-
- getTelemetryDispatcher(activity).queuePingForUpload(activity, pingBuilder);
- }
- });
- }
-
- private void maybeSetOptionalMeasurements(final Context context, final SharedPreferences sharedPrefs,
- final TelemetryCorePingBuilder pingBuilder) {
- final String distributionId = sharedPrefs.getString(DistributionStoreCallback.PREF_DISTRIBUTION_ID, null);
- if (distributionId != null) {
- pingBuilder.setOptDistributionID(distributionId);
- }
-
- final ExtendedJSONObject searchCounts = SearchCountMeasurements.getAndZeroSearch(sharedPrefs);
- if (searchCounts.size() > 0) {
- pingBuilder.setOptSearchCounts(searchCounts);
- }
-
- final String campaignId = CampaignIdMeasurements.getCampaignIdFromPrefs(context);
- if (campaignId != null) {
- pingBuilder.setOptCampaignId(campaignId);
- }
- }
-
- @Override
- public void onCampaignIdChanged(String campaignId) {
- CampaignIdMeasurements.updateCampaignIdPref(getBrowserApp(), campaignId);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java
deleted file mode 100644
index c702bb92c..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, you can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-package org.mozilla.gecko.telemetry;
-
-import android.content.Context;
-import android.support.annotation.WorkerThread;
-import android.util.Log;
-import org.mozilla.gecko.telemetry.pingbuilders.TelemetryCorePingBuilder;
-import org.mozilla.gecko.telemetry.schedulers.TelemetryUploadScheduler;
-import org.mozilla.gecko.telemetry.schedulers.TelemetryUploadAllPingsImmediatelyScheduler;
-import org.mozilla.gecko.telemetry.stores.TelemetryJSONFilePingStore;
-import org.mozilla.gecko.telemetry.stores.TelemetryPingStore;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import java.io.File;
-import java.io.IOException;
-
-/**
- * The entry-point for Java-based telemetry. This class handles:
- * * Initializing the Stores & Schedulers.
- * * Queueing upload requests for a given ping.
- *
- * To test Telemetry , see {@link TelemetryConstants} &
- * https://wiki.mozilla.org/Mobile/Fennec/Android/Java_telemetry.
- *
- * The full architecture is:
- *
- * Fennec -(PingBuilder)-> Dispatcher -2-> Scheduler -> UploadService
- * | 1 |
- * Store <--------------------------
- *
- * The store acts as a single store of truth and contains a list of all
- * pings waiting to be uploaded. The dispatcher will queue a ping to upload
- * by writing it to the store. Later, the UploadService will try to upload
- * this queued ping by reading directly from the store.
- *
- * To implement a new ping type, you should:
- * 1) Implement a {@link org.mozilla.gecko.telemetry.pingbuilders.TelemetryPingBuilder} for your ping type.
- * 2) Re-use a ping store in .../stores/ or implement a new one: {@link TelemetryPingStore}. The
- * type of store may be affected by robustness requirements (e.g. do you have data in addition to
- * pings that need to be atomically updated when a ping is stored?) and performance requirements.
- * 3) Re-use an upload scheduler in .../schedulers/ or implement a new one: {@link TelemetryUploadScheduler}.
- * 4) Initialize your Store & (if new) Scheduler in the constructor of this class
- * 5) Add a queuePingForUpload method for your PingBuilder class (see
- * {@link #queuePingForUpload(Context, TelemetryCorePingBuilder)})
- * 6) In Fennec, where you want to store a ping and attempt upload, create a PingBuilder and
- * pass it to the new queuePingForUpload method.
- */
-public class TelemetryDispatcher {
- private static final String LOGTAG = "Gecko" + TelemetryDispatcher.class.getSimpleName();
-
- private static final String STORE_CONTAINER_DIR_NAME = "telemetry_java";
- private static final String CORE_STORE_DIR_NAME = "core";
-
- private final TelemetryJSONFilePingStore coreStore;
-
- private final TelemetryUploadAllPingsImmediatelyScheduler uploadAllPingsImmediatelyScheduler;
-
- @WorkerThread // via TelemetryJSONFilePingStore
- public TelemetryDispatcher(final String profilePath, final String profileName) {
- final String storePath = profilePath + File.separator + STORE_CONTAINER_DIR_NAME;
-
- // There are measurements in the core ping (e.g. seq #) that would ideally be atomically updated
- // when the ping is stored. However, for simplicity, we use the json store and accept the possible
- // loss of data (see bug 1243585 comment 16+ for more).
- coreStore = new TelemetryJSONFilePingStore(new File(storePath, CORE_STORE_DIR_NAME), profileName);
-
- uploadAllPingsImmediatelyScheduler = new TelemetryUploadAllPingsImmediatelyScheduler();
- }
-
- private void queuePingForUpload(final Context context, final TelemetryPing ping, final TelemetryPingStore store,
- final TelemetryUploadScheduler scheduler) {
- final QueuePingRunnable runnable = new QueuePingRunnable(context, ping, store, scheduler);
- ThreadUtils.postToBackgroundThread(runnable); // TODO: Investigate how busy this thread is. See if we want another.
- }
-
- /**
- * Queues the given ping for upload and potentially schedules upload. This method can be called from any thread.
- */
- public void queuePingForUpload(final Context context, final TelemetryCorePingBuilder pingBuilder) {
- final TelemetryPing ping = pingBuilder.build();
- queuePingForUpload(context, ping, coreStore, uploadAllPingsImmediatelyScheduler);
- }
-
- private static class QueuePingRunnable implements Runnable {
- private final Context applicationContext;
- private final TelemetryPing ping;
- private final TelemetryPingStore store;
- private final TelemetryUploadScheduler scheduler;
-
- public QueuePingRunnable(final Context context, final TelemetryPing ping, final TelemetryPingStore store,
- final TelemetryUploadScheduler scheduler) {
- this.applicationContext = context.getApplicationContext();
- this.ping = ping;
- this.store = store;
- this.scheduler = scheduler;
- }
-
- @Override
- public void run() {
- // We block while storing the ping so the scheduled upload is guaranteed to have the newly-stored value.
- try {
- store.storePing(ping);
- } catch (final IOException e) {
- // Don't log exception to avoid leaking profile path.
- Log.e(LOGTAG, "Unable to write ping to disk. Continuing with upload attempt");
- }
-
- if (scheduler.isReadyToUpload(store)) {
- scheduler.scheduleUpload(applicationContext, store);
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java
deleted file mode 100644
index b6ee9c2d8..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.telemetry;
-
-import org.mozilla.gecko.sync.ExtendedJSONObject;
-
-/**
- * Container for telemetry data and the data necessary to upload it.
- *
- * The doc ID is used by a Store to manipulate its internal pings and should
- * be the same value found in the urlPath.
- *
- * If you want to create one of these, consider extending
- * {@link org.mozilla.gecko.telemetry.pingbuilders.TelemetryPingBuilder}
- * or one of its descendants.
- */
-public class TelemetryPing {
- private final String urlPath;
- private final ExtendedJSONObject payload;
- private final String docID;
-
- public TelemetryPing(final String urlPath, final ExtendedJSONObject payload, final String docID) {
- this.urlPath = urlPath;
- this.payload = payload;
- this.docID = docID;
- }
-
- public String getURLPath() { return urlPath; }
- public ExtendedJSONObject getPayload() { return payload; }
- public String getDocID() { return docID; }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPreferences.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPreferences.java
deleted file mode 100644
index 329f5b803..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPreferences.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.telemetry;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.PrefsHelper;
-import org.mozilla.gecko.PrefsHelper.PrefHandler;
-
-import java.lang.ref.WeakReference;
-
-/**
- * Manages getting and setting any preferences related to telemetry.
- *
- * This class persists any Gecko preferences beyond shutdown so that these values
- * can be accessed on the next run before Gecko is started as we expect Telemetry
- * to run before Gecko is available.
- */
-public class TelemetryPreferences {
- private TelemetryPreferences() {}
-
- private static final String GECKO_PREF_SERVER_URL = "toolkit.telemetry.server";
- private static final String SHARED_PREF_SERVER_URL = "telemetry-serverUrl";
-
- // Defaults are a mirror of about:config defaults so we can access them before Gecko is available.
- private static final String DEFAULT_SERVER_URL = "https://incoming.telemetry.mozilla.org";
-
- private static final String[] OBSERVED_PREFS = {
- GECKO_PREF_SERVER_URL,
- };
-
- public static String getServerSchemeHostPort(final Context context, final String profileName) {
- return getSharedPrefs(context, profileName).getString(SHARED_PREF_SERVER_URL, DEFAULT_SERVER_URL);
- }
-
- public static void initPreferenceObserver(final Context context, final String profileName) {
- final PrefHandler prefHandler = new TelemetryPrefHandler(context, profileName);
- PrefsHelper.addObserver(OBSERVED_PREFS, prefHandler); // gets preference value when gecko starts.
- }
-
- private static SharedPreferences getSharedPrefs(final Context context, final String profileName) {
- return GeckoSharedPrefs.forProfileName(context, profileName);
- }
-
- private static class TelemetryPrefHandler extends PrefsHelper.PrefHandlerBase {
- private final WeakReference<Context> contextWeakReference;
- private final String profileName;
-
- private TelemetryPrefHandler(final Context context, final String profileName) {
- contextWeakReference = new WeakReference<>(context);
- this.profileName = profileName;
- }
-
- @Override
- public void prefValue(final String pref, final String value) {
- final Context context = contextWeakReference.get();
- if (context == null) {
- return;
- }
-
- if (!pref.equals(GECKO_PREF_SERVER_URL)) {
- throw new IllegalStateException("Unknown preference: " + pref);
- }
-
- getSharedPrefs(context, profileName).edit()
- .putString(SHARED_PREF_SERVER_URL, value)
- .apply();
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java
deleted file mode 100644
index 543281174..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java
+++ /dev/null
@@ -1,347 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.telemetry;
-
-import android.app.IntentService;
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-import ch.boye.httpclientandroidlib.HttpHeaders;
-import ch.boye.httpclientandroidlib.HttpResponse;
-import ch.boye.httpclientandroidlib.client.ClientProtocolException;
-import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
-import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-import org.mozilla.gecko.restrictions.Restrictable;
-import org.mozilla.gecko.restrictions.Restrictions;
-import org.mozilla.gecko.sync.ExtendedJSONObject;
-import org.mozilla.gecko.sync.net.BaseResource;
-import org.mozilla.gecko.sync.net.BaseResourceDelegate;
-import org.mozilla.gecko.sync.net.Resource;
-import org.mozilla.gecko.telemetry.stores.TelemetryPingStore;
-import org.mozilla.gecko.util.DateUtil;
-import org.mozilla.gecko.util.NetworkUtils;
-import org.mozilla.gecko.util.StringUtils;
-
-import java.io.IOException;
-import java.net.URISyntaxException;
-import java.security.GeneralSecurityException;
-import java.util.Calendar;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-/**
- * The service that handles retrieving a list of telemetry pings to upload from the given
- * {@link TelemetryPingStore}, uploading those payloads to the associated server, and reporting
- * back to the Store which uploads were a success.
- */
-public class TelemetryUploadService extends IntentService {
- private static final String LOGTAG = StringUtils.safeSubstring("Gecko" + TelemetryUploadService.class.getSimpleName(), 0, 23);
- private static final String WORKER_THREAD_NAME = LOGTAG + "Worker";
-
- public static final String ACTION_UPLOAD = "upload";
- public static final String EXTRA_STORE = "store";
-
- // TelemetryUploadService can run in a background thread so for future proofing, we set it volatile.
- private static volatile boolean isDisabled = false;
-
- public static void setDisabled(final boolean isDisabled) {
- TelemetryUploadService.isDisabled = isDisabled;
- if (isDisabled) {
- Log.d(LOGTAG, "Telemetry upload disabled (env var?");
- }
- }
-
- public TelemetryUploadService() {
- super(WORKER_THREAD_NAME);
-
- // Intent redelivery can fail hard (e.g. we OOM as we try to upload, the Intent gets redelivered, repeat)
- // so for simplicity, we avoid it. We expect the upload service to eventually get called again by the caller.
- setIntentRedelivery(false);
- }
-
- /**
- * Handles a ping with the mandatory extras:
- * * EXTRA_STORE: A {@link TelemetryPingStore} where the pings to upload are located
- */
- @Override
- public void onHandleIntent(final Intent intent) {
- Log.d(LOGTAG, "Service started");
-
- if (!isReadyToUpload(this, intent)) {
- return;
- }
-
- final TelemetryPingStore store = intent.getParcelableExtra(EXTRA_STORE);
- final boolean wereAllUploadsSuccessful = uploadPendingPingsFromStore(this, store);
- store.maybePrunePings();
- Log.d(LOGTAG, "Service finished: upload and prune attempts completed");
-
- if (!wereAllUploadsSuccessful) {
- // If we had an upload failure, we should stop the IntentService and drop any
- // pending Intents in the queue so we don't waste resources (e.g. battery)
- // trying to upload when there's likely to be another connection failure.
- Log.d(LOGTAG, "Clearing Intent queue due to connection failures");
- stopSelf();
- }
- }
-
- /**
- * @return true if all pings were uploaded successfully, false otherwise.
- */
- private static boolean uploadPendingPingsFromStore(final Context context, final TelemetryPingStore store) {
- final List<TelemetryPing> pingsToUpload = store.getAllPings();
- if (pingsToUpload.isEmpty()) {
- return true;
- }
-
- final String serverSchemeHostPort = TelemetryPreferences.getServerSchemeHostPort(context, store.getProfileName());
- final HashSet<String> successfulUploadIDs = new HashSet<>(pingsToUpload.size()); // used for side effects.
- final PingResultDelegate delegate = new PingResultDelegate(successfulUploadIDs);
- for (final TelemetryPing ping : pingsToUpload) {
- // TODO: It'd be great to re-use the same HTTP connection for each upload request.
- delegate.setDocID(ping.getDocID());
- final String url = serverSchemeHostPort + "/" + ping.getURLPath();
- uploadPayload(url, ping.getPayload(), delegate);
-
- // There are minimal gains in trying to upload if we already failed one attempt.
- if (delegate.hadConnectionError()) {
- break;
- }
- }
-
- final boolean wereAllUploadsSuccessful = !delegate.hadConnectionError();
- if (wereAllUploadsSuccessful) {
- // We don't log individual successful uploads to avoid log spam.
- Log.d(LOGTAG, "Telemetry upload success!");
- }
- store.onUploadAttemptComplete(successfulUploadIDs);
- return wereAllUploadsSuccessful;
- }
-
- private static void uploadPayload(final String url, final ExtendedJSONObject payload, final ResultDelegate delegate) {
- final BaseResource resource;
- try {
- resource = new BaseResource(url);
- } catch (final URISyntaxException e) {
- Log.w(LOGTAG, "URISyntaxException for server URL when creating BaseResource: returning.");
- return;
- }
-
- delegate.setResource(resource);
- resource.delegate = delegate;
- resource.setShouldCompressUploadedEntity(true);
- resource.setShouldChunkUploadsHint(false); // Telemetry servers don't support chunking.
-
- // We're in a background thread so we don't have any reason to do this asynchronously.
- // If we tried, onStartCommand would return and IntentService might stop itself before we finish.
- resource.postBlocking(payload);
- }
-
- private static boolean isReadyToUpload(final Context context, final Intent intent) {
- // Sanity check: is upload enabled? Generally, the caller should check this before starting the service.
- // Since we don't have the profile here, we rely on the caller to check the enabled state for the profile.
- if (!isUploadEnabledByAppConfig(context)) {
- Log.w(LOGTAG, "Upload is not available by configuration; returning");
- return false;
- }
-
- if (!NetworkUtils.isConnected(context)) {
- Log.w(LOGTAG, "Network is not connected; returning");
- return false;
- }
-
- if (!isIntentValid(intent)) {
- Log.w(LOGTAG, "Received invalid Intent; returning");
- return false;
- }
-
- if (!ACTION_UPLOAD.equals(intent.getAction())) {
- Log.w(LOGTAG, "Unknown action: " + intent.getAction() + ". Returning");
- return false;
- }
-
- return true;
- }
-
- /**
- * Determines if the telemetry upload feature is enabled via the application configuration. Prefer to use
- * {@link #isUploadEnabledByProfileConfig(Context, GeckoProfile)} if the profile is available as it takes into
- * account more information.
- *
- * You may wish to also check if the network is connected when calling this method.
- *
- * Note that this method logs debug statements when upload is disabled.
- */
- public static boolean isUploadEnabledByAppConfig(final Context context) {
- if (!TelemetryConstants.UPLOAD_ENABLED) {
- Log.d(LOGTAG, "Telemetry upload feature is compile-time disabled");
- return false;
- }
-
- if (isDisabled) {
- Log.d(LOGTAG, "Telemetry upload feature is disabled by intent (in testing?)");
- return false;
- }
-
- if (!GeckoPreferences.getBooleanPref(context, GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true)) {
- Log.d(LOGTAG, "Telemetry upload opt-out");
- return false;
- }
-
- if (Restrictions.isRestrictedProfile(context) &&
- !Restrictions.isAllowed(context, Restrictable.HEALTH_REPORT)) {
- Log.d(LOGTAG, "Telemetry upload feature disabled by admin profile");
- return false;
- }
-
- return true;
- }
-
- /**
- * Determines if the telemetry upload feature is enabled via profile & application level configurations. This is the
- * preferred method.
- *
- * You may wish to also check if the network is connected when calling this method.
- *
- * Note that this method logs debug statements when upload is disabled.
- */
- public static boolean isUploadEnabledByProfileConfig(final Context context, final GeckoProfile profile) {
- if (profile.inGuestMode()) {
- Log.d(LOGTAG, "Profile is in guest mode");
- return false;
- }
-
- return isUploadEnabledByAppConfig(context);
- }
-
- private static boolean isIntentValid(final Intent intent) {
- // Intent can be null. Bug 1025937.
- if (intent == null) {
- Log.d(LOGTAG, "Received null intent");
- return false;
- }
-
- if (intent.getParcelableExtra(EXTRA_STORE) == null) {
- Log.d(LOGTAG, "Received invalid store in Intent");
- return false;
- }
-
- return true;
- }
-
- /**
- * Logs on success & failure and appends the set ID to the given Set on success.
- *
- * Note: you *must* set the ping ID before attempting upload or we'll throw!
- *
- * We use mutation on the set ID and the successful upload array to avoid object allocation.
- */
- private static class PingResultDelegate extends ResultDelegate {
- // We persist pings and don't need to worry about losing data so we keep these
- // durations short to save resources (e.g. battery).
- private static final int SOCKET_TIMEOUT_MILLIS = (int) TimeUnit.SECONDS.toMillis(30);
- private static final int CONNECTION_TIMEOUT_MILLIS = (int) TimeUnit.SECONDS.toMillis(30);
-
- /** The store ID of the ping currently being uploaded. Use {@link #getDocID()} to access it. */
- private String docID = null;
- private final Set<String> successfulUploadIDs;
-
- private boolean hadConnectionError = false;
-
- public PingResultDelegate(final Set<String> successfulUploadIDs) {
- super();
- this.successfulUploadIDs = successfulUploadIDs;
- }
-
- @Override
- public int socketTimeout() {
- return SOCKET_TIMEOUT_MILLIS;
- }
-
- @Override
- public int connectionTimeout() {
- return CONNECTION_TIMEOUT_MILLIS;
- }
-
- private String getDocID() {
- if (docID == null) {
- throw new IllegalStateException("Expected ping ID to have been updated before retrieval");
- }
- return docID;
- }
-
- public void setDocID(final String id) {
- docID = id;
- }
-
- @Override
- public String getUserAgent() {
- return TelemetryConstants.USER_AGENT;
- }
-
- @Override
- public void handleHttpResponse(final HttpResponse response) {
- final int status = response.getStatusLine().getStatusCode();
- switch (status) {
- case 200:
- case 201:
- successfulUploadIDs.add(getDocID());
- break;
- default:
- Log.w(LOGTAG, "Telemetry upload failure. HTTP status: " + status);
- hadConnectionError = true;
- }
- }
-
- @Override
- public void handleHttpProtocolException(final ClientProtocolException e) {
- // We don't log the exception to prevent leaking user data.
- Log.w(LOGTAG, "HttpProtocolException when trying to upload telemetry");
- hadConnectionError = true;
- }
-
- @Override
- public void handleHttpIOException(final IOException e) {
- // We don't log the exception to prevent leaking user data.
- Log.w(LOGTAG, "HttpIOException when trying to upload telemetry");
- hadConnectionError = true;
- }
-
- @Override
- public void handleTransportException(final GeneralSecurityException e) {
- // We don't log the exception to prevent leaking user data.
- Log.w(LOGTAG, "Transport exception when trying to upload telemetry");
- hadConnectionError = true;
- }
-
- private boolean hadConnectionError() {
- return hadConnectionError;
- }
-
- @Override
- public void addHeaders(final HttpRequestBase request, final DefaultHttpClient client) {
- super.addHeaders(request, client);
- request.addHeader(HttpHeaders.DATE, DateUtil.getDateInHTTPFormat(Calendar.getInstance().getTime()));
- }
- }
-
- /**
- * A hack because I want to set the resource after the Delegate is constructed.
- * Be sure to call {@link #setResource(Resource)}!
- */
- private static abstract class ResultDelegate extends BaseResourceDelegate {
- public ResultDelegate() {
- super(null);
- }
-
- protected void setResource(final Resource resource) {
- this.resource = resource;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/CampaignIdMeasurements.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/CampaignIdMeasurements.java
deleted file mode 100644
index 61229b21b..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/CampaignIdMeasurements.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, you can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-package org.mozilla.gecko.telemetry.measurements;
-
-import android.content.Context;
-import android.support.annotation.NonNull;
-import android.text.TextUtils;
-
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.adjust.AttributionHelperListener;
-
-/**
- * A class to retrieve and store the campaign Id pref that is used when the Adjust SDK gives us
- * new attribution from the {@link AttributionHelperListener}.
- */
-public class CampaignIdMeasurements {
- private static final String PREF_CAMPAIGN_ID = "measurements-campaignId";
-
- public static String getCampaignIdFromPrefs(@NonNull final Context context) {
- return GeckoSharedPrefs.forProfile(context)
- .getString(PREF_CAMPAIGN_ID, null);
- }
-
- public static void updateCampaignIdPref(@NonNull final Context context, @NonNull final String campaignId) {
- if (TextUtils.isEmpty(campaignId)) {
- return;
- }
- GeckoSharedPrefs.forProfile(context)
- .edit()
- .putString(PREF_CAMPAIGN_ID, campaignId)
- .apply();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SearchCountMeasurements.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SearchCountMeasurements.java
deleted file mode 100644
index c08ad6c02..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SearchCountMeasurements.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, you can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-package org.mozilla.gecko.telemetry.measurements;
-
-import android.content.SharedPreferences;
-import android.support.annotation.NonNull;
-import android.support.annotation.VisibleForTesting;
-import org.mozilla.gecko.sync.ExtendedJSONObject;
-
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * A place to store and retrieve the number of times a user has searched with a specific engine from a
- * specific location. This is designed for use as a telemetry core ping measurement.
- *
- * The implementation works by storing a preference for each engine-location pair and incrementing them
- * each time {@link #incrementSearch(SharedPreferences, String, String)} is called. In order to
- * retrieve the full set of keys later, we store all the available key names in another preference.
- *
- * When we retrieve the keys in {@link #getAndZeroSearch(SharedPreferences)} (using the set of keys
- * preference), the values saved to the preferences are returned and the preferences are removed
- * (i.e. zeroed) from Shared Preferences. The reason we remove the preferences (rather than actually
- * zeroing them) is to avoid bloating shared preferences if 1) the set of engines ever changes or
- * 2) we remove this feature.
- *
- * Since we increment a value on each successive search, which doesn't take up more space, we don't
- * have to worry about using excess disk space if the measurements are never zeroed (e.g. telemetry
- * upload is disabled). In the worst case, we overflow the integer and may return negative values.
- *
- * This class is thread-safe by locking access to its public methods. When this class was written, incrementing &
- * retrieval were called from multiple threads so rather than enforcing the callers keep their threads straight, it
- * was simpler to lock all access.
- */
-public class SearchCountMeasurements {
- /** The set of "engine + where" keys we've stored; used for retrieving stored engines. */
- @VisibleForTesting static final String PREF_SEARCH_KEYSET = "measurements-search-count-keyset";
- private static final String PREF_SEARCH_PREFIX = "measurements-search-count-engine-"; // + "engine.where"
-
- private SearchCountMeasurements() {}
-
- public static synchronized void incrementSearch(@NonNull final SharedPreferences prefs,
- @NonNull final String engineIdentifier, @NonNull final String where) {
- final String engineWhereStr = engineIdentifier + "." + where;
- final String key = getEngineSearchCountKey(engineWhereStr);
-
- final int count = prefs.getInt(key, 0);
- prefs.edit().putInt(key, count + 1).apply();
-
- unionKeyToSearchKeyset(prefs, engineWhereStr);
- }
-
- /**
- * @param key Engine of the form, "engine.where"
- */
- private static void unionKeyToSearchKeyset(@NonNull final SharedPreferences prefs, @NonNull final String key) {
- final Set<String> keysFromPrefs = prefs.getStringSet(PREF_SEARCH_KEYSET, Collections.<String>emptySet());
- if (keysFromPrefs.contains(key)) {
- return;
- }
-
- // String set returned by shared prefs cannot be modified so we copy.
- final Set<String> keysToSave = new HashSet<>(keysFromPrefs);
- keysToSave.add(key);
- prefs.edit().putStringSet(PREF_SEARCH_KEYSET, keysToSave).apply();
- }
-
- /**
- * Gets and zeroes search counts.
- *
- * We return ExtendedJSONObject for now because that's the format needed by the core telemetry ping.
- */
- public static synchronized ExtendedJSONObject getAndZeroSearch(@NonNull final SharedPreferences prefs) {
- final ExtendedJSONObject out = new ExtendedJSONObject();
- final SharedPreferences.Editor editor = prefs.edit();
-
- final Set<String> keysFromPrefs = prefs.getStringSet(PREF_SEARCH_KEYSET, Collections.<String>emptySet());
- for (final String engineWhereStr : keysFromPrefs) {
- final String key = getEngineSearchCountKey(engineWhereStr);
- out.put(engineWhereStr, prefs.getInt(key, 0));
- editor.remove(key);
- }
- editor.remove(PREF_SEARCH_KEYSET)
- .apply();
- return out;
- }
-
- /**
- * @param engineWhereStr string of the form "engine.where"
- * @return the key for the engines' search counts in shared preferences
- */
- @VisibleForTesting static String getEngineSearchCountKey(final String engineWhereStr) {
- return PREF_SEARCH_PREFIX + engineWhereStr;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SessionMeasurements.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SessionMeasurements.java
deleted file mode 100644
index 6f7d2127a..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SessionMeasurements.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, you can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-package org.mozilla.gecko.telemetry.measurements;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.support.annotation.UiThread;
-import android.support.annotation.VisibleForTesting;
-import org.mozilla.gecko.GeckoSharedPrefs;
-
-import java.util.concurrent.TimeUnit;
-
-/**
- * A class to measure the number of user sessions & their durations. It was created for use with the
- * telemetry core ping. A session is the time between {@link #recordSessionStart()} and
- * {@link #recordSessionEnd(Context)}.
- *
- * This class is thread-safe, provided the thread annotations are followed. Under the hood, this class uses
- * SharedPreferences & because there is no atomic getAndSet operation, we synchronize access to it.
- */
-public class SessionMeasurements {
- @VisibleForTesting static final String PREF_SESSION_COUNT = "measurements-session-count";
- @VisibleForTesting static final String PREF_SESSION_DURATION = "measurements-session-duration";
-
- private boolean sessionStarted = false;
- private long timeAtSessionStartNano = -1;
-
- @UiThread // we assume this will be called on the same thread as session end so we don't have to synchronize sessionStarted.
- public void recordSessionStart() {
- if (sessionStarted) {
- throw new IllegalStateException("Trying to start session but it is already started");
- }
- sessionStarted = true;
- timeAtSessionStartNano = getSystemTimeNano();
- }
-
- @UiThread // we assume this will be called on the same thread as session start so we don't have to synchronize sessionStarted.
- public void recordSessionEnd(final Context context) {
- if (!sessionStarted) {
- throw new IllegalStateException("Expected session to be started before session end is called");
- }
- sessionStarted = false;
-
- final long sessionElapsedSeconds = TimeUnit.NANOSECONDS.toSeconds(getSystemTimeNano() - timeAtSessionStartNano);
- final SharedPreferences sharedPrefs = getSharedPreferences(context);
- synchronized (this) {
- final int sessionCount = sharedPrefs.getInt(PREF_SESSION_COUNT, 0);
- final long totalElapsedSeconds = sharedPrefs.getLong(PREF_SESSION_DURATION, 0);
- sharedPrefs.edit()
- .putInt(PREF_SESSION_COUNT, sessionCount + 1)
- .putLong(PREF_SESSION_DURATION, totalElapsedSeconds + sessionElapsedSeconds)
- .apply();
- }
- }
-
- /**
- * Gets the session measurements since the last time the measurements were last retrieved.
- */
- public synchronized SessionMeasurementsContainer getAndResetSessionMeasurements(final Context context) {
- final SharedPreferences sharedPrefs = getSharedPreferences(context);
- final int sessionCount = sharedPrefs.getInt(PREF_SESSION_COUNT, 0);
- final long totalElapsedSeconds = sharedPrefs.getLong(PREF_SESSION_DURATION, 0);
- sharedPrefs.edit()
- .putInt(PREF_SESSION_COUNT, 0)
- .putLong(PREF_SESSION_DURATION, 0)
- .apply();
- return new SessionMeasurementsContainer(sessionCount, totalElapsedSeconds);
- }
-
- @VisibleForTesting SharedPreferences getSharedPreferences(final Context context) {
- return GeckoSharedPrefs.forProfile(context);
- }
-
- /**
- * Returns (roughly) the system uptime in nanoseconds. A less coupled implementation would
- * take this value from the caller of recordSession*, however, we do this internally to ensure
- * the caller uses both a time system consistent between the start & end calls and uses the
- * appropriate time system (i.e. not wall time, which can change when the clock is changed).
- */
- @VisibleForTesting long getSystemTimeNano() { // TODO: necessary?
- return System.nanoTime();
- }
-
- public static final class SessionMeasurementsContainer {
- /** The number of sessions. */
- public final int sessionCount;
- /** The number of seconds elapsed in ALL sessions included in {@link #sessionCount}. */
- public final long elapsedSeconds;
-
- private SessionMeasurementsContainer(final int sessionCount, final long elapsedSeconds) {
- this.sessionCount = sessionCount;
- this.elapsedSeconds = elapsedSeconds;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryCorePingBuilder.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryCorePingBuilder.java
deleted file mode 100644
index 3f5480f37..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryCorePingBuilder.java
+++ /dev/null
@@ -1,247 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-package org.mozilla.gecko.telemetry.pingbuilders;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.os.Build;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.WorkerThread;
-import android.text.TextUtils;
-
-import android.util.Log;
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.Locales;
-import org.mozilla.gecko.search.SearchEngine;
-import org.mozilla.gecko.sync.ExtendedJSONObject;
-import org.mozilla.gecko.telemetry.TelemetryPing;
-import org.mozilla.gecko.util.DateUtil;
-import org.mozilla.gecko.Experiments;
-import org.mozilla.gecko.util.StringUtils;
-
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Locale;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Builds a {@link TelemetryPing} representing a core ping.
- *
- * See https://gecko.readthedocs.org/en/latest/toolkit/components/telemetry/telemetry/core-ping.html
- * for details on the core ping.
- */
-public class TelemetryCorePingBuilder extends TelemetryPingBuilder {
- private static final String LOGTAG = StringUtils.safeSubstring(TelemetryCorePingBuilder.class.getSimpleName(), 0, 23);
-
- // For legacy reasons, this preference key is not namespaced with "core".
- private static final String PREF_SEQ_COUNT = "telemetry-seqCount";
-
- private static final String NAME = "core";
- private static final int VERSION_VALUE = 7; // For version history, see toolkit/components/telemetry/docs/core-ping.rst
- private static final String OS_VALUE = "Android";
-
- private static final String ARCHITECTURE = "arch";
- private static final String CAMPAIGN_ID = "campaignId";
- private static final String CLIENT_ID = "clientId";
- private static final String DEFAULT_SEARCH_ENGINE = "defaultSearch";
- private static final String DEVICE = "device";
- private static final String DISTRIBUTION_ID = "distributionId";
- private static final String EXPERIMENTS = "experiments";
- private static final String LOCALE = "locale";
- private static final String OS_ATTR = "os";
- private static final String OS_VERSION = "osversion";
- private static final String PING_CREATION_DATE = "created";
- private static final String PROFILE_CREATION_DATE = "profileDate";
- private static final String SEARCH_COUNTS = "searches";
- private static final String SEQ = "seq";
- private static final String SESSION_COUNT = "sessions";
- private static final String SESSION_DURATION = "durations";
- private static final String TIMEZONE_OFFSET = "tz";
- private static final String VERSION_ATTR = "v";
-
- public TelemetryCorePingBuilder(final Context context) {
- initPayloadConstants(context);
- }
-
- private void initPayloadConstants(final Context context) {
- payload.put(VERSION_ATTR, VERSION_VALUE);
- payload.put(OS_ATTR, OS_VALUE);
-
- // We limit the device descriptor to 32 characters because it can get long. We give fewer characters to the
- // manufacturer because we're less likely to have manufacturers with similar names than we are for a
- // manufacturer to have two devices with the similar names (e.g. Galaxy S6 vs. Galaxy Note 6).
- final String deviceDescriptor =
- StringUtils.safeSubstring(Build.MANUFACTURER, 0, 12) + '-' + StringUtils.safeSubstring(Build.MODEL, 0, 19);
-
- final Calendar nowCalendar = Calendar.getInstance();
- final DateFormat pingCreationDateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
-
- payload.put(ARCHITECTURE, AppConstants.ANDROID_CPU_ARCH);
- payload.put(DEVICE, deviceDescriptor);
- payload.put(LOCALE, Locales.getLanguageTag(Locale.getDefault()));
- payload.put(OS_VERSION, Integer.toString(Build.VERSION.SDK_INT)); // A String for cross-platform reasons.
- payload.put(PING_CREATION_DATE, pingCreationDateFormat.format(nowCalendar.getTime()));
- payload.put(TIMEZONE_OFFSET, DateUtil.getTimezoneOffsetInMinutesForGivenDate(nowCalendar));
- payload.putArray(EXPERIMENTS, Experiments.getActiveExperiments(context));
- }
-
- @Override
- public String getDocType() {
- return NAME;
- }
-
- @Override
- public String[] getMandatoryFields() {
- return new String[] {
- ARCHITECTURE,
- CLIENT_ID,
- DEFAULT_SEARCH_ENGINE,
- DEVICE,
- LOCALE,
- OS_ATTR,
- OS_VERSION,
- PING_CREATION_DATE,
- PROFILE_CREATION_DATE,
- SEQ,
- TIMEZONE_OFFSET,
- VERSION_ATTR,
- };
- }
-
- public TelemetryCorePingBuilder setClientID(@NonNull final String clientID) {
- if (clientID == null) {
- throw new IllegalArgumentException("Expected non-null clientID");
- }
- payload.put(CLIENT_ID, clientID);
- return this;
- }
-
- /**
- * @param engine the default search engine identifier, or null if there is an error.
- */
- public TelemetryCorePingBuilder setDefaultSearchEngine(@Nullable final String engine) {
- if (engine != null && engine.isEmpty()) {
- throw new IllegalArgumentException("Received empty string. Expected identifier or null.");
- }
- payload.put(DEFAULT_SEARCH_ENGINE, engine);
- return this;
- }
-
- public TelemetryCorePingBuilder setOptDistributionID(@NonNull final String distributionID) {
- if (distributionID == null) {
- throw new IllegalArgumentException("Expected non-null distribution ID");
- }
- payload.put(DISTRIBUTION_ID, distributionID);
- return this;
- }
-
- /**
- * @param searchCounts non-empty JSON with {"engine.where": <int-count>}
- */
- public TelemetryCorePingBuilder setOptSearchCounts(@NonNull final ExtendedJSONObject searchCounts) {
- if (searchCounts == null) {
- throw new IllegalStateException("Expected non-null search counts");
- } else if (searchCounts.size() == 0) {
- throw new IllegalStateException("Expected non-empty search counts");
- }
-
- payload.put(SEARCH_COUNTS, searchCounts);
- return this;
- }
-
- public TelemetryCorePingBuilder setOptCampaignId(final String campaignId) {
- if (campaignId == null) {
- throw new IllegalStateException("Expected non-null campaign ID.");
- }
- payload.put(CAMPAIGN_ID, campaignId);
- return this;
- }
-
- /**
- * @param date The profile creation date in days to the unix epoch (not millis!), or null if there is an error.
- */
- public TelemetryCorePingBuilder setProfileCreationDate(@Nullable final Long date) {
- if (date != null && date < 0) {
- throw new IllegalArgumentException("Expect positive date value. Received: " + date);
- }
- payload.put(PROFILE_CREATION_DATE, date);
- return this;
- }
-
- /**
- * @param seq a positive sequence number.
- */
- public TelemetryCorePingBuilder setSequenceNumber(final int seq) {
- if (seq < 0) {
- // Since this is an increasing value, it's possible we can overflow into negative values and get into a
- // crash loop so we don't crash on invalid arg - we can investigate if we see negative values on the server.
- Log.w(LOGTAG, "Expected positive sequence number. Received: " + seq);
- }
- payload.put(SEQ, seq);
- return this;
- }
-
- public TelemetryCorePingBuilder setSessionCount(final int sessionCount) {
- if (sessionCount < 0) {
- // Since this is an increasing value, it's possible we can overflow into negative values and get into a
- // crash loop so we don't crash on invalid arg - we can investigate if we see negative values on the server.
- Log.w(LOGTAG, "Expected positive session count. Received: " + sessionCount);
- }
- payload.put(SESSION_COUNT, sessionCount);
- return this;
- }
-
- public TelemetryCorePingBuilder setSessionDuration(final long sessionDuration) {
- if (sessionDuration < 0) {
- // Since this is an increasing value, it's possible we can overflow into negative values and get into a
- // crash loop so we don't crash on invalid arg - we can investigate if we see negative values on the server.
- Log.w(LOGTAG, "Expected positive session duration. Received: " + sessionDuration);
- }
- payload.put(SESSION_DURATION, sessionDuration);
- return this;
- }
-
- /**
- * Gets the sequence number from shared preferences and increments it in the prefs. This method
- * is not thread safe.
- */
- @WorkerThread // synchronous shared prefs write.
- public static int getAndIncrementSequenceNumber(final SharedPreferences sharedPrefsForProfile) {
- final int seq = sharedPrefsForProfile.getInt(PREF_SEQ_COUNT, 1);
-
- sharedPrefsForProfile.edit().putInt(PREF_SEQ_COUNT, seq + 1).apply();
- return seq;
- }
-
- /**
- * @return the profile creation date in the format expected by
- * {@link TelemetryCorePingBuilder#setProfileCreationDate(Long)}.
- */
- @WorkerThread
- public static Long getProfileCreationDate(final Context context, final GeckoProfile profile) {
- final long profileMillis = profile.getAndPersistProfileCreationDate(context);
- if (profileMillis < 0) {
- return null;
- }
- return (long) Math.floor((double) profileMillis / TimeUnit.DAYS.toMillis(1));
- }
-
- /**
- * @return the search engine identifier in the format expected by the core ping.
- */
- @Nullable
- public static String getEngineIdentifier(@Nullable final SearchEngine searchEngine) {
- if (searchEngine == null) {
- return null;
- }
- final String identifier = searchEngine.getIdentifier();
- return TextUtils.isEmpty(identifier) ? null : identifier;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryPingBuilder.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryPingBuilder.java
deleted file mode 100644
index 57fa0fd8b..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryPingBuilder.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-package org.mozilla.gecko.telemetry.pingbuilders;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.sync.ExtendedJSONObject;
-import org.mozilla.gecko.telemetry.TelemetryPing;
-
-import java.util.Set;
-import java.util.UUID;
-
-/**
- * A generic Builder for {@link TelemetryPing} instances. Each overriding class is
- * expected to create a specific type of ping (e.g. "core").
- *
- * This base class handles the common ping operations under the hood:
- * * Validating mandatory fields
- * * Forming the server url
- */
-abstract class TelemetryPingBuilder {
- // In the server url, the initial path directly after the "scheme://host:port/"
- private static final String SERVER_INITIAL_PATH = "submit/telemetry";
-
- private final String serverPath;
- protected final ExtendedJSONObject payload;
- private final String docID;
-
- public TelemetryPingBuilder() {
- docID = UUID.randomUUID().toString();
- serverPath = getTelemetryServerPath(getDocType(), docID);
- payload = new ExtendedJSONObject();
- }
-
- /**
- * @return the name of the ping (e.g. "core")
- */
- public abstract String getDocType();
-
- /**
- * @return the fields that are mandatory for the resultant ping to be uploaded to
- * the server. These will be validated before the ping is built.
- */
- public abstract String[] getMandatoryFields();
-
- public TelemetryPing build() {
- validatePayload();
- return new TelemetryPing(serverPath, payload, docID);
- }
-
- private void validatePayload() {
- final Set<String> keySet = payload.keySet();
- for (final String mandatoryField : getMandatoryFields()) {
- if (!keySet.contains(mandatoryField)) {
- throw new IllegalArgumentException("Builder does not contain mandatory field: " +
- mandatoryField);
- }
- }
- }
-
- /**
- * Returns a url of the format:
- * http://hostname/submit/telemetry/docId/docType/appName/appVersion/appUpdateChannel/appBuildID
- *
- * @param docType The name of the ping (e.g. "main")
- * @return a url at which to POST the telemetry data to
- */
- private static String getTelemetryServerPath(final String docType, final String docID) {
- final String appName = AppConstants.MOZ_APP_BASENAME;
- final String appVersion = AppConstants.MOZ_APP_VERSION;
- final String appUpdateChannel = AppConstants.MOZ_UPDATE_CHANNEL;
- final String appBuildId = AppConstants.MOZ_APP_BUILDID;
-
- // The compiler will optimize a single String concatenation into a StringBuilder statement.
- // If you change this `return`, be sure to keep it as a single statement to keep it optimized!
- return SERVER_INITIAL_PATH + '/' +
- docID + '/' +
- docType + '/' +
- appName + '/' +
- appVersion + '/' +
- appUpdateChannel + '/' +
- appBuildId;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadAllPingsImmediatelyScheduler.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadAllPingsImmediatelyScheduler.java
deleted file mode 100644
index 047a646c3..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadAllPingsImmediatelyScheduler.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, you can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-package org.mozilla.gecko.telemetry.schedulers;
-
-import android.content.Context;
-import android.content.Intent;
-import org.mozilla.gecko.telemetry.stores.TelemetryPingStore;
-import org.mozilla.gecko.telemetry.TelemetryUploadService;
-
-/**
- * Schedules an upload with all pings to be sent immediately.
- */
-public class TelemetryUploadAllPingsImmediatelyScheduler implements TelemetryUploadScheduler {
-
- @Override
- public boolean isReadyToUpload(final TelemetryPingStore store) {
- // We're ready since we don't have any conditions to wait on (e.g. on wifi, accumulated X pings).
- return true;
- }
-
- @Override
- public void scheduleUpload(final Context applicationContext, final TelemetryPingStore store) {
- final Intent i = new Intent(TelemetryUploadService.ACTION_UPLOAD);
- i.setClass(applicationContext, TelemetryUploadService.class);
- i.putExtra(TelemetryUploadService.EXTRA_STORE, store);
- applicationContext.startService(i);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadScheduler.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadScheduler.java
deleted file mode 100644
index 63305aad5..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadScheduler.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, you can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-package org.mozilla.gecko.telemetry.schedulers;
-
-import android.content.Context;
-import org.mozilla.gecko.telemetry.stores.TelemetryPingStore;
-
-/**
- * An implementation of this class can investigate the given {@link TelemetryPingStore} to
- * decide if it's ready to upload the pings inside that Store (e.g. on wifi? have we
- * accumulated X pings?) and can schedule that upload. Typically, the upload will be
- * scheduled by sending an {@link android.content.Intent} to the
- * {@link org.mozilla.gecko.telemetry.TelemetryUploadService}, either immediately or
- * via an external scheduler (e.g. {@link android.app.job.JobScheduler}).
- *
- * N.B.: If the Store is not ready to upload, an implementation *should not* try to reschedule
- * the check to see if it's time to upload - this is expected to be handled by the caller.
- */
-public interface TelemetryUploadScheduler {
- boolean isReadyToUpload(TelemetryPingStore store);
- void scheduleUpload(Context applicationContext, TelemetryPingStore store);
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryJSONFilePingStore.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryJSONFilePingStore.java
deleted file mode 100644
index d52382146..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryJSONFilePingStore.java
+++ /dev/null
@@ -1,301 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, you can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-package org.mozilla.gecko.telemetry.stores;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.VisibleForTesting;
-import android.support.annotation.WorkerThread;
-import android.util.Log;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.sync.ExtendedJSONObject;
-import org.mozilla.gecko.sync.NonObjectJSONException;
-import org.mozilla.gecko.telemetry.TelemetryPing;
-import org.mozilla.gecko.util.FileUtils;
-import org.mozilla.gecko.util.FileUtils.FileLastModifiedComparator;
-import org.mozilla.gecko.util.FileUtils.FilenameRegexFilter;
-import org.mozilla.gecko.util.FileUtils.FilenameWhitelistFilter;
-import org.mozilla.gecko.util.StringUtils;
-import org.mozilla.gecko.util.UUIDUtil;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.FilenameFilter;
-import java.io.IOException;
-import java.nio.channels.FileLock;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
-
-/**
- * An implementation of TelemetryPingStore that is backed by JSON files.
- *
- * This implementation seeks simplicity. Each ping to upload is stored in its own file with its doc ID
- * as the filename. The doc ID is sent with a ping to be uploaded and is expected to be returned with
- * {@link #onUploadAttemptComplete(Set)} so the associated file can be removed.
- *
- * During prune, the pings with the oldest modified time will be removed first. Different filesystems will
- * handle clock skew (e.g. manual time changes, daylight savings time, changing timezones) in different ways
- * and we accept that these modified times may not be consistent - newer data is not more important than
- * older data and the choice to delete the oldest data first is largely arbitrary so we don't care if
- * the timestamps are occasionally inconsistent.
- *
- * Using separate files for this store allows for less restrictive concurrency:
- * * requires locking: {@link #storePing(TelemetryPing)} writes a new file
- * * requires locking: {@link #getAllPings()} reads all files, including those potentially being written,
- * hence locking
- * * no locking: {@link #maybePrunePings()} deletes the least recently written pings, none of which should
- * be currently written
- * * no locking: {@link #onUploadAttemptComplete(Set)} deletes the given pings, none of which should be
- * currently written
- */
-public class TelemetryJSONFilePingStore extends TelemetryPingStore {
- private static final String LOGTAG = StringUtils.safeSubstring(
- "Gecko" + TelemetryJSONFilePingStore.class.getSimpleName(), 0, 23);
-
- @VisibleForTesting static final int MAX_PING_COUNT = 40; // TODO: value.
-
- // We keep the key names short to reduce storage size impact.
- @VisibleForTesting static final String KEY_PAYLOAD = "p";
- @VisibleForTesting static final String KEY_URL_PATH = "u";
-
- private final File storeDir;
- private final FilenameFilter uuidFilenameFilter;
- private final FileLastModifiedComparator fileLastModifiedComparator = new FileLastModifiedComparator();
-
- @WorkerThread // Writes to disk
- public TelemetryJSONFilePingStore(final File storeDir, final String profileName) {
- super(profileName);
- if (storeDir.exists() && !storeDir.isDirectory()) {
- // An alternative is to create a new directory, but we wouldn't
- // be able to access it later so it's better to throw.
- throw new IllegalStateException("Store dir unexpectedly exists & is not a directory - cannot continue");
- }
-
- this.storeDir = storeDir;
- this.storeDir.mkdirs();
- uuidFilenameFilter = new FilenameRegexFilter(UUIDUtil.UUID_PATTERN);
-
- if (!this.storeDir.canRead() || !this.storeDir.canWrite() || !this.storeDir.canExecute()) {
- throw new IllegalStateException("Cannot read, write, or execute store dir: " +
- this.storeDir.canRead() + " " + this.storeDir.canWrite() + " " + this.storeDir.canExecute());
- }
- }
-
- @VisibleForTesting File getPingFile(final String docID) {
- return new File(storeDir, docID);
- }
-
- @Override
- public void storePing(final TelemetryPing ping) throws IOException {
- final String output;
- try {
- output = new JSONObject()
- .put(KEY_PAYLOAD, ping.getPayload())
- .put(KEY_URL_PATH, ping.getURLPath())
- .toString();
- } catch (final JSONException e) {
- // Do not log the exception to avoid leaking personal data.
- throw new IOException("Unable to create JSON to store to disk");
- }
-
- final FileOutputStream outputStream = new FileOutputStream(getPingFile(ping.getDocID()), false);
- blockForLockAndWriteFileAndCloseStream(outputStream, output);
- }
-
- @Override
- public void maybePrunePings() {
- final File[] files = storeDir.listFiles(uuidFilenameFilter);
- if (files == null) {
- return;
- }
-
- if (files.length < MAX_PING_COUNT) {
- return;
- }
-
- // It's possible that multiple files will have the same timestamp: in this case they are treated
- // as equal by the fileLastModifiedComparator. We therefore have to use a sorted list (as
- // opposed to a set, or map).
- final ArrayList<File> sortedFiles = new ArrayList<>(Arrays.asList(files));
- Collections.sort(sortedFiles, fileLastModifiedComparator);
- deleteSmallestFiles(sortedFiles, files.length - MAX_PING_COUNT);
- }
-
- private void deleteSmallestFiles(final ArrayList<File> files, final int numFilesToRemove) {
- final Iterator<File> it = files.iterator();
- int i = 0;
-
- while (i < numFilesToRemove) {
- i += 1;
-
- // Sorted list so we're iterating over ascending files.
- final File file = it.next(); // file count > files to remove so this should not throw.
- file.delete();
- }
- }
-
- @Override
- public ArrayList<TelemetryPing> getAllPings() {
- final File[] fileArray = storeDir.listFiles(uuidFilenameFilter);
- if (fileArray == null) {
- // Intentionally don't log all info for the store directory to prevent leaking the path.
- Log.w(LOGTAG, "listFiles unexpectedly returned null - unable to retrieve pings. Debug: exists? " +
- storeDir.exists() + "; directory? " + storeDir.isDirectory());
- return new ArrayList<>(1);
- }
-
- final List<File> files = Arrays.asList(fileArray);
- Collections.sort(files, fileLastModifiedComparator); // oldest to newest
- final ArrayList<TelemetryPing> out = new ArrayList<>(files.size());
- for (final File file : files) {
- final JSONObject obj = lockAndReadJSONFromFile(file);
- if (obj == null) {
- // We log in the method to get the JSONObject if we return null.
- continue;
- }
-
- try {
- final String url = obj.getString(KEY_URL_PATH);
- final ExtendedJSONObject payload = new ExtendedJSONObject(obj.getString(KEY_PAYLOAD));
- out.add(new TelemetryPing(url, payload, file.getName()));
- } catch (final IOException | JSONException | NonObjectJSONException e) {
- Log.w(LOGTAG, "Bad json in ping. Ignoring.");
- continue;
- }
- }
- return out;
- }
-
- /**
- * Logs if there is an error.
- *
- * @return the JSON object from the given file or null if there is an error.
- */
- private JSONObject lockAndReadJSONFromFile(final File file) {
- // lockAndReadFileAndCloseStream doesn't handle file size of 0.
- if (file.length() == 0) {
- Log.w(LOGTAG, "Unexpected empty file: " + file.getName() + ". Ignoring");
- return null;
- }
-
- final FileInputStream inputStream;
- try {
- inputStream = new FileInputStream(file);
- } catch (final FileNotFoundException e) {
- // permission problem might also cause same exception. To get more debug information.
- String fileInfo = String.format("existence: %b, can write: %b, size: %d.",
- file.exists(), file.canWrite(), file.length());
- String msg = String.format(
- "Expected file to exist but got exception in thread: %s. File info - %s",
- Thread.currentThread().getName(), fileInfo);
- throw new IllegalStateException(msg);
- }
-
- final JSONObject obj;
- try {
- // Potential optimization: re-use the same buffer for reading from files.
- obj = lockAndReadFileAndCloseStream(inputStream, (int) file.length());
- } catch (final IOException | JSONException e) {
- // We couldn't read this file so let's just skip it. These potentially
- // corrupted files should be removed when the data is pruned.
- Log.w(LOGTAG, "Error when reading file: " + file.getName() + " Likely corrupted. Ignoring");
- return null;
- }
-
- if (obj == null) {
- Log.d(LOGTAG, "Could not read given file: " + file.getName() + " File is locked. Ignoring");
- }
- return obj;
- }
-
- @Override
- public void onUploadAttemptComplete(final Set<String> successfulRemoveIDs) {
- if (successfulRemoveIDs.isEmpty()) {
- return;
- }
-
- final File[] files = storeDir.listFiles(new FilenameWhitelistFilter(successfulRemoveIDs));
- for (final File file : files) {
- file.delete();
- }
- }
-
- /**
- * Locks the given {@link FileOutputStream} and writes the given String. This method will close the given stream.
- *
- * Note: this method blocks until a file lock can be acquired.
- */
- private static void blockForLockAndWriteFileAndCloseStream(final FileOutputStream outputStream, final String str)
- throws IOException {
- try {
- final FileLock lock = outputStream.getChannel().lock(0, Long.MAX_VALUE, false);
- if (lock != null) {
- // The file lock is released when the stream is closed. If we try to redundantly close it, we get
- // a ClosedChannelException. To be safe, we could catch that every time but there is a performance
- // hit to exception handling so instead we assume the file lock will be closed.
- FileUtils.writeStringToOutputStreamAndCloseStream(outputStream, str);
- }
- } finally {
- outputStream.close(); // redundant: closed when the stream is closed, but let's be safe.
- }
- }
-
- /**
- * Locks the given {@link FileInputStream} and reads the data. This method will close the given stream.
- *
- * Note: this method returns null when a lock could not be acquired.
- */
- private static JSONObject lockAndReadFileAndCloseStream(final FileInputStream inputStream, final int fileSize)
- throws IOException, JSONException {
- try {
- final FileLock lock = inputStream.getChannel().tryLock(0, Long.MAX_VALUE, true); // null when lock not acquired
- if (lock == null) {
- return null;
- }
- // The file lock is released when the stream is closed. If we try to redundantly close it, we get
- // a ClosedChannelException. To be safe, we could catch that every time but there is a performance
- // hit to exception handling so instead we assume the file lock will be closed.
- return new JSONObject(FileUtils.readStringFromInputStreamAndCloseStream(inputStream, fileSize));
- } finally {
- inputStream.close(); // redundant: closed when the stream is closed, but let's be safe.
- }
- }
-
- public static final Parcelable.Creator<TelemetryJSONFilePingStore> CREATOR = new Parcelable.Creator<TelemetryJSONFilePingStore>() {
- @Override
- public TelemetryJSONFilePingStore createFromParcel(final Parcel source) {
- final String storeDirPath = source.readString();
- final String profileName = source.readString();
- return new TelemetryJSONFilePingStore(new File(storeDirPath), profileName);
- }
-
- @Override
- public TelemetryJSONFilePingStore[] newArray(final int size) {
- return new TelemetryJSONFilePingStore[size];
- }
- };
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(final Parcel dest, final int flags) {
- dest.writeString(storeDir.getAbsolutePath());
- dest.writeString(getProfileName());
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryPingStore.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryPingStore.java
deleted file mode 100644
index 7d781cf26..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryPingStore.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, you can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-package org.mozilla.gecko.telemetry.stores;
-
-import android.os.Parcelable;
-import org.mozilla.gecko.telemetry.TelemetryPing;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.Set;
-
-/**
- * Persistent storage for TelemetryPings that are queued for upload.
- *
- * An implementation of this class is expected to be thread-safe. Additionally,
- * multiple instances can be created and run simultaneously so they must be able
- * to synchronize state (or be stateless!).
- *
- * The pings in {@link #getAllPings()} and {@link #maybePrunePings()} are returned in the
- * same order in order to guarantee consistent results.
- */
-public abstract class TelemetryPingStore implements Parcelable {
- private final String profileName;
-
- public TelemetryPingStore(final String profileName) {
- this.profileName = profileName;
- }
-
- /**
- * @return the profile name associated with this store.
- */
- public String getProfileName() {
- return profileName;
- }
-
- /**
- * @return a list of all the telemetry pings in the store that are ready for upload, ascending oldest to newest.
- */
- public abstract List<TelemetryPing> getAllPings();
-
- /**
- * Save a ping to the store.
- *
- * @param ping the ping to store
- * @throws IOException for underlying store access errors
- */
- public abstract void storePing(TelemetryPing ping) throws IOException;
-
- /**
- * Removes telemetry pings from the store if there are too many pings or they take up too much space.
- * Pings should be removed from oldest to newest.
- */
- public abstract void maybePrunePings();
-
- /**
- * Removes the successfully uploaded pings from the database and performs another other actions necessary
- * for when upload is completed.
- *
- * @param successfulRemoveIDs doc ids of pings that were successfully uploaded
- */
- public abstract void onUploadAttemptComplete(Set<String> successfulRemoveIDs);
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/text/FloatingActionModeCallback.java b/mobile/android/base/java/org/mozilla/gecko/text/FloatingActionModeCallback.java
deleted file mode 100644
index 07f17590d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/text/FloatingActionModeCallback.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.text;
-
-import android.annotation.TargetApi;
-import android.graphics.Rect;
-import android.os.Build;
-import android.view.ActionMode;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-
-import org.mozilla.gecko.GeckoAppShell;
-
-import java.util.List;
-
-@TargetApi(Build.VERSION_CODES.M)
-public class FloatingActionModeCallback extends ActionMode.Callback2 {
- private FloatingToolbarTextSelection textSelection;
- private List<TextAction> actions;
-
- public FloatingActionModeCallback(FloatingToolbarTextSelection textSelection, List<TextAction> actions) {
- this.textSelection = textSelection;
- this.actions = actions;
- }
-
- public void updateActions(List<TextAction> actions) {
- this.actions = actions;
- }
-
- @Override
- public boolean onCreateActionMode(ActionMode mode, Menu menu) {
- return true;
- }
-
- @Override
- public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
- menu.clear();
-
- for (int i = 0; i < actions.size(); i++) {
- final TextAction action = actions.get(i);
- menu.add(Menu.NONE, i, action.getFloatingOrder(), action.getLabel());
- }
-
- return true;
- }
-
- @Override
- public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
- final TextAction action = actions.get(item.getItemId());
-
- GeckoAppShell.notifyObservers("TextSelection:Action", action.getId());
-
- return true;
- }
-
- @Override
- public void onDestroyActionMode(ActionMode mode) {}
-
- @Override
- public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
- final Rect contentRect = textSelection.contentRect;
- if (contentRect != null) {
- outRect.set(contentRect);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/text/FloatingToolbarTextSelection.java b/mobile/android/base/java/org/mozilla/gecko/text/FloatingToolbarTextSelection.java
deleted file mode 100644
index 7a09624d4..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/text/FloatingToolbarTextSelection.java
+++ /dev/null
@@ -1,206 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.text;
-
-import android.annotation.TargetApi;
-import android.app.Activity;
-import android.graphics.Rect;
-import android.os.Build;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.ActionMode;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.gfx.LayerView;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import java.util.List;
-
-import ch.boye.httpclientandroidlib.util.TextUtils;
-
-/**
- * Floating toolbar for text selection actions. Only on Android 6+.
- */
-@TargetApi(Build.VERSION_CODES.M)
-public class FloatingToolbarTextSelection implements TextSelection, GeckoEventListener {
- private static final String LOGTAG = "GeckoFloatTextSelection";
-
- // This is an additional offset we add to the height of the selection. This will avoid that the
- // floating toolbar overlays the bottom handle(s).
- private static final int HANDLES_OFFSET_DP = 20;
-
- private final Activity activity;
- private final LayerView layerView;
- private final int[] locationInWindow;
- private final float handlesOffset;
-
- private ActionMode actionMode;
- private FloatingActionModeCallback actionModeCallback;
- private String selectionID;
- /* package-private */ Rect contentRect;
-
- public FloatingToolbarTextSelection(Activity activity, LayerView layerView) {
- this.activity = activity;
- this.layerView = layerView;
- this.locationInWindow = new int[2];
-
- this.handlesOffset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
- HANDLES_OFFSET_DP, activity.getResources().getDisplayMetrics());
- }
-
- @Override
- public boolean dismiss() {
- if (finishActionMode()) {
- endTextSelection();
- return true;
- }
-
- return false;
- }
-
- private void endTextSelection() {
- if (TextUtils.isEmpty(selectionID)) {
- return;
- }
-
- final JSONObject args = new JSONObject();
- try {
- args.put("selectionID", selectionID);
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error building JSON arguments for TextSelection:End", e);
- return;
- }
-
- GeckoAppShell.notifyObservers("TextSelection:End", args.toString());
- }
-
- @Override
- public void create() {
- registerForEvents();
- }
-
- @Override
- public void destroy() {
- unregisterFromEvents();
- }
-
- private void registerForEvents() {
- GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
- "TextSelection:ActionbarInit",
- "TextSelection:ActionbarStatus",
- "TextSelection:ActionbarUninit",
- "TextSelection:Update",
- "TextSelection:Visibility");
- }
-
- private void unregisterFromEvents() {
- GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
- "TextSelection:ActionbarInit",
- "TextSelection:ActionbarStatus",
- "TextSelection:ActionbarUninit",
- "TextSelection:Update",
- "TextSelection:Visibility");
- }
-
- @Override
- public void handleMessage(final String event, final JSONObject message) {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- handleOnMainThread(event, message);
- }
- });
- }
-
- private void handleOnMainThread(final String event, final JSONObject message) {
- if ("TextSelection:ActionbarInit".equals(event)) {
- Telemetry.sendUIEvent(TelemetryContract.Event.SHOW,
- TelemetryContract.Method.CONTENT, "text_selection");
-
- selectionID = message.optString("selectionID");
- } else if ("TextSelection:ActionbarStatus".equals(event)) {
- // Ensure async updates from SearchService for example are valid.
- if (selectionID != message.optString("selectionID")) {
- return;
- }
-
- updateRect(message);
-
- if (!isRectVisible()) {
- finishActionMode();
- } else {
- startActionMode(TextAction.fromEventMessage(message));
- }
- } else if ("TextSelection:ActionbarUninit".equals(event)) {
- finishActionMode();
- } else if ("TextSelection:Update".equals(event)) {
- startActionMode(TextAction.fromEventMessage(message));
- } else if ("TextSelection:Visibility".equals(event)) {
- finishActionMode();
- }
- }
-
- private void startActionMode(List<TextAction> actions) {
- if (actionMode != null) {
- actionModeCallback.updateActions(actions);
- actionMode.invalidate();
- return;
- }
-
- actionModeCallback = new FloatingActionModeCallback(this, actions);
- actionMode = activity.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING);
- }
-
- private boolean finishActionMode() {
- if (actionMode != null) {
- actionMode.finish();
- actionMode = null;
- actionModeCallback = null;
- return true;
- }
-
- return false;
- }
-
- /**
- * If the content rect is a point (left == right and top == bottom) then this means that the
- * content rect is not in the currently visible part.
- */
- private boolean isRectVisible() {
- // There's another case of an empty rect where just left == right but not top == bottom.
- // That's the rect for a collapsed selection. While technically this rect isn't visible too
- // we are not interested in this case because we do not want to hide the toolbar.
- return contentRect.left != contentRect.right || contentRect.top != contentRect.bottom;
- }
-
- private void updateRect(JSONObject message) {
- try {
- final double x = message.getDouble("x");
- final double y = (int) message.getDouble("y");
- final double width = (int) message.getDouble("width");
- final double height = (int) message.getDouble("height");
-
- final float zoomFactor = layerView.getZoomFactor();
- layerView.getLocationInWindow(locationInWindow);
-
- contentRect = new Rect(
- (int) (x * zoomFactor + locationInWindow[0]),
- (int) (y * zoomFactor + locationInWindow[1]),
- (int) ((x + width) * zoomFactor + locationInWindow[0]),
- (int) ((y + height) * zoomFactor + locationInWindow[1] +
- (height > 0 ? handlesOffset : 0)));
- } catch (JSONException e) {
- Log.w(LOGTAG, "Could not calculate content rect", e);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/text/TextAction.java b/mobile/android/base/java/org/mozilla/gecko/text/TextAction.java
deleted file mode 100644
index 9fcbce4a4..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/text/TextAction.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.text;
-
-import android.util.Log;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Text selection action like "copy", "paste", ..
- */
-public class TextAction {
- private static final String LOGTAG = "GeckoTextAction";
-
- private String id;
- private String label;
- private int order;
- private int floatingOrder;
-
- private TextAction() {}
-
- public static List<TextAction> fromEventMessage(JSONObject message) {
- final List<TextAction> actions = new ArrayList<>();
-
- try {
- final JSONArray array = message.getJSONArray("actions");
-
- for (int i = 0; i < array.length(); i++) {
- final JSONObject object = array.getJSONObject(i);
-
- final TextAction action = new TextAction();
- action.id = object.getString("id");
- action.label = object.getString("label");
- action.order = object.getInt("order");
- action.floatingOrder = object.optInt("floatingOrder", i);
-
- actions.add(action);
- }
- } catch (JSONException e) {
- Log.w(LOGTAG, "Could not parse text actions", e);
- }
-
- return actions;
- }
-
- public String getId() {
- return id;
- }
-
- public String getLabel() {
- return label;
- }
-
- public int getOrder() {
- return order;
- }
-
- public int getFloatingOrder() {
- return floatingOrder;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/text/TextSelection.java b/mobile/android/base/java/org/mozilla/gecko/text/TextSelection.java
deleted file mode 100644
index 29e8e43f5..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/text/TextSelection.java
+++ /dev/null
@@ -1,13 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.text;
-
-public interface TextSelection {
- void create();
-
- boolean dismiss();
-
- void destroy();
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/AutocompleteHandler.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/AutocompleteHandler.java
deleted file mode 100644
index 4a1559823..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/AutocompleteHandler.java
+++ /dev/null
@@ -1,10 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.toolbar;
-
-public interface AutocompleteHandler {
- void onAutocomplete(String res);
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/BackButton.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/BackButton.java
deleted file mode 100644
index 267c95e09..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/BackButton.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.toolbar;
-
-import android.content.Context;
-import android.graphics.Path;
-import android.util.AttributeSet;
-
-public class BackButton extends NavButton {
- public BackButton(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
- super.onSizeChanged(width, height, oldWidth, oldHeight);
-
- mPath.reset();
- mPath.addCircle(width / 2, height / 2, width / 2, Path.Direction.CW);
-
- mBorderPath.reset();
- mBorderPath.addCircle(width / 2, height / 2, (width / 2) - (mBorderWidth / 2), Path.Direction.CW);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbar.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbar.java
deleted file mode 100644
index b24e3b3ea..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbar.java
+++ /dev/null
@@ -1,960 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.toolbar;
-
-import java.util.ArrayList;
-import java.util.EnumSet;
-import java.util.List;
-
-import android.support.annotation.Nullable;
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.BrowserApp;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.SiteIdentity;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.TouchEventInterceptor;
-import org.mozilla.gecko.animation.PropertyAnimator;
-import org.mozilla.gecko.animation.PropertyAnimator.PropertyAnimationListener;
-import org.mozilla.gecko.animation.ViewHelper;
-import org.mozilla.gecko.lwt.LightweightTheme;
-import org.mozilla.gecko.lwt.LightweightThemeDrawable;
-import org.mozilla.gecko.menu.GeckoMenu;
-import org.mozilla.gecko.menu.MenuPopup;
-import org.mozilla.gecko.tabs.TabHistoryController;
-import org.mozilla.gecko.toolbar.ToolbarDisplayLayout.OnStopListener;
-import org.mozilla.gecko.toolbar.ToolbarDisplayLayout.OnTitleChangeListener;
-import org.mozilla.gecko.toolbar.ToolbarDisplayLayout.UpdateFlags;
-import org.mozilla.gecko.util.Clipboard;
-import org.mozilla.gecko.util.HardwareUtils;
-import org.mozilla.gecko.util.MenuUtils;
-import org.mozilla.gecko.widget.themed.ThemedFrameLayout;
-import org.mozilla.gecko.widget.themed.ThemedImageButton;
-import org.mozilla.gecko.widget.themed.ThemedImageView;
-import org.mozilla.gecko.widget.themed.ThemedRelativeLayout;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.StateListDrawable;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.ContextMenu;
-import android.view.LayoutInflater;
-import android.view.MenuInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewTreeObserver.OnGlobalLayoutListener;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.Button;
-import android.widget.LinearLayout;
-import android.widget.PopupWindow;
-import android.support.annotation.NonNull;
-
-/**
-* {@code BrowserToolbar} is single entry point for users of the toolbar
-* subsystem i.e. this should be the only import outside the 'toolbar'
-* package.
-*
-* {@code BrowserToolbar} serves at the single event bus for all
-* sub-components in the toolbar. It tracks tab events and gecko messages
-* and update the state of its inner components accordingly.
-*
-* It has two states, display and edit, which are controlled by
-* ToolbarEditLayout and ToolbarDisplayLayout. In display state, the toolbar
-* displays the current state for the selected tab. In edit state, it shows
-* a text entry for searching bookmarks/history. {@code BrowserToolbar}
-* provides public API to enter, cancel, and commit the edit state as well
-* as a set of listeners to allow {@code BrowserToolbar} users to react
-* to state changes accordingly.
-*/
-public abstract class BrowserToolbar extends ThemedRelativeLayout
- implements Tabs.OnTabsChangedListener,
- GeckoMenu.ActionItemBarPresenter {
- private static final String LOGTAG = "GeckoToolbar";
-
- private static final int LIGHTWEIGHT_THEME_INVERT_ALPHA = 34; // 255 - alpha = invert_alpha
-
- public interface OnActivateListener {
- public void onActivate();
- }
-
- public interface OnCommitListener {
- public void onCommit();
- }
-
- public interface OnDismissListener {
- public void onDismiss();
- }
-
- public interface OnFilterListener {
- public void onFilter(String searchText, AutocompleteHandler handler);
- }
-
- public interface OnStartEditingListener {
- public void onStartEditing();
- }
-
- public interface OnStopEditingListener {
- public void onStopEditing();
- }
-
- protected enum UIMode {
- EDIT,
- DISPLAY
- }
-
- protected final ToolbarDisplayLayout urlDisplayLayout;
- protected final ToolbarEditLayout urlEditLayout;
- protected final View urlBarEntry;
- protected boolean isSwitchingTabs;
- protected final ThemedImageButton tabsButton;
-
- private ToolbarProgressView progressBar;
- protected final TabCounter tabsCounter;
- protected final ThemedFrameLayout menuButton;
- protected final ThemedImageView menuIcon;
- private MenuPopup menuPopup;
- protected final List<View> focusOrder;
-
- private OnActivateListener activateListener;
- private OnFocusChangeListener focusChangeListener;
- private OnStartEditingListener startEditingListener;
- private OnStopEditingListener stopEditingListener;
- private TouchEventInterceptor mTouchEventInterceptor;
-
- protected final BrowserApp activity;
-
- protected UIMode uiMode;
- protected TabHistoryController tabHistoryController;
-
- private final Paint shadowPaint;
- private final int shadowColor;
- private final int shadowPrivateColor;
- private final int shadowSize;
-
- private final ToolbarPrefs prefs;
-
- public abstract boolean isAnimating();
-
- protected abstract boolean isTabsButtonOffscreen();
-
- protected abstract void updateNavigationButtons(Tab tab);
-
- protected abstract void triggerStartEditingTransition(PropertyAnimator animator);
- protected abstract void triggerStopEditingTransition();
- public abstract void triggerTabsPanelTransition(PropertyAnimator animator, boolean areTabsShown);
-
- /**
- * Returns a Drawable overlaid with the theme's bitmap.
- */
- protected Drawable getLWTDefaultStateSetDrawable() {
- return getTheme().getDrawable(this);
- }
-
- public static BrowserToolbar create(final Context context, final AttributeSet attrs) {
- final boolean isLargeResource = context.getResources().getBoolean(R.bool.is_large_resource);
- final BrowserToolbar toolbar;
- if (isLargeResource) {
- toolbar = new BrowserToolbarTablet(context, attrs);
- } else {
- toolbar = new BrowserToolbarPhone(context, attrs);
- }
- return toolbar;
- }
-
- protected BrowserToolbar(final Context context, final AttributeSet attrs) {
- super(context, attrs);
- setWillNotDraw(false);
-
- // BrowserToolbar is attached to BrowserApp only.
- activity = (BrowserApp) context;
-
- LayoutInflater.from(context).inflate(R.layout.browser_toolbar, this);
-
- Tabs.registerOnTabsChangedListener(this);
- isSwitchingTabs = true;
-
- urlDisplayLayout = (ToolbarDisplayLayout) findViewById(R.id.display_layout);
- urlBarEntry = findViewById(R.id.url_bar_entry);
- urlEditLayout = (ToolbarEditLayout) findViewById(R.id.edit_layout);
-
- tabsButton = (ThemedImageButton) findViewById(R.id.tabs);
- tabsCounter = (TabCounter) findViewById(R.id.tabs_counter);
- tabsCounter.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
-
- menuButton = (ThemedFrameLayout) findViewById(R.id.menu);
- menuIcon = (ThemedImageView) findViewById(R.id.menu_icon);
-
- // The focusOrder List should be filled by sub-classes.
- focusOrder = new ArrayList<View>();
-
- final Resources res = getResources();
- shadowSize = res.getDimensionPixelSize(R.dimen.browser_toolbar_shadow_size);
-
- shadowPaint = new Paint();
- shadowColor = ContextCompat.getColor(context, R.color.url_bar_shadow);
- shadowPrivateColor = ContextCompat.getColor(context, R.color.url_bar_shadow_private);
- shadowPaint.setColor(shadowColor);
- shadowPaint.setStrokeWidth(0.0f);
-
- setUIMode(UIMode.DISPLAY);
-
- prefs = new ToolbarPrefs();
- urlDisplayLayout.setToolbarPrefs(prefs);
- urlEditLayout.setToolbarPrefs(prefs);
-
- setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() {
- @Override
- public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
- // Do not show the context menu while editing
- if (isEditing()) {
- return;
- }
-
- // NOTE: Use MenuUtils.safeSetVisible because some actions might
- // be on the Page menu
- MenuInflater inflater = activity.getMenuInflater();
- inflater.inflate(R.menu.titlebar_contextmenu, menu);
-
- String clipboard = Clipboard.getText();
- if (TextUtils.isEmpty(clipboard)) {
- menu.findItem(R.id.pasteandgo).setVisible(false);
- menu.findItem(R.id.paste).setVisible(false);
- }
-
- Tab tab = Tabs.getInstance().getSelectedTab();
- if (tab != null) {
- String url = tab.getURL();
- if (url == null) {
- menu.findItem(R.id.copyurl).setVisible(false);
- menu.findItem(R.id.add_to_launcher).setVisible(false);
- }
-
- MenuUtils.safeSetVisible(menu, R.id.subscribe, tab.hasFeeds());
- MenuUtils.safeSetVisible(menu, R.id.add_search_engine, tab.hasOpenSearch());
- } else {
- // if there is no tab, remove anything tab dependent
- menu.findItem(R.id.copyurl).setVisible(false);
- menu.findItem(R.id.add_to_launcher).setVisible(false);
- MenuUtils.safeSetVisible(menu, R.id.subscribe, false);
- MenuUtils.safeSetVisible(menu, R.id.add_search_engine, false);
- }
- }
- });
-
- setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- if (activateListener != null) {
- activateListener.onActivate();
- }
- }
- });
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- prefs.open();
-
- urlDisplayLayout.setOnStopListener(new OnStopListener() {
- @Override
- public Tab onStop() {
- final Tab tab = Tabs.getInstance().getSelectedTab();
- if (tab != null) {
- tab.doStop();
- return tab;
- }
-
- return null;
- }
- });
-
- urlDisplayLayout.setOnTitleChangeListener(new OnTitleChangeListener() {
- @Override
- public void onTitleChange(CharSequence title) {
- final String contentDescription;
- if (title != null) {
- contentDescription = title.toString();
- } else {
- contentDescription = activity.getString(R.string.url_bar_default_text);
- }
-
- // The title and content description should
- // always be sync.
- setContentDescription(contentDescription);
- }
- });
-
- urlEditLayout.setOnFocusChangeListener(new View.OnFocusChangeListener() {
- @Override
- public void onFocusChange(View v, boolean hasFocus) {
- // This will select the url bar when entering editing mode.
- setSelected(hasFocus);
- if (focusChangeListener != null) {
- focusChangeListener.onFocusChange(v, hasFocus);
- }
- }
- });
-
- tabsButton.setOnClickListener(new Button.OnClickListener() {
- @Override
- public void onClick(View v) {
- // Clear focus so a back press with the tabs
- // panel open does not go to the editing field.
- urlEditLayout.clearFocus();
-
- toggleTabs();
- }
- });
- tabsButton.setImageLevel(0);
-
- menuButton.setOnClickListener(new Button.OnClickListener() {
- @Override
- public void onClick(View view) {
- // Drop the soft keyboard.
- urlEditLayout.clearFocus();
- activity.openOptionsMenu();
- }
- });
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- prefs.close();
- }
-
- @Override
- public void draw(Canvas canvas) {
- super.draw(canvas);
-
- final int height = getHeight();
- canvas.drawRect(0, height - shadowSize, getWidth(), height, shadowPaint);
- }
-
- public void onParentFocus() {
- urlEditLayout.onParentFocus();
- }
-
- public void setProgressBar(ToolbarProgressView progressBar) {
- this.progressBar = progressBar;
- }
-
- public void setTabHistoryController(TabHistoryController tabHistoryController) {
- this.tabHistoryController = tabHistoryController;
- }
-
- public void refresh() {
- urlDisplayLayout.dismissSiteIdentityPopup();
- }
-
- public boolean onBackPressed() {
- // If we exit editing mode during the animation,
- // we're put into an inconsistent state (bug 1017276).
- if (isEditing() && !isAnimating()) {
- Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL,
- TelemetryContract.Method.BACK);
- cancelEdit();
- return true;
- }
-
- return urlDisplayLayout.dismissSiteIdentityPopup();
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
-
- if (h != oldh) {
- // Post this to happen outside of onSizeChanged, as this may cause
- // a layout change and relayouts within a layout change don't work.
- post(new Runnable() {
- @Override
- public void run() {
- activity.refreshToolbarHeight();
- }
- });
- }
- }
-
- public void saveTabEditingState(final TabEditingState editingState) {
- urlEditLayout.saveTabEditingState(editingState);
- }
-
- public void restoreTabEditingState(final TabEditingState editingState) {
- if (!isEditing()) {
- throw new IllegalStateException("Expected to be editing");
- }
-
- urlEditLayout.restoreTabEditingState(editingState);
- }
-
- @Override
- public void onTabChanged(@Nullable Tab tab, Tabs.TabEvents msg, String data) {
- Log.d(LOGTAG, "onTabChanged: " + msg);
- final Tabs tabs = Tabs.getInstance();
-
- // These conditions are split into three phases:
- // * Always do first
- // * Handling specific to the selected tab
- // * Always do afterwards.
-
- switch (msg) {
- case ADDED:
- case CLOSED:
- updateTabCount(tabs.getDisplayCount());
- break;
- case RESTORED:
- // TabCount fixup after OOM
- case SELECTED:
- urlDisplayLayout.dismissSiteIdentityPopup();
- updateTabCount(tabs.getDisplayCount());
- isSwitchingTabs = true;
- break;
- }
-
- if (tabs.isSelectedTab(tab)) {
- final EnumSet<UpdateFlags> flags = EnumSet.noneOf(UpdateFlags.class);
-
- // Progress-related handling
- switch (msg) {
- case START:
- updateProgressVisibility(tab, Tab.LOAD_PROGRESS_INIT);
- // Fall through.
- case ADDED:
- case LOCATION_CHANGE:
- case LOAD_ERROR:
- case LOADED:
- case STOP:
- flags.add(UpdateFlags.PROGRESS);
- if (progressBar.getVisibility() == View.VISIBLE) {
- progressBar.animateProgress(tab.getLoadProgress());
- }
- break;
-
- case SELECTED:
- flags.add(UpdateFlags.PROGRESS);
- updateProgressVisibility();
- break;
- }
-
- switch (msg) {
- case STOP:
- // Reset the title in case we haven't navigated
- // to a new page yet.
- flags.add(UpdateFlags.TITLE);
- // Fall through.
- case START:
- case CLOSED:
- case ADDED:
- updateNavigationButtons(tab);
- break;
-
- case SELECTED:
- flags.add(UpdateFlags.PRIVATE_MODE);
- setPrivateMode(tab.isPrivate());
- // Fall through.
- case LOAD_ERROR:
- case LOCATION_CHANGE:
- // We're displaying the tab URL in place of the title,
- // so we always need to update our "title" here as well.
- flags.add(UpdateFlags.TITLE);
- flags.add(UpdateFlags.FAVICON);
- flags.add(UpdateFlags.SITE_IDENTITY);
-
- updateNavigationButtons(tab);
- break;
-
- case TITLE:
- flags.add(UpdateFlags.TITLE);
- break;
-
- case FAVICON:
- flags.add(UpdateFlags.FAVICON);
- break;
-
- case SECURITY_CHANGE:
- flags.add(UpdateFlags.SITE_IDENTITY);
- break;
- }
-
- if (!flags.isEmpty() && tab != null) {
- updateDisplayLayout(tab, flags);
- }
- }
-
- switch (msg) {
- case SELECTED:
- case LOAD_ERROR:
- case LOCATION_CHANGE:
- isSwitchingTabs = false;
- }
- }
-
- private void updateProgressVisibility() {
- final Tab selectedTab = Tabs.getInstance().getSelectedTab();
- // The selected tab may be null if GeckoApp (and thus the
- // selected tab) are not yet initialized (bug 1090287).
- if (selectedTab != null) {
- updateProgressVisibility(selectedTab, selectedTab.getLoadProgress());
- }
- }
-
- private void updateProgressVisibility(Tab selectedTab, int progress) {
- if (!isEditing() && selectedTab.getState() == Tab.STATE_LOADING) {
- progressBar.setProgress(progress);
- progressBar.setPrivateMode(selectedTab.isPrivate());
- progressBar.setVisibility(View.VISIBLE);
- } else {
- progressBar.setVisibility(View.GONE);
- }
- }
-
- protected boolean isVisible() {
- return ViewHelper.getTranslationY(this) == 0;
- }
-
- @Override
- public void setNextFocusDownId(int nextId) {
- super.setNextFocusDownId(nextId);
- tabsButton.setNextFocusDownId(nextId);
- urlDisplayLayout.setNextFocusDownId(nextId);
- menuButton.setNextFocusDownId(nextId);
- }
-
- public boolean hideVirtualKeyboard() {
- InputMethodManager imm =
- (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
- return imm.hideSoftInputFromWindow(tabsButton.getWindowToken(), 0);
- }
-
- private void showSelectedTabs() {
- Tab tab = Tabs.getInstance().getSelectedTab();
- if (tab != null) {
- if (!tab.isPrivate())
- activity.showNormalTabs();
- else
- activity.showPrivateTabs();
- }
- }
-
- private void toggleTabs() {
- if (activity.areTabsShown()) {
- return;
- }
-
- if (hideVirtualKeyboard()) {
- getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
- @Override
- public void onGlobalLayout() {
- getViewTreeObserver().removeGlobalOnLayoutListener(this);
- showSelectedTabs();
- }
- });
- } else {
- showSelectedTabs();
- }
- }
-
- protected void updateTabCount(final int count) {
- // If toolbar is in edit mode on a phone, this means the entry is expanded
- // and the tabs button is translated offscreen. Don't trigger tabs counter
- // updates until the tabs button is back on screen.
- // See stopEditing()
- if (isTabsButtonOffscreen()) {
- return;
- }
-
- // Set TabCounter based on visibility
- if (isVisible() && ViewHelper.getAlpha(tabsCounter) != 0 && !isEditing()) {
- tabsCounter.setCountWithAnimation(count);
- } else {
- tabsCounter.setCount(count);
- }
-
- // Update A11y information
- tabsButton.setContentDescription((count > 1) ?
- activity.getString(R.string.num_tabs, count) :
- activity.getString(R.string.one_tab));
- }
-
- private void updateDisplayLayout(@NonNull Tab tab, EnumSet<UpdateFlags> flags) {
- if (isSwitchingTabs) {
- flags.add(UpdateFlags.DISABLE_ANIMATIONS);
- }
-
- urlDisplayLayout.updateFromTab(tab, flags);
-
- if (flags.contains(UpdateFlags.TITLE)) {
- if (!isEditing()) {
- urlEditLayout.setText(tab.getURL());
- }
- }
-
- if (flags.contains(UpdateFlags.PROGRESS)) {
- updateFocusOrder();
- }
- }
-
- private void updateFocusOrder() {
- if (focusOrder.size() == 0) {
- throw new IllegalStateException("Expected focusOrder to be initialized in subclass");
- }
-
- View prevView = null;
-
- // If the element that has focus becomes disabled or invisible, focus
- // is given to the URL bar.
- boolean needsNewFocus = false;
-
- for (View view : focusOrder) {
- if (view.getVisibility() != View.VISIBLE || !view.isEnabled()) {
- if (view.hasFocus()) {
- needsNewFocus = true;
- }
- continue;
- }
-
- if (view.getId() == R.id.menu_items) {
- final LinearLayout actionItemBar = (LinearLayout) view;
- final int childCount = actionItemBar.getChildCount();
- for (int child = 0; child < childCount; child++) {
- View childView = actionItemBar.getChildAt(child);
- if (prevView != null) {
- childView.setNextFocusLeftId(prevView.getId());
- prevView.setNextFocusRightId(childView.getId());
- }
- prevView = childView;
- }
- } else {
- if (prevView != null) {
- view.setNextFocusLeftId(prevView.getId());
- prevView.setNextFocusRightId(view.getId());
- }
- prevView = view;
- }
- }
-
- if (needsNewFocus) {
- requestFocus();
- }
- }
-
- public void setToolBarButtonsAlpha(float alpha) {
- ViewHelper.setAlpha(tabsCounter, alpha);
- if (!HardwareUtils.isTablet()) {
- ViewHelper.setAlpha(menuIcon, alpha);
- }
- }
-
- public void onEditSuggestion(String suggestion) {
- if (!isEditing()) {
- return;
- }
-
- urlEditLayout.onEditSuggestion(suggestion);
- }
-
- public void setTitle(CharSequence title) {
- urlDisplayLayout.setTitle(title);
- }
-
- public void setOnActivateListener(final OnActivateListener listener) {
- activateListener = listener;
- }
-
- public void setOnCommitListener(OnCommitListener listener) {
- urlEditLayout.setOnCommitListener(listener);
- }
-
- public void setOnDismissListener(OnDismissListener listener) {
- urlEditLayout.setOnDismissListener(listener);
- }
-
- public void setOnFilterListener(OnFilterListener listener) {
- urlEditLayout.setOnFilterListener(listener);
- }
-
- @Override
- public void setOnFocusChangeListener(OnFocusChangeListener listener) {
- focusChangeListener = listener;
- }
-
- public void setOnStartEditingListener(OnStartEditingListener listener) {
- startEditingListener = listener;
- }
-
- public void setOnStopEditingListener(OnStopEditingListener listener) {
- stopEditingListener = listener;
- }
-
- protected void showUrlEditLayout() {
- setUrlEditLayoutVisibility(true, null);
- }
-
- protected void showUrlEditLayout(final PropertyAnimator animator) {
- setUrlEditLayoutVisibility(true, animator);
- }
-
- protected void hideUrlEditLayout() {
- setUrlEditLayoutVisibility(false, null);
- }
-
- protected void hideUrlEditLayout(final PropertyAnimator animator) {
- setUrlEditLayoutVisibility(false, animator);
- }
-
- protected void setUrlEditLayoutVisibility(final boolean showEditLayout, PropertyAnimator animator) {
- if (showEditLayout) {
- urlEditLayout.prepareShowAnimation(animator);
- }
-
- // If this view is GONE, we trigger a measure pass when setting the view to
- // VISIBLE. Since this will occur during the toolbar open animation, it causes jank.
- final int hiddenViewVisibility = View.INVISIBLE;
-
- if (animator == null) {
- final View viewToShow = (showEditLayout ? urlEditLayout : urlDisplayLayout);
- final View viewToHide = (showEditLayout ? urlDisplayLayout : urlEditLayout);
-
- viewToHide.setVisibility(hiddenViewVisibility);
- viewToShow.setVisibility(View.VISIBLE);
- return;
- }
-
- animator.addPropertyAnimationListener(new PropertyAnimationListener() {
- @Override
- public void onPropertyAnimationStart() {
- if (!showEditLayout) {
- urlEditLayout.setVisibility(hiddenViewVisibility);
- urlDisplayLayout.setVisibility(View.VISIBLE);
- }
- }
-
- @Override
- public void onPropertyAnimationEnd() {
- if (showEditLayout) {
- urlDisplayLayout.setVisibility(hiddenViewVisibility);
- urlEditLayout.setVisibility(View.VISIBLE);
- }
- }
- });
- }
-
- private void setUIMode(final UIMode uiMode) {
- this.uiMode = uiMode;
- urlEditLayout.setEnabled(uiMode == UIMode.EDIT);
- }
-
- /**
- * Returns whether or not the URL bar is in editing mode (url bar is expanded, hiding the new
- * tab button). Note that selection state is independent of editing mode.
- */
- public boolean isEditing() {
- return (uiMode == UIMode.EDIT);
- }
-
- public void startEditing(String url, PropertyAnimator animator) {
- if (isEditing()) {
- return;
- }
-
- urlEditLayout.setText(url != null ? url : "");
-
- setUIMode(UIMode.EDIT);
-
- updateProgressVisibility();
-
- if (startEditingListener != null) {
- startEditingListener.onStartEditing();
- }
-
- triggerStartEditingTransition(animator);
- }
-
- /**
- * Exits edit mode without updating the toolbar title.
- *
- * @return the url that was entered
- */
- public String cancelEdit() {
- Telemetry.stopUISession(TelemetryContract.Session.AWESOMESCREEN);
- return stopEditing();
- }
-
- /**
- * Exits edit mode, updating the toolbar title with the url that was just entered.
- *
- * @return the url that was entered
- */
- public String commitEdit() {
- Tab tab = Tabs.getInstance().getSelectedTab();
- if (tab != null) {
- tab.resetSiteIdentity();
- }
-
- final String url = stopEditing();
- if (!TextUtils.isEmpty(url)) {
- setTitle(url);
- }
- return url;
- }
-
- private String stopEditing() {
- final String url = urlEditLayout.getText();
- if (!isEditing()) {
- return url;
- }
- setUIMode(UIMode.DISPLAY);
-
- if (stopEditingListener != null) {
- stopEditingListener.onStopEditing();
- }
-
- updateProgressVisibility();
- triggerStopEditingTransition();
-
- return url;
- }
-
- @Override
- public void setPrivateMode(boolean isPrivate) {
- super.setPrivateMode(isPrivate);
-
- tabsButton.setPrivateMode(isPrivate);
- menuButton.setPrivateMode(isPrivate);
- urlEditLayout.setPrivateMode(isPrivate);
-
- shadowPaint.setColor(isPrivate ? shadowPrivateColor : shadowColor);
- }
-
- public void show() {
- setVisibility(View.VISIBLE);
- }
-
- public void hide() {
- setVisibility(View.GONE);
- }
-
- public View getDoorHangerAnchor() {
- return urlDisplayLayout;
- }
-
- public void onDestroy() {
- Tabs.unregisterOnTabsChangedListener(this);
- urlDisplayLayout.destroy();
- }
-
- public boolean openOptionsMenu() {
- // Initialize the popup.
- if (menuPopup == null) {
- View panel = activity.getMenuPanel();
- menuPopup = new MenuPopup(activity);
- menuPopup.setPanelView(panel);
-
- menuPopup.setOnDismissListener(new PopupWindow.OnDismissListener() {
- @Override
- public void onDismiss() {
- activity.onOptionsMenuClosed(null);
- }
- });
- }
-
- GeckoAppShell.getGeckoInterface().invalidateOptionsMenu();
- if (!menuPopup.isShowing()) {
- menuPopup.showAsDropDown(menuButton);
- }
-
- return true;
- }
-
- public boolean closeOptionsMenu() {
- if (menuPopup != null && menuPopup.isShowing()) {
- menuPopup.dismiss();
- }
-
- return true;
- }
-
- @Override
- public void onLightweightThemeChanged() {
- final Drawable drawable = getLWTDefaultStateSetDrawable();
- if (drawable == null) {
- return;
- }
-
- final StateListDrawable stateList = new StateListDrawable();
- stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.tabs_tray_grey_pressed));
- stateList.addState(EMPTY_STATE_SET, drawable);
-
- setBackgroundDrawable(stateList);
- }
-
- public void setTouchEventInterceptor(TouchEventInterceptor interceptor) {
- mTouchEventInterceptor = interceptor;
- }
-
- @Override
- public boolean onInterceptTouchEvent(MotionEvent event) {
- if (mTouchEventInterceptor != null && mTouchEventInterceptor.onInterceptTouchEvent(this, event)) {
- return true;
- }
- return super.onInterceptTouchEvent(event);
- }
-
- @Override
- public void onLightweightThemeReset() {
- setBackgroundResource(R.drawable.url_bar_bg);
- }
-
- public static LightweightThemeDrawable getLightweightThemeDrawable(final View view,
- final LightweightTheme theme, final int colorResID) {
- final int color = ContextCompat.getColor(view.getContext(), colorResID);
-
- final LightweightThemeDrawable drawable = theme.getColorDrawable(view, color);
- if (drawable != null) {
- drawable.setAlpha(LIGHTWEIGHT_THEME_INVERT_ALPHA, LIGHTWEIGHT_THEME_INVERT_ALPHA);
- }
-
- return drawable;
- }
-
- public static class TabEditingState {
- // The edited text from the most recent time this tab was unselected.
- protected String lastEditingText;
- protected int selectionStart;
- protected int selectionEnd;
-
- public boolean isBrowserSearchShown;
-
- public void copyFrom(final TabEditingState s2) {
- lastEditingText = s2.lastEditingText;
- selectionStart = s2.selectionStart;
- selectionEnd = s2.selectionEnd;
-
- isBrowserSearchShown = s2.isBrowserSearchShown;
- }
-
- public boolean isBrowserSearchShown() {
- return isBrowserSearchShown;
- }
-
- public void setIsBrowserSearchShown(final boolean isShown) {
- isBrowserSearchShown = isShown;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarPhone.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarPhone.java
deleted file mode 100644
index a5fc57f1a..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarPhone.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.toolbar;
-
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.animation.PropertyAnimator;
-import org.mozilla.gecko.animation.PropertyAnimator.PropertyAnimationListener;
-import org.mozilla.gecko.util.HardwareUtils;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-
-/**
- * A toolbar implementation for phones.
- */
-class BrowserToolbarPhone extends BrowserToolbarPhoneBase {
-
- private final PropertyAnimationListener showEditingListener;
- private final PropertyAnimationListener stopEditingListener;
-
- protected boolean isAnimatingEntry;
-
- protected BrowserToolbarPhone(final Context context, final AttributeSet attrs) {
- super(context, attrs);
-
- // Create these listeners here, once, to avoid constructing new listeners
- // each time they are set on an animator (i.e. each time the url bar is clicked).
- showEditingListener = new PropertyAnimationListener() {
- @Override
- public void onPropertyAnimationStart() { /* Do nothing */ }
-
- @Override
- public void onPropertyAnimationEnd() {
- isAnimatingEntry = false;
- }
- };
-
- stopEditingListener = new PropertyAnimationListener() {
- @Override
- public void onPropertyAnimationStart() { /* Do nothing */ }
-
- @Override
- public void onPropertyAnimationEnd() {
- urlBarTranslatingEdge.setVisibility(View.INVISIBLE);
-
- final PropertyAnimator buttonsAnimator = new PropertyAnimator(300);
- urlDisplayLayout.prepareStopEditingAnimation(buttonsAnimator);
- buttonsAnimator.start();
-
- isAnimatingEntry = false;
-
- // Trigger animation to update the tabs counter once the
- // tabs button is back on screen.
- updateTabCountAndAnimate(Tabs.getInstance().getDisplayCount());
- }
- };
- }
-
- @Override
- public boolean isAnimating() {
- return isAnimatingEntry;
- }
-
- @Override
- protected void triggerStartEditingTransition(final PropertyAnimator animator) {
- if (isAnimatingEntry) {
- return;
- }
-
- // The animation looks cleaner if the text in the URL bar is
- // not selected so clear the selection by clearing focus.
- urlEditLayout.clearFocus();
-
- urlDisplayLayout.prepareStartEditingAnimation();
- addAnimationsForEditing(animator, true);
- showUrlEditLayout(animator);
- urlBarTranslatingEdge.setVisibility(View.VISIBLE);
- animator.addPropertyAnimationListener(showEditingListener);
-
- isAnimatingEntry = true; // To be correct, this should be called last.
- }
-
- @Override
- protected void triggerStopEditingTransition() {
- final PropertyAnimator animator = new PropertyAnimator(250);
- animator.setUseHardwareLayer(false);
-
- addAnimationsForEditing(animator, false);
- hideUrlEditLayout(animator);
- animator.addPropertyAnimationListener(stopEditingListener);
-
- isAnimatingEntry = true;
- animator.start();
- }
-
- private void addAnimationsForEditing(final PropertyAnimator animator, final boolean isEditing) {
- final int curveTranslation;
- final int entryTranslation;
- if (isEditing) {
- curveTranslation = getUrlBarCurveTranslation();
- entryTranslation = getUrlBarEntryTranslation();
- } else {
- curveTranslation = 0;
- entryTranslation = 0;
- }
-
- // Slide toolbar elements.
- animator.attach(urlBarTranslatingEdge,
- PropertyAnimator.Property.TRANSLATION_X,
- entryTranslation);
- animator.attach(tabsButton,
- PropertyAnimator.Property.TRANSLATION_X,
- curveTranslation);
- animator.attach(tabsCounter,
- PropertyAnimator.Property.TRANSLATION_X,
- curveTranslation);
- animator.attach(menuButton,
- PropertyAnimator.Property.TRANSLATION_X,
- curveTranslation);
- animator.attach(menuIcon,
- PropertyAnimator.Property.TRANSLATION_X,
- curveTranslation);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarPhoneBase.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarPhoneBase.java
deleted file mode 100644
index 5588ddcd3..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarPhoneBase.java
+++ /dev/null
@@ -1,219 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.toolbar;
-
-import java.util.Arrays;
-
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.animation.PropertyAnimator;
-import org.mozilla.gecko.animation.ViewHelper;
-import org.mozilla.gecko.widget.themed.ThemedImageView;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.animation.AccelerateInterpolator;
-import android.view.animation.Interpolator;
-import android.widget.ImageView;
-
-/**
- * A base implementations of the browser toolbar for phones.
- * This class manages any Views, variables, etc. that are exclusive to phone.
- */
-abstract class BrowserToolbarPhoneBase extends BrowserToolbar {
-
- protected final ImageView urlBarTranslatingEdge;
- protected final ThemedImageView editCancel;
-
- private final Path roundCornerShape;
- private final Paint roundCornerPaint;
-
- private final Interpolator buttonsInterpolator = new AccelerateInterpolator();
-
- public BrowserToolbarPhoneBase(final Context context, final AttributeSet attrs) {
- super(context, attrs);
- final Resources res = context.getResources();
-
- urlBarTranslatingEdge = (ImageView) findViewById(R.id.url_bar_translating_edge);
-
- // This will clip the translating edge's image at 60% of its width
- urlBarTranslatingEdge.getDrawable().setLevel(6000);
-
- editCancel = (ThemedImageView) findViewById(R.id.edit_cancel);
-
- focusOrder.add(this);
- focusOrder.addAll(urlDisplayLayout.getFocusOrder());
- focusOrder.addAll(Arrays.asList(tabsButton, menuButton));
-
- roundCornerShape = new Path();
- roundCornerShape.moveTo(0, 0);
- roundCornerShape.lineTo(30, 0);
- roundCornerShape.cubicTo(0, 0, 0, 0, 0, 30);
- roundCornerShape.lineTo(0, 0);
-
- roundCornerPaint = new Paint();
- roundCornerPaint.setAntiAlias(true);
- roundCornerPaint.setColor(ContextCompat.getColor(context, R.color.text_and_tabs_tray_grey));
- roundCornerPaint.setStrokeWidth(0.0f);
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- editCancel.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- // If we exit editing mode during the animation,
- // we're put into an inconsistent state (bug 1017276).
- if (!isAnimating()) {
- Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL,
- TelemetryContract.Method.ACTIONBAR,
- getResources().getResourceEntryName(editCancel.getId()));
- cancelEdit();
- }
- }
- });
- }
-
- @Override
- public void setPrivateMode(final boolean isPrivate) {
- super.setPrivateMode(isPrivate);
- editCancel.setPrivateMode(isPrivate);
- }
-
- @Override
- protected boolean isTabsButtonOffscreen() {
- return isEditing();
- }
-
- @Override
- public boolean addActionItem(final View actionItem) {
- // We have no action item bar.
- return false;
- }
-
- @Override
- public void removeActionItem(final View actionItem) {
- // We have no action item bar.
- }
-
- @Override
- protected void updateNavigationButtons(final Tab tab) {
- // We have no navigation buttons so do nothing.
- }
-
- @Override
- public void draw(final Canvas canvas) {
- super.draw(canvas);
-
- if (uiMode == UIMode.DISPLAY) {
- canvas.drawPath(roundCornerShape, roundCornerPaint);
- }
- }
-
- @Override
- public void triggerTabsPanelTransition(final PropertyAnimator animator, final boolean areTabsShown) {
- if (areTabsShown) {
- ViewHelper.setAlpha(tabsCounter, 0.0f);
- ViewHelper.setAlpha(menuIcon, 0.0f);
- return;
- }
-
- final PropertyAnimator buttonsAnimator =
- new PropertyAnimator(animator.getDuration(), buttonsInterpolator);
- buttonsAnimator.attach(tabsCounter,
- PropertyAnimator.Property.ALPHA,
- 1.0f);
- buttonsAnimator.attach(menuIcon,
- PropertyAnimator.Property.ALPHA,
- 1.0f);
- buttonsAnimator.start();
- }
-
- /**
- * Returns the number of pixels the url bar translating edge
- * needs to translate to the right to enter its editing mode state.
- * A negative value means the edge must translate to the left.
- */
- protected int getUrlBarEntryTranslation() {
- // Find the distance from the right-edge of the url bar (where we're translating from) to
- // the left-edge of the cancel button (where we're translating to; note that the cancel
- // button must be laid out, i.e. not View.GONE).
- return editCancel.getLeft() - urlBarEntry.getRight();
- }
-
- protected int getUrlBarCurveTranslation() {
- return getWidth() - tabsButton.getLeft();
- }
-
- protected void updateTabCountAndAnimate(final int count) {
- // Don't animate if the toolbar is hidden.
- if (!isVisible()) {
- updateTabCount(count);
- return;
- }
-
- // If toolbar is in edit mode on a phone, this means the entry is expanded
- // and the tabs button is translated offscreen. Don't trigger tabs counter
- // updates until the tabs button is back on screen.
- // See stopEditing()
- if (!isTabsButtonOffscreen()) {
- tabsCounter.setCount(count);
-
- tabsButton.setContentDescription((count > 1) ?
- activity.getString(R.string.num_tabs, count) :
- activity.getString(R.string.one_tab));
- }
- }
-
- @Override
- protected void setUrlEditLayoutVisibility(final boolean showEditLayout,
- final PropertyAnimator animator) {
- super.setUrlEditLayoutVisibility(showEditLayout, animator);
-
- if (animator == null) {
- editCancel.setVisibility(showEditLayout ? View.VISIBLE : View.INVISIBLE);
- return;
- }
-
- animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
- @Override
- public void onPropertyAnimationStart() {
- if (!showEditLayout) {
- editCancel.setVisibility(View.INVISIBLE);
- }
- }
-
- @Override
- public void onPropertyAnimationEnd() {
- if (showEditLayout) {
- editCancel.setVisibility(View.VISIBLE);
- }
- }
- });
- }
-
- @Override
- public void onLightweightThemeChanged() {
- super.onLightweightThemeChanged();
- editCancel.onLightweightThemeChanged();
- }
-
- @Override
- public void onLightweightThemeReset() {
- super.onLightweightThemeReset();
- editCancel.onLightweightThemeReset();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarTablet.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarTablet.java
deleted file mode 100644
index 215934161..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarTablet.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.toolbar;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.animation.PropertyAnimator;
-import org.mozilla.gecko.animation.ViewHelper;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-
-/**
- * The toolbar implementation for tablet.
- */
-class BrowserToolbarTablet extends BrowserToolbarTabletBase {
-
- private static final int FORWARD_ANIMATION_DURATION = 450;
-
- private enum ForwardButtonState {
- HIDDEN,
- DISPLAYED,
- TRANSITIONING,
- }
-
- private final int forwardButtonTranslationWidth;
-
- private ForwardButtonState forwardButtonState;
-
- private boolean backButtonWasEnabledOnStartEditing;
-
- public BrowserToolbarTablet(final Context context, final AttributeSet attrs) {
- super(context, attrs);
-
- forwardButtonTranslationWidth =
- getResources().getDimensionPixelOffset(R.dimen.tablet_nav_button_width);
-
- // The forward button is initially expanded (in the layout file)
- // so translate it for start of the expansion animation; future
- // iterations translate it to this position when hiding and will already be set up.
- ViewHelper.setTranslationX(forwardButton, -forwardButtonTranslationWidth);
-
- // TODO: Move this to *TabletBase when old tablet is removed.
- // We don't want users clicking the forward button in transitions, but we don't want it to
- // look disabled to avoid flickering complications (e.g. disabled in editing mode), so undo
- // the work of the super class' constructor.
- forwardButton.setEnabled(true);
-
- updateForwardButtonState(ForwardButtonState.HIDDEN);
- }
-
- private void updateForwardButtonState(final ForwardButtonState state) {
- forwardButtonState = state;
- forwardButton.setEnabled(forwardButtonState == ForwardButtonState.DISPLAYED);
- }
-
- @Override
- public boolean isAnimating() {
- return false;
- }
-
- @Override
- protected void triggerStartEditingTransition(final PropertyAnimator animator) {
- showUrlEditLayout();
- }
-
- @Override
- protected void triggerStopEditingTransition() {
- hideUrlEditLayout();
- }
-
- @Override
- protected void animateForwardButton(final ForwardButtonAnimation animation) {
- final boolean willShowForward = (animation == ForwardButtonAnimation.SHOW);
- if ((forwardButtonState != ForwardButtonState.HIDDEN && willShowForward) ||
- (forwardButtonState != ForwardButtonState.DISPLAYED && !willShowForward)) {
- return;
- }
- updateForwardButtonState(ForwardButtonState.TRANSITIONING);
-
- // We want the forward button to show immediately when switching tabs
- final PropertyAnimator forwardAnim =
- new PropertyAnimator(isSwitchingTabs ? 10 : FORWARD_ANIMATION_DURATION);
-
- forwardAnim.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
- @Override
- public void onPropertyAnimationStart() {
- if (!willShowForward) {
- // Set the margin before the transition when hiding the forward button. We
- // have to do this so that the favicon isn't clipped during the transition
- MarginLayoutParams layoutParams =
- (MarginLayoutParams) urlDisplayLayout.getLayoutParams();
- layoutParams.leftMargin = 0;
-
- // Do the same on the URL edit container
- layoutParams = (MarginLayoutParams) urlEditLayout.getLayoutParams();
- layoutParams.leftMargin = 0;
-
- requestLayout();
- // Note, we already translated the favicon, site security, and text field
- // in prepareForwardAnimation, so they should appear to have not moved at
- // all at this point.
- }
- }
-
- @Override
- public void onPropertyAnimationEnd() {
- final ForwardButtonState newForwardButtonState;
- if (willShowForward) {
- // Increase the margins to ensure the text does not run outside the View.
- MarginLayoutParams layoutParams =
- (MarginLayoutParams) urlDisplayLayout.getLayoutParams();
- layoutParams.leftMargin = forwardButtonTranslationWidth;
-
- layoutParams = (MarginLayoutParams) urlEditLayout.getLayoutParams();
- layoutParams.leftMargin = forwardButtonTranslationWidth;
-
- newForwardButtonState = ForwardButtonState.DISPLAYED;
- } else {
- newForwardButtonState = ForwardButtonState.HIDDEN;
- }
-
- urlDisplayLayout.finishForwardAnimation();
- updateForwardButtonState(newForwardButtonState);
-
- requestLayout();
- }
- });
-
- prepareForwardAnimation(forwardAnim, animation, forwardButtonTranslationWidth);
- forwardAnim.start();
- }
-
- private void prepareForwardAnimation(PropertyAnimator anim, ForwardButtonAnimation animation, int width) {
- if (animation == ForwardButtonAnimation.HIDE) {
- anim.attach(forwardButton,
- PropertyAnimator.Property.TRANSLATION_X,
- -width);
- anim.attach(forwardButton,
- PropertyAnimator.Property.ALPHA,
- 0);
-
- } else {
- anim.attach(forwardButton,
- PropertyAnimator.Property.TRANSLATION_X,
- 0);
- anim.attach(forwardButton,
- PropertyAnimator.Property.ALPHA,
- 1);
- }
-
- urlDisplayLayout.prepareForwardAnimation(anim, animation, width);
- }
-
- @Override
- public void triggerTabsPanelTransition(final PropertyAnimator animator, final boolean areTabsShown) {
- // Do nothing.
- }
-
- @Override
- public void setToolBarButtonsAlpha(float alpha) {
- // Do nothing.
- }
-
-
- @Override
- public void startEditing(final String url, final PropertyAnimator animator) {
- // We already know the forward button state - no need to store it here.
- backButtonWasEnabledOnStartEditing = backButton.isEnabled();
-
- backButton.setEnabled(false);
- forwardButton.setEnabled(false);
-
- super.startEditing(url, animator);
- }
-
- @Override
- public String commitEdit() {
- stopEditingNewTablet();
- return super.commitEdit();
- }
-
- @Override
- public String cancelEdit() {
- // This can get called when we're not editing but we only want
- // to make these changes when leaving editing mode.
- if (isEditing()) {
- stopEditingNewTablet();
-
- backButton.setEnabled(backButtonWasEnabledOnStartEditing);
- updateForwardButtonState(forwardButtonState);
- }
-
- return super.cancelEdit();
- }
-
- private void stopEditingNewTablet() {
- // Undo the changes caused by calling setEnabled for forwardButton in startEditing.
- // Note that this should be called first so the enabled state of the
- // forward button is set to the proper value.
- forwardButton.setEnabled(true);
- }
-
- @Override
- protected Drawable getLWTDefaultStateSetDrawable() {
- return BrowserToolbar.getLightweightThemeDrawable(this, getTheme(), R.color.toolbar_grey);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarTabletBase.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarTabletBase.java
deleted file mode 100644
index e818bb95c..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarTabletBase.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.toolbar;
-
-import java.util.Arrays;
-
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.tabs.TabHistoryController;
-import org.mozilla.gecko.menu.MenuItemActionBar;
-import org.mozilla.gecko.util.HardwareUtils;
-import org.mozilla.gecko.widget.themed.ThemedTextView;
-
-import android.content.Context;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewGroup.LayoutParams;
-import android.widget.Button;
-import android.widget.ImageButton;
-import android.widget.LinearLayout;
-
-/**
- * A base implementations of the browser toolbar for tablets.
- * This class manages any Views, variables, etc. that are exclusive to tablet.
- */
-abstract class BrowserToolbarTabletBase extends BrowserToolbar {
-
- protected enum ForwardButtonAnimation {
- SHOW,
- HIDE
- }
-
- protected final LinearLayout actionItemBar;
-
- protected final BackButton backButton;
- protected final ForwardButton forwardButton;
-
- protected final View menuButtonMarginView;
-
- private final PorterDuffColorFilter privateBrowsingTabletMenuItemColorFilter;
-
- protected abstract void animateForwardButton(ForwardButtonAnimation animation);
-
- public BrowserToolbarTabletBase(final Context context, final AttributeSet attrs) {
- super(context, attrs);
-
- actionItemBar = (LinearLayout) findViewById(R.id.menu_items);
-
- backButton = (BackButton) findViewById(R.id.back);
- backButton.setEnabled(false);
- forwardButton = (ForwardButton) findViewById(R.id.forward);
- forwardButton.setEnabled(false);
- initButtonListeners();
-
- focusOrder.addAll(Arrays.asList(tabsButton, (View) backButton, (View) forwardButton, this));
- focusOrder.addAll(urlDisplayLayout.getFocusOrder());
- focusOrder.addAll(Arrays.asList(actionItemBar, menuButton));
-
- urlDisplayLayout.updateSiteIdentityAnchor(backButton);
-
- privateBrowsingTabletMenuItemColorFilter = new PorterDuffColorFilter(
- ContextCompat.getColor(context, R.color.tabs_tray_icon_grey), PorterDuff.Mode.SRC_IN);
-
- menuButtonMarginView = findViewById(R.id.menu_margin);
- if (menuButtonMarginView != null) {
- menuButtonMarginView.setVisibility(View.VISIBLE);
- }
- }
-
- private void initButtonListeners() {
- backButton.setOnClickListener(new Button.OnClickListener() {
- @Override
- public void onClick(View view) {
- Tabs.getInstance().getSelectedTab().doBack();
- }
- });
- backButton.setOnLongClickListener(new Button.OnLongClickListener() {
- @Override
- public boolean onLongClick(View view) {
- return tabHistoryController.showTabHistory(Tabs.getInstance().getSelectedTab(),
- TabHistoryController.HistoryAction.BACK);
- }
- });
-
- forwardButton.setOnClickListener(new Button.OnClickListener() {
- @Override
- public void onClick(View view) {
- Tabs.getInstance().getSelectedTab().doForward();
- }
- });
- forwardButton.setOnLongClickListener(new Button.OnLongClickListener() {
- @Override
- public boolean onLongClick(View view) {
- return tabHistoryController.showTabHistory(Tabs.getInstance().getSelectedTab(),
- TabHistoryController.HistoryAction.FORWARD);
- }
- });
- }
-
- @Override
- protected boolean isTabsButtonOffscreen() {
- return false;
- }
-
- @Override
- public boolean addActionItem(final View actionItem) {
- actionItemBar.addView(actionItem, LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
- return true;
- }
-
- @Override
- public void removeActionItem(final View actionItem) {
- actionItemBar.removeView(actionItem);
- }
-
- @Override
- protected void updateNavigationButtons(final Tab tab) {
- backButton.setEnabled(canDoBack(tab));
- animateForwardButton(
- canDoForward(tab) ? ForwardButtonAnimation.SHOW : ForwardButtonAnimation.HIDE);
- }
-
- @Override
- public void setNextFocusDownId(int nextId) {
- super.setNextFocusDownId(nextId);
- backButton.setNextFocusDownId(nextId);
- forwardButton.setNextFocusDownId(nextId);
- }
-
- @Override
- public void setPrivateMode(final boolean isPrivate) {
- super.setPrivateMode(isPrivate);
-
- // If we had backgroundTintList, we could remove the colorFilter
- // code in favor of setPrivateMode (bug 1197432).
- final PorterDuffColorFilter colorFilter =
- isPrivate ? privateBrowsingTabletMenuItemColorFilter : null;
- setTabsCounterPrivateMode(isPrivate, colorFilter);
-
- backButton.setPrivateMode(isPrivate);
- forwardButton.setPrivateMode(isPrivate);
- menuIcon.setPrivateMode(isPrivate);
- for (int i = 0; i < actionItemBar.getChildCount(); ++i) {
- final MenuItemActionBar child = (MenuItemActionBar) actionItemBar.getChildAt(i);
- child.setPrivateMode(isPrivate);
- }
- }
-
- private void setTabsCounterPrivateMode(final boolean isPrivate, final PorterDuffColorFilter colorFilter) {
- // The TabsCounter is a TextSwitcher which cycles two views
- // to provide animations, hence looping over these two children.
- for (int i = 0; i < 2; ++i) {
- final ThemedTextView view = (ThemedTextView) tabsCounter.getChildAt(i);
- view.setPrivateMode(isPrivate);
- view.getBackground().mutate().setColorFilter(colorFilter);
- }
-
- // To prevent animation of the background,
- // it is set to a different Drawable.
- tabsCounter.getBackground().mutate().setColorFilter(colorFilter);
- }
-
- @Override
- public View getDoorHangerAnchor() {
- return backButton;
- }
-
- protected boolean canDoBack(final Tab tab) {
- return (tab.canDoBack() && !isEditing());
- }
-
- protected boolean canDoForward(final Tab tab) {
- return (tab.canDoForward() && !isEditing());
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/CanvasDelegate.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/CanvasDelegate.java
deleted file mode 100644
index 55567fba3..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/CanvasDelegate.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.toolbar;
-
-import org.mozilla.gecko.AppConstants.Versions;
-
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.PorterDuff.Mode;
-import android.graphics.PorterDuffXfermode;
-import android.graphics.Shader;
-
-class CanvasDelegate {
- Paint mPaint;
- PorterDuffXfermode mMode;
- DrawManager mDrawManager;
-
- // DrawManager would do a default draw of the background.
- static interface DrawManager {
- public void defaultDraw(Canvas canvas);
- }
-
- CanvasDelegate(DrawManager drawManager, Mode mode, Paint paint) {
- mDrawManager = drawManager;
-
- // DST_IN masks, DST_OUT clips.
- mMode = new PorterDuffXfermode(mode);
-
- mPaint = paint;
- }
-
- void draw(Canvas canvas, Path path, int width, int height) {
- // Save the canvas. All PorterDuff operations should be done in a offscreen bitmap.
- int count = canvas.saveLayer(0, 0, width, height, null,
- Canvas.MATRIX_SAVE_FLAG |
- Canvas.CLIP_SAVE_FLAG |
- Canvas.HAS_ALPHA_LAYER_SAVE_FLAG |
- Canvas.FULL_COLOR_LAYER_SAVE_FLAG |
- Canvas.CLIP_TO_LAYER_SAVE_FLAG);
-
- // Do a default draw.
- mDrawManager.defaultDraw(canvas);
-
- if (path != null && !path.isEmpty()) {
- // ICS added double-buffering, which made it easier for drawing the Path directly over the DST.
- // In pre-ICS, drawPath() doesn't seem to use ARGB_8888 mode for performance, hence transparency is not preserved.
- mPaint.setXfermode(mMode);
- canvas.drawPath(path, mPaint);
- }
-
- // Restore the canvas.
- canvas.restoreToCount(count);
- }
-
- void setShader(Shader shader) {
- mPaint.setShader(shader);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/ForwardButton.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/ForwardButton.java
deleted file mode 100644
index f95bb5e8a..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/ForwardButton.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.toolbar;
-
-import android.content.Context;
-import android.util.AttributeSet;
-
-public class ForwardButton extends NavButton {
- public ForwardButton(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
- super.onSizeChanged(width, height, oldWidth, oldHeight);
-
- mBorderPath.reset();
- mBorderPath.moveTo(width - mBorderWidth, 0);
- mBorderPath.lineTo(width - mBorderWidth, height);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/NavButton.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/NavButton.java
deleted file mode 100644
index 68194e222..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/NavButton.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.toolbar;
-
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.StateListDrawable;
-import android.util.AttributeSet;
-
-abstract class NavButton extends ShapedButton {
- protected final Path mBorderPath;
- protected final Paint mBorderPaint;
- protected final float mBorderWidth;
-
- protected final int mBorderColor;
- protected final int mBorderColorPrivate;
-
- public NavButton(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- final Resources res = getResources();
- mBorderColor = ContextCompat.getColor(context, R.color.disabled_grey);
- mBorderColorPrivate = ContextCompat.getColor(context, R.color.toolbar_icon_grey);
- mBorderWidth = res.getDimension(R.dimen.nav_button_border_width);
-
- // Paint to draw the border.
- mBorderPaint = new Paint();
- mBorderPaint.setAntiAlias(true);
- mBorderPaint.setStrokeWidth(mBorderWidth);
- mBorderPaint.setStyle(Paint.Style.STROKE);
-
- // Path is masked.
- mBorderPath = new Path();
-
- setPrivateMode(false);
- }
-
- @Override
- public void setPrivateMode(boolean isPrivate) {
- super.setPrivateMode(isPrivate);
- mBorderPaint.setColor(isPrivate ? mBorderColorPrivate : mBorderColor);
- }
-
- @Override
- public void draw(Canvas canvas) {
- super.draw(canvas);
-
- // Draw the border on top.
- canvas.drawPath(mBorderPath, mBorderPaint);
- }
-
- // The drawable is constructed as per @drawable/url_bar_nav_button.
- @Override
- public void onLightweightThemeChanged() {
- final Drawable drawable = BrowserToolbar.getLightweightThemeDrawable(this, getTheme(), R.color.toolbar_grey);
-
- if (drawable == null) {
- return;
- }
-
- final StateListDrawable stateList = new StateListDrawable();
- stateList.addState(PRIVATE_PRESSED_STATE_SET, getColorDrawable(R.color.placeholder_active_grey));
- stateList.addState(PRESSED_ENABLED_STATE_SET, getColorDrawable(R.color.toolbar_grey_pressed));
- stateList.addState(PRIVATE_FOCUSED_STATE_SET, getColorDrawable(R.color.text_and_tabs_tray_grey));
- stateList.addState(FOCUSED_STATE_SET, getColorDrawable(R.color.tablet_highlight_focused));
- stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.tabs_tray_grey_pressed));
- stateList.addState(EMPTY_STATE_SET, drawable);
-
- setBackgroundDrawable(stateList);
- }
-
- @Override
- public void onLightweightThemeReset() {
- setBackgroundResource(R.drawable.url_bar_nav_button);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/PageActionLayout.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/PageActionLayout.java
deleted file mode 100644
index 9361d5907..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/PageActionLayout.java
+++ /dev/null
@@ -1,371 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.toolbar;
-
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.ResourceDrawableUtils;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.widget.GeckoPopupMenu;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.util.AttributeSet;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-
-import java.util.Iterator;
-import java.util.List;
-import java.util.UUID;
-import java.util.ArrayList;
-
-public class PageActionLayout extends LinearLayout implements NativeEventListener,
- View.OnClickListener,
- View.OnLongClickListener {
- private static final String MENU_BUTTON_KEY = "MENU_BUTTON_KEY";
- private static final int DEFAULT_PAGE_ACTIONS_SHOWN = 2;
-
- private final Context mContext;
- private final LinearLayout mLayout;
- private final List<PageAction> mPageActionList;
-
- private GeckoPopupMenu mPageActionsMenu;
-
- // By default it's two, can be changed by calling setNumberShown(int)
- private int mMaxVisiblePageActions;
-
- public PageActionLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- mContext = context;
- mLayout = this;
-
- mPageActionList = new ArrayList<PageAction>();
- setNumberShown(DEFAULT_PAGE_ACTIONS_SHOWN);
- refreshPageActionIcons();
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
- "PageActions:Add",
- "PageActions:Remove");
- }
-
- @Override
- protected void onDetachedFromWindow() {
- GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
- "PageActions:Add",
- "PageActions:Remove");
-
- super.onDetachedFromWindow();
- }
-
- private void setNumberShown(int count) {
- ThreadUtils.assertOnUiThread();
-
- mMaxVisiblePageActions = count;
-
- for (int index = 0; index < count; index++) {
- if ((getChildCount() - 1) < index) {
- mLayout.addView(createImageButton());
- }
- }
- }
-
- @Override
- public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) {
- // NativeJSObject cannot be used off of the Gecko thread, so convert it to a Bundle.
- final Bundle bundle = message.toBundle();
-
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- handleUiMessage(event, bundle);
- }
- });
- }
-
- private void handleUiMessage(final String event, final Bundle message) {
- ThreadUtils.assertOnUiThread();
-
- if (event.equals("PageActions:Add")) {
- final String id = message.getString("id");
- final String title = message.getString("title");
- final String imageURL = message.getString("icon");
- final boolean important = message.getBoolean("important");
-
- addPageAction(id, title, imageURL, new OnPageActionClickListeners() {
- @Override
- public void onClick(String id) {
- GeckoAppShell.notifyObservers("PageActions:Clicked", id);
- }
-
- @Override
- public boolean onLongClick(String id) {
- GeckoAppShell.notifyObservers("PageActions:LongClicked", id);
- return true;
- }
- }, important);
- } else if (event.equals("PageActions:Remove")) {
- final String id = message.getString("id");
-
- removePageAction(id);
- }
- }
-
- private void addPageAction(final String id, final String title, final String imageData,
- final OnPageActionClickListeners onPageActionClickListeners, boolean important) {
- ThreadUtils.assertOnUiThread();
-
- final PageAction pageAction = new PageAction(id, title, null, onPageActionClickListeners, important);
-
- int insertAt = mPageActionList.size();
- while (insertAt > 0 && mPageActionList.get(insertAt - 1).isImportant()) {
- insertAt--;
- }
- mPageActionList.add(insertAt, pageAction);
-
- ResourceDrawableUtils.getDrawable(mContext, imageData, new ResourceDrawableUtils.BitmapLoader() {
- @Override
- public void onBitmapFound(final Drawable d) {
- if (mPageActionList.contains(pageAction)) {
- pageAction.setDrawable(d);
- refreshPageActionIcons();
- }
- }
- });
- }
-
- private void removePageAction(String id) {
- ThreadUtils.assertOnUiThread();
-
- final Iterator<PageAction> iter = mPageActionList.iterator();
- while (iter.hasNext()) {
- final PageAction pageAction = iter.next();
- if (pageAction.getID().equals(id)) {
- iter.remove();
- refreshPageActionIcons();
- return;
- }
- }
- }
-
- private ImageButton createImageButton() {
- ThreadUtils.assertOnUiThread();
-
- final int width = mContext.getResources().getDimensionPixelSize(R.dimen.page_action_button_width);
- ImageButton imageButton = new ImageButton(mContext, null, R.style.UrlBar_ImageButton);
- imageButton.setLayoutParams(new LayoutParams(width, LayoutParams.MATCH_PARENT));
- imageButton.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
- imageButton.setOnClickListener(this);
- imageButton.setOnLongClickListener(this);
- return imageButton;
- }
-
- @Override
- public void onClick(View v) {
- String buttonClickedId = (String)v.getTag();
- if (buttonClickedId != null) {
- if (buttonClickedId.equals(MENU_BUTTON_KEY)) {
- showMenu(v, mPageActionList.size() - mMaxVisiblePageActions + 1);
- } else {
- getPageActionWithId(buttonClickedId).onClick();
- }
- }
- }
-
- @Override
- public boolean onLongClick(View v) {
- String buttonClickedId = (String)v.getTag();
- if (buttonClickedId.equals(MENU_BUTTON_KEY)) {
- showMenu(v, mPageActionList.size() - mMaxVisiblePageActions + 1);
- return true;
- } else {
- return getPageActionWithId(buttonClickedId).onLongClick();
- }
- }
-
- private void setActionForView(final ImageButton view, final PageAction pageAction) {
- ThreadUtils.assertOnUiThread();
-
- if (pageAction == null) {
- view.setTag(null);
- view.setImageDrawable(null);
- view.setVisibility(View.GONE);
- view.setContentDescription(null);
- return;
- }
-
- view.setTag(pageAction.getID());
- view.setImageDrawable(pageAction.getDrawable());
- view.setVisibility(View.VISIBLE);
- view.setContentDescription(pageAction.getTitle());
- }
-
- private void refreshPageActionIcons() {
- ThreadUtils.assertOnUiThread();
-
- final Resources resources = mContext.getResources();
- for (int i = 0; i < this.getChildCount(); i++) {
- final ImageButton v = (ImageButton) this.getChildAt(i);
- final PageAction pageAction = getPageActionForViewAt(i);
-
- // If there are more page actions than buttons, set the menu icon.
- // Otherwise, set the page action's icon if there is a page action.
- if ((i == this.getChildCount() - 1) && (mPageActionList.size() > mMaxVisiblePageActions)) {
- v.setTag(MENU_BUTTON_KEY);
- v.setImageDrawable(resources.getDrawable(R.drawable.icon_pageaction));
- v.setVisibility((pageAction != null) ? View.VISIBLE : View.GONE);
- v.setContentDescription(resources.getString(R.string.page_action_dropmarker_description));
- } else {
- setActionForView(v, pageAction);
- }
- }
- }
-
- private PageAction getPageActionForViewAt(int index) {
- ThreadUtils.assertOnUiThread();
-
- /**
- * We show the user the most recent pageaction added since this keeps the user aware of any new page actions being added
- * Also, the order of the pageAction is important i.e. if a page action is added, instead of shifting the pagactions to the
- * left to make space for the new one, it would be more visually appealing to have the pageaction appear in the blank space.
- *
- * buttonIndex is needed for this reason because every new View added to PageActionLayout gets added to the right of its neighbouring View.
- * Hence the button on the very leftmost has the index 0. We want our pageactions to start from the rightmost
- * and hence we maintain the insertion order of the child Views which is essentially the reverse of their index
- */
-
- final int buttonIndex = (this.getChildCount() - 1) - index;
-
- if (mPageActionList.size() > buttonIndex) {
- // Return the pageactions starting from the end of the list for the number of visible pageactions.
- final int buttonCount = Math.min(mPageActionList.size(), getChildCount());
- return mPageActionList.get((mPageActionList.size() - buttonCount) + buttonIndex);
- }
- return null;
- }
-
- private PageAction getPageActionWithId(String id) {
- ThreadUtils.assertOnUiThread();
-
- for (PageAction pageAction : mPageActionList) {
- if (pageAction.getID().equals(id)) {
- return pageAction;
- }
- }
- return null;
- }
-
- private void showMenu(View pageActionButton, int toShow) {
- ThreadUtils.assertOnUiThread();
-
- if (mPageActionsMenu == null) {
- mPageActionsMenu = new GeckoPopupMenu(pageActionButton.getContext(), pageActionButton);
- mPageActionsMenu.inflate(0);
- mPageActionsMenu.setOnMenuItemClickListener(new GeckoPopupMenu.OnMenuItemClickListener() {
- @Override
- public boolean onMenuItemClick(MenuItem item) {
- int id = item.getItemId();
- for (int i = 0; i < mPageActionList.size(); i++) {
- PageAction pageAction = mPageActionList.get(i);
- if (pageAction.key() == id) {
- pageAction.onClick();
- return true;
- }
- }
- return false;
- }
- });
- }
- Menu menu = mPageActionsMenu.getMenu();
- menu.clear();
-
- for (int i = 0; i < mPageActionList.size() && i < toShow; i++) {
- PageAction pageAction = mPageActionList.get(i);
- MenuItem item = menu.add(Menu.NONE, pageAction.key(), Menu.NONE, pageAction.getTitle());
- item.setIcon(pageAction.getDrawable());
- }
- mPageActionsMenu.show();
- }
-
- private static interface OnPageActionClickListeners {
- public void onClick(String id);
- public boolean onLongClick(String id);
- }
-
- private static class PageAction {
- private final OnPageActionClickListeners mOnPageActionClickListeners;
- private Drawable mDrawable;
- private final String mTitle;
- private final String mId;
- private final int key;
- private final boolean mImportant;
-
- public PageAction(String id,
- String title,
- Drawable image,
- OnPageActionClickListeners onPageActionClickListeners,
- boolean important) {
- mId = id;
- mTitle = title;
- mDrawable = image;
- mOnPageActionClickListeners = onPageActionClickListeners;
- mImportant = important;
-
- key = UUID.fromString(mId.subSequence(1, mId.length() - 2).toString()).hashCode();
- }
-
- public Drawable getDrawable() {
- return mDrawable;
- }
-
- public void setDrawable(Drawable d) {
- mDrawable = d;
- }
-
- public String getTitle() {
- return mTitle;
- }
-
- public String getID() {
- return mId;
- }
-
- public int key() {
- return key;
- }
-
- public boolean isImportant() {
- return mImportant;
- }
-
- public void onClick() {
- if (mOnPageActionClickListeners != null) {
- mOnPageActionClickListeners.onClick(mId);
- }
- }
-
- public boolean onLongClick() {
- if (mOnPageActionClickListeners != null) {
- return mOnPageActionClickListeners.onLongClick(mId);
- }
- return false;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/PhoneTabsButton.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/PhoneTabsButton.java
deleted file mode 100644
index 416485494..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/PhoneTabsButton.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.toolbar;
-
-import android.content.Context;
-import android.util.AttributeSet;
-
-import org.mozilla.gecko.tabs.TabCurve;
-
-public class PhoneTabsButton extends ShapedButton {
- public PhoneTabsButton(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
- super.onSizeChanged(width, height, oldWidth, oldHeight);
-
- mPath.reset();
-
- mPath.moveTo(0, 0);
- TabCurve.drawFromTop(mPath, 0, height, TabCurve.Direction.RIGHT);
- mPath.lineTo(width, height);
- mPath.lineTo(width, 0);
- mPath.lineTo(0, 0);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/ShapedButton.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/ShapedButton.java
deleted file mode 100644
index 003dada2d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/ShapedButton.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.toolbar;
-
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.lwt.LightweightThemeDrawable;
-import org.mozilla.gecko.widget.themed.ThemedImageButton;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.PorterDuff.Mode;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.StateListDrawable;
-import android.util.AttributeSet;
-
-/**
- * A ImageButton with a custom drawn path and lightweight theme support. Note that {@link ShapedButtonFrameLayout}
- * copies the lwt support so if you change it here, you should probably change it there.
- */
-public class ShapedButton extends ThemedImageButton
- implements CanvasDelegate.DrawManager {
-
- protected final Path mPath;
- protected final CanvasDelegate mCanvasDelegate;
-
- public ShapedButton(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- // Path is clipped.
- mPath = new Path();
-
- final Paint paint = new Paint();
- paint.setAntiAlias(true);
- paint.setColor(ContextCompat.getColor(context, R.color.canvas_delegate_paint));
- paint.setStrokeWidth(0.0f);
- mCanvasDelegate = new CanvasDelegate(this, Mode.DST_IN, paint);
-
- setWillNotDraw(false);
- }
-
- @Override
- @SuppressLint("MissingSuperCall") // Super gets called from defaultDraw().
- // It is intentionally not called in the other case.
- public void draw(Canvas canvas) {
- if (mCanvasDelegate != null)
- mCanvasDelegate.draw(canvas, mPath, getWidth(), getHeight());
- else
- defaultDraw(canvas);
- }
-
- @Override
- public void defaultDraw(Canvas canvas) {
- super.draw(canvas);
- }
-
- // The drawable is constructed as per @drawable/shaped_button.
- @Override
- public void onLightweightThemeChanged() {
- final int background = ContextCompat.getColor(getContext(), R.color.text_and_tabs_tray_grey);
- final LightweightThemeDrawable lightWeight = getTheme().getColorDrawable(this, background);
-
- if (lightWeight == null)
- return;
-
- lightWeight.setAlpha(34, 34);
-
- final StateListDrawable stateList = new StateListDrawable();
- stateList.addState(PRESSED_ENABLED_STATE_SET, getColorDrawable(R.color.highlight_shaped));
- stateList.addState(FOCUSED_STATE_SET, getColorDrawable(R.color.highlight_shaped_focused));
- stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.text_and_tabs_tray_grey));
- stateList.addState(EMPTY_STATE_SET, lightWeight);
-
- setBackgroundDrawable(stateList);
- }
-
- @Override
- public void onLightweightThemeReset() {
- setBackgroundResource(R.drawable.shaped_button);
- }
-
- @Override
- public void setBackgroundDrawable(Drawable drawable) {
- if (getBackground() == null || drawable == null) {
- super.setBackgroundDrawable(drawable);
- return;
- }
-
- int[] padding = new int[] { getPaddingLeft(),
- getPaddingTop(),
- getPaddingRight(),
- getPaddingBottom()
- };
- drawable.setLevel(getBackground().getLevel());
- super.setBackgroundDrawable(drawable);
-
- setPadding(padding[0], padding[1], padding[2], padding[3]);
- }
-
- @Override
- public void setBackgroundResource(int resId) {
- setBackgroundDrawable(getResources().getDrawable(resId));
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/ShapedButtonFrameLayout.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/ShapedButtonFrameLayout.java
deleted file mode 100644
index c14829aec..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/ShapedButtonFrameLayout.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.toolbar;
-
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.lwt.LightweightThemeDrawable;
-import org.mozilla.gecko.widget.themed.ThemedFrameLayout;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.StateListDrawable;
-import android.util.AttributeSet;
-
-/** A FrameLayout with lightweight theme support. Note that {@link ShapedButton}'s lwt support is basically the same so
- * if you change it here, you should probably change it there. Note also that this doesn't have ShapedButton's path code
- * so shouldn't have "ShapedButton" in the name, but I wanted to make the connection apparent so I left it.
- */
-public class ShapedButtonFrameLayout extends ThemedFrameLayout {
-
- public ShapedButtonFrameLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- // The drawable is constructed as per @drawable/shaped_button.
- @Override
- public void onLightweightThemeChanged() {
- final int background = ContextCompat.getColor(getContext(), R.color.text_and_tabs_tray_grey);
- final LightweightThemeDrawable lightWeight = getTheme().getColorDrawable(this, background);
-
- if (lightWeight == null)
- return;
-
- lightWeight.setAlpha(34, 34);
-
- final StateListDrawable stateList = new StateListDrawable();
- stateList.addState(PRESSED_ENABLED_STATE_SET, getColorDrawable(R.color.highlight_shaped));
- stateList.addState(FOCUSED_STATE_SET, getColorDrawable(R.color.highlight_shaped_focused));
- stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.text_and_tabs_tray_grey));
- stateList.addState(EMPTY_STATE_SET, lightWeight);
-
- setBackgroundDrawable(stateList);
- }
-
- @Override
- public void onLightweightThemeReset() {
- setBackgroundResource(R.drawable.shaped_button);
- }
-
- @Override
- public void setBackgroundDrawable(Drawable drawable) {
- if (getBackground() == null || drawable == null) {
- super.setBackgroundDrawable(drawable);
- return;
- }
-
- int[] padding = new int[] { getPaddingLeft(),
- getPaddingTop(),
- getPaddingRight(),
- getPaddingBottom()
- };
- drawable.setLevel(getBackground().getLevel());
- super.setBackgroundDrawable(drawable);
-
- setPadding(padding[0], padding[1], padding[2], padding[3]);
- }
-
- @Override
- public void setBackgroundResource(int resId) {
- setBackgroundDrawable(getResources().getDrawable(resId));
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/SiteIdentityPopup.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/SiteIdentityPopup.java
deleted file mode 100644
index 14230a2ec..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/SiteIdentityPopup.java
+++ /dev/null
@@ -1,571 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.toolbar;
-
-import android.content.ClipData;
-import android.content.ClipboardManager;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.support.design.widget.Snackbar;
-import android.support.v4.content.ContextCompat;
-import android.widget.ImageView;
-import android.widget.Toast;
-import org.json.JSONException;
-import org.json.JSONArray;
-import org.mozilla.gecko.AboutPages;
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.SiteIdentity;
-import org.mozilla.gecko.SiteIdentity.SecurityMode;
-import org.mozilla.gecko.SiteIdentity.MixedMode;
-import org.mozilla.gecko.SiteIdentity.TrackingMode;
-import org.mozilla.gecko.SnackbarBuilder;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.widget.AnchoredPopup;
-import org.mozilla.gecko.widget.DoorHanger;
-import org.mozilla.gecko.widget.DoorHanger.OnButtonClickListener;
-import org.json.JSONObject;
-
-import android.app.Activity;
-import android.content.Context;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-import org.mozilla.gecko.widget.DoorhangerConfig;
-import org.mozilla.gecko.widget.SiteLogins;
-
-/**
- * SiteIdentityPopup is a singleton class that displays site identity data in
- * an arrow panel popup hanging from the lock icon in the browser toolbar.
- *
- * A site identity icon may be displayed in the url, and is set in <code>ToolbarDisplayLayout</code>.
- */
-public class SiteIdentityPopup extends AnchoredPopup implements GeckoEventListener {
-
- public static enum ButtonType { DISABLE, ENABLE, KEEP_BLOCKING, CANCEL, COPY }
-
- private static final String LOGTAG = "GeckoSiteIdentityPopup";
-
- private static final String MIXED_CONTENT_SUPPORT_URL =
- "https://support.mozilla.org/kb/how-does-insecure-content-affect-safety-android";
- private static final String TRACKING_CONTENT_SUPPORT_URL =
- "https://support.mozilla.org/kb/firefox-android-tracking-protection";
-
- // Placeholder string.
- private final static String FORMAT_S = "%s";
-
- private final Resources mResources;
- private SiteIdentity mSiteIdentity;
-
- private LinearLayout mIdentity;
-
- private LinearLayout mIdentityKnownContainer;
-
- private ImageView mIcon;
- private TextView mTitle;
- private TextView mSecurityState;
- private TextView mMixedContentActivity;
- private TextView mOwner;
- private TextView mOwnerSupplemental;
- private TextView mVerifier;
- private TextView mLink;
- private TextView mSiteSettingsLink;
-
- private View mDivider;
-
- private DoorHanger mTrackingContentNotification;
- private DoorHanger mSelectLoginDoorhanger;
-
- private final OnButtonClickListener mContentButtonClickListener;
-
- public SiteIdentityPopup(Context context) {
- super(context);
-
- mResources = mContext.getResources();
-
- mContentButtonClickListener = new ContentNotificationButtonListener();
-
- GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
- "Doorhanger:Logins",
- "Permissions:CheckResult");
- }
-
- @Override
- protected void init() {
- super.init();
-
- // Make the popup focusable so it doesn't inadvertently trigger click events elsewhere
- // which may reshow the popup (see bug 785156)
- setFocusable(true);
-
- LayoutInflater inflater = LayoutInflater.from(mContext);
- mIdentity = (LinearLayout) inflater.inflate(R.layout.site_identity, null);
- mContent.addView(mIdentity);
-
- mIdentityKnownContainer =
- (LinearLayout) mIdentity.findViewById(R.id.site_identity_known_container);
-
- mIcon = (ImageView) mIdentity.findViewById(R.id.site_identity_icon);
- mTitle = (TextView) mIdentity.findViewById(R.id.site_identity_title);
- mSecurityState = (TextView) mIdentity.findViewById(R.id.site_identity_state);
- mMixedContentActivity = (TextView) mIdentity.findViewById(R.id.mixed_content_activity);
-
- mOwner = (TextView) mIdentityKnownContainer.findViewById(R.id.owner);
- mOwnerSupplemental = (TextView) mIdentityKnownContainer.findViewById(R.id.owner_supplemental);
- mVerifier = (TextView) mIdentityKnownContainer.findViewById(R.id.verifier);
- mDivider = mIdentity.findViewById(R.id.divider_doorhanger);
-
- mLink = (TextView) mIdentity.findViewById(R.id.site_identity_link);
- mLink.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- Tabs.getInstance().loadUrlInTab(MIXED_CONTENT_SUPPORT_URL);
- }
- });
-
- mSiteSettingsLink = (TextView) mIdentity.findViewById(R.id.site_settings_link);
- }
-
- private void updateIdentity(final SiteIdentity siteIdentity) {
- if (!mInflated) {
- init();
- }
-
- final boolean isIdentityKnown = (siteIdentity.getSecurityMode() == SecurityMode.IDENTIFIED ||
- siteIdentity.getSecurityMode() == SecurityMode.VERIFIED);
- updateConnectionState(siteIdentity);
- toggleIdentityKnownContainerVisibility(isIdentityKnown);
-
- if (isIdentityKnown) {
- updateIdentityInformation(siteIdentity);
- }
-
- GeckoAppShell.notifyObservers("Permissions:Check", null);
- }
-
- @Override
- public void handleMessage(String event, JSONObject geckoObject) {
- if ("Doorhanger:Logins".equals(event)) {
- try {
- final Tab selectedTab = Tabs.getInstance().getSelectedTab();
- if (selectedTab != null) {
- final JSONObject data = geckoObject.getJSONObject("data");
- addLoginsToTab(data);
- }
- if (isShowing()) {
- addSelectLoginDoorhanger(selectedTab);
- }
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error accessing logins in Doorhanger:Logins message", e);
- }
- } else if ("Permissions:CheckResult".equals(event)) {
- final boolean hasPermissions = geckoObject.optBoolean("hasPermissions", false);
- if (hasPermissions) {
- mSiteSettingsLink.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- GeckoAppShell.notifyObservers("Permissions:Get", null);
- dismiss();
- }
- });
- }
-
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- mSiteSettingsLink.setVisibility(hasPermissions ? View.VISIBLE : View.GONE);
- }
- });
- }
- }
-
- private void addLoginsToTab(JSONObject data) throws JSONException {
- final JSONArray logins = data.getJSONArray("logins");
-
- final SiteLogins siteLogins = new SiteLogins(logins);
- Tabs.getInstance().getSelectedTab().setSiteLogins(siteLogins);
- }
-
- private void addSelectLoginDoorhanger(Tab tab) throws JSONException {
- final SiteLogins siteLogins = tab.getSiteLogins();
- if (siteLogins == null) {
- return;
- }
-
- final JSONArray logins = siteLogins.getLogins();
- if (logins.length() == 0) {
- return;
- }
-
- final JSONObject login = (JSONObject) logins.get(0);
-
- // Create button click listener for copying a password to the clipboard.
- final OnButtonClickListener buttonClickListener = new OnButtonClickListener() {
- Activity activity = (Activity) mContext;
- @Override
- public void onButtonClick(JSONObject response, DoorHanger doorhanger) {
- try {
- final int buttonId = response.getInt("callback");
- if (buttonId == ButtonType.COPY.ordinal()) {
- final ClipboardManager manager = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
- String password;
- if (response.has("password")) {
- // Click listener being called from List Dialog.
- password = response.optString("password");
- } else {
- password = login.getString("password");
- }
-
- manager.setPrimaryClip(ClipData.newPlainText("password", password));
-
- SnackbarBuilder.builder(activity)
- .message(R.string.doorhanger_login_select_toast_copy)
- .duration(Snackbar.LENGTH_SHORT)
- .buildAndShow();
- }
- dismiss();
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error handling Select login button click", e);
- SnackbarBuilder.builder(activity)
- .message(R.string.doorhanger_login_select_toast_copy_error)
- .duration(Snackbar.LENGTH_SHORT)
- .buildAndShow();
- }
- }
- };
-
- final DoorhangerConfig config = new DoorhangerConfig(DoorHanger.Type.LOGIN, buttonClickListener);
-
- // Set buttons.
- config.setButton(mContext.getString(R.string.button_cancel), ButtonType.CANCEL.ordinal(), false);
- config.setButton(mContext.getString(R.string.button_copy), ButtonType.COPY.ordinal(), true);
-
- // Set message.
- String username = ((JSONObject) logins.get(0)).getString("username");
- if (TextUtils.isEmpty(username)) {
- username = mContext.getString(R.string.doorhanger_login_no_username);
- }
-
- final String message = mContext.getString(R.string.doorhanger_login_select_message).replace(FORMAT_S, username);
- config.setMessage(message);
-
- // Set options.
- final JSONObject options = new JSONObject();
-
- // Add action text only if there are other logins to select.
- if (logins.length() > 1) {
-
- final JSONObject actionText = new JSONObject();
- actionText.put("type", "SELECT");
-
- final JSONObject bundle = new JSONObject();
- bundle.put("logins", logins);
-
- actionText.put("bundle", bundle);
- options.put("actionText", actionText);
- }
-
- config.setOptions(options);
-
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- if (!mInflated) {
- init();
- }
-
- removeSelectLoginDoorhanger();
-
- mSelectLoginDoorhanger = DoorHanger.Get(mContext, config);
- mContent.addView(mSelectLoginDoorhanger);
- mDivider.setVisibility(View.VISIBLE);
- }
- });
- }
-
- private void removeSelectLoginDoorhanger() {
- if (mSelectLoginDoorhanger != null) {
- mContent.removeView(mSelectLoginDoorhanger);
- mSelectLoginDoorhanger = null;
- }
- }
-
- private void toggleIdentityKnownContainerVisibility(final boolean isIdentityKnown) {
- final int identityInfoVisibility = isIdentityKnown ? View.VISIBLE : View.GONE;
- mIdentityKnownContainer.setVisibility(identityInfoVisibility);
- }
-
- /**
- * Update the Site Identity content to reflect connection state.
- *
- * The connection state should reflect the combination of:
- * a) Connection encryption
- * b) Mixed Content state (Active/Display Mixed content, loaded, blocked, none, etc)
- * and update the icons and strings to inform the user of that state.
- *
- * @param siteIdentity SiteIdentity information about the connection.
- */
- private void updateConnectionState(final SiteIdentity siteIdentity) {
- if (siteIdentity.getSecurityMode() == SecurityMode.CHROMEUI) {
- mSecurityState.setText(R.string.identity_connection_chromeui);
- mSecurityState.setTextColor(ContextCompat.getColor(mContext, R.color.placeholder_active_grey));
-
- mIcon.setImageResource(R.drawable.icon);
- clearSecurityStateIcon();
-
- mMixedContentActivity.setVisibility(View.GONE);
- mLink.setVisibility(View.GONE);
- } else if (!siteIdentity.isSecure()) {
- if (siteIdentity.getMixedModeActive() == MixedMode.MIXED_CONTENT_LOADED) {
- // Active Mixed Content loaded because user has disabled blocking.
- mIcon.setImageResource(R.drawable.lock_disabled);
- clearSecurityStateIcon();
- mMixedContentActivity.setVisibility(View.VISIBLE);
- mMixedContentActivity.setText(R.string.mixed_content_protection_disabled);
-
- mLink.setVisibility(View.VISIBLE);
- } else if (siteIdentity.getMixedModeDisplay() == MixedMode.MIXED_CONTENT_LOADED) {
- // Passive Mixed Content loaded.
- mIcon.setImageResource(R.drawable.lock_inactive);
- setSecurityStateIcon(R.drawable.warning_major, 1);
- mMixedContentActivity.setVisibility(View.VISIBLE);
- if (siteIdentity.getMixedModeActive() == MixedMode.MIXED_CONTENT_BLOCKED) {
- mMixedContentActivity.setText(R.string.mixed_content_blocked_some);
- } else {
- mMixedContentActivity.setText(R.string.mixed_content_display_loaded);
- }
- mLink.setVisibility(View.VISIBLE);
-
- } else {
- // Unencrypted connection with no mixed content.
- mIcon.setImageResource(R.drawable.globe_light);
- clearSecurityStateIcon();
-
- mMixedContentActivity.setVisibility(View.GONE);
- mLink.setVisibility(View.GONE);
- }
-
- mSecurityState.setText(R.string.identity_connection_insecure);
- mSecurityState.setTextColor(ContextCompat.getColor(mContext, R.color.placeholder_active_grey));
- } else {
- // Connection is secure.
- mIcon.setImageResource(R.drawable.lock_secure);
-
- setSecurityStateIcon(R.drawable.img_check, 2);
- mSecurityState.setTextColor(ContextCompat.getColor(mContext, R.color.affirmative_green));
- mSecurityState.setText(R.string.identity_connection_secure);
-
- // Mixed content has been blocked, if present.
- if (siteIdentity.getMixedModeActive() == MixedMode.MIXED_CONTENT_BLOCKED ||
- siteIdentity.getMixedModeDisplay() == MixedMode.MIXED_CONTENT_BLOCKED) {
- mMixedContentActivity.setVisibility(View.VISIBLE);
- mMixedContentActivity.setText(R.string.mixed_content_blocked_all);
- mLink.setVisibility(View.VISIBLE);
- } else {
- mMixedContentActivity.setVisibility(View.GONE);
- mLink.setVisibility(View.GONE);
- }
- }
- }
-
- private void clearSecurityStateIcon() {
- mSecurityState.setCompoundDrawablePadding(0);
- mSecurityState.setCompoundDrawables(null, null, null, null);
- }
-
- private void setSecurityStateIcon(int resource, int factor) {
- final Drawable stateIcon = ContextCompat.getDrawable(mContext, resource);
- stateIcon.setBounds(0, 0, stateIcon.getIntrinsicWidth() / factor, stateIcon.getIntrinsicHeight() / factor);
- mSecurityState.setCompoundDrawables(stateIcon, null, null, null);
- mSecurityState.setCompoundDrawablePadding((int) mResources.getDimension(R.dimen.doorhanger_drawable_padding));
- }
- private void updateIdentityInformation(final SiteIdentity siteIdentity) {
- String owner = siteIdentity.getOwner();
- if (owner == null) {
- mOwner.setVisibility(View.GONE);
- mOwnerSupplemental.setVisibility(View.GONE);
- } else {
- mOwner.setVisibility(View.VISIBLE);
- mOwner.setText(owner);
-
- // Supplemental data is optional.
- final String supplemental = siteIdentity.getSupplemental();
- if (!TextUtils.isEmpty(supplemental)) {
- mOwnerSupplemental.setText(supplemental);
- mOwnerSupplemental.setVisibility(View.VISIBLE);
- } else {
- mOwnerSupplemental.setVisibility(View.GONE);
- }
- }
-
- final String verifier = siteIdentity.getVerifier();
- mVerifier.setText(verifier);
- }
-
- private void addTrackingContentNotification(boolean blocked) {
- // Remove any existing tracking content notification.
- removeTrackingContentNotification();
-
- final DoorhangerConfig config = new DoorhangerConfig(DoorHanger.Type.TRACKING, mContentButtonClickListener);
-
- final int icon = blocked ? R.drawable.shield_enabled : R.drawable.shield_disabled;
-
- final JSONObject options = new JSONObject();
- final JSONObject tracking = new JSONObject();
- try {
- tracking.put("enabled", blocked);
- options.put("tracking_protection", tracking);
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error adding tracking protection options", e);
- }
- config.setOptions(options);
-
- config.setLink(mContext.getString(R.string.learn_more), TRACKING_CONTENT_SUPPORT_URL);
-
- addNotificationButtons(config, blocked);
-
- mTrackingContentNotification = DoorHanger.Get(mContext, config);
-
- mTrackingContentNotification.setIcon(icon);
-
- mContent.addView(mTrackingContentNotification);
- mDivider.setVisibility(View.VISIBLE);
- }
-
- private void removeTrackingContentNotification() {
- if (mTrackingContentNotification != null) {
- mContent.removeView(mTrackingContentNotification);
- mTrackingContentNotification = null;
- }
- }
-
- private void addNotificationButtons(DoorhangerConfig config, boolean blocked) {
- if (blocked) {
- config.setButton(mContext.getString(R.string.disable_protection), ButtonType.DISABLE.ordinal(), false);
- } else {
- config.setButton(mContext.getString(R.string.enable_protection), ButtonType.ENABLE.ordinal(), true);
- }
- }
-
- /*
- * @param identityData A JSONObject that holds the current tab's identity data.
- */
- void setSiteIdentity(SiteIdentity siteIdentity) {
- mSiteIdentity = siteIdentity;
- }
-
- @Override
- public void show() {
- if (mSiteIdentity == null) {
- Log.e(LOGTAG, "Can't show site identity popup for undefined state");
- return;
- }
-
- // Verified about: pages have the CHROMEUI SiteIdentity, however there can also
- // be unverified about: pages for which "This site's identity is unknown" or
- // "This is a secure Firefox page" are both misleading, so don't show a popup.
- final Tab selectedTab = Tabs.getInstance().getSelectedTab();
- if (selectedTab != null &&
- AboutPages.isAboutPage(selectedTab.getURL()) &&
- mSiteIdentity.getSecurityMode() != SecurityMode.CHROMEUI) {
- Log.d(LOGTAG, "We don't show site identity popups for unverified about: pages");
- return;
- }
-
- updateIdentity(mSiteIdentity);
-
- final TrackingMode trackingMode = mSiteIdentity.getTrackingMode();
- if (trackingMode != TrackingMode.UNKNOWN) {
- addTrackingContentNotification(trackingMode == TrackingMode.TRACKING_CONTENT_BLOCKED);
- }
-
- try {
- addSelectLoginDoorhanger(selectedTab);
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error adding selectLogin doorhanger", e);
- }
-
- if (mSiteIdentity.getSecurityMode() == SecurityMode.CHROMEUI) {
- // For about: pages we display the product icon in place of the verified/globe
- // image, hence we don't also set the favicon (for most about pages the
- // favicon is the product icon, hence we'd be showing the same icon twice).
- mTitle.setText(R.string.moz_app_displayname);
- } else {
- mTitle.setText(selectedTab.getBaseDomain());
-
- final Bitmap favicon = selectedTab.getFavicon();
- if (favicon != null) {
- final Drawable faviconDrawable = new BitmapDrawable(mResources, favicon);
- final int dimen = (int) mResources.getDimension(R.dimen.browser_toolbar_favicon_size);
- faviconDrawable.setBounds(0, 0, dimen, dimen);
-
- mTitle.setCompoundDrawables(faviconDrawable, null, null, null);
- mTitle.setCompoundDrawablePadding((int) mContext.getResources().getDimension(R.dimen.doorhanger_drawable_padding));
- }
- }
-
- showDividers();
-
- super.show();
- }
-
- // Show the right dividers
- private void showDividers() {
- final int count = mContent.getChildCount();
- DoorHanger lastVisibleDoorHanger = null;
-
- for (int i = 0; i < count; i++) {
- final View child = mContent.getChildAt(i);
-
- if (!(child instanceof DoorHanger)) {
- continue;
- }
-
- DoorHanger dh = (DoorHanger) child;
- dh.showDivider();
- if (dh.getVisibility() == View.VISIBLE) {
- lastVisibleDoorHanger = dh;
- }
- }
-
- if (lastVisibleDoorHanger != null) {
- lastVisibleDoorHanger.hideDivider();
- }
- }
-
- void destroy() {
- GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
- "Doorhanger:Logins",
- "Permissions:CheckResult");
- }
-
- @Override
- public void dismiss() {
- super.dismiss();
- removeTrackingContentNotification();
- removeSelectLoginDoorhanger();
- mTitle.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
- mDivider.setVisibility(View.GONE);
- }
-
- private class ContentNotificationButtonListener implements OnButtonClickListener {
- @Override
- public void onButtonClick(JSONObject response, DoorHanger doorhanger) {
- GeckoAppShell.notifyObservers("Session:Reload", response.toString());
- dismiss();
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/TabCounter.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/TabCounter.java
deleted file mode 100644
index 1e0ca516b..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/TabCounter.java
+++ /dev/null
@@ -1,154 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.toolbar;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.animation.Rotate3DAnimation;
-import org.mozilla.gecko.widget.themed.ThemedTextSwitcher;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.animation.AlphaAnimation;
-import android.view.animation.AnimationSet;
-import android.widget.ViewSwitcher;
-
-public class TabCounter extends ThemedTextSwitcher
- implements ViewSwitcher.ViewFactory {
-
- private static final float CENTER_X = 0.5f;
- private static final float CENTER_Y = 1.25f;
- private static final int DURATION = 500;
- private static final float Z_DISTANCE = 200;
-
- private final AnimationSet mFlipInForward;
- private final AnimationSet mFlipInBackward;
- private final AnimationSet mFlipOutForward;
- private final AnimationSet mFlipOutBackward;
- private final LayoutInflater mInflater;
- private final int mLayoutId;
-
- private int mCount;
- public static final int MAX_VISIBLE_TABS = 99;
- public static final String SO_MANY_TABS_OPEN = "∞";
-
- private enum FadeMode {
- FADE_IN,
- FADE_OUT
- }
-
- public TabCounter(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabCounter);
- mLayoutId = a.getResourceId(R.styleable.TabCounter_android_layout, R.layout.tabs_counter);
- a.recycle();
-
- mInflater = LayoutInflater.from(context);
-
- mFlipInForward = createAnimation(-90, 0, FadeMode.FADE_IN, -1 * Z_DISTANCE, false);
- mFlipInBackward = createAnimation(90, 0, FadeMode.FADE_IN, Z_DISTANCE, false);
- mFlipOutForward = createAnimation(0, -90, FadeMode.FADE_OUT, -1 * Z_DISTANCE, true);
- mFlipOutBackward = createAnimation(0, 90, FadeMode.FADE_OUT, Z_DISTANCE, true);
-
- removeAllViews();
- setFactory(this);
-
- if (Versions.feature16Plus) {
- // This adds the TextSwitcher to the a11y node tree, where we in turn
- // could make it return an empty info node. If we don't do this the
- // TextSwitcher's child TextViews get picked up, and we don't want
- // that since the tabs ImageButton is already properly labeled for
- // accessibility.
- setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
- setAccessibilityDelegate(new View.AccessibilityDelegate() {
- @Override
- public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {}
- });
- }
- }
-
- void setCountWithAnimation(int count) {
- // Don't animate from initial state
- if (mCount == 0) {
- setCount(count);
- return;
- }
-
- if (mCount == count) {
- return;
- }
-
- // don't animate if there are still over MAX_VISIBLE_TABS tabs open
- if (mCount > MAX_VISIBLE_TABS && count > MAX_VISIBLE_TABS) {
- mCount = count;
- return;
- }
-
- if (count < mCount) {
- setInAnimation(mFlipInBackward);
- setOutAnimation(mFlipOutForward);
- } else {
- setInAnimation(mFlipInForward);
- setOutAnimation(mFlipOutBackward);
- }
-
- // Eliminate screen artifact. Set explicit In/Out animation pair order. This will always
- // animate pair in In->Out child order, prevent alternating use of the Out->In case.
- setDisplayedChild(0);
-
- // Set In value, trigger animation to Out value
- setCurrentText(formatForDisplay(mCount));
- setText(formatForDisplay(count));
-
- mCount = count;
- }
-
- private String formatForDisplay(int count) {
- if (count > MAX_VISIBLE_TABS) {
- return SO_MANY_TABS_OPEN;
- }
- return String.valueOf(count);
- }
-
- void setCount(int count) {
- setCurrentText(formatForDisplay(count));
- mCount = count;
- }
-
- // Alpha animations in editing mode cause action bar corruption on the
- // Nexus 7 (bug 961749). As a workaround, skip these animations in editing
- // mode.
- void onEnterEditingMode() {
- final int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- getChildAt(i).clearAnimation();
- }
- }
-
- private AnimationSet createAnimation(float startAngle, float endAngle,
- FadeMode fadeMode,
- float zEnd, boolean reverse) {
- final Context context = getContext();
- AnimationSet set = new AnimationSet(context, null);
- set.addAnimation(new Rotate3DAnimation(startAngle, endAngle, CENTER_X, CENTER_Y, zEnd, reverse));
- set.addAnimation(fadeMode == FadeMode.FADE_IN ? new AlphaAnimation(0.0f, 1.0f) :
- new AlphaAnimation(1.0f, 0.0f));
- set.setDuration(DURATION);
- set.setInterpolator(context, android.R.anim.accelerate_interpolator);
- return set;
- }
-
- @Override
- public View makeView() {
- return mInflater.inflate(mLayoutId, null);
- }
-
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarDisplayLayout.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarDisplayLayout.java
deleted file mode 100644
index 163ed4a51..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarDisplayLayout.java
+++ /dev/null
@@ -1,530 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.toolbar;
-
-import java.util.Arrays;
-import java.util.EnumSet;
-import java.util.List;
-
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.AboutPages;
-import org.mozilla.gecko.BrowserApp;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.reader.ReaderModeUtils;
-import org.mozilla.gecko.SiteIdentity;
-import org.mozilla.gecko.SiteIdentity.MixedMode;
-import org.mozilla.gecko.SiteIdentity.SecurityMode;
-import org.mozilla.gecko.SiteIdentity.TrackingMode;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.animation.PropertyAnimator;
-import org.mozilla.gecko.animation.ViewHelper;
-import org.mozilla.gecko.toolbar.BrowserToolbarTabletBase.ForwardButtonAnimation;
-import org.mozilla.gecko.Experiments;
-import org.mozilla.gecko.util.HardwareUtils;
-import org.mozilla.gecko.util.StringUtils;
-import org.mozilla.gecko.widget.themed.ThemedLinearLayout;
-import org.mozilla.gecko.widget.themed.ThemedTextView;
-
-import android.content.Context;
-import android.os.SystemClock;
-import android.support.annotation.NonNull;
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.SpannableStringBuilder;
-import android.text.TextUtils;
-import android.text.style.ForegroundColorSpan;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.Button;
-import android.widget.ImageButton;
-
-import com.keepsafe.switchboard.SwitchBoard;
-
-/**
-* {@code ToolbarDisplayLayout} is the UI for when the toolbar is in
-* display state. It's used to display the state of the currently selected
-* tab. It should always be updated through a single entry point
-* (updateFromTab) and should never track any tab events or gecko messages
-* on its own to keep it as dumb as possible.
-*
-* The UI has two possible modes: progress and display which are triggered
-* when UpdateFlags.PROGRESS is used depending on the current tab state.
-* The progress mode is triggered when the tab is loading a page. Display mode
-* is used otherwise.
-*
-* {@code ToolbarDisplayLayout} is meant to be owned by {@code BrowserToolbar}
-* which is the main event bus for the toolbar subsystem.
-*/
-public class ToolbarDisplayLayout extends ThemedLinearLayout {
-
- private static final String LOGTAG = "GeckoToolbarDisplayLayout";
- private boolean mTrackingProtectionEnabled;
-
- // To be used with updateFromTab() to allow the caller
- // to give enough context for the requested state change.
- enum UpdateFlags {
- TITLE,
- FAVICON,
- PROGRESS,
- SITE_IDENTITY,
- PRIVATE_MODE,
-
- // Disable any animation that might be
- // triggered from this state change. Mostly
- // used on tab switches, see BrowserToolbar.
- DISABLE_ANIMATIONS
- }
-
- private enum UIMode {
- PROGRESS,
- DISPLAY
- }
-
- interface OnStopListener {
- Tab onStop();
- }
-
- interface OnTitleChangeListener {
- void onTitleChange(CharSequence title);
- }
-
- private final BrowserApp mActivity;
-
- private UIMode mUiMode;
-
- private boolean mIsAttached;
-
- private final ThemedTextView mTitle;
- private final int mTitlePadding;
- private ToolbarPrefs mPrefs;
- private OnTitleChangeListener mTitleChangeListener;
-
- private final ImageButton mSiteSecurity;
-
- private final ImageButton mStop;
- private OnStopListener mStopListener;
-
- private final PageActionLayout mPageActionLayout;
-
- private final SiteIdentityPopup mSiteIdentityPopup;
- private int mSecurityImageLevel;
-
- // Security level constants, which map to the icons / levels defined in:
- // http://dxr.mozilla.org/mozilla-central/source/mobile/android/base/java/org/mozilla/gecko/resources/drawable/site_security_level.xml
- // Default level (unverified pages) - globe icon:
- private static final int LEVEL_DEFAULT_GLOBE = 0;
- // Levels for displaying Mixed Content state icons.
- private static final int LEVEL_WARNING_MINOR = 3;
- private static final int LEVEL_LOCK_DISABLED = 4;
- // Levels for displaying Tracking Protection state icons.
- private static final int LEVEL_SHIELD_ENABLED = 5;
- private static final int LEVEL_SHIELD_DISABLED = 6;
- // Icon used for about:home
- private static final int LEVEL_SEARCH_ICON = 999;
-
- private final ForegroundColorSpan mUrlColorSpan;
- private final ForegroundColorSpan mPrivateUrlColorSpan;
- private final ForegroundColorSpan mBlockedColorSpan;
- private final ForegroundColorSpan mDomainColorSpan;
- private final ForegroundColorSpan mPrivateDomainColorSpan;
- private final ForegroundColorSpan mCertificateOwnerColorSpan;
-
- public ToolbarDisplayLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- setOrientation(HORIZONTAL);
-
- mActivity = (BrowserApp) context;
-
- LayoutInflater.from(context).inflate(R.layout.toolbar_display_layout, this);
-
- mTitle = (ThemedTextView) findViewById(R.id.url_bar_title);
- mTitlePadding = mTitle.getPaddingRight();
-
- mUrlColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.url_bar_urltext));
- mPrivateUrlColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.url_bar_urltext_private));
- mBlockedColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.url_bar_blockedtext));
- mDomainColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.url_bar_domaintext));
- mPrivateDomainColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.url_bar_domaintext_private));
- mCertificateOwnerColorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.affirmative_green));
-
- mSiteSecurity = (ImageButton) findViewById(R.id.site_security);
-
- mSiteIdentityPopup = new SiteIdentityPopup(mActivity);
- mSiteIdentityPopup.setAnchor(this);
- mSiteIdentityPopup.setOnVisibilityChangeListener(mActivity);
-
- mStop = (ImageButton) findViewById(R.id.stop);
- mPageActionLayout = (PageActionLayout) findViewById(R.id.page_action_layout);
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- mIsAttached = true;
-
- mSiteSecurity.setOnClickListener(new Button.OnClickListener() {
- @Override
- public void onClick(View view) {
- mSiteIdentityPopup.show();
- }
- });
-
- mStop.setOnClickListener(new Button.OnClickListener() {
- @Override
- public void onClick(View v) {
- if (mStopListener != null) {
- // Force toolbar to switch to Display mode
- // immediately based on the stopped tab.
- final Tab tab = mStopListener.onStop();
- if (tab != null) {
- updateUiMode(UIMode.DISPLAY);
- }
- }
- }
- });
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- mIsAttached = false;
- }
-
- @Override
- public void setNextFocusDownId(int nextId) {
- mStop.setNextFocusDownId(nextId);
- mSiteSecurity.setNextFocusDownId(nextId);
- mPageActionLayout.setNextFocusDownId(nextId);
- }
-
- void setToolbarPrefs(final ToolbarPrefs prefs) {
- mPrefs = prefs;
- }
-
- void updateFromTab(@NonNull Tab tab, EnumSet<UpdateFlags> flags) {
- // Several parts of ToolbarDisplayLayout's state depends
- // on the views being attached to the view tree.
- if (!mIsAttached) {
- return;
- }
-
- if (flags.contains(UpdateFlags.TITLE)) {
- updateTitle(tab);
- }
-
- if (flags.contains(UpdateFlags.SITE_IDENTITY)) {
- updateSiteIdentity(tab);
- }
-
- if (flags.contains(UpdateFlags.PROGRESS)) {
- updateProgress(tab);
- }
-
- if (flags.contains(UpdateFlags.PRIVATE_MODE)) {
- mTitle.setPrivateMode(tab.isPrivate());
- }
- }
-
- void setTitle(CharSequence title) {
- mTitle.setText(title);
-
- if (mTitleChangeListener != null) {
- mTitleChangeListener.onTitleChange(title);
- }
- }
-
- private void updateTitle(@NonNull Tab tab) {
- // Keep the title unchanged if there's no selected tab,
- // or if the tab is entering reader mode.
- if (tab.isEnteringReaderMode()) {
- return;
- }
-
- final String url = tab.getURL();
-
- // Setting a null title will ensure we just see the
- // "Enter Search or Address" placeholder text.
- if (AboutPages.isTitlelessAboutPage(url)) {
- setTitle(null);
- setContentDescription(null);
- return;
- }
-
- // Show the about:blocked page title in red, regardless of prefs
- if (tab.getErrorType() == Tab.ErrorType.BLOCKED) {
- final String title = tab.getDisplayTitle();
-
- final SpannableStringBuilder builder = new SpannableStringBuilder(title);
- builder.setSpan(mBlockedColorSpan, 0, title.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
-
- setTitle(builder);
- setContentDescription(null);
- return;
- }
-
- final String baseDomain = tab.getBaseDomain();
-
- String strippedURL = stripAboutReaderURL(url);
-
- final boolean isHttpOrHttps = StringUtils.isHttpOrHttps(strippedURL);
-
- if (mPrefs.shouldTrimUrls()) {
- strippedURL = StringUtils.stripCommonSubdomains(StringUtils.stripScheme(strippedURL));
- }
-
- // The URL bar does not support RTL currently (See bug 928688 and meta bug 702845).
- // Displaying a URL using RTL (or mixed) characters can lead to an undesired reordering
- // of elements of the URL. That's why we are forcing the URL to use LTR (bug 1284372).
- strippedURL = StringUtils.forceLTR(strippedURL);
-
- // This value is not visible to screen readers but we rely on it when running UI tests. Screen
- // readers will instead focus BrowserToolbar and read the "base domain" from there. UI tests
- // will read the content description to obtain the full URL for performing assertions.
- setContentDescription(strippedURL);
-
- final SiteIdentity siteIdentity = tab.getSiteIdentity();
- if (siteIdentity.hasOwner() && SwitchBoard.isInExperiment(mActivity, Experiments.URLBAR_SHOW_EV_CERT_OWNER)) {
- // Show Owner of EV certificate as title
- updateTitleFromSiteIdentity(siteIdentity);
- } else if (isHttpOrHttps && !HardwareUtils.isTablet() && !TextUtils.isEmpty(baseDomain)
- && SwitchBoard.isInExperiment(mActivity, Experiments.URLBAR_SHOW_ORIGIN_ONLY)) {
- // Show just the base domain as title
- setTitle(baseDomain);
- } else {
- // Display full URL with base domain highlighted as title
- updateAndColorTitleFromFullURL(strippedURL, baseDomain, tab.isPrivate());
- }
- }
-
- private void updateTitleFromSiteIdentity(SiteIdentity siteIdentity) {
- final String title;
-
- if (siteIdentity.hasCountry()) {
- title = String.format("%s (%s)", siteIdentity.getOwner(), siteIdentity.getCountry());
- } else {
- title = siteIdentity.getOwner();
- }
-
- final SpannableString spannable = new SpannableString(title);
- spannable.setSpan(mCertificateOwnerColorSpan, 0, title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
-
- setTitle(spannable);
- }
-
- private void updateAndColorTitleFromFullURL(String url, String baseDomain, boolean isPrivate) {
- if (TextUtils.isEmpty(baseDomain)) {
- setTitle(url);
- return;
- }
-
- int index = url.indexOf(baseDomain);
- if (index == -1) {
- setTitle(url);
- return;
- }
-
- final SpannableStringBuilder builder = new SpannableStringBuilder(url);
-
- builder.setSpan(isPrivate ? mPrivateUrlColorSpan : mUrlColorSpan, 0, url.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- builder.setSpan(isPrivate ? mPrivateDomainColorSpan : mDomainColorSpan,
- index, index + baseDomain.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
-
- setTitle(builder);
- }
-
- private String stripAboutReaderURL(final String url) {
- if (!AboutPages.isAboutReader(url)) {
- return url;
- }
-
- return ReaderModeUtils.stripAboutReaderUrl(url);
- }
-
- private void updateSiteIdentity(@NonNull Tab tab) {
- final SiteIdentity siteIdentity = tab.getSiteIdentity();
-
- mSiteIdentityPopup.setSiteIdentity(siteIdentity);
-
- final SecurityMode securityMode;
- final MixedMode activeMixedMode;
- final MixedMode displayMixedMode;
- final TrackingMode trackingMode;
- if (siteIdentity == null) {
- securityMode = SecurityMode.UNKNOWN;
- activeMixedMode = MixedMode.UNKNOWN;
- displayMixedMode = MixedMode.UNKNOWN;
- trackingMode = TrackingMode.UNKNOWN;
- } else {
- securityMode = siteIdentity.getSecurityMode();
- activeMixedMode = siteIdentity.getMixedModeActive();
- displayMixedMode = siteIdentity.getMixedModeDisplay();
- trackingMode = siteIdentity.getTrackingMode();
- }
-
- // This is a bit tricky, but we have one icon and three potential indicators.
- // Default to the identity level
- int imageLevel = securityMode.ordinal();
-
- // about: pages should default to having no icon too (the same as SecurityMode.UNKNOWN), however
- // SecurityMode.CHROMEUI has a different ordinal - hence we need to manually reset it here.
- // (We then continue and process the tracking / mixed content icons as usual, even for about: pages, as they
- // can still load external sites.)
- if (securityMode == SecurityMode.CHROMEUI) {
- imageLevel = LEVEL_DEFAULT_GLOBE; // == SecurityMode.UNKNOWN.ordinal()
- }
-
- // Check to see if any protection was overridden first
- if (AboutPages.isTitlelessAboutPage(tab.getURL())) {
- // We always want to just show a search icon on about:home
- imageLevel = LEVEL_SEARCH_ICON;
- } else if (trackingMode == TrackingMode.TRACKING_CONTENT_LOADED) {
- imageLevel = LEVEL_SHIELD_DISABLED;
- } else if (trackingMode == TrackingMode.TRACKING_CONTENT_BLOCKED) {
- imageLevel = LEVEL_SHIELD_ENABLED;
- } else if (activeMixedMode == MixedMode.MIXED_CONTENT_LOADED) {
- imageLevel = LEVEL_LOCK_DISABLED;
- } else if (displayMixedMode == MixedMode.MIXED_CONTENT_LOADED) {
- imageLevel = LEVEL_WARNING_MINOR;
- }
-
- if (mSecurityImageLevel != imageLevel) {
- mSecurityImageLevel = imageLevel;
- mSiteSecurity.setImageLevel(mSecurityImageLevel);
- updatePageActions();
- }
-
- mTrackingProtectionEnabled = trackingMode == TrackingMode.TRACKING_CONTENT_BLOCKED;
- }
-
- private void updateProgress(@NonNull Tab tab) {
- final boolean shouldShowThrobber = tab.getState() == Tab.STATE_LOADING;
-
- updateUiMode(shouldShowThrobber ? UIMode.PROGRESS : UIMode.DISPLAY);
-
- if (Tab.STATE_SUCCESS == tab.getState() && mTrackingProtectionEnabled) {
- mActivity.showTrackingProtectionPromptIfApplicable();
- }
- }
-
- private void updateUiMode(UIMode uiMode) {
- if (mUiMode == uiMode) {
- return;
- }
-
- mUiMode = uiMode;
-
- // The "Throbber start" and "Throbber stop" log messages in this method
- // are needed by S1/S2 tests (http://mrcote.info/phonedash/#).
- // See discussion in Bug 804457. Bug 805124 tracks paring these down.
- if (mUiMode == UIMode.PROGRESS) {
- Log.i(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - Throbber start");
- } else {
- Log.i(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - Throbber stop");
- }
-
- updatePageActions();
- }
-
- private void updatePageActions() {
- final boolean isShowingProgress = (mUiMode == UIMode.PROGRESS);
-
- mStop.setVisibility(isShowingProgress ? View.VISIBLE : View.GONE);
- mPageActionLayout.setVisibility(!isShowingProgress ? View.VISIBLE : View.GONE);
-
- // We want title to fill the whole space available for it when there are icons
- // being shown on the right side of the toolbar as the icons already have some
- // padding in them. This is just to avoid wasting space when icons are shown.
- mTitle.setPadding(0, 0, (!isShowingProgress ? mTitlePadding : 0), 0);
- }
-
- List<View> getFocusOrder() {
- return Arrays.asList(mSiteSecurity, mPageActionLayout, mStop);
- }
-
- void setOnStopListener(OnStopListener listener) {
- mStopListener = listener;
- }
-
- void setOnTitleChangeListener(OnTitleChangeListener listener) {
- mTitleChangeListener = listener;
- }
-
- /**
- * Update the Site Identity popup anchor.
- *
- * Tablet UI has a tablet-specific doorhanger anchor, so update it after all the views
- * are inflated.
- * @param view View to use as the anchor for the Site Identity popup.
- */
- void updateSiteIdentityAnchor(View view) {
- mSiteIdentityPopup.setAnchor(view);
- }
-
- void prepareForwardAnimation(PropertyAnimator anim, ForwardButtonAnimation animation, int width) {
- if (animation == ForwardButtonAnimation.HIDE) {
- // We animate these items individually, rather than this entire view,
- // so that we don't animate certain views, e.g. the stop button.
- anim.attach(mTitle,
- PropertyAnimator.Property.TRANSLATION_X,
- 0);
- anim.attach(mSiteSecurity,
- PropertyAnimator.Property.TRANSLATION_X,
- 0);
-
- // We're hiding the forward button. We're going to reset the margin before
- // the animation starts, so we shift these items to the right so that they don't
- // appear to move initially.
- ViewHelper.setTranslationX(mTitle, width);
- ViewHelper.setTranslationX(mSiteSecurity, width);
- } else {
- anim.attach(mTitle,
- PropertyAnimator.Property.TRANSLATION_X,
- width);
- anim.attach(mSiteSecurity,
- PropertyAnimator.Property.TRANSLATION_X,
- width);
- }
- }
-
- void finishForwardAnimation() {
- ViewHelper.setTranslationX(mTitle, 0);
- ViewHelper.setTranslationX(mSiteSecurity, 0);
- }
-
- void prepareStartEditingAnimation() {
- // Hide page actions/stop buttons immediately
- ViewHelper.setAlpha(mPageActionLayout, 0);
- ViewHelper.setAlpha(mStop, 0);
- }
-
- void prepareStopEditingAnimation(PropertyAnimator anim) {
- // Fade toolbar buttons (page actions, stop) after the entry
- // is shrunk back to its original size.
- anim.attach(mPageActionLayout,
- PropertyAnimator.Property.ALPHA,
- 1);
-
- anim.attach(mStop,
- PropertyAnimator.Property.ALPHA,
- 1);
- }
-
- boolean dismissSiteIdentityPopup() {
- if (mSiteIdentityPopup != null && mSiteIdentityPopup.isShowing()) {
- mSiteIdentityPopup.dismiss();
- return true;
- }
-
- return false;
- }
-
- void destroy() {
- mSiteIdentityPopup.destroy();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditLayout.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditLayout.java
deleted file mode 100644
index c9731a401..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditLayout.java
+++ /dev/null
@@ -1,348 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.toolbar;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.graphics.drawable.Drawable;
-import android.speech.RecognizerIntent;
-import android.widget.Button;
-import android.widget.ImageButton;
-import org.mozilla.gecko.ActivityHandlerHelper;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.animation.PropertyAnimator;
-import org.mozilla.gecko.animation.PropertyAnimator.PropertyAnimationListener;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener;
-import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener;
-import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener;
-import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
-import org.mozilla.gecko.util.ActivityResultHandler;
-import org.mozilla.gecko.util.DrawableUtil;
-import org.mozilla.gecko.util.HardwareUtils;
-import org.mozilla.gecko.util.StringUtils;
-import org.mozilla.gecko.util.InputOptionsUtils;
-import org.mozilla.gecko.widget.themed.ThemedLinearLayout;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.ImageView;
-
-import java.util.List;
-
-/**
-* {@code ToolbarEditLayout} is the UI for when the toolbar is in
-* edit state. It controls a text entry ({@code ToolbarEditText})
-* and its matching 'go' button which changes depending on the
-* current type of text in the entry.
-*/
-public class ToolbarEditLayout extends ThemedLinearLayout {
-
- public interface OnSearchStateChangeListener {
- public void onSearchStateChange(boolean isActive);
- }
-
- private final ImageView mSearchIcon;
-
- private final ToolbarEditText mEditText;
-
- private final ImageButton mVoiceInput;
- private final ImageButton mQrCode;
-
- private OnFocusChangeListener mFocusChangeListener;
-
- private boolean showKeyboardOnFocus = false; // Indicates if we need to show the keyboard after the app resumes
-
- public ToolbarEditLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- setOrientation(HORIZONTAL);
-
- LayoutInflater.from(context).inflate(R.layout.toolbar_edit_layout, this);
- mSearchIcon = (ImageView) findViewById(R.id.search_icon);
-
- mEditText = (ToolbarEditText) findViewById(R.id.url_edit_text);
-
- mVoiceInput = (ImageButton) findViewById(R.id.mic);
- mQrCode = (ImageButton) findViewById(R.id.qrcode);
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- if (HardwareUtils.isTablet()) {
- mSearchIcon.setVisibility(View.VISIBLE);
- }
-
- mEditText.setOnFocusChangeListener(new OnFocusChangeListener() {
- @Override
- public void onFocusChange(View v, boolean hasFocus) {
- if (mFocusChangeListener != null) {
- mFocusChangeListener.onFocusChange(ToolbarEditLayout.this, hasFocus);
-
- // Checking if voice and QR code input are enabled each time the user taps on the URL bar
- if (hasFocus) {
- if (voiceIsEnabled(getContext(), getResources().getString(R.string.voicesearch_prompt))) {
- mVoiceInput.setVisibility(View.VISIBLE);
- } else {
- mVoiceInput.setVisibility(View.GONE);
- }
-
- if (qrCodeIsEnabled(getContext())) {
- mQrCode.setVisibility(View.VISIBLE);
- } else {
- mQrCode.setVisibility(View.GONE);
- }
- }
- }
- }
- });
-
- mEditText.setOnSearchStateChangeListener(new OnSearchStateChangeListener() {
- @Override
- public void onSearchStateChange(boolean isActive) {
- updateSearchIcon(isActive);
- }
- });
-
- mVoiceInput.setOnClickListener(new Button.OnClickListener() {
- @Override
- public void onClick(View v) {
- launchVoiceRecognizer();
- }
- });
-
- mQrCode.setOnClickListener(new Button.OnClickListener() {
- @Override
- public void onClick(View v) {
- launchQRCodeReader();
- }
- });
-
- // Set an inactive search icon on tablet devices when in editing mode
- updateSearchIcon(false);
- }
-
- /**
- * Update the search icon at the left of the edittext based
- * on its state.
- *
- * @param isActive The state of the edittext. Active is when the initialized
- * text has changed and is not empty.
- */
- void updateSearchIcon(boolean isActive) {
- if (!HardwareUtils.isTablet()) {
- return;
- }
-
- // When on tablet show a magnifying glass in editing mode
- final int searchDrawableId = R.drawable.search_icon_active;
- final Drawable searchDrawable;
- if (!isActive) {
- searchDrawable = DrawableUtil.tintDrawableWithColorRes(getContext(), searchDrawableId, R.color.placeholder_grey);
- } else {
- if (isPrivateMode()) {
- searchDrawable = DrawableUtil.tintDrawableWithColorRes(getContext(), searchDrawableId, R.color.tabs_tray_icon_grey);
- } else {
- searchDrawable = getResources().getDrawable(searchDrawableId);
- }
- }
-
- mSearchIcon.setImageDrawable(searchDrawable);
- }
-
- @Override
- public void setOnFocusChangeListener(OnFocusChangeListener listener) {
- mFocusChangeListener = listener;
- }
-
- @Override
- public void setEnabled(boolean enabled) {
- super.setEnabled(enabled);
- mEditText.setEnabled(enabled);
- }
-
- @Override
- public void setPrivateMode(boolean isPrivate) {
- super.setPrivateMode(isPrivate);
- mEditText.setPrivateMode(isPrivate);
- }
-
- /**
- * Called when the parent gains focus (on app launch and resume)
- */
- public void onParentFocus() {
- if (showKeyboardOnFocus) {
- showKeyboardOnFocus = false;
-
- Activity activity = GeckoAppShell.getGeckoInterface().getActivity();
- activity.runOnUiThread(new Runnable() {
- public void run() {
- mEditText.requestFocus();
- showSoftInput();
- }
- });
- }
-
- // Checking if qr code is supported after resuming the app
- if (qrCodeIsEnabled(getContext())) {
- mQrCode.setVisibility(View.VISIBLE);
- } else {
- mQrCode.setVisibility(View.GONE);
- }
- }
-
- void setToolbarPrefs(final ToolbarPrefs prefs) {
- mEditText.setToolbarPrefs(prefs);
- }
-
- private void showSoftInput() {
- InputMethodManager imm =
- (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
- imm.showSoftInput(mEditText, InputMethodManager.SHOW_IMPLICIT);
- }
-
- void prepareShowAnimation(final PropertyAnimator animator) {
- if (animator == null) {
- mEditText.requestFocus();
- showSoftInput();
- return;
- }
-
- animator.addPropertyAnimationListener(new PropertyAnimationListener() {
- @Override
- public void onPropertyAnimationStart() {
- mEditText.requestFocus();
- }
-
- @Override
- public void onPropertyAnimationEnd() {
- showSoftInput();
- }
- });
- }
-
- void setOnCommitListener(OnCommitListener listener) {
- mEditText.setOnCommitListener(listener);
- }
-
- void setOnDismissListener(OnDismissListener listener) {
- mEditText.setOnDismissListener(listener);
- }
-
- void setOnFilterListener(OnFilterListener listener) {
- mEditText.setOnFilterListener(listener);
- }
-
- void onEditSuggestion(String suggestion) {
- mEditText.setText(suggestion);
- mEditText.setSelection(mEditText.getText().length());
- mEditText.requestFocus();
-
- showSoftInput();
- }
-
- void setText(String text) {
- mEditText.setText(text);
- }
-
- String getText() {
- return mEditText.getText().toString();
- }
-
- protected void saveTabEditingState(final TabEditingState editingState) {
- editingState.lastEditingText = mEditText.getNonAutocompleteText();
- editingState.selectionStart = mEditText.getSelectionStart();
- editingState.selectionEnd = mEditText.getSelectionEnd();
- }
-
- protected void restoreTabEditingState(final TabEditingState editingState) {
- mEditText.setText(editingState.lastEditingText);
- mEditText.setSelection(editingState.selectionStart, editingState.selectionEnd);
- }
-
- private boolean voiceIsEnabled(Context context, String prompt) {
- final boolean voiceIsSupported = InputOptionsUtils.supportsVoiceRecognizer(context, prompt);
- if (!voiceIsSupported) {
- return false;
- }
- return GeckoSharedPrefs.forApp(context)
- .getBoolean(GeckoPreferences.PREFS_VOICE_INPUT_ENABLED, true);
- }
-
- private void launchVoiceRecognizer() {
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "voice_input_launch");
- final Intent intent = InputOptionsUtils.createVoiceRecognizerIntent(getResources().getString(R.string.voicesearch_prompt));
-
- Activity activity = GeckoAppShell.getGeckoInterface().getActivity();
- ActivityHandlerHelper.startIntentForActivity(activity, intent, new ActivityResultHandler() {
- @Override
- public void onActivityResult(int resultCode, Intent data) {
- if (resultCode != Activity.RESULT_OK) {
- return;
- }
-
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "voice_input_success");
- // We have RESULT_OK, not RESULT_NO_MATCH so it should be safe to assume that
- // we have at least one match. We only need one: this will be
- // used for showing the user search engines with this search term in it.
- List<String> voiceStrings = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
- String text = voiceStrings.get(0);
- mEditText.setText(text);
- mEditText.setSelection(0, text.length());
-
- final InputMethodManager imm =
- (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
- imm.showSoftInput(mEditText, InputMethodManager.SHOW_IMPLICIT);
- }
- });
- }
-
- private boolean qrCodeIsEnabled(Context context) {
- final boolean qrCodeIsSupported = InputOptionsUtils.supportsQrCodeReader(context);
- if (!qrCodeIsSupported) {
- return false;
- }
- return GeckoSharedPrefs.forApp(context)
- .getBoolean(GeckoPreferences.PREFS_QRCODE_ENABLED, true);
- }
-
- private void launchQRCodeReader() {
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "qrcode_input_launch");
- final Intent intent = InputOptionsUtils.createQRCodeReaderIntent();
-
- Activity activity = GeckoAppShell.getGeckoInterface().getActivity();
- ActivityHandlerHelper.startIntentForActivity(activity, intent, new ActivityResultHandler() {
- @Override
- public void onActivityResult(int resultCode, Intent intent) {
- if (resultCode == Activity.RESULT_OK) {
- String text = intent.getStringExtra("SCAN_RESULT");
- if (!StringUtils.isSearchQuery(text, false)) {
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "qrcode_input_success");
- mEditText.setText(text);
- mEditText.selectAll();
-
- // Queuing up the keyboard show action.
- // At this point the app has not resumed yet, and trying to show
- // the keyboard will fail.
- showKeyboardOnFocus = true;
- }
- }
- // We can get the SCAN_RESULT_FORMAT, SCAN_RESULT_BYTES,
- // SCAN_RESULT_ORIENTATION and SCAN_RESULT_ERROR_CORRECTION_LEVEL
- // as well as the actual result, which may hold a URL.
- }
- });
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditText.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditText.java
deleted file mode 100644
index b385f815a..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditText.java
+++ /dev/null
@@ -1,630 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.toolbar;
-
-import org.mozilla.gecko.AboutPages;
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.CustomEditText;
-import org.mozilla.gecko.InputMethods;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener;
-import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener;
-import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener;
-import org.mozilla.gecko.toolbar.ToolbarEditLayout.OnSearchStateChangeListener;
-import org.mozilla.gecko.util.GamepadUtils;
-import org.mozilla.gecko.util.StringUtils;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.text.Editable;
-import android.text.NoCopySpan;
-import android.text.Selection;
-import android.text.Spanned;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.text.style.BackgroundColorSpan;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.View;
-import android.view.inputmethod.BaseInputConnection;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
-import android.view.inputmethod.InputConnectionWrapper;
-import android.view.inputmethod.InputMethodManager;
-import android.view.accessibility.AccessibilityEvent;
-import android.widget.TextView;
-
-/**
-* {@code ToolbarEditText} is the text entry used when the toolbar
-* is in edit state. It handles all the necessary input method machinery.
-* It's meant to be owned by {@code ToolbarEditLayout}.
-*/
-public class ToolbarEditText extends CustomEditText
- implements AutocompleteHandler {
-
- private static final String LOGTAG = "GeckoToolbarEditText";
- private static final NoCopySpan AUTOCOMPLETE_SPAN = new NoCopySpan.Concrete();
-
- private final Context mContext;
-
- private OnCommitListener mCommitListener;
- private OnDismissListener mDismissListener;
- private OnFilterListener mFilterListener;
- private OnSearchStateChangeListener mSearchStateChangeListener;
-
- private ToolbarPrefs mPrefs;
-
- // The previous autocomplete result returned to us
- private String mAutoCompleteResult = "";
- // Length of the user-typed portion of the result
- private int mAutoCompletePrefixLength;
- // If text change is due to us setting autocomplete
- private boolean mSettingAutoComplete;
- // Spans used for marking the autocomplete text
- private Object[] mAutoCompleteSpans;
- // Do not process autocomplete result
- private boolean mDiscardAutoCompleteResult;
-
- public ToolbarEditText(Context context, AttributeSet attrs) {
- super(context, attrs);
- mContext = context;
- }
-
- void setOnCommitListener(OnCommitListener listener) {
- mCommitListener = listener;
- }
-
- void setOnDismissListener(OnDismissListener listener) {
- mDismissListener = listener;
- }
-
- void setOnFilterListener(OnFilterListener listener) {
- mFilterListener = listener;
- }
-
- void setOnSearchStateChangeListener(OnSearchStateChangeListener listener) {
- mSearchStateChangeListener = listener;
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
- setOnKeyListener(new KeyListener());
- setOnKeyPreImeListener(new KeyPreImeListener());
- setOnSelectionChangedListener(new SelectionChangeListener());
- addTextChangedListener(new TextChangeListener());
- }
-
- @Override
- public void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
- super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
-
- // Make search icon inactive when edit toolbar search term isn't a user entered
- // search term
- final boolean isActive = !TextUtils.isEmpty(getText());
- if (mSearchStateChangeListener != null) {
- mSearchStateChangeListener.onSearchStateChange(isActive);
- }
-
- if (gainFocus) {
- resetAutocompleteState();
- return;
- }
-
- removeAutocomplete(getText());
-
- final InputMethodManager imm = InputMethods.getInputMethodManager(mContext);
- try {
- imm.restartInput(this);
- imm.hideSoftInputFromWindow(getWindowToken(), 0);
- } catch (NullPointerException e) {
- Log.e(LOGTAG, "InputMethodManagerService, why are you throwing"
- + " a NullPointerException? See bug 782096", e);
- }
- }
-
- @Override
- public void setText(final CharSequence text, final TextView.BufferType type) {
- final String textString = (text == null) ? "" : text.toString();
-
- // If we're on the home or private browsing page, we don't set the "about" url.
- final CharSequence finalText;
- if (AboutPages.isAboutHome(textString) || AboutPages.isAboutPrivateBrowsing(textString)) {
- finalText = "";
- } else {
- finalText = text;
- }
-
- super.setText(finalText, type);
-
- // Any autocomplete text would have been overwritten, so reset our autocomplete states.
- resetAutocompleteState();
- }
-
- @Override
- public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
- // We need to bypass the isShown() check in the default implementation
- // for TYPE_VIEW_TEXT_SELECTION_CHANGED events so that accessibility
- // services could detect a url change.
- if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED &&
- getParent() != null && !isShown()) {
- onInitializeAccessibilityEvent(event);
- dispatchPopulateAccessibilityEvent(event);
- getParent().requestSendAccessibilityEvent(this, event);
- } else {
- super.sendAccessibilityEventUnchecked(event);
- }
- }
-
- void setToolbarPrefs(final ToolbarPrefs prefs) {
- mPrefs = prefs;
- }
-
- /**
- * Mark the start of autocomplete changes so our text change
- * listener does not react to changes in autocomplete text
- */
- private void beginSettingAutocomplete() {
- beginBatchEdit();
- mSettingAutoComplete = true;
- }
-
- /**
- * Mark the end of autocomplete changes
- */
- private void endSettingAutocomplete() {
- mSettingAutoComplete = false;
- endBatchEdit();
- }
-
- /**
- * Reset autocomplete states to their initial values
- */
- private void resetAutocompleteState() {
- mAutoCompleteSpans = new Object[] {
- // Span to mark the autocomplete text
- AUTOCOMPLETE_SPAN,
- // Span to change the autocomplete text color
- new BackgroundColorSpan(getHighlightColor())
- };
-
- mAutoCompleteResult = "";
-
- // Pretend we already autocompleted the existing text,
- // so that actions like backspacing don't trigger autocompletion.
- mAutoCompletePrefixLength = getText().length();
-
- // Show the cursor.
- setCursorVisible(true);
- }
-
- protected String getNonAutocompleteText() {
- return getNonAutocompleteText(getText());
- }
-
- /**
- * Get the portion of text that is not marked as autocomplete text.
- *
- * @param text Current text content that may include autocomplete text
- */
- private static String getNonAutocompleteText(final Editable text) {
- final int start = text.getSpanStart(AUTOCOMPLETE_SPAN);
- if (start < 0) {
- // No autocomplete text; return the whole string.
- return text.toString();
- }
-
- // Only return the portion that's not autocomplete text
- return TextUtils.substring(text, 0, start);
- }
-
- /**
- * Remove any autocomplete text
- *
- * @param text Current text content that may include autocomplete text
- */
- private boolean removeAutocomplete(final Editable text) {
- final int start = text.getSpanStart(AUTOCOMPLETE_SPAN);
- if (start < 0) {
- // No autocomplete text
- return false;
- }
-
- beginSettingAutocomplete();
-
- // When we call delete() here, the autocomplete spans we set are removed as well.
- text.delete(start, text.length());
-
- // Keep mAutoCompletePrefixLength the same because the prefix has not changed.
- // Clear mAutoCompleteResult to make sure we get fresh autocomplete text next time.
- mAutoCompleteResult = "";
-
- // Reshow the cursor.
- setCursorVisible(true);
-
- endSettingAutocomplete();
- return true;
- }
-
- /**
- * Convert any autocomplete text to regular text
- *
- * @param text Current text content that may include autocomplete text
- */
- private boolean commitAutocomplete(final Editable text) {
- final int start = text.getSpanStart(AUTOCOMPLETE_SPAN);
- if (start < 0) {
- // No autocomplete text
- return false;
- }
-
- beginSettingAutocomplete();
-
- // Remove all spans here to convert from autocomplete text to regular text
- for (final Object span : mAutoCompleteSpans) {
- text.removeSpan(span);
- }
-
- // Keep mAutoCompleteResult the same because the result has not changed.
- // Reset mAutoCompletePrefixLength because the prefix now includes the autocomplete text.
- mAutoCompletePrefixLength = text.length();
-
- // Reshow the cursor.
- setCursorVisible(true);
-
- endSettingAutocomplete();
-
- // Filter on the new text
- if (mFilterListener != null) {
- mFilterListener.onFilter(text.toString(), null);
- }
- return true;
- }
-
- /**
- * Add autocomplete text based on the result URI.
- *
- * @param result Result URI to be turned into autocomplete text
- */
- @Override
- public final void onAutocomplete(final String result) {
- // If mDiscardAutoCompleteResult is true, we temporarily disabled
- // autocomplete (due to backspacing, etc.) and we should bail early.
- if (mDiscardAutoCompleteResult) {
- return;
- }
-
- if (!isEnabled() || result == null) {
- mAutoCompleteResult = "";
- return;
- }
-
- final Editable text = getText();
- final int textLength = text.length();
- final int resultLength = result.length();
- final int autoCompleteStart = text.getSpanStart(AUTOCOMPLETE_SPAN);
- mAutoCompleteResult = result;
-
- if (autoCompleteStart > -1) {
- // Autocomplete text already exists; we should replace existing autocomplete text.
-
- // If the result and the current text don't have the same prefixes,
- // the result is stale and we should wait for the another result to come in.
- if (!TextUtils.regionMatches(result, 0, text, 0, autoCompleteStart)) {
- return;
- }
-
- beginSettingAutocomplete();
-
- // Replace the existing autocomplete text with new one.
- // replace() preserves the autocomplete spans that we set before.
- text.replace(autoCompleteStart, textLength, result, autoCompleteStart, resultLength);
-
- // Reshow the cursor if there is no longer any autocomplete text.
- if (autoCompleteStart == resultLength) {
- setCursorVisible(true);
- }
-
- endSettingAutocomplete();
-
- } else {
- // No autocomplete text yet; we should add autocomplete text
-
- // If the result prefix doesn't match the current text,
- // the result is stale and we should wait for the another result to come in.
- if (resultLength <= textLength ||
- !TextUtils.regionMatches(result, 0, text, 0, textLength)) {
- return;
- }
-
- final Object[] spans = text.getSpans(textLength, textLength, Object.class);
- final int[] spanStarts = new int[spans.length];
- final int[] spanEnds = new int[spans.length];
- final int[] spanFlags = new int[spans.length];
-
- // Save selection/composing span bounds so we can restore them later.
- for (int i = 0; i < spans.length; i++) {
- final Object span = spans[i];
- final int spanFlag = text.getSpanFlags(span);
-
- // We don't care about spans that are not selection or composing spans.
- // For those spans, spanFlag[i] will be 0 and we don't restore them.
- if ((spanFlag & Spanned.SPAN_COMPOSING) == 0 &&
- (span != Selection.SELECTION_START) &&
- (span != Selection.SELECTION_END)) {
- continue;
- }
-
- spanStarts[i] = text.getSpanStart(span);
- spanEnds[i] = text.getSpanEnd(span);
- spanFlags[i] = spanFlag;
- }
-
- beginSettingAutocomplete();
-
- // First add trailing text.
- text.append(result, textLength, resultLength);
-
- // Restore selection/composing spans.
- for (int i = 0; i < spans.length; i++) {
- final int spanFlag = spanFlags[i];
- if (spanFlag == 0) {
- // Skip if the span was ignored before.
- continue;
- }
- text.setSpan(spans[i], spanStarts[i], spanEnds[i], spanFlag);
- }
-
- // Mark added text as autocomplete text.
- for (final Object span : mAutoCompleteSpans) {
- text.setSpan(span, textLength, resultLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- }
-
- // Hide the cursor.
- setCursorVisible(false);
-
- // Make sure the autocomplete text is visible. If the autocomplete text is too
- // long, it would appear the cursor will be scrolled out of view. However, this
- // is not the case in practice, because EditText still makes sure the cursor is
- // still in view.
- bringPointIntoView(resultLength);
-
- endSettingAutocomplete();
- }
- }
-
- private static boolean hasCompositionString(Editable content) {
- Object[] spans = content.getSpans(0, content.length(), Object.class);
-
- if (spans != null) {
- for (Object span : spans) {
- if ((content.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) {
- // Found composition string.
- return true;
- }
- }
- }
-
- return false;
- }
-
- /**
- * Code to handle deleting autocomplete first when backspacing.
- * If there is no autocomplete text, both removeAutocomplete() and commitAutocomplete()
- * are no-ops and return false. Therefore we can use them here without checking explicitly
- * if we have autocomplete text or not.
- */
- @Override
- public InputConnection onCreateInputConnection(final EditorInfo outAttrs) {
- final InputConnection ic = super.onCreateInputConnection(outAttrs);
- if (ic == null) {
- return null;
- }
-
- return new InputConnectionWrapper(ic, false) {
- @Override
- public boolean deleteSurroundingText(final int beforeLength, final int afterLength) {
- if (removeAutocomplete(getText())) {
- // If we have autocomplete text, the cursor is at the boundary between
- // regular and autocomplete text. So regardless of which direction we
- // are deleting, we should delete the autocomplete text first.
- // Make the IME aware that we interrupted the deleteSurroundingText call,
- // by restarting the IME.
- final InputMethodManager imm = InputMethods.getInputMethodManager(mContext);
- if (imm != null) {
- imm.restartInput(ToolbarEditText.this);
- }
- return false;
- }
- return super.deleteSurroundingText(beforeLength, afterLength);
- }
-
- private boolean removeAutocompleteOnComposing(final CharSequence text) {
- final Editable editable = getText();
- final int composingStart = BaseInputConnection.getComposingSpanStart(editable);
- final int composingEnd = BaseInputConnection.getComposingSpanEnd(editable);
- // We only delete the autocomplete text when the user is backspacing,
- // i.e. when the composing text is getting shorter.
- if (composingStart >= 0 &&
- composingEnd >= 0 &&
- (composingEnd - composingStart) > text.length() &&
- removeAutocomplete(editable)) {
- // Make the IME aware that we interrupted the setComposingText call,
- // by having finishComposingText() send change notifications to the IME.
- finishComposingText();
- setComposingRegion(composingStart, composingEnd);
- return true;
- }
- return false;
- }
-
- @Override
- public boolean commitText(CharSequence text, int newCursorPosition) {
- if (removeAutocompleteOnComposing(text)) {
- return false;
- }
- return super.commitText(text, newCursorPosition);
- }
-
- @Override
- public boolean setComposingText(final CharSequence text, final int newCursorPosition) {
- if (removeAutocompleteOnComposing(text)) {
- return false;
- }
- return super.setComposingText(text, newCursorPosition);
- }
- };
- }
-
- private class SelectionChangeListener implements OnSelectionChangedListener {
- @Override
- public void onSelectionChanged(final int selStart, final int selEnd) {
- // The user has repositioned the cursor somewhere. We need to adjust
- // the autocomplete text depending on where the new cursor is.
-
- final Editable text = getText();
- final int start = text.getSpanStart(AUTOCOMPLETE_SPAN);
-
- if (mSettingAutoComplete || start < 0 || (start == selStart && start == selEnd)) {
- // Do not commit autocomplete text if there is no autocomplete text
- // or if selection is still at start of autocomplete text
- return;
- }
-
- if (selStart <= start && selEnd <= start) {
- // The cursor is in user-typed text; remove any autocomplete text.
- removeAutocomplete(text);
- } else {
- // The cursor is in the autocomplete text; commit it so it becomes regular text.
- commitAutocomplete(text);
- }
- }
- }
-
- private class TextChangeListener implements TextWatcher {
- @Override
- public void afterTextChanged(final Editable editable) {
- if (!isEnabled() || mSettingAutoComplete) {
- return;
- }
-
- final String text = getNonAutocompleteText(editable);
- final int textLength = text.length();
- boolean doAutocomplete = mPrefs.shouldAutocomplete();
-
- if (StringUtils.isSearchQuery(text, false)) {
- doAutocomplete = false;
- } else if (mAutoCompletePrefixLength > textLength) {
- // If you're hitting backspace (the string is getting smaller), don't autocomplete
- doAutocomplete = false;
- }
-
- mAutoCompletePrefixLength = textLength;
-
- // If we are not autocompleting, we set mDiscardAutoCompleteResult to true
- // to discard any autocomplete results that are in-flight, and vice versa.
- mDiscardAutoCompleteResult = !doAutocomplete;
-
- if (doAutocomplete && mAutoCompleteResult.startsWith(text)) {
- // If this text already matches our autocomplete text, autocomplete likely
- // won't change. Just reuse the old autocomplete value.
- onAutocomplete(mAutoCompleteResult);
- doAutocomplete = false;
- } else {
- // Otherwise, remove the old autocomplete text
- // until any new autocomplete text gets added.
- removeAutocomplete(editable);
- }
-
- // Update search icon with an active state since user is typing
- if (mSearchStateChangeListener != null) {
- mSearchStateChangeListener.onSearchStateChange(textLength > 0);
- }
-
- if (mFilterListener != null) {
- mFilterListener.onFilter(text, doAutocomplete ? ToolbarEditText.this : null);
- }
- }
-
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count,
- int after) {
- // do nothing
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before,
- int count) {
- // do nothing
- }
- }
-
- private class KeyPreImeListener implements OnKeyPreImeListener {
- @Override
- public boolean onKeyPreIme(View v, int keyCode, KeyEvent event) {
- // We only want to process one event per tap
- if (event.getAction() != KeyEvent.ACTION_DOWN) {
- return false;
- }
-
- if (keyCode == KeyEvent.KEYCODE_ENTER) {
- // If the edit text has a composition string, don't submit the text yet.
- // ENTER is needed to commit the composition string.
- final Editable content = getText();
- if (!hasCompositionString(content)) {
- if (mCommitListener != null) {
- mCommitListener.onCommit();
- }
-
- return true;
- }
- }
-
- if (keyCode == KeyEvent.KEYCODE_BACK) {
- // Drop the virtual keyboard.
- clearFocus();
- return true;
- }
-
- return false;
- }
- }
-
- private class KeyListener implements View.OnKeyListener {
- @Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- if (keyCode == KeyEvent.KEYCODE_ENTER || GamepadUtils.isActionKey(event)) {
- if (event.getAction() != KeyEvent.ACTION_DOWN) {
- return true;
- }
-
- if (mCommitListener != null) {
- mCommitListener.onCommit();
- }
-
- return true;
- }
-
- if (GamepadUtils.isBackKey(event)) {
- if (mDismissListener != null) {
- mDismissListener.onDismiss();
- }
-
- return true;
- }
-
- if ((keyCode == KeyEvent.KEYCODE_DEL ||
- (keyCode == KeyEvent.KEYCODE_FORWARD_DEL)) &&
- removeAutocomplete(getText())) {
- // Delete autocomplete text when backspacing or forward deleting.
- return true;
- }
-
- return false;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarPrefs.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarPrefs.java
deleted file mode 100644
index f881de154..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarPrefs.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.toolbar;
-
-import org.mozilla.gecko.PrefsHelper;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.util.ThreadUtils;
-
-class ToolbarPrefs {
- private static final String PREF_AUTOCOMPLETE_ENABLED = "browser.urlbar.autocomplete.enabled";
- private static final String PREF_TRIM_URLS = "browser.urlbar.trimURLs";
-
- private static final String[] PREFS = {
- PREF_AUTOCOMPLETE_ENABLED,
- PREF_TRIM_URLS
- };
-
- private final TitlePrefsHandler HANDLER = new TitlePrefsHandler();
-
- private volatile boolean enableAutocomplete;
- private volatile boolean trimUrls;
-
- ToolbarPrefs() {
- // Skip autocompletion while Gecko is loading.
- // We will get the correct pref value once Gecko is loaded.
- enableAutocomplete = false;
- trimUrls = true;
- }
-
- boolean shouldAutocomplete() {
- return enableAutocomplete;
- }
-
- boolean shouldTrimUrls() {
- return trimUrls;
- }
-
- void open() {
- PrefsHelper.addObserver(PREFS, HANDLER);
- }
-
- void close() {
- PrefsHelper.removeObserver(HANDLER);
- }
-
- private void triggerTitleChangeListener() {
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- final Tabs tabs = Tabs.getInstance();
- final Tab tab = tabs.getSelectedTab();
- if (tab != null) {
- tabs.notifyListeners(tab, Tabs.TabEvents.TITLE);
- }
- }
- });
- }
-
- private class TitlePrefsHandler extends PrefsHelper.PrefHandlerBase {
- @Override
- public void prefValue(String pref, boolean value) {
- if (PREF_AUTOCOMPLETE_ENABLED.equals(pref)) {
- enableAutocomplete = value;
-
- } else if (PREF_TRIM_URLS.equals(pref)) {
- // Handles PREF_TRIM_URLS, which should usually be a boolean.
- if (value != trimUrls) {
- trimUrls = value;
- triggerTitleChangeListener();
- }
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarProgressView.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarProgressView.java
deleted file mode 100644
index 43181cbef..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarProgressView.java
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.mozilla.gecko.toolbar;
-
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.widget.themed.ThemedImageView;
-import org.mozilla.gecko.util.WeakReferenceHandler;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.os.Handler;
-import android.os.Message;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.animation.Animation;
-
-/**
- * Progress view used for page loads.
- *
- * Because we're given limited information about the page load progress, the
- * bar also includes incremental animation between each step to improve
- * perceived performance.
- */
-public class ToolbarProgressView extends ThemedImageView {
- private static final int MAX_PROGRESS = 10000;
- private static final int MSG_UPDATE = 0;
- private static final int MSG_HIDE = 1;
- private static final int STEPS = 10;
- private static final int DELAY = 40;
-
- private int mTargetProgress;
- private int mIncrement;
- private Rect mBounds;
- private Handler mHandler;
- private int mCurrentProgress;
-
- private PorterDuffColorFilter mPrivateBrowsingColorFilter;
-
- public ToolbarProgressView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- init(context);
- }
-
- public ToolbarProgressView(Context context, AttributeSet attrs) {
- super(context, attrs);
- init(context);
- }
-
- private void init(Context ctx) {
- mBounds = new Rect(0, 0, 0, 0);
- mTargetProgress = 0;
-
- mPrivateBrowsingColorFilter = new PorterDuffColorFilter(
- ContextCompat.getColor(ctx, R.color.private_browsing_purple), PorterDuff.Mode.SRC_IN);
-
- mHandler = new ToolbarProgressHandler(this);
- }
-
- @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;
- }
-
- @Override
- public void onDraw(Canvas canvas) {
- final Drawable d = getDrawable();
- d.setBounds(mBounds);
- d.draw(canvas);
- }
-
- /**
- * Immediately sets the progress bar to the given progress percentage.
- *
- * @param progress Percentage (0-100) to which progress bar should be set
- */
- void setProgress(int progressPercentage) {
- mCurrentProgress = mTargetProgress = getAbsoluteProgress(progressPercentage);
- updateBounds();
-
- clearMessages();
- }
-
- /**
- * Animates the progress bar from the current progress value to the given
- * progress percentage.
- *
- * @param progress Percentage (0-100) to which progress bar should be animated
- */
- void animateProgress(int progressPercentage) {
- final int absoluteProgress = getAbsoluteProgress(progressPercentage);
- if (absoluteProgress <= mTargetProgress) {
- // After we manually click stop, we can still receive page load
- // events (e.g., DOMContentLoaded). Updating for other updates
- // after a STOP event can freeze the progress bar, so guard against
- // that here.
- return;
- }
-
- mTargetProgress = absoluteProgress;
- mIncrement = (mTargetProgress - mCurrentProgress) / STEPS;
-
- clearMessages();
- mHandler.sendEmptyMessage(MSG_UPDATE);
- }
-
- private void clearMessages() {
- mHandler.removeMessages(MSG_UPDATE);
- mHandler.removeMessages(MSG_HIDE);
- }
-
- private int getAbsoluteProgress(int progressPercentage) {
- if (progressPercentage < 0) {
- return 0;
- }
-
- if (progressPercentage > 100) {
- return 100;
- }
-
- return progressPercentage * MAX_PROGRESS / 100;
- }
-
- private void updateBounds() {
- mBounds.right = getWidth() * mCurrentProgress / MAX_PROGRESS;
- invalidate();
- }
-
- @Override
- public void setPrivateMode(final boolean isPrivate) {
- super.setPrivateMode(isPrivate);
-
- // Note: android:tint is better but ColorStateLists are not supported until API 21.
- if (isPrivate) {
- setColorFilter(mPrivateBrowsingColorFilter);
- } else {
- clearColorFilter();
- }
- }
-
- private static class ToolbarProgressHandler extends WeakReferenceHandler<ToolbarProgressView> {
- public ToolbarProgressHandler(final ToolbarProgressView that) {
- super(that);
- }
-
- @Override
- public void handleMessage(Message msg) {
- final ToolbarProgressView that = mTarget.get();
- if (that == null) {
- return;
- }
-
- switch (msg.what) {
- case MSG_UPDATE:
- that.mCurrentProgress = Math.min(that.mTargetProgress, that.mCurrentProgress + that.mIncrement);
-
- that.updateBounds();
-
- if (that.mCurrentProgress < that.mTargetProgress) {
- final int delay = (that.mTargetProgress < MAX_PROGRESS) ? DELAY : DELAY / 4;
- sendMessageDelayed(that.mHandler.obtainMessage(msg.what), delay);
- } else if (that.mCurrentProgress == MAX_PROGRESS) {
- sendMessageDelayed(that.mHandler.obtainMessage(MSG_HIDE), DELAY);
- }
- break;
-
- case MSG_HIDE:
- that.setVisibility(View.GONE);
- break;
- }
- }
- };
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/trackingprotection/TrackingProtectionPrompt.java b/mobile/android/base/java/org/mozilla/gecko/trackingprotection/TrackingProtectionPrompt.java
deleted file mode 100644
index dcc62b6d4..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/trackingprotection/TrackingProtectionPrompt.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.trackingprotection;
-
-import org.mozilla.gecko.Locales;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-import org.mozilla.gecko.util.HardwareUtils;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.MotionEvent;
-import android.view.View;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-
-public class TrackingProtectionPrompt extends Locales.LocaleAwareActivity {
- public static final String LOGTAG = "Gecko" + TrackingProtectionPrompt.class.getSimpleName();
-
- // Flag set during animation to prevent animation multiple-start.
- private boolean isAnimating;
-
- private View containerView;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- showPrompt();
- }
-
- private void showPrompt() {
- setContentView(R.layout.tracking_protection_prompt);
-
- findViewById(R.id.ok_button).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- onConfirmButtonPressed();
- }
- });
- findViewById(R.id.link_text).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- slideOut();
- final Intent settingsIntent = new Intent(TrackingProtectionPrompt.this, GeckoPreferences.class);
- GeckoPreferences.setResourceToOpen(settingsIntent, "preferences_privacy");
- startActivity(settingsIntent);
-
- // Don't use a transition to settings if we're on a device where that
- // would look bad.
- if (HardwareUtils.IS_KINDLE_DEVICE) {
- overridePendingTransition(0, 0);
- }
- }
- });
-
- containerView = findViewById(R.id.tracking_protection_inner_container);
-
- containerView.setTranslationY(500);
- containerView.setAlpha(0);
-
- final Animator translateAnimator = ObjectAnimator.ofFloat(containerView, "translationY", 0);
- translateAnimator.setDuration(400);
-
- final Animator alphaAnimator = ObjectAnimator.ofFloat(containerView, "alpha", 1);
- alphaAnimator.setStartDelay(200);
- alphaAnimator.setDuration(600);
-
- final AnimatorSet set = new AnimatorSet();
- set.playTogether(alphaAnimator, translateAnimator);
- set.setStartDelay(400);
-
- set.start();
- }
-
- @Override
- public void finish() {
- super.finish();
-
- // Don't perform an activity-dismiss animation.
- overridePendingTransition(0, 0);
- }
-
- private void onConfirmButtonPressed() {
- slideOut();
- }
-
- /**
- * Slide the overlay down off the screen and destroy it.
- */
- private void slideOut() {
- if (isAnimating) {
- return;
- }
-
- isAnimating = true;
-
- ObjectAnimator animator = ObjectAnimator.ofFloat(containerView, "translationY", containerView.getHeight());
- animator.addListener(new AnimatorListenerAdapter() {
-
- @Override
- public void onAnimationEnd(Animator animation) {
- finish();
- }
-
- });
- animator.start();
- }
-
- /**
- * Close the dialog if back is pressed.
- */
- @Override
- public void onBackPressed() {
- slideOut();
- }
-
- /**
- * Close the dialog if the anything that isn't a button is tapped.
- */
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- slideOut();
- return true;
- }
- }
diff --git a/mobile/android/base/java/org/mozilla/gecko/updater/PostUpdateHandler.java b/mobile/android/base/java/org/mozilla/gecko/updater/PostUpdateHandler.java
deleted file mode 100644
index f0ad78e77..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/updater/PostUpdateHandler.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.updater;
-
-import android.content.Context;
-import android.content.res.AssetManager;
-import android.content.SharedPreferences;
-import android.util.Log;
-
-import com.keepsafe.switchboard.SwitchBoard;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.BrowserApp;
-import org.mozilla.gecko.delegates.BrowserAppDelegateWithReference;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-import org.mozilla.gecko.util.IOUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.InputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.Enumeration;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
-
-/**
- * Perform tasks in the background after the app has been installed/updated.
- */
-public class PostUpdateHandler extends BrowserAppDelegateWithReference {
- private static final String LOGTAG = "PostUpdateHandler";
-
- @Override
- public void onStart(final BrowserApp browserApp) {
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- final SharedPreferences prefs = GeckoSharedPrefs.forApp(browserApp);
-
- // Check if this is a new installation or if the app has been updated since the last start.
- if (!AppConstants.MOZ_APP_BUILDID.equals(prefs.getString(GeckoPreferences.PREFS_APP_UPDATE_LAST_BUILD_ID, null))) {
- Log.d(LOGTAG, "Build ID changed since last start: '" + AppConstants.MOZ_APP_BUILDID + "', '" + prefs.getString(GeckoPreferences.PREFS_APP_UPDATE_LAST_BUILD_ID, null) + "'");
-
- // Copy the bundled system add-ons from the APK to the data directory.
- copyFeaturesFromAPK(browserApp);
- }
- }
- });
- }
-
- /**
- * Copies the /assets/features folder out of the APK and into the app's data directory.
- */
- private void copyFeaturesFromAPK(BrowserApp browserApp) {
- Log.d(LOGTAG, "Copying system add-ons from APK to dataDir");
-
- final String dataDir = browserApp.getApplicationInfo().dataDir;
- final SharedPreferences prefs = GeckoSharedPrefs.forApp(browserApp);
- final AssetManager assetManager = browserApp.getContext().getAssets();
-
- try {
- final String[] assetNames = assetManager.list("features");
-
- for (int i = 0; i < assetNames.length; i++) {
- final String assetPath = "features/" + assetNames[i];
-
- Log.d(LOGTAG, "Copying '" + assetPath + "' from APK to dataDir");
-
- final InputStream assetStream = assetManager.open(assetPath);
- final File outFile = getDataFile(dataDir, assetPath);
-
- if (outFile == null) {
- continue;
- }
-
- final OutputStream outStream = new FileOutputStream(outFile);
-
- try {
- IOUtils.copy(assetStream, outStream);
- } catch (IOException e) {
- Log.e(LOGTAG, "Error copying '" + assetPath + "' from APK to dataDir");
- } finally {
- outStream.close();
- }
- }
- } catch (IOException e) {
- Log.e(LOGTAG, "Error retrieving packaged system add-ons from APK", e);
- }
-
- // Save the Build ID so we don't perform post-update operations again until the app is updated.
- prefs.edit().putString(GeckoPreferences.PREFS_APP_UPDATE_LAST_BUILD_ID, AppConstants.MOZ_APP_BUILDID).apply();
- }
-
- /**
- * Return a File instance in the data directory, ensuring
- * that the parent exists.
- *
- * @return null if the parents could not be created.
- */
- private File getDataFile(final String dataDir, final String name) {
- File outFile = new File(dataDir, name);
- File dir = outFile.getParentFile();
-
- if (!dir.exists()) {
- Log.d(LOGTAG, "Creating " + dir.getAbsolutePath());
- if (!dir.mkdirs()) {
- Log.e(LOGTAG, "Unable to create directories: " + dir.getAbsolutePath());
- return null;
- }
- }
-
- return outFile;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/updater/UpdateService.java b/mobile/android/base/java/org/mozilla/gecko/updater/UpdateService.java
deleted file mode 100644
index 7ccc43e28..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/updater/UpdateService.java
+++ /dev/null
@@ -1,795 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.updater;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.CrashHandler;
-import org.mozilla.gecko.R;
-
-import org.mozilla.apache.commons.codec.binary.Hex;
-
-import org.mozilla.gecko.permissions.Permissions;
-import org.mozilla.gecko.util.ProxySelector;
-import org.w3c.dom.Document;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-
-import android.Manifest;
-import android.app.AlarmManager;
-import android.app.IntentService;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.net.Uri;
-import android.net.wifi.WifiManager;
-import android.net.wifi.WifiManager.WifiLock;
-import android.os.Environment;
-import android.provider.Settings;
-import android.support.v4.app.NotificationManagerCompat;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.net.ConnectivityManagerCompat;
-import android.support.v4.app.NotificationCompat;
-import android.support.v4.app.NotificationCompat.Builder;
-import android.util.Log;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.URI;
-import java.net.URL;
-import java.net.URLConnection;
-import java.security.MessageDigest;
-import java.util.Calendar;
-import java.util.GregorianCalendar;
-import java.util.List;
-import java.util.TimeZone;
-
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-
-public class UpdateService extends IntentService {
- private static final int BUFSIZE = 8192;
- private static final int NOTIFICATION_ID = 0x3e40ddbd;
-
- private static final String LOGTAG = "UpdateService";
-
- private static final int INTERVAL_LONG = 86400000; // in milliseconds
- private static final int INTERVAL_SHORT = 14400000; // again, in milliseconds
- private static final int INTERVAL_RETRY = 3600000;
-
- private static final String PREFS_NAME = "UpdateService";
- private static final String KEY_LAST_BUILDID = "UpdateService.lastBuildID";
- private static final String KEY_LAST_HASH_FUNCTION = "UpdateService.lastHashFunction";
- private static final String KEY_LAST_HASH_VALUE = "UpdateService.lastHashValue";
- private static final String KEY_LAST_FILE_NAME = "UpdateService.lastFileName";
- private static final String KEY_LAST_ATTEMPT_DATE = "UpdateService.lastAttemptDate";
- private static final String KEY_AUTODOWNLOAD_POLICY = "UpdateService.autoDownloadPolicy";
- private static final String KEY_UPDATE_URL = "UpdateService.updateUrl";
-
- private SharedPreferences mPrefs;
-
- private NotificationManagerCompat mNotificationManager;
- private ConnectivityManager mConnectivityManager;
- private Builder mBuilder;
-
- private volatile WifiLock mWifiLock;
-
- private boolean mDownloading;
- private boolean mCancelDownload;
- private boolean mApplyImmediately;
-
- private CrashHandler mCrashHandler;
-
- public enum AutoDownloadPolicy {
- NONE(-1),
- WIFI(0),
- DISABLED(1),
- ENABLED(2);
-
- public final int value;
-
- private AutoDownloadPolicy(int value) {
- this.value = value;
- }
-
- private final static AutoDownloadPolicy[] sValues = AutoDownloadPolicy.values();
-
- public static AutoDownloadPolicy get(int value) {
- for (AutoDownloadPolicy id: sValues) {
- if (id.value == value) {
- return id;
- }
- }
- return NONE;
- }
-
- public static AutoDownloadPolicy get(String name) {
- for (AutoDownloadPolicy id: sValues) {
- if (name.equalsIgnoreCase(id.toString())) {
- return id;
- }
- }
- return NONE;
- }
- }
-
- private enum CheckUpdateResult {
- // Keep these in sync with mobile/android/chrome/content/about.xhtml
- NOT_AVAILABLE,
- AVAILABLE,
- DOWNLOADING,
- DOWNLOADED
- }
-
-
- public UpdateService() {
- super("updater");
- }
-
- @Override
- public void onCreate () {
- mCrashHandler = CrashHandler.createDefaultCrashHandler(getApplicationContext());
-
- super.onCreate();
-
- mPrefs = getSharedPreferences(PREFS_NAME, 0);
- mNotificationManager = NotificationManagerCompat.from(this);
- mConnectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
- mWifiLock = ((WifiManager)getSystemService(Context.WIFI_SERVICE))
- .createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, PREFS_NAME);
- mCancelDownload = false;
- }
-
- @Override
- public void onDestroy() {
- mCrashHandler.unregister();
- mCrashHandler = null;
-
- if (mWifiLock.isHeld()) {
- mWifiLock.release();
- }
- }
-
- @Override
- public synchronized int onStartCommand (Intent intent, int flags, int startId) {
- // If we are busy doing a download, the new Intent here would normally be queued for
- // execution once that is done. In this case, however, we want to flip the boolean
- // while that is running, so handle that now.
- if (mDownloading && UpdateServiceHelper.ACTION_APPLY_UPDATE.equals(intent.getAction())) {
- Log.i(LOGTAG, "will apply update when download finished");
-
- mApplyImmediately = true;
- showDownloadNotification();
- } else if (UpdateServiceHelper.ACTION_CANCEL_DOWNLOAD.equals(intent.getAction())) {
- mCancelDownload = true;
- } else {
- super.onStartCommand(intent, flags, startId);
- }
-
- return Service.START_REDELIVER_INTENT;
- }
-
- @Override
- protected void onHandleIntent (final Intent intent) {
- if (UpdateServiceHelper.ACTION_REGISTER_FOR_UPDATES.equals(intent.getAction())) {
- AutoDownloadPolicy policy = AutoDownloadPolicy.get(
- intent.getIntExtra(UpdateServiceHelper.EXTRA_AUTODOWNLOAD_NAME,
- AutoDownloadPolicy.NONE.value));
-
- if (policy != AutoDownloadPolicy.NONE) {
- setAutoDownloadPolicy(policy);
- }
-
- String url = intent.getStringExtra(UpdateServiceHelper.EXTRA_UPDATE_URL_NAME);
- if (url != null) {
- setUpdateUrl(url);
- }
-
- registerForUpdates(false);
- } else if (UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE.equals(intent.getAction())) {
- startUpdate(intent.getIntExtra(UpdateServiceHelper.EXTRA_UPDATE_FLAGS_NAME, 0));
- // Use this instead for forcing a download from about:fennec
- // startUpdate(UpdateServiceHelper.FLAG_FORCE_DOWNLOAD | UpdateServiceHelper.FLAG_REINSTALL);
- } else if (UpdateServiceHelper.ACTION_DOWNLOAD_UPDATE.equals(intent.getAction())) {
- // We always want to do the download and apply it here
- mApplyImmediately = true;
- startUpdate(UpdateServiceHelper.FLAG_FORCE_DOWNLOAD);
- } else if (UpdateServiceHelper.ACTION_APPLY_UPDATE.equals(intent.getAction())) {
- applyUpdate(intent.getStringExtra(UpdateServiceHelper.EXTRA_PACKAGE_PATH_NAME));
- }
- }
-
- private static boolean hasFlag(int flags, int flag) {
- return (flags & flag) == flag;
- }
-
- private void sendCheckUpdateResult(CheckUpdateResult result) {
- Intent resultIntent = new Intent(UpdateServiceHelper.ACTION_CHECK_UPDATE_RESULT);
- resultIntent.putExtra("result", result.toString());
- sendBroadcast(resultIntent);
- }
-
- private int getUpdateInterval(boolean isRetry) {
- int interval;
- if (isRetry) {
- interval = INTERVAL_RETRY;
- } else if (!AppConstants.RELEASE_OR_BETA) {
- interval = INTERVAL_SHORT;
- } else {
- interval = INTERVAL_LONG;
- }
-
- return interval;
- }
-
- private void registerForUpdates(boolean isRetry) {
- Calendar lastAttempt = getLastAttemptDate();
- Calendar now = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
-
- int interval = getUpdateInterval(isRetry);
-
- if (lastAttempt == null || (now.getTimeInMillis() - lastAttempt.getTimeInMillis()) > interval) {
- // We've either never attempted an update, or we are passed the desired
- // time. Start an update now.
- Log.i(LOGTAG, "no update has ever been attempted, checking now");
- startUpdate(0);
- return;
- }
-
- AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
- if (manager == null)
- return;
-
- PendingIntent pending = PendingIntent.getService(this, 0, new Intent(UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE, null, this, UpdateService.class), PendingIntent.FLAG_UPDATE_CURRENT);
- manager.cancel(pending);
-
- lastAttempt.setTimeInMillis(lastAttempt.getTimeInMillis() + interval);
- Log.i(LOGTAG, "next update will be at: " + lastAttempt.getTime());
-
- manager.set(AlarmManager.RTC_WAKEUP, lastAttempt.getTimeInMillis(), pending);
- }
-
- private void startUpdate(final int flags) {
- setLastAttemptDate();
-
- NetworkInfo netInfo = mConnectivityManager.getActiveNetworkInfo();
- if (netInfo == null || !netInfo.isConnected()) {
- Log.i(LOGTAG, "not connected to the network");
- registerForUpdates(true);
- sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE);
- return;
- }
-
- registerForUpdates(false);
-
- final UpdateInfo info = findUpdate(hasFlag(flags, UpdateServiceHelper.FLAG_REINSTALL));
- boolean haveUpdate = (info != null);
-
- if (!haveUpdate) {
- Log.i(LOGTAG, "no update available");
- sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE);
- return;
- }
-
- Log.i(LOGTAG, "update available, buildID = " + info.buildID);
-
- Permissions.from(this)
- .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
- .doNotPrompt()
- .andFallback(new Runnable() {
- @Override
- public void run() {
- showPermissionNotification();
- sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE);
- }})
- .run(new Runnable() {
- @Override
- public void run() {
- startDownload(info, flags);
- }});
- }
-
- private void startDownload(UpdateInfo info, int flags) {
- AutoDownloadPolicy policy = getAutoDownloadPolicy();
-
- // We only start a download automatically if one of following criteria are met:
- //
- // - We have a FORCE_DOWNLOAD flag passed in
- // - The preference is set to 'always'
- // - The preference is set to 'wifi' and we are using a non-metered network (i.e. the user
- // is OK with large data transfers occurring)
- //
- boolean shouldStartDownload = hasFlag(flags, UpdateServiceHelper.FLAG_FORCE_DOWNLOAD) ||
- policy == AutoDownloadPolicy.ENABLED ||
- (policy == AutoDownloadPolicy.WIFI && !ConnectivityManagerCompat.isActiveNetworkMetered(mConnectivityManager));
-
- if (!shouldStartDownload) {
- Log.i(LOGTAG, "not initiating automatic update download due to policy " + policy.toString());
- sendCheckUpdateResult(CheckUpdateResult.AVAILABLE);
-
- // We aren't autodownloading here, so prompt to start the update
- Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_DOWNLOAD_UPDATE);
- notificationIntent.setClass(this, UpdateService.class);
- PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
-
- NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
- builder.setSmallIcon(R.drawable.ic_status_logo);
- builder.setWhen(System.currentTimeMillis());
- builder.setAutoCancel(true);
- builder.setContentTitle(getString(R.string.updater_start_title));
- builder.setContentText(getString(R.string.updater_start_select));
- builder.setContentIntent(contentIntent);
-
- mNotificationManager.notify(NOTIFICATION_ID, builder.build());
-
- return;
- }
-
- File pkg = downloadUpdatePackage(info, hasFlag(flags, UpdateServiceHelper.FLAG_OVERWRITE_EXISTING));
- if (pkg == null) {
- sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE);
- return;
- }
-
- Log.i(LOGTAG, "have update package at " + pkg);
-
- saveUpdateInfo(info, pkg);
- sendCheckUpdateResult(CheckUpdateResult.DOWNLOADED);
-
- if (mApplyImmediately) {
- applyUpdate(pkg);
- } else {
- // Prompt to apply the update
-
- Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_APPLY_UPDATE);
- notificationIntent.setClass(this, UpdateService.class);
- notificationIntent.putExtra(UpdateServiceHelper.EXTRA_PACKAGE_PATH_NAME, pkg.getAbsolutePath());
- PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
-
-
- NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
- builder.setSmallIcon(R.drawable.ic_status_logo);
- builder.setWhen(System.currentTimeMillis());
- builder.setAutoCancel(true);
- builder.setContentTitle(getString(R.string.updater_apply_title));
- builder.setContentText(getString(R.string.updater_apply_select));
- builder.setContentIntent(contentIntent);
-
- mNotificationManager.notify(NOTIFICATION_ID, builder.build());
- }
- }
-
- private UpdateInfo findUpdate(boolean force) {
- try {
- URI uri = getUpdateURI(force);
-
- if (uri == null) {
- Log.e(LOGTAG, "failed to get update URI");
- return null;
- }
-
- DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
- Document dom = builder.parse(ProxySelector.openConnectionWithProxy(uri).getInputStream());
-
- NodeList nodes = dom.getElementsByTagName("update");
- if (nodes == null || nodes.getLength() == 0)
- return null;
-
- Node updateNode = nodes.item(0);
- Node buildIdNode = updateNode.getAttributes().getNamedItem("buildID");
- if (buildIdNode == null)
- return null;
-
- nodes = dom.getElementsByTagName("patch");
- if (nodes == null || nodes.getLength() == 0)
- return null;
-
- Node patchNode = nodes.item(0);
- Node urlNode = patchNode.getAttributes().getNamedItem("URL");
- Node hashFunctionNode = patchNode.getAttributes().getNamedItem("hashFunction");
- Node hashValueNode = patchNode.getAttributes().getNamedItem("hashValue");
- Node sizeNode = patchNode.getAttributes().getNamedItem("size");
-
- if (urlNode == null || hashFunctionNode == null ||
- hashValueNode == null || sizeNode == null) {
- return null;
- }
-
- // Fill in UpdateInfo from the XML data
- UpdateInfo info = new UpdateInfo();
- info.uri = new URI(urlNode.getTextContent());
- info.buildID = buildIdNode.getTextContent();
- info.hashFunction = hashFunctionNode.getTextContent();
- info.hashValue = hashValueNode.getTextContent();
-
- try {
- info.size = Integer.parseInt(sizeNode.getTextContent());
- } catch (NumberFormatException e) {
- Log.e(LOGTAG, "Failed to find APK size: ", e);
- return null;
- }
-
- // Make sure we have all the stuff we need to apply the update
- if (!info.isValid()) {
- Log.e(LOGTAG, "missing some required update information, have: " + info);
- return null;
- }
-
- return info;
- } catch (Exception e) {
- Log.e(LOGTAG, "failed to check for update: ", e);
- return null;
- }
- }
-
- private MessageDigest createMessageDigest(String hashFunction) {
- String javaHashFunction = null;
-
- if ("sha512".equalsIgnoreCase(hashFunction)) {
- javaHashFunction = "SHA-512";
- } else {
- Log.e(LOGTAG, "Unhandled hash function: " + hashFunction);
- return null;
- }
-
- try {
- return MessageDigest.getInstance(javaHashFunction);
- } catch (java.security.NoSuchAlgorithmException e) {
- Log.e(LOGTAG, "Couldn't find algorithm " + javaHashFunction, e);
- return null;
- }
- }
-
- private void showDownloadNotification() {
- showDownloadNotification(null);
- }
-
- private void showDownloadNotification(File downloadFile) {
-
- Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_APPLY_UPDATE);
- notificationIntent.setClass(this, UpdateService.class);
-
- Intent cancelIntent = new Intent(UpdateServiceHelper.ACTION_CANCEL_DOWNLOAD);
- cancelIntent.setClass(this, UpdateService.class);
-
- if (downloadFile != null)
- notificationIntent.putExtra(UpdateServiceHelper.EXTRA_PACKAGE_PATH_NAME, downloadFile.getAbsolutePath());
-
- PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
- PendingIntent deleteIntent = PendingIntent.getService(this, 0, cancelIntent, PendingIntent.FLAG_CANCEL_CURRENT);
-
- mBuilder = new NotificationCompat.Builder(this);
- mBuilder.setContentTitle(getResources().getString(R.string.updater_downloading_title))
- .setContentText(mApplyImmediately ? "" : getResources().getString(R.string.updater_downloading_select))
- .setSmallIcon(android.R.drawable.stat_sys_download)
- .setContentIntent(contentIntent)
- .setDeleteIntent(deleteIntent);
-
- mBuilder.setProgress(100, 0, true);
- mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
- }
-
- private void showDownloadFailure() {
- Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE);
- notificationIntent.setClass(this, UpdateService.class);
- PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
-
- NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
- builder.setSmallIcon(R.drawable.ic_status_logo);
- builder.setWhen(System.currentTimeMillis());
- builder.setContentTitle(getString(R.string.updater_downloading_title_failed));
- builder.setContentText(getString(R.string.updater_downloading_retry));
- builder.setContentIntent(contentIntent);
-
- mNotificationManager.notify(NOTIFICATION_ID, builder.build());
- }
-
- private boolean deleteUpdatePackage(String path) {
- if (path == null) {
- return false;
- }
-
- File pkg = new File(path);
- if (!pkg.exists()) {
- return false;
- }
-
- pkg.delete();
- Log.i(LOGTAG, "deleted update package: " + path);
-
- return true;
- }
-
- private File downloadUpdatePackage(UpdateInfo info, boolean overwriteExisting) {
- URL url = null;
- try {
- url = info.uri.toURL();
- } catch (java.net.MalformedURLException e) {
- Log.e(LOGTAG, "failed to read URL: ", e);
- return null;
- }
-
- File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
- path.mkdirs();
- String fileName = new File(url.getFile()).getName();
- File downloadFile = new File(path, fileName);
-
- if (!overwriteExisting && info.buildID.equals(getLastBuildID()) && downloadFile.exists()) {
- // The last saved buildID is the same as the one for the current update. We also have a file
- // already downloaded, so it's probably the package we want. Verify it to be sure and just
- // return that if it matches.
-
- if (verifyDownloadedPackage(downloadFile)) {
- Log.i(LOGTAG, "using existing update package");
- return downloadFile;
- } else {
- // Didn't match, so we're going to download a new one.
- downloadFile.delete();
- }
- }
-
- if (!info.buildID.equals(getLastBuildID())) {
- // Delete the previous package when a new version becomes available.
- deleteUpdatePackage(getLastFileName());
- }
-
- Log.i(LOGTAG, "downloading update package");
- sendCheckUpdateResult(CheckUpdateResult.DOWNLOADING);
-
- OutputStream output = null;
- InputStream input = null;
-
- mDownloading = true;
- mCancelDownload = false;
- showDownloadNotification(downloadFile);
-
- try {
- NetworkInfo netInfo = mConnectivityManager.getActiveNetworkInfo();
- if (netInfo != null && netInfo.isConnected() &&
- netInfo.getType() == ConnectivityManager.TYPE_WIFI) {
- mWifiLock.acquire();
- }
-
- URLConnection conn = ProxySelector.openConnectionWithProxy(info.uri);
- int length = conn.getContentLength();
-
- output = new BufferedOutputStream(new FileOutputStream(downloadFile));
- input = new BufferedInputStream(conn.getInputStream());
-
- byte[] buf = new byte[BUFSIZE];
- int len = 0;
-
- int bytesRead = 0;
- int lastNotify = 0;
-
- while ((len = input.read(buf, 0, BUFSIZE)) > 0 && !mCancelDownload) {
- output.write(buf, 0, len);
- bytesRead += len;
- // Updating the notification takes time so only do it every 1MB
- if (bytesRead - lastNotify > 1048576) {
- mBuilder.setProgress(length, bytesRead, false);
- mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
- lastNotify = bytesRead;
- }
- }
-
- mNotificationManager.cancel(NOTIFICATION_ID);
-
- // if the download was canceled by the user
- // delete the update package
- if (mCancelDownload) {
- Log.i(LOGTAG, "download canceled by user!");
- downloadFile.delete();
-
- return null;
- } else {
- Log.i(LOGTAG, "completed update download!");
- return downloadFile;
- }
- } catch (Exception e) {
- downloadFile.delete();
- showDownloadFailure();
-
- Log.e(LOGTAG, "failed to download update: ", e);
- return null;
- } finally {
- try {
- if (input != null)
- input.close();
- } catch (java.io.IOException e) { }
-
- try {
- if (output != null)
- output.close();
- } catch (java.io.IOException e) { }
-
- mDownloading = false;
-
- if (mWifiLock.isHeld()) {
- mWifiLock.release();
- }
- }
- }
-
- private boolean verifyDownloadedPackage(File updateFile) {
- MessageDigest digest = createMessageDigest(getLastHashFunction());
- if (digest == null)
- return false;
-
- InputStream input = null;
-
- try {
- input = new BufferedInputStream(new FileInputStream(updateFile));
-
- byte[] buf = new byte[BUFSIZE];
- int len;
- while ((len = input.read(buf, 0, BUFSIZE)) > 0) {
- digest.update(buf, 0, len);
- }
- } catch (java.io.IOException e) {
- Log.e(LOGTAG, "Failed to verify update package: ", e);
- return false;
- } finally {
- try {
- if (input != null)
- input.close();
- } catch (java.io.IOException e) { }
- }
-
- String hex = Hex.encodeHexString(digest.digest());
- if (!hex.equals(getLastHashValue())) {
- Log.e(LOGTAG, "Package hash does not match");
- return false;
- }
-
- return true;
- }
-
- private void applyUpdate(String updatePath) {
- if (updatePath == null) {
- updatePath = getLastFileName();
- }
-
- if (updatePath != null) {
- applyUpdate(new File(updatePath));
- }
- }
-
- private void applyUpdate(File updateFile) {
- mApplyImmediately = false;
-
- if (!updateFile.exists())
- return;
-
- Log.i(LOGTAG, "Verifying package: " + updateFile);
-
- if (!verifyDownloadedPackage(updateFile)) {
- Log.e(LOGTAG, "Not installing update, failed verification");
- return;
- }
-
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setDataAndType(Uri.fromFile(updateFile), "application/vnd.android.package-archive");
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent);
- }
-
- private void showPermissionNotification() {
- Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
- Uri.fromParts("package", getPackageName(), null));
-
- PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
-
- NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle()
- .bigText(getString(R.string.updater_permission_text));
-
- Notification notification = new NotificationCompat.Builder(this)
- .setContentTitle(getString(R.string.updater_permission_title))
- .setContentText(getString(R.string.updater_permission_text))
- .setStyle(bigTextStyle)
- .setAutoCancel(true)
- .setSmallIcon(R.drawable.ic_status_logo)
- .setColor(ContextCompat.getColor(this, R.color.rejection_red))
- .setContentIntent(pendingIntent)
- .build();
-
- NotificationManagerCompat.from(this)
- .notify(R.id.updateServicePermissionNotification, notification);
- }
-
- private String getLastBuildID() {
- return mPrefs.getString(KEY_LAST_BUILDID, null);
- }
-
- private String getLastHashFunction() {
- return mPrefs.getString(KEY_LAST_HASH_FUNCTION, null);
- }
-
- private String getLastHashValue() {
- return mPrefs.getString(KEY_LAST_HASH_VALUE, null);
- }
-
- private String getLastFileName() {
- return mPrefs.getString(KEY_LAST_FILE_NAME, null);
- }
-
- private Calendar getLastAttemptDate() {
- long lastAttempt = mPrefs.getLong(KEY_LAST_ATTEMPT_DATE, -1);
- if (lastAttempt < 0)
- return null;
-
- GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
- cal.setTimeInMillis(lastAttempt);
- return cal;
- }
-
- private void setLastAttemptDate() {
- SharedPreferences.Editor editor = mPrefs.edit();
- editor.putLong(KEY_LAST_ATTEMPT_DATE, System.currentTimeMillis());
- editor.commit();
- }
-
- private AutoDownloadPolicy getAutoDownloadPolicy() {
- return AutoDownloadPolicy.get(mPrefs.getInt(KEY_AUTODOWNLOAD_POLICY, AutoDownloadPolicy.WIFI.value));
- }
-
- private void setAutoDownloadPolicy(AutoDownloadPolicy policy) {
- SharedPreferences.Editor editor = mPrefs.edit();
- editor.putInt(KEY_AUTODOWNLOAD_POLICY, policy.value);
- editor.commit();
- }
-
- private URI getUpdateURI(boolean force) {
- return UpdateServiceHelper.expandUpdateURI(this, mPrefs.getString(KEY_UPDATE_URL, null), force);
- }
-
- private void setUpdateUrl(String url) {
- SharedPreferences.Editor editor = mPrefs.edit();
- editor.putString(KEY_UPDATE_URL, url);
- editor.commit();
- }
-
- private void saveUpdateInfo(UpdateInfo info, File downloaded) {
- SharedPreferences.Editor editor = mPrefs.edit();
- editor.putString(KEY_LAST_BUILDID, info.buildID);
- editor.putString(KEY_LAST_HASH_FUNCTION, info.hashFunction);
- editor.putString(KEY_LAST_HASH_VALUE, info.hashValue);
- editor.putString(KEY_LAST_FILE_NAME, downloaded.toString());
- editor.commit();
- }
-
- private class UpdateInfo {
- public URI uri;
- public String buildID;
- public String hashFunction;
- public String hashValue;
- public int size;
-
- private boolean isNonEmpty(String s) {
- return s != null && s.length() > 0;
- }
-
- public boolean isValid() {
- return uri != null && isNonEmpty(buildID) &&
- isNonEmpty(hashFunction) && isNonEmpty(hashValue) && size > 0;
- }
-
- @Override
- public String toString() {
- return "uri = " + uri + ", buildID = " + buildID + ", hashFunction = " + hashFunction + ", hashValue = " + hashValue + ", size = " + size;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/updater/UpdateServiceHelper.java b/mobile/android/base/java/org/mozilla/gecko/updater/UpdateServiceHelper.java
deleted file mode 100644
index c4d198ae7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/updater/UpdateServiceHelper.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.updater;
-
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.PrefsHelper;
-import org.mozilla.gecko.util.ContextUtils;
-import org.mozilla.gecko.util.GeckoJarReader;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ApplicationInfo;
-import android.os.Build;
-import android.util.Log;
-
-import java.net.URI;
-import java.util.ArrayList;
-import java.util.HashMap;
-
-public class UpdateServiceHelper {
- public static final String ACTION_REGISTER_FOR_UPDATES = AppConstants.ANDROID_PACKAGE_NAME + ".REGISTER_FOR_UPDATES";
- public static final String ACTION_UNREGISTER_FOR_UPDATES = AppConstants.ANDROID_PACKAGE_NAME + ".UNREGISTER_FOR_UPDATES";
- public static final String ACTION_CHECK_FOR_UPDATE = AppConstants.ANDROID_PACKAGE_NAME + ".CHECK_FOR_UPDATE";
- public static final String ACTION_CHECK_UPDATE_RESULT = AppConstants.ANDROID_PACKAGE_NAME + ".CHECK_UPDATE_RESULT";
- public static final String ACTION_DOWNLOAD_UPDATE = AppConstants.ANDROID_PACKAGE_NAME + ".DOWNLOAD_UPDATE";
- public static final String ACTION_APPLY_UPDATE = AppConstants.ANDROID_PACKAGE_NAME + ".APPLY_UPDATE";
- public static final String ACTION_CANCEL_DOWNLOAD = AppConstants.ANDROID_PACKAGE_NAME + ".CANCEL_DOWNLOAD";
-
- // Flags for ACTION_CHECK_FOR_UPDATE
- protected static final int FLAG_FORCE_DOWNLOAD = 1;
- protected static final int FLAG_OVERWRITE_EXISTING = 1 << 1;
- protected static final int FLAG_REINSTALL = 1 << 2;
- protected static final int FLAG_RETRY = 1 << 3;
-
- // Name of the Intent extra for the autodownload policy, used with ACTION_REGISTER_FOR_UPDATES
- protected static final String EXTRA_AUTODOWNLOAD_NAME = "autodownload";
-
- // Name of the Intent extra that holds the flags for ACTION_CHECK_FOR_UPDATE
- protected static final String EXTRA_UPDATE_FLAGS_NAME = "updateFlags";
-
- // Name of the Intent extra that holds the APK path, used with ACTION_APPLY_UPDATE
- protected static final String EXTRA_PACKAGE_PATH_NAME = "packagePath";
-
- // Name of the Intent extra for the update URL, used with ACTION_REGISTER_FOR_UPDATES
- protected static final String EXTRA_UPDATE_URL_NAME = "updateUrl";
-
- private static final String LOGTAG = "UpdateServiceHelper";
- private static final String DEFAULT_UPDATE_LOCALE = "en-US";
-
- // So that updates can be disabled by tests.
- private static volatile boolean isEnabled = true;
-
- private enum Pref {
- AUTO_DOWNLOAD_POLICY("app.update.autodownload"),
- UPDATE_URL("app.update.url.android");
-
- public final String name;
-
- private Pref(String name) {
- this.name = name;
- }
-
- public final static String[] names;
-
- @Override
- public String toString() {
- return this.name;
- }
-
- static {
- ArrayList<String> nameList = new ArrayList<String>();
-
- for (Pref id: Pref.values()) {
- nameList.add(id.toString());
- }
-
- names = nameList.toArray(new String[0]);
- }
- }
-
- @RobocopTarget
- public static void setEnabled(final boolean enabled) {
- isEnabled = enabled;
- }
-
- public static URI expandUpdateURI(Context context, String updateUri, boolean force) {
- if (updateUri == null) {
- return null;
- }
-
- PackageManager pm = context.getPackageManager();
-
- String pkgSpecial = AppConstants.MOZ_PKG_SPECIAL != null ?
- "-" + AppConstants.MOZ_PKG_SPECIAL :
- "";
- String locale = DEFAULT_UPDATE_LOCALE;
-
- try {
- ApplicationInfo info = pm.getApplicationInfo(AppConstants.ANDROID_PACKAGE_NAME, 0);
- String updateLocaleUrl = "jar:jar:file://" + info.sourceDir + "!/" + AppConstants.OMNIJAR_NAME + "!/update.locale";
-
- final String jarLocale = GeckoJarReader.getText(context, updateLocaleUrl);
- if (jarLocale != null) {
- locale = jarLocale.trim();
- }
- } catch (android.content.pm.PackageManager.NameNotFoundException e) {
- // Shouldn't really be possible, but fallback to default locale
- Log.i(LOGTAG, "Failed to read update locale file, falling back to " + locale);
- }
-
- String url = updateUri.replace("%PRODUCT%", AppConstants.MOZ_APP_BASENAME)
- .replace("%VERSION%", AppConstants.MOZ_APP_VERSION)
- .replace("%BUILD_ID%", force ? "0" : AppConstants.MOZ_APP_BUILDID)
- .replace("%BUILD_TARGET%", "Android_" + AppConstants.MOZ_APP_ABI + pkgSpecial)
- .replace("%LOCALE%", locale)
- .replace("%CHANNEL%", AppConstants.MOZ_UPDATE_CHANNEL)
- .replace("%OS_VERSION%", Build.VERSION.RELEASE)
- .replace("%DISTRIBUTION%", "default")
- .replace("%DISTRIBUTION_VERSION%", "default")
- .replace("%MOZ_VERSION%", AppConstants.MOZILLA_VERSION);
-
- try {
- return new URI(url);
- } catch (java.net.URISyntaxException e) {
- Log.e(LOGTAG, "Failed to create update url: ", e);
- return null;
- }
- }
-
- public static boolean isUpdaterEnabled(final Context context) {
- return AppConstants.MOZ_UPDATER && isEnabled && !ContextUtils.isInstalledFromGooglePlay(context);
- }
-
- public static void setUpdateUrl(Context context, String url) {
- registerForUpdates(context, null, url);
- }
-
- public static void setAutoDownloadPolicy(Context context, UpdateService.AutoDownloadPolicy policy) {
- registerForUpdates(context, policy, null);
- }
-
- public static void checkForUpdate(Context context) {
- if (context == null) {
- return;
- }
-
- context.startService(createIntent(context, ACTION_CHECK_FOR_UPDATE));
- }
-
- public static void downloadUpdate(Context context) {
- if (context == null) {
- return;
- }
-
- context.startService(createIntent(context, ACTION_DOWNLOAD_UPDATE));
- }
-
- public static void applyUpdate(Context context) {
- if (context == null) {
- return;
- }
-
- context.startService(createIntent(context, ACTION_APPLY_UPDATE));
- }
-
- public static void registerForUpdates(final Context context) {
- if (!isUpdaterEnabled(context)) {
- return;
- }
-
- final HashMap<String, Object> prefs = new HashMap<String, Object>();
-
- PrefsHelper.getPrefs(Pref.names, new PrefsHelper.PrefHandlerBase() {
- @Override public void prefValue(String pref, String value) {
- prefs.put(pref, value);
- }
-
- @Override public void finish() {
- UpdateServiceHelper.registerForUpdates(context,
- UpdateService.AutoDownloadPolicy.get(
- (String) prefs.get(Pref.AUTO_DOWNLOAD_POLICY.toString())),
- (String) prefs.get(Pref.UPDATE_URL.toString()));
- }
- });
- }
-
- public static void registerForUpdates(Context context, UpdateService.AutoDownloadPolicy policy, String url) {
- if (!isUpdaterEnabled(context)) {
- return;
- }
-
- Intent intent = createIntent(context, ACTION_REGISTER_FOR_UPDATES);
-
- if (policy != null) {
- intent.putExtra(EXTRA_AUTODOWNLOAD_NAME, policy.value);
- }
-
- if (url != null) {
- intent.putExtra(EXTRA_UPDATE_URL_NAME, url);
- }
-
- context.startService(intent);
- }
-
- private static Intent createIntent(Context context, String action) {
- return new Intent(action, null, context, UpdateService.class);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/util/ColorUtil.java b/mobile/android/base/java/org/mozilla/gecko/util/ColorUtil.java
deleted file mode 100644
index ec227d1ce..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/util/ColorUtil.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.util;
-
-import android.graphics.Color;
-
-public class ColorUtil {
- public static int darken(final int color, final double fraction) {
- int red = Color.red(color);
- int green = Color.green(color);
- int blue = Color.blue(color);
- red = darkenColor(red, fraction);
- green = darkenColor(green, fraction);
- blue = darkenColor(blue, fraction);
- final int alpha = Color.alpha(color);
- return Color.argb(alpha, red, green, blue);
- }
-
- public static int getReadableTextColor(final int backgroundColor) {
- final int greyValue = grayscaleFromRGB(backgroundColor);
- // 186 chosen rather than the seemingly obvious 128 because of gamma.
- if (greyValue < 186) {
- return Color.WHITE;
- } else {
- return Color.BLACK;
- }
- }
-
- private static int darkenColor(final int color, final double fraction) {
- return (int) Math.max(color - (color * fraction), 0);
- }
-
- private static int grayscaleFromRGB(final int color) {
- final int red = Color.red(color);
- final int green = Color.green(color);
- final int blue = Color.blue(color);
- // Magic weighting taken from a stackoverflow post, supposedly related to how
- // humans perceive color.
- return (int) (0.299 * red + 0.587 * green + 0.114 * blue);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/util/DrawableUtil.java b/mobile/android/base/java/org/mozilla/gecko/util/DrawableUtil.java
deleted file mode 100644
index f3c9eef83..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/util/DrawableUtil.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.util;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.drawable.Drawable;
-import android.support.annotation.CheckResult;
-import android.support.annotation.ColorInt;
-import android.support.annotation.ColorRes;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.NonNull;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.graphics.drawable.DrawableCompat;
-
-import org.mozilla.gecko.AppConstants;
-
-public class DrawableUtil {
-
- /**
- * Tints the given drawable with the given color and returns it.
- */
- @CheckResult
- public static Drawable tintDrawable(@NonNull final Context context,
- @DrawableRes final int drawableID,
- @ColorInt final int color) {
- final Drawable icon = DrawableCompat.wrap(ResourceDrawableUtils.getDrawable(context, drawableID)
- .mutate());
- DrawableCompat.setTint(icon, color);
- return icon;
- }
-
- /**
- * Tints the given drawable with the given color and returns it.
- */
- @CheckResult
- public static Drawable tintDrawableWithColorRes(@NonNull final Context context,
- @DrawableRes final int drawableID,
- @ColorRes final int colorID) {
- return tintDrawable(context, drawableID, ContextCompat.getColor(context, colorID));
- }
-
- /**
- * Tints the given drawable with the given tint list and returns it. Note that you
- * should no longer use the argument Drawable because the argument is not mutated
- * on pre-Lollipop devices but is mutated on L+ due to differences in the Support
- * Library implementation (bug 1193950).
- */
- @CheckResult
- public static Drawable tintDrawableWithStateList(@NonNull final Drawable drawable,
- @NonNull final ColorStateList colorList) {
- final Drawable wrappedDrawable = DrawableCompat.wrap(drawable.mutate());
- DrawableCompat.setTintList(wrappedDrawable, colorList);
-
- // DrawableCompat on pre-L doesn't handle its bounds correctly, and by default therefore won't
- // be rendered - we need to manually copy the bounds as a workaround:
- if (AppConstants.Versions.preMarshmallow) {
- wrappedDrawable.setBounds(0, 0, wrappedDrawable.getIntrinsicHeight(), wrappedDrawable.getIntrinsicHeight());
- }
-
- return wrappedDrawable;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/util/ResourceDrawableUtils.java b/mobile/android/base/java/org/mozilla/gecko/util/ResourceDrawableUtils.java
deleted file mode 100644
index 1e5c2a723..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/util/ResourceDrawableUtils.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-package org.mozilla.gecko.util;
-
-import android.content.Context;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.NonNull;
-import android.support.v7.widget.AppCompatDrawableManager;
-import android.text.TextUtils;
-import android.util.Log;
-
-import org.mozilla.gecko.util.GeckoJarReader;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.UIAsyncTask;
-
-import java.io.InputStream;
-import java.net.URL;
-
-import static org.mozilla.gecko.gfx.BitmapUtils.getBitmapFromDataURI;
-import static org.mozilla.gecko.gfx.BitmapUtils.getResource;
-
-public class ResourceDrawableUtils {
- private static final String LOGTAG = "ResourceDrawableUtils";
-
- public static Drawable getDrawable(@NonNull final Context context,
- @DrawableRes final int drawableID) {
- // TODO: upgrade this call to use AppCompatResources when upgrading to support library >= 24.2
- // https://developer.android.com/reference/android/support/v7/content/res/AppCompatResources.html#getDrawable(android.content.Context,%20int)
- return AppCompatDrawableManager.get().getDrawable(context, drawableID);
- }
-
- public interface BitmapLoader {
- public void onBitmapFound(Drawable d);
- }
-
- public static void runOnBitmapFoundOnUiThread(final BitmapLoader loader, final Drawable d) {
- if (ThreadUtils.isOnUiThread()) {
- loader.onBitmapFound(d);
- return;
- }
-
- ThreadUtils.postToUiThread(new Runnable() {
- @Override
- public void run() {
- loader.onBitmapFound(d);
- }
- });
- }
-
- /**
- * Attempts to find a drawable associated with a given string, using its URI scheme to determine
- * how to load the drawable. The BitmapLoader's `onBitmapFound` method is always called, and
- * will be called with `null` if no drawable is found.
- *
- * The BitmapLoader `onBitmapFound` method always runs on the UI thread.
- */
- public static void getDrawable(final Context context, final String data, final BitmapLoader loader) {
- if (TextUtils.isEmpty(data)) {
- runOnBitmapFoundOnUiThread(loader, null);
- return;
- }
-
- if (data.startsWith("data")) {
- final BitmapDrawable d = new BitmapDrawable(context.getResources(), getBitmapFromDataURI(data));
- runOnBitmapFoundOnUiThread(loader, d);
- return;
- }
-
- if (data.startsWith("jar:") || data.startsWith("file://")) {
- (new UIAsyncTask.WithoutParams<Drawable>(ThreadUtils.getBackgroundHandler()) {
- @Override
- public Drawable doInBackground() {
- try {
- if (data.startsWith("jar:jar")) {
- return GeckoJarReader.getBitmapDrawable(
- context, context.getResources(), data);
- }
-
- // Don't attempt to validate the JAR signature when loading an add-on icon
- if (data.startsWith("jar:file")) {
- return GeckoJarReader.getBitmapDrawable(
- context, context.getResources(), Uri.decode(data));
- }
-
- final URL url = new URL(data);
- final InputStream is = (InputStream) url.getContent();
- try {
- return Drawable.createFromStream(is, "src");
- } finally {
- is.close();
- }
- } catch (Exception e) {
- Log.w(LOGTAG, "Unable to set icon", e);
- }
- return null;
- }
-
- @Override
- public void onPostExecute(Drawable drawable) {
- loader.onBitmapFound(drawable);
- }
- }).execute();
- return;
- }
-
- if (data.startsWith("-moz-icon://")) {
- final Uri imageUri = Uri.parse(data);
- final String ssp = imageUri.getSchemeSpecificPart();
- final String resource = ssp.substring(ssp.lastIndexOf('/') + 1);
-
- try {
- final Drawable d = context.getPackageManager().getApplicationIcon(resource);
- runOnBitmapFoundOnUiThread(loader, d);
- } catch (Exception ex) { }
-
- return;
- }
-
- if (data.startsWith("drawable://")) {
- final Uri imageUri = Uri.parse(data);
- final int id = getResource(context, imageUri);
- final Drawable d = getDrawable(context, id);
-
- runOnBitmapFoundOnUiThread(loader, d);
- return;
- }
-
- runOnBitmapFoundOnUiThread(loader, null);
- }
-
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/util/TouchTargetUtil.java b/mobile/android/base/java/org/mozilla/gecko/util/TouchTargetUtil.java
deleted file mode 100644
index 6414dec9f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/util/TouchTargetUtil.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.util;
-
-import android.graphics.Rect;
-import android.view.TouchDelegate;
-import android.view.View;
-
-import org.mozilla.gecko.R;
-
-public class TouchTargetUtil {
- /**
- * Ensures that a given targetView has a large enough touch area to ensure it can be selected.
- * A TouchDelegate will be added to the enclosingView as necessary.
- *
- * @param targetView
- * @param enclosingView
- */
- public static void ensureTargetHitArea(final View targetView, final View enclosingView) {
- enclosingView.post(new Runnable() {
- @Override
- public void run() {
- Rect delegateArea = new Rect();
- targetView.getHitRect(delegateArea);
-
- final int targetHitArea = enclosingView.getContext().getResources().getDimensionPixelSize(R.dimen.touch_target_size);
-
- final int widthDelta = (targetHitArea - delegateArea.width()) / 2;
- delegateArea.right += widthDelta;
- delegateArea.left -= widthDelta;
-
- final int heightDelta = (targetHitArea - delegateArea.height()) / 2;
- delegateArea.bottom += heightDelta;
- delegateArea.top -= heightDelta;
-
- if (heightDelta <= 0 && widthDelta <= 0) {
- return;
- }
-
- TouchDelegate touchDelegate = new TouchDelegate(delegateArea, targetView);
- enclosingView.setTouchDelegate(touchDelegate);
- }
- });
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/util/UnusedResourcesUtil.java b/mobile/android/base/java/org/mozilla/gecko/util/UnusedResourcesUtil.java
deleted file mode 100644
index 0033e72a0..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/util/UnusedResourcesUtil.java
+++ /dev/null
@@ -1,128 +0,0 @@
-package org.mozilla.gecko.util;
-
-import org.mozilla.gecko.R;
-
-/**
- * (linter: UnusedResources) We use resources in places Android Lint can't check (e.g. JS) - this is
- * a set of those references so Android Lint stops complaining.
- */
-@SuppressWarnings("unused")
-final class UnusedResourcesUtil {
- public static final int[] CONSTANTS = {
- R.dimen.match_parent,
- R.dimen.wrap_content,
- };
-
- public static final int[] USED_IN_BRANDING = {
- R.drawable.large_icon
- };
-
- public static final int[] USED_IN_COLOR_PALETTE = {
- R.color.private_browsing_purple, // This will be used eventually, then this item removed.
- };
-
- public static final int[] USED_IN_CRASH_REPORTER = {
- R.string.crash_allow_contact2,
- R.string.crash_close_label,
- R.string.crash_comment,
- R.string.crash_email,
- R.string.crash_include_url2,
- R.string.crash_message2,
- R.string.crash_restart_label,
- R.string.crash_send_report_message3,
- R.string.crash_sorry,
- };
-
- public static final int[] USED_IN_JS = {
- R.drawable.ab_search,
- R.drawable.alert_camera,
- R.drawable.alert_download,
- R.drawable.alert_download_animation,
- R.drawable.alert_mic,
- R.drawable.alert_mic_camera,
- R.drawable.casting,
- R.drawable.casting_active,
- R.drawable.close,
- R.drawable.homepage_banner_firstrun,
- R.drawable.icon_openinapp,
- R.drawable.pause,
- R.drawable.phone,
- R.drawable.play,
- R.drawable.reader,
- R.drawable.reader_active,
- R.drawable.sync_promo,
- R.drawable.undo_button_icon,
- };
-
- public static final int[] USED_IN_MANIFEST = {
- R.drawable.search_launcher,
- R.string.crash_reporter_title,
- R.xml.fxaccount_authenticator,
- R.xml.fxaccount_syncadapter,
- R.xml.search_widget_info,
- R.xml.searchable,
- };
-
- public static final int[] USED_IN_SUGGESTEDSITES = {
- R.drawable.suggestedsites_amazon,
- R.drawable.suggestedsites_facebook,
- R.drawable.suggestedsites_restricted_fxsupport,
- R.drawable.suggestedsites_restricted_mozilla,
- R.drawable.suggestedsites_twitter,
- R.drawable.suggestedsites_webmaker,
- R.drawable.suggestedsites_wikipedia,
- R.drawable.suggestedsites_youtube,
- };
-
- public static final int[] USED_IN_BOOKMARKDEFAULTS = {
- R.raw.bookmarkdefaults_favicon_addons,
- R.raw.bookmarkdefaults_favicon_support,
- R.raw.bookmarkdefaults_favicon_restricted_support,
- R.raw.bookmarkdefaults_favicon_restricted_webmaker,
- R.string.bookmarkdefaults_title_restricted_support,
- R.string.bookmarkdefaults_url_restricted_support,
- R.string.bookmarkdefaults_title_restricted_webmaker,
- R.string.bookmarkdefaults_url_restricted_webmaker,
- };
-
- public static final int[] USED_IN_PREFS = {
- R.xml.preferences_advanced,
- R.xml.preferences_accessibility,
- R.xml.preferences_home,
- R.xml.preferences_privacy,
- R.xml.preferences_privacy_clear_tablet,
- R.xml.preferences_default_browser_tablet
- };
-
- // We are migrating to Gradle 2.10 and the Android Gradle plugin 2.0. The new plugin does find
- // more unused resources but we are not ready to remove them yet. Some of the resources are going
- // to be reused soon. This is a temporary solution so that the gradle migration is not blocked.
- // See bug 1263390 / bug 1268414.
- public static final int[] TEMPORARY_UNUSED_WHILE_MIGRATING_GRADLE = {
- R.color.remote_tabs_setup_button_background_hit,
-
- R.drawable.remote_tabs_setup_button_background,
-
- R.style.TabsPanelSectionBase,
- R.style.TabsPanelSection,
- R.style.TabsPanelItemBase,
- R.style.TabsPanelItem,
- R.style.TabsPanelItem_TextAppearance,
- R.style.TabsPanelItem_TextAppearance_Header,
- R.style.TabsPanelItem_TextAppearance_Linkified,
- R.style.TabWidget,
- R.style.GeckoDialogTitle,
- R.style.GeckoDialogTitle_SubTitle,
- R.style.RemoteTabsPanelItem,
- R.style.RemoteTabsPanelItem_TextAppearance,
- R.style.RemoteTabsPanelItem_TextAppearance_Header,
- R.style.RemoteTabsPanelItem_TextAppearance_Linkified,
- R.style.RemoteTabsPanelItem_Button,
- };
-
- // String resources that are used in the full-pane Activity Stream that are temporarily
- // not needed while Activity Stream is part of the HomePager
- public static final int[] TEMPORARY_UNUSED_ACTIVITY_STREAM = {
- R.string.activity_stream_topsites
- };
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/util/ViewUtil.java b/mobile/android/base/java/org/mozilla/gecko/util/ViewUtil.java
deleted file mode 100644
index 180e821e7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/util/ViewUtil.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-package org.mozilla.gecko.util;
-
-import android.content.res.TypedArray;
-import android.view.View;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.R;
-
-public class ViewUtil {
-
- /**
- * Enable a circular touch ripple for a given view. This is intended for borderless views,
- * such as (3-dot) menu buttons.
- *
- * Because of platform limitations a square ripple is used on Android 4.
- */
- public static void enableTouchRipple(View view) {
- final TypedArray backgroundDrawableArray;
- if (AppConstants.Versions.feature21Plus) {
- backgroundDrawableArray = view.getContext().obtainStyledAttributes(new int[] { R.attr.selectableItemBackgroundBorderless });
- } else {
- backgroundDrawableArray = view.getContext().obtainStyledAttributes(new int[] { R.attr.selectableItemBackground });
- }
-
- // This call is deprecated, but the replacement setBackground(Drawable) isn't available
- // until API 16.
- view.setBackgroundDrawable(backgroundDrawableArray.getDrawable(0));
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/ActivityChooserModel.java b/mobile/android/base/java/org/mozilla/gecko/widget/ActivityChooserModel.java
deleted file mode 100644
index 8cde1ee05..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/ActivityChooserModel.java
+++ /dev/null
@@ -1,1359 +0,0 @@
-/*
- * 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.
- */
-
-/**
- * Mozilla: Changing the package.
- */
-//package android.widget;
-package org.mozilla.gecko.widget;
-
-// Mozilla: New import
-import android.accounts.Account;
-import android.content.pm.PackageManager;
-
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.db.TabsAccessor;
-import org.mozilla.gecko.distribution.Distribution;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.fxa.FirefoxAccounts;
-import org.mozilla.gecko.fxa.SyncStatusListener;
-import org.mozilla.gecko.overlays.ui.ShareDialog;
-import org.mozilla.gecko.R;
-import java.io.File;
-
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ResolveInfo;
-import android.database.Cursor;
-import android.database.DataSetObservable;
-import android.os.AsyncTask;
-import android.text.TextUtils;
-import android.util.Log;
-import android.util.Xml;
-
-/**
- * Mozilla: Unused import.
- */
-//import com.android.internal.content.PackageMonitor;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-import org.xmlpull.v1.XmlSerializer;
-
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.math.BigDecimal;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-
-/**
- * <p>
- * This class represents a data model for choosing a component for handing a
- * given {@link Intent}. The model is responsible for querying the system for
- * activities that can handle the given intent and order found activities
- * based on historical data of previous choices. The historical data is stored
- * in an application private file. If a client does not want to have persistent
- * choice history the file can be omitted, thus the activities will be ordered
- * based on historical usage for the current session.
- * <p>
- * </p>
- * For each backing history file there is a singleton instance of this class. Thus,
- * several clients that specify the same history file will share the same model. Note
- * that if multiple clients are sharing the same model they should implement semantically
- * equivalent functionality since setting the model intent will change the found
- * activities and they may be inconsistent with the functionality of some of the clients.
- * For example, choosing a share activity can be implemented by a single backing
- * model and two different views for performing the selection. If however, one of the
- * views is used for sharing but the other for importing, for example, then each
- * view should be backed by a separate model.
- * </p>
- * <p>
- * The way clients interact with this class is as follows:
- * </p>
- * <p>
- * <pre>
- * <code>
- * // Get a model and set it to a couple of clients with semantically similar function.
- * ActivityChooserModel dataModel =
- * ActivityChooserModel.get(context, "task_specific_history_file_name.xml");
- *
- * ActivityChooserModelClient modelClient1 = getActivityChooserModelClient1();
- * modelClient1.setActivityChooserModel(dataModel);
- *
- * ActivityChooserModelClient modelClient2 = getActivityChooserModelClient2();
- * modelClient2.setActivityChooserModel(dataModel);
- *
- * // Set an intent to choose a an activity for.
- * dataModel.setIntent(intent);
- * <pre>
- * <code>
- * </p>
- * <p>
- * <strong>Note:</strong> This class is thread safe.
- * </p>
- *
- * @hide
- */
-public class ActivityChooserModel extends DataSetObservable {
-
- /**
- * Client that utilizes an {@link ActivityChooserModel}.
- */
- public interface ActivityChooserModelClient {
-
- /**
- * Sets the {@link ActivityChooserModel}.
- *
- * @param dataModel The model.
- */
- public void setActivityChooserModel(ActivityChooserModel dataModel);
- }
-
- /**
- * Defines a sorter that is responsible for sorting the activities
- * based on the provided historical choices and an intent.
- */
- public interface ActivitySorter {
-
- /**
- * Sorts the <code>activities</code> in descending order of relevance
- * based on previous history and an intent.
- *
- * @param intent The {@link Intent}.
- * @param activities Activities to be sorted.
- * @param historicalRecords Historical records.
- */
- // This cannot be done by a simple comparator since an Activity weight
- // is computed from history. Note that Activity implements Comparable.
- public void sort(Intent intent, List<ActivityResolveInfo> activities,
- List<HistoricalRecord> historicalRecords);
- }
-
- /**
- * Listener for choosing an activity.
- */
- public interface OnChooseActivityListener {
-
- /**
- * Called when an activity has been chosen. The client can decide whether
- * an activity can be chosen and if so the caller of
- * {@link ActivityChooserModel#chooseActivity(int)} will receive and {@link Intent}
- * for launching it.
- * <p>
- * <strong>Note:</strong> Modifying the intent is not permitted and
- * any changes to the latter will be ignored.
- * </p>
- *
- * @param host The listener's host model.
- * @param intent The intent for launching the chosen activity.
- * @return Whether the intent is handled and should not be delivered to clients.
- *
- * @see ActivityChooserModel#chooseActivity(int)
- */
- public boolean onChooseActivity(ActivityChooserModel host, Intent intent);
- }
-
- /**
- * Flag for selecting debug mode.
- */
- private static final boolean DEBUG = false;
-
- /**
- * Tag used for logging.
- */
- static final String LOG_TAG = ActivityChooserModel.class.getSimpleName();
-
- /**
- * The root tag in the history file.
- */
- private static final String TAG_HISTORICAL_RECORDS = "historical-records";
-
- /**
- * The tag for a record in the history file.
- */
- private static final String TAG_HISTORICAL_RECORD = "historical-record";
-
- /**
- * Attribute for the activity.
- */
- private static final String ATTRIBUTE_ACTIVITY = "activity";
-
- /**
- * Attribute for the choice time.
- */
- private static final String ATTRIBUTE_TIME = "time";
-
- /**
- * Attribute for the choice weight.
- */
- private static final String ATTRIBUTE_WEIGHT = "weight";
-
- /**
- * The default maximal length of the choice history.
- */
- public static final int DEFAULT_HISTORY_MAX_LENGTH = 50;
-
- /**
- * The amount with which to inflate a chosen activity when set as default.
- */
- private static final int DEFAULT_ACTIVITY_INFLATION = 5;
-
- /**
- * Default weight for a choice record.
- */
- private static final float DEFAULT_HISTORICAL_RECORD_WEIGHT = 1.0f;
-
- /**
- * The extension of the history file.
- */
- private static final String HISTORY_FILE_EXTENSION = ".xml";
-
- /**
- * An invalid item index.
- */
- private static final int INVALID_INDEX = -1;
-
- /**
- * Lock to guard the model registry.
- */
- private static final Object sRegistryLock = new Object();
-
- /**
- * This the registry for data models.
- */
- private static final Map<String, ActivityChooserModel> sDataModelRegistry =
- new HashMap<String, ActivityChooserModel>();
-
- /**
- * Lock for synchronizing on this instance.
- */
- private final Object mInstanceLock = new Object();
-
- /**
- * List of activities that can handle the current intent.
- */
- private final List<ActivityResolveInfo> mActivities = new ArrayList<ActivityResolveInfo>();
-
- /**
- * List with historical choice records.
- */
- private final List<HistoricalRecord> mHistoricalRecords = new ArrayList<HistoricalRecord>();
-
- /**
- * Monitor for added and removed packages.
- */
- /**
- * Mozilla: Converted from a PackageMonitor to a DataModelPackageMonitor to avoid importing a new class.
- */
- private final DataModelPackageMonitor mPackageMonitor = new DataModelPackageMonitor();
-
- /**
- * Context for accessing resources.
- */
- final Context mContext;
-
- /**
- * The name of the history file that backs this model.
- */
- final String mHistoryFileName;
-
- /**
- * The intent for which a activity is being chosen.
- */
- private Intent mIntent;
-
- /**
- * The sorter for ordering activities based on intent and past choices.
- */
- private ActivitySorter mActivitySorter = new DefaultSorter();
-
- /**
- * The maximal length of the choice history.
- */
- private int mHistoryMaxSize = DEFAULT_HISTORY_MAX_LENGTH;
-
- /**
- * Flag whether choice history can be read. In general many clients can
- * share the same data model and {@link #readHistoricalDataIfNeeded()} may be called
- * by arbitrary of them any number of times. Therefore, this class guarantees
- * that the very first read succeeds and subsequent reads can be performed
- * only after a call to {@link #persistHistoricalDataIfNeeded()} followed by change
- * of the share records.
- */
- boolean mCanReadHistoricalData = true;
-
- /**
- * Flag whether the choice history was read. This is used to enforce that
- * before calling {@link #persistHistoricalDataIfNeeded()} a call to
- * {@link #persistHistoricalDataIfNeeded()} has been made. This aims to avoid a
- * scenario in which a choice history file exits, it is not read yet and
- * it is overwritten. Note that always all historical records are read in
- * full and the file is rewritten. This is necessary since we need to
- * purge old records that are outside of the sliding window of past choices.
- */
- private boolean mReadShareHistoryCalled;
-
- /**
- * Flag whether the choice records have changed. In general many clients can
- * share the same data model and {@link #persistHistoricalDataIfNeeded()} may be called
- * by arbitrary of them any number of times. Therefore, this class guarantees
- * that choice history will be persisted only if it has changed.
- */
- private boolean mHistoricalRecordsChanged = true;
-
- /**
- * Flag whether to reload the activities for the current intent.
- */
- boolean mReloadActivities;
-
- /**
- * Policy for controlling how the model handles chosen activities.
- */
- private OnChooseActivityListener mActivityChooserModelPolicy;
-
- /**
- * Mozilla: Share overlay variables.
- */
- private final SyncStatusListener mSyncStatusListener = new SyncStatusDelegate();
-
- /**
- * Gets the data model backed by the contents of the provided file with historical data.
- * Note that only one data model is backed by a given file, thus multiple calls with
- * the same file name will return the same model instance. If no such instance is present
- * it is created.
- *
- * <p>
- * <strong>Always use difference historical data files for semantically different actions.
- * For example, sharing is different from importing.</strong>
- * </p>
- *
- * @param context Context for loading resources.
- * @param historyFileName File name with choice history, <code>null</code>
- * if the model should not be backed by a file. In this case the activities
- * will be ordered only by data from the current session.
- *
- * @return The model.
- */
- public static ActivityChooserModel get(Context context, String historyFileName) {
- synchronized (sRegistryLock) {
- ActivityChooserModel dataModel = sDataModelRegistry.get(historyFileName);
- if (dataModel == null) {
- dataModel = new ActivityChooserModel(context, historyFileName);
- sDataModelRegistry.put(historyFileName, dataModel);
- }
- return dataModel;
- }
- }
-
- /**
- * Creates a new instance.
- *
- * @param context Context for loading resources.
- * @param historyFileName The history XML file.
- */
- private ActivityChooserModel(Context context, String historyFileName) {
- mContext = context.getApplicationContext();
- if (!TextUtils.isEmpty(historyFileName)
- && !historyFileName.endsWith(HISTORY_FILE_EXTENSION)) {
- mHistoryFileName = historyFileName + HISTORY_FILE_EXTENSION;
- } else {
- mHistoryFileName = historyFileName;
- }
-
- /**
- * Mozilla: Uses modified receiver
- */
- mPackageMonitor.register(mContext);
-
- /**
- * Mozilla: Add Sync Status Listener.
- */
- // TODO: We only need to add a sync status listener if the ShareDialog passes the intent filter.
- FirefoxAccounts.addSyncStatusListener(mSyncStatusListener);
- }
-
- /**
- * Sets an intent for which to choose a activity.
- * <p>
- * <strong>Note:</strong> Clients must set only semantically similar
- * intents for each data model.
- * <p>
- *
- * @param intent The intent.
- */
- public void setIntent(Intent intent) {
- synchronized (mInstanceLock) {
- if (mIntent == intent) {
- return;
- }
- mIntent = intent;
- mReloadActivities = true;
- ensureConsistentState();
- }
- }
-
- /**
- * Gets the intent for which a activity is being chosen.
- *
- * @return The intent.
- */
- public Intent getIntent() {
- synchronized (mInstanceLock) {
- return mIntent;
- }
- }
-
- /**
- * Gets the number of activities that can handle the intent.
- *
- * @return The activity count.
- *
- * @see #setIntent(Intent)
- */
- public int getActivityCount() {
- synchronized (mInstanceLock) {
- ensureConsistentState();
- return mActivities.size();
- }
- }
-
- /**
- * Gets an activity at a given index.
- *
- * @return The activity.
- *
- * @see ActivityResolveInfo
- * @see #setIntent(Intent)
- */
- public ResolveInfo getActivity(int index) {
- synchronized (mInstanceLock) {
- ensureConsistentState();
- return mActivities.get(index).resolveInfo;
- }
- }
-
- /**
- * Gets the index of a the given activity.
- *
- * @param activity The activity index.
- *
- * @return The index if found, -1 otherwise.
- */
- public int getActivityIndex(ResolveInfo activity) {
- synchronized (mInstanceLock) {
- ensureConsistentState();
- List<ActivityResolveInfo> activities = mActivities;
- final int activityCount = activities.size();
- for (int i = 0; i < activityCount; i++) {
- ActivityResolveInfo currentActivity = activities.get(i);
- if (currentActivity.resolveInfo == activity) {
- return i;
- }
- }
- return INVALID_INDEX;
- }
- }
-
- /**
- * Chooses a activity to handle the current intent. This will result in
- * adding a historical record for that action and construct intent with
- * its component name set such that it can be immediately started by the
- * client.
- * <p>
- * <strong>Note:</strong> By calling this method the client guarantees
- * that the returned intent will be started. This intent is returned to
- * the client solely to let additional customization before the start.
- * </p>
- *
- * @return An {@link Intent} for launching the activity or null if the
- * policy has consumed the intent or there is not current intent
- * set via {@link #setIntent(Intent)}.
- *
- * @see HistoricalRecord
- * @see OnChooseActivityListener
- */
- public Intent chooseActivity(int index) {
- synchronized (mInstanceLock) {
- if (mIntent == null) {
- return null;
- }
-
- ensureConsistentState();
-
- ActivityResolveInfo chosenActivity = mActivities.get(index);
-
- ComponentName chosenName = new ComponentName(
- chosenActivity.resolveInfo.activityInfo.packageName,
- chosenActivity.resolveInfo.activityInfo.name);
-
- Intent choiceIntent = new Intent(mIntent);
- choiceIntent.setComponent(chosenName);
-
- if (mActivityChooserModelPolicy != null) {
- // Do not allow the policy to change the intent.
- Intent choiceIntentCopy = new Intent(choiceIntent);
- final boolean handled = mActivityChooserModelPolicy.onChooseActivity(this,
- choiceIntentCopy);
- if (handled) {
- return null;
- }
- }
-
- HistoricalRecord historicalRecord = new HistoricalRecord(chosenName,
- System.currentTimeMillis(), DEFAULT_HISTORICAL_RECORD_WEIGHT);
- addHistoricalRecord(historicalRecord);
-
- return choiceIntent;
- }
- }
-
- /**
- * Sets the listener for choosing an activity.
- *
- * @param listener The listener.
- */
- public void setOnChooseActivityListener(OnChooseActivityListener listener) {
- synchronized (mInstanceLock) {
- mActivityChooserModelPolicy = listener;
- }
- }
-
- /**
- * Gets the default activity, The default activity is defined as the one
- * with highest rank i.e. the first one in the list of activities that can
- * handle the intent.
- *
- * @return The default activity, <code>null</code> id not activities.
- *
- * @see #getActivity(int)
- */
- public ResolveInfo getDefaultActivity() {
- synchronized (mInstanceLock) {
- ensureConsistentState();
- if (!mActivities.isEmpty()) {
- return mActivities.get(0).resolveInfo;
- }
- }
- return null;
- }
-
- /**
- * Sets the default activity. The default activity is set by adding a
- * historical record with weight high enough that this activity will
- * become the highest ranked. Such a strategy guarantees that the default
- * will eventually change if not used. Also the weight of the record for
- * setting a default is inflated with a constant amount to guarantee that
- * it will stay as default for awhile.
- *
- * @param index The index of the activity to set as default.
- */
- public void setDefaultActivity(int index) {
- synchronized (mInstanceLock) {
- ensureConsistentState();
-
- ActivityResolveInfo newDefaultActivity = mActivities.get(index);
- ActivityResolveInfo oldDefaultActivity = mActivities.get(0);
-
- final float weight;
- if (oldDefaultActivity != null) {
- // Add a record with weight enough to boost the chosen at the top.
- weight = oldDefaultActivity.weight - newDefaultActivity.weight
- + DEFAULT_ACTIVITY_INFLATION;
- } else {
- weight = DEFAULT_HISTORICAL_RECORD_WEIGHT;
- }
-
- ComponentName defaultName = new ComponentName(
- newDefaultActivity.resolveInfo.activityInfo.packageName,
- newDefaultActivity.resolveInfo.activityInfo.name);
- HistoricalRecord historicalRecord = new HistoricalRecord(defaultName,
- System.currentTimeMillis(), weight);
- addHistoricalRecord(historicalRecord);
- }
- }
-
- /**
- * Persists the history data to the backing file if the latter
- * was provided. Calling this method before a call to {@link #readHistoricalDataIfNeeded()}
- * throws an exception. Calling this method more than one without choosing an
- * activity has not effect.
- *
- * @throws IllegalStateException If this method is called before a call to
- * {@link #readHistoricalDataIfNeeded()}.
- */
- private void persistHistoricalDataIfNeeded() {
- if (!mReadShareHistoryCalled) {
- throw new IllegalStateException("No preceding call to #readHistoricalData");
- }
- if (!mHistoricalRecordsChanged) {
- return;
- }
- mHistoricalRecordsChanged = false;
- if (!TextUtils.isEmpty(mHistoryFileName)) {
- /**
- * Mozilla: Converted to a normal task.execute call so that this works on < ICS phones.
- */
- new PersistHistoryAsyncTask().execute(new ArrayList<HistoricalRecord>(mHistoricalRecords), mHistoryFileName);
- }
- }
-
- /**
- * Sets the sorter for ordering activities based on historical data and an intent.
- *
- * @param activitySorter The sorter.
- *
- * @see ActivitySorter
- */
- public void setActivitySorter(ActivitySorter activitySorter) {
- synchronized (mInstanceLock) {
- if (mActivitySorter == activitySorter) {
- return;
- }
- mActivitySorter = activitySorter;
- if (sortActivitiesIfNeeded()) {
- notifyChanged();
- }
- }
- }
-
- /**
- * Sets the maximal size of the historical data. Defaults to
- * {@link #DEFAULT_HISTORY_MAX_LENGTH}
- * <p>
- * <strong>Note:</strong> Setting this property will immediately
- * enforce the specified max history size by dropping enough old
- * historical records to enforce the desired size. Thus, any
- * records that exceed the history size will be discarded and
- * irreversibly lost.
- * </p>
- *
- * @param historyMaxSize The max history size.
- */
- public void setHistoryMaxSize(int historyMaxSize) {
- synchronized (mInstanceLock) {
- if (mHistoryMaxSize == historyMaxSize) {
- return;
- }
- mHistoryMaxSize = historyMaxSize;
- pruneExcessiveHistoricalRecordsIfNeeded();
- if (sortActivitiesIfNeeded()) {
- notifyChanged();
- }
- }
- }
-
- /**
- * Gets the history max size.
- *
- * @return The history max size.
- */
- public int getHistoryMaxSize() {
- synchronized (mInstanceLock) {
- return mHistoryMaxSize;
- }
- }
-
- /**
- * Gets the history size.
- *
- * @return The history size.
- */
- public int getHistorySize() {
- synchronized (mInstanceLock) {
- ensureConsistentState();
- return mHistoricalRecords.size();
- }
- }
-
- public int getDistinctActivityCountInHistory() {
- synchronized (mInstanceLock) {
- ensureConsistentState();
- final List<String> packages = new ArrayList<String>();
- for (HistoricalRecord record : mHistoricalRecords) {
- String activity = record.activity.flattenToString();
- if (!packages.contains(activity)) {
- packages.add(activity);
- }
- }
- return packages.size();
- }
- }
-
- @Override
- protected void finalize() throws Throwable {
- super.finalize();
-
- /**
- * Mozilla: Not needed for the application.
- */
- mPackageMonitor.unregister();
- FirefoxAccounts.removeSyncStatusListener(mSyncStatusListener);
- }
-
- /**
- * Ensures the model is in a consistent state which is the
- * activities for the current intent have been loaded, the
- * most recent history has been read, and the activities
- * are sorted.
- */
- private void ensureConsistentState() {
- boolean stateChanged = loadActivitiesIfNeeded();
- stateChanged |= readHistoricalDataIfNeeded();
- pruneExcessiveHistoricalRecordsIfNeeded();
- if (stateChanged) {
- sortActivitiesIfNeeded();
- notifyChanged();
- }
- }
-
- /**
- * Sorts the activities if necessary which is if there is a
- * sorter, there are some activities to sort, and there is some
- * historical data.
- *
- * @return Whether sorting was performed.
- */
- private boolean sortActivitiesIfNeeded() {
- if (mActivitySorter != null && mIntent != null
- && !mActivities.isEmpty() && !mHistoricalRecords.isEmpty()) {
- mActivitySorter.sort(mIntent, mActivities,
- Collections.unmodifiableList(mHistoricalRecords));
- return true;
- }
- return false;
- }
-
- /**
- * Loads the activities for the current intent if needed which is
- * if they are not already loaded for the current intent.
- *
- * @return Whether loading was performed.
- */
- private boolean loadActivitiesIfNeeded() {
- if (mReloadActivities && mIntent != null) {
- mReloadActivities = false;
- mActivities.clear();
- List<ResolveInfo> resolveInfos = mContext.getPackageManager()
- .queryIntentActivities(mIntent, 0);
- final int resolveInfoCount = resolveInfos.size();
-
- /**
- * Mozilla: Temporary variables to prevent performance degradation in the loop.
- */
- final PackageManager packageManager = mContext.getPackageManager();
- final String channelToRemoveLabel = mContext.getResources().getString(R.string.overlay_share_label);
- final String shareDialogClassName = ShareDialog.class.getCanonicalName();
-
- for (int i = 0; i < resolveInfoCount; i++) {
- ResolveInfo resolveInfo = resolveInfos.get(i);
-
- /**
- * Mozilla: We want "Add to Firefox" to appear differently inside of Firefox than
- * from external applications - override the name and icon here.
- *
- * Do not display the menu item if there are no devices to share to.
- *
- * Note: we check both the class name and the label to ensure we only change the
- * label of the current channel.
- */
- if (shareDialogClassName.equals(resolveInfo.activityInfo.name) &&
- channelToRemoveLabel.equals(resolveInfo.loadLabel(packageManager))) {
- // Don't add the menu item if there are no devices to share to.
- if (!hasOtherSyncClients()) {
- continue;
- }
-
- resolveInfo.labelRes = R.string.overlay_share_send_other;
- resolveInfo.icon = R.drawable.icon_shareplane;
- }
-
- mActivities.add(new ActivityResolveInfo(resolveInfo));
- }
- return true;
- }
- return false;
- }
-
- /**
- * Reads the historical data if necessary which is it has
- * changed, there is a history file, and there is not persist
- * in progress.
- *
- * @return Whether reading was performed.
- */
- private boolean readHistoricalDataIfNeeded() {
- if (mCanReadHistoricalData && mHistoricalRecordsChanged &&
- !TextUtils.isEmpty(mHistoryFileName)) {
- mCanReadHistoricalData = false;
- mReadShareHistoryCalled = true;
- readHistoricalDataImpl();
- return true;
- }
- return false;
- }
-
- /**
- * Adds a historical record.
- *
- * @param historicalRecord The record to add.
- * @return True if the record was added.
- */
- private boolean addHistoricalRecord(HistoricalRecord historicalRecord) {
- final boolean added = mHistoricalRecords.add(historicalRecord);
- if (added) {
- mHistoricalRecordsChanged = true;
- pruneExcessiveHistoricalRecordsIfNeeded();
- persistHistoricalDataIfNeeded();
- sortActivitiesIfNeeded();
- notifyChanged();
- }
- return added;
- }
-
- /**
- * Removes all historical records for this pkg.
- *
- * @param historicalRecord The pkg to delete records for.
- * @return True if the record was added.
- */
- boolean removeHistoricalRecordsForPackage(final String pkg) {
- boolean removed = false;
-
- for (Iterator<HistoricalRecord> i = mHistoricalRecords.iterator(); i.hasNext();) {
- final HistoricalRecord record = i.next();
- if (record.activity.getPackageName().equals(pkg)) {
- i.remove();
- removed = true;
- }
- }
-
- if (removed) {
- mHistoricalRecordsChanged = true;
- pruneExcessiveHistoricalRecordsIfNeeded();
- persistHistoricalDataIfNeeded();
- sortActivitiesIfNeeded();
- notifyChanged();
- }
-
- return removed;
- }
-
- /**
- * Prunes older excessive records to guarantee maxHistorySize.
- */
- private void pruneExcessiveHistoricalRecordsIfNeeded() {
- final int pruneCount = mHistoricalRecords.size() - mHistoryMaxSize;
- if (pruneCount <= 0) {
- return;
- }
- mHistoricalRecordsChanged = true;
- for (int i = 0; i < pruneCount; i++) {
- HistoricalRecord prunedRecord = mHistoricalRecords.remove(0);
- if (DEBUG) {
- Log.i(LOG_TAG, "Pruned: " + prunedRecord);
- }
- }
- }
-
- /**
- * Represents a record in the history.
- */
- public final static class HistoricalRecord {
-
- /**
- * The activity name.
- */
- public final ComponentName activity;
-
- /**
- * The choice time.
- */
- public final long time;
-
- /**
- * The record weight.
- */
- public final float weight;
-
- /**
- * Creates a new instance.
- *
- * @param activityName The activity component name flattened to string.
- * @param time The time the activity was chosen.
- * @param weight The weight of the record.
- */
- public HistoricalRecord(String activityName, long time, float weight) {
- this(ComponentName.unflattenFromString(activityName), time, weight);
- }
-
- /**
- * Creates a new instance.
- *
- * @param activityName The activity name.
- * @param time The time the activity was chosen.
- * @param weight The weight of the record.
- */
- public HistoricalRecord(ComponentName activityName, long time, float weight) {
- this.activity = activityName;
- this.time = time;
- this.weight = weight;
- }
-
- @Override
- public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = prime * result + ((activity == null) ? 0 : activity.hashCode());
- result = prime * result + (int) (time ^ (time >>> 32));
- result = prime * result + Float.floatToIntBits(weight);
- return result;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj == null) {
- return false;
- }
- if (getClass() != obj.getClass()) {
- return false;
- }
- HistoricalRecord other = (HistoricalRecord) obj;
- if (activity == null) {
- if (other.activity != null) {
- return false;
- }
- } else if (!activity.equals(other.activity)) {
- return false;
- }
- if (time != other.time) {
- return false;
- }
- if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) {
- return false;
- }
- return true;
- }
-
- @Override
- public String toString() {
- StringBuilder builder = new StringBuilder();
- builder.append("[");
- builder.append("; activity:").append(activity);
- builder.append("; time:").append(time);
- builder.append("; weight:").append(new BigDecimal(weight));
- builder.append("]");
- return builder.toString();
- }
- }
-
- /**
- * Represents an activity.
- */
- public final class ActivityResolveInfo implements Comparable<ActivityResolveInfo> {
-
- /**
- * The {@link ResolveInfo} of the activity.
- */
- public final ResolveInfo resolveInfo;
-
- /**
- * Weight of the activity. Useful for sorting.
- */
- public float weight;
-
- /**
- * Creates a new instance.
- *
- * @param resolveInfo activity {@link ResolveInfo}.
- */
- public ActivityResolveInfo(ResolveInfo resolveInfo) {
- this.resolveInfo = resolveInfo;
- }
-
- @Override
- public int hashCode() {
- return 31 + Float.floatToIntBits(weight);
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj == null) {
- return false;
- }
- if (getClass() != obj.getClass()) {
- return false;
- }
- ActivityResolveInfo other = (ActivityResolveInfo) obj;
- if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) {
- return false;
- }
- return true;
- }
-
- @Override
- public int compareTo(ActivityResolveInfo another) {
- return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight);
- }
-
- @Override
- public String toString() {
- StringBuilder builder = new StringBuilder();
- builder.append("[");
- builder.append("resolveInfo:").append(resolveInfo.toString());
- builder.append("; weight:").append(new BigDecimal(weight));
- builder.append("]");
- return builder.toString();
- }
- }
-
- /**
- * Default activity sorter implementation.
- */
- private final class DefaultSorter implements ActivitySorter {
- private static final float WEIGHT_DECAY_COEFFICIENT = 0.95f;
-
- private final Map<String, ActivityResolveInfo> mPackageNameToActivityMap =
- new HashMap<String, ActivityResolveInfo>();
-
- @Override
- public void sort(Intent intent, List<ActivityResolveInfo> activities,
- List<HistoricalRecord> historicalRecords) {
- Map<String, ActivityResolveInfo> packageNameToActivityMap =
- mPackageNameToActivityMap;
- packageNameToActivityMap.clear();
-
- final int activityCount = activities.size();
- for (int i = 0; i < activityCount; i++) {
- ActivityResolveInfo activity = activities.get(i);
- activity.weight = 0.0f;
-
- // Make sure we're using a non-ambiguous name here
- ComponentName chosenName = new ComponentName(
- activity.resolveInfo.activityInfo.packageName,
- activity.resolveInfo.activityInfo.name);
- String packageName = chosenName.flattenToString();
- packageNameToActivityMap.put(packageName, activity);
- }
-
- final int lastShareIndex = historicalRecords.size() - 1;
- float nextRecordWeight = 1;
- for (int i = lastShareIndex; i >= 0; i--) {
- HistoricalRecord historicalRecord = historicalRecords.get(i);
- String packageName = historicalRecord.activity.flattenToString();
- ActivityResolveInfo activity = packageNameToActivityMap.get(packageName);
- if (activity != null) {
- activity.weight += historicalRecord.weight * nextRecordWeight;
- nextRecordWeight = nextRecordWeight * WEIGHT_DECAY_COEFFICIENT;
- }
- }
-
- Collections.sort(activities);
-
- if (DEBUG) {
- for (int i = 0; i < activityCount; i++) {
- Log.i(LOG_TAG, "Sorted: " + activities.get(i));
- }
- }
- }
- }
-
- /**
- * Command for reading the historical records from a file off the UI thread.
- */
- private void readHistoricalDataImpl() {
- try {
- GeckoProfile profile = GeckoProfile.get(mContext);
- File f = profile.getFile(mHistoryFileName);
- if (!f.exists()) {
- // Fall back to the non-profile aware file if it exists...
- File oldFile = new File(mHistoryFileName);
- oldFile.renameTo(f);
- }
- readHistoricalDataFromStream(new FileInputStream(f));
- } catch (FileNotFoundException fnfe) {
- final Distribution dist = Distribution.getInstance(mContext);
- dist.addOnDistributionReadyCallback(new Distribution.ReadyCallback() {
- @Override
- public void distributionNotFound() {
- }
-
- @Override
- public void distributionFound(Distribution distribution) {
- try {
- File distFile = dist.getDistributionFile("quickshare/" + mHistoryFileName);
- if (distFile == null) {
- if (DEBUG) {
- Log.i(LOG_TAG, "Could not open historical records file: " + mHistoryFileName);
- }
- return;
- }
- readHistoricalDataFromStream(new FileInputStream(distFile));
- } catch (Exception ex) {
- if (DEBUG) {
- Log.i(LOG_TAG, "Could not open historical records file: " + mHistoryFileName);
- }
- return;
- }
- }
-
- @Override
- public void distributionArrivedLate(Distribution distribution) {
- distributionFound(distribution);
- }
- });
- }
- }
-
- void readHistoricalDataFromStream(FileInputStream fis) {
- try {
- XmlPullParser parser = Xml.newPullParser();
- parser.setInput(fis, null);
-
- int type = XmlPullParser.START_DOCUMENT;
- while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) {
- type = parser.next();
- }
-
- if (!TAG_HISTORICAL_RECORDS.equals(parser.getName())) {
- throw new XmlPullParserException("Share records file does not start with "
- + TAG_HISTORICAL_RECORDS + " tag.");
- }
-
- List<HistoricalRecord> historicalRecords = mHistoricalRecords;
- historicalRecords.clear();
-
- while (true) {
- type = parser.next();
- if (type == XmlPullParser.END_DOCUMENT) {
- break;
- }
- if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
- continue;
- }
- String nodeName = parser.getName();
- if (!TAG_HISTORICAL_RECORD.equals(nodeName)) {
- throw new XmlPullParserException("Share records file not well-formed.");
- }
-
- String activity = parser.getAttributeValue(null, ATTRIBUTE_ACTIVITY);
- final long time =
- Long.parseLong(parser.getAttributeValue(null, ATTRIBUTE_TIME));
- final float weight =
- Float.parseFloat(parser.getAttributeValue(null, ATTRIBUTE_WEIGHT));
- HistoricalRecord readRecord = new HistoricalRecord(activity, time, weight);
- historicalRecords.add(readRecord);
-
- if (DEBUG) {
- Log.i(LOG_TAG, "Read " + readRecord.toString());
- }
- }
-
- if (DEBUG) {
- Log.i(LOG_TAG, "Read " + historicalRecords.size() + " historical records.");
- }
- } catch (XmlPullParserException | IOException xppe) {
- Log.e(LOG_TAG, "Error reading historical record file: " + mHistoryFileName, xppe);
- } finally {
- if (fis != null) {
- try {
- fis.close();
- } catch (IOException ioe) {
- /* ignore */
- }
- }
- }
- }
-
- /**
- * Command for persisting the historical records to a file off the UI thread.
- */
- private final class PersistHistoryAsyncTask extends AsyncTask<Object, Void, Void> {
-
- @Override
- @SuppressWarnings("unchecked")
- public Void doInBackground(Object... args) {
- List<HistoricalRecord> historicalRecords = (List<HistoricalRecord>) args[0];
- String historyFileName = (String) args[1];
-
- FileOutputStream fos = null;
-
- try {
- // Mozilla - Update the location we save files to
- GeckoProfile profile = GeckoProfile.get(mContext);
- File file = profile.getFile(historyFileName);
- fos = new FileOutputStream(file);
- } catch (FileNotFoundException fnfe) {
- Log.e(LOG_TAG, "Error writing historical record file: " + historyFileName, fnfe);
- return null;
- }
-
- XmlSerializer serializer = Xml.newSerializer();
-
- try {
- serializer.setOutput(fos, null);
- serializer.startDocument("UTF-8", true);
- serializer.startTag(null, TAG_HISTORICAL_RECORDS);
-
- final int recordCount = historicalRecords.size();
- for (int i = 0; i < recordCount; i++) {
- HistoricalRecord record = historicalRecords.remove(0);
- serializer.startTag(null, TAG_HISTORICAL_RECORD);
- serializer.attribute(null, ATTRIBUTE_ACTIVITY,
- record.activity.flattenToString());
- serializer.attribute(null, ATTRIBUTE_TIME, String.valueOf(record.time));
- serializer.attribute(null, ATTRIBUTE_WEIGHT, String.valueOf(record.weight));
- serializer.endTag(null, TAG_HISTORICAL_RECORD);
- if (DEBUG) {
- Log.i(LOG_TAG, "Wrote " + record.toString());
- }
- }
-
- serializer.endTag(null, TAG_HISTORICAL_RECORDS);
- serializer.endDocument();
-
- if (DEBUG) {
- Log.i(LOG_TAG, "Wrote " + recordCount + " historical records.");
- }
- } catch (IllegalArgumentException | IOException | IllegalStateException e) {
- Log.e(LOG_TAG, "Error writing historical record file: " + mHistoryFileName, e);
- } finally {
- mCanReadHistoricalData = true;
- if (fos != null) {
- try {
- fos.close();
- } catch (IOException e) {
- /* ignore */
- }
- }
- }
- return null;
- }
- }
-
- /**
- * Keeps in sync the historical records and activities with the installed applications.
- */
- /**
- * Mozilla: Adapted significantly
- */
- private static final String LOGTAG = "GeckoActivityChooserModel";
- private final class DataModelPackageMonitor extends BroadcastReceiver {
- Context mContext;
-
- public DataModelPackageMonitor() { }
-
- public void register(Context context) {
- mContext = context;
-
- String[] intents = new String[] {
- Intent.ACTION_PACKAGE_REMOVED,
- Intent.ACTION_PACKAGE_ADDED,
- Intent.ACTION_PACKAGE_CHANGED
- };
-
- for (String intent : intents) {
- IntentFilter removeFilter = new IntentFilter(intent);
- removeFilter.addDataScheme("package");
- context.registerReceiver(this, removeFilter);
- }
- }
-
- public void unregister() {
- mContext.unregisterReceiver(this);
- mContext = null;
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
- if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
- String packageName = intent.getData().getSchemeSpecificPart();
- removeHistoricalRecordsForPackage(packageName);
- }
-
- mReloadActivities = true;
- }
- }
-
- /**
- * Mozilla: Return whether or not there are other synced clients.
- */
- private boolean hasOtherSyncClients() {
- // ClientsDatabaseAccessor returns stale data (bug 1145896) so we work around this by
- // checking if we have accounts set up - if not, we can't have any clients.
- if (!FirefoxAccounts.firefoxAccountsExist(mContext)) {
- return false;
- }
-
- final BrowserDB browserDB = BrowserDB.from(mContext);
- final TabsAccessor tabsAccessor = browserDB.getTabsAccessor();
- final Cursor remoteClientsCursor = tabsAccessor
- .getRemoteClientsByRecencyCursor(mContext);
- if (remoteClientsCursor == null) {
- return false;
- }
-
- try {
- return remoteClientsCursor.getCount() > 0;
- } finally {
- remoteClientsCursor.close();
- }
- }
-
- /**
- * Mozilla: Reload activities on sync.
- */
- private class SyncStatusDelegate implements SyncStatusListener {
- @Override
- public Context getContext() {
- return mContext;
- }
-
- @Override
- public Account getAccount() {
- return FirefoxAccounts.getFirefoxAccount(getContext());
- }
-
- @Override
- public void onSyncStarted() {
- }
-
- @Override
- public void onSyncFinished() {
- // TODO: We only need to reload activities when the number of devices changes.
- // This may not be worth it if we have to touch the DB to get the client count.
- synchronized (mInstanceLock) {
- mReloadActivities = true;
- }
- }
- }
-}
-
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/AllCapsTextView.java b/mobile/android/base/java/org/mozilla/gecko/widget/AllCapsTextView.java
deleted file mode 100644
index 6bd1e36e4..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/AllCapsTextView.java
+++ /dev/null
@@ -1,21 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.widget.TextView;
-
-public class AllCapsTextView extends TextView {
-
- public AllCapsTextView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public void setText(CharSequence text, BufferType type) {
- super.setText(text.toString().toUpperCase(), type);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/AnchoredPopup.java b/mobile/android/base/java/org/mozilla/gecko/widget/AnchoredPopup.java
deleted file mode 100644
index a504c5832..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/AnchoredPopup.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.R;
-
-import android.app.Activity;
-import android.content.Context;
-import android.graphics.drawable.BitmapDrawable;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-import android.widget.PopupWindow;
-import org.mozilla.gecko.util.HardwareUtils;
-
-/**
- * AnchoredPopup is the base class for doorhanger notifications, and is anchored to the urlbar.
- */
-public abstract class AnchoredPopup extends PopupWindow {
- public interface OnVisibilityChangeListener {
- public void onDoorHangerShow();
- public void onDoorHangerHide();
- }
-
- private View mAnchor;
- private OnVisibilityChangeListener onVisibilityChangeListener;
-
- protected RoundedCornerLayout mContent;
- protected boolean mInflated;
-
- protected final Context mContext;
-
- public AnchoredPopup(Context context) {
- super(context);
-
- mContext = context;
-
- setAnimationStyle(R.style.PopupAnimation);
- }
-
- protected void init() {
- // Hide the default window background. Passing null prevents the below setOutTouchable()
- // call from working, so use an empty BitmapDrawable instead.
- setBackgroundDrawable(new BitmapDrawable(mContext.getResources()));
-
- // Allow the popup to be dismissed when touching outside.
- setOutsideTouchable(true);
-
- // PopupWindow has a default width and height of 0, so set the width here.
- int width = (int) mContext.getResources().getDimension(R.dimen.doorhanger_width);
- setWindowLayoutMode(0, ViewGroup.LayoutParams.WRAP_CONTENT);
- setWidth(width);
-
- final LayoutInflater inflater = LayoutInflater.from(mContext);
- final View layout = inflater.inflate(R.layout.anchored_popup, null);
- setContentView(layout);
-
- mContent = (RoundedCornerLayout) layout.findViewById(R.id.content);
-
- mInflated = true;
- }
-
- /**
- * Sets the anchor for this popup.
- *
- * @param anchor Anchor view for positioning the arrow.
- */
- public void setAnchor(View anchor) {
- mAnchor = anchor;
- }
-
- public void setOnVisibilityChangeListener(OnVisibilityChangeListener listener) {
- onVisibilityChangeListener = listener;
- }
-
- /**
- * Shows the popup with the arrow pointing to the center of the anchor view. If the anchor
- * isn't visible, the popup will just be shown at the top of the root view.
- */
- public void show() {
- if (!mInflated) {
- throw new IllegalStateException("ArrowPopup#init() must be called before ArrowPopup#show()");
- }
-
- if (onVisibilityChangeListener != null) {
- onVisibilityChangeListener.onDoorHangerShow();
- }
-
- final int[] anchorLocation = new int[2];
- if (mAnchor != null) {
- mAnchor.getLocationInWindow(anchorLocation);
- }
-
- // The doorhanger should overlap the bottom of the urlbar.
- int offsetY = mContext.getResources().getDimensionPixelOffset(R.dimen.doorhanger_offsetY);
- final View decorView = ((Activity) mContext).getWindow().getDecorView();
-
- final boolean validAnchor = (mAnchor != null) && (anchorLocation[1] > 0);
- if (HardwareUtils.isTablet()) {
- if (validAnchor) {
- showAsDropDown(mAnchor, 0, 0);
- } else {
- // The anchor will be offscreen if the dynamic toolbar is hidden, so anticipate the re-shown position
- // of the toolbar.
- final int offsetX = mContext.getResources().getDimensionPixelOffset(R.dimen.doorhanger_offsetX);
- showAtLocation(decorView, Gravity.TOP | Gravity.LEFT, offsetX, offsetY);
- }
- } else {
- // If the anchor is null or out of the window bounds, just show the popup at the top of the
- // root view.
- final View anchor = validAnchor ? mAnchor : decorView;
-
- showAtLocation(anchor, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, offsetY);
- }
- }
-
- @Override
- public void dismiss() {
- super.dismiss();
- if (onVisibilityChangeListener != null) {
- onVisibilityChangeListener.onDoorHangerHide();
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/AnimatedHeightLayout.java b/mobile/android/base/java/org/mozilla/gecko/widget/AnimatedHeightLayout.java
deleted file mode 100644
index f1343b0fb..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/AnimatedHeightLayout.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import org.mozilla.gecko.animation.HeightChangeAnimation;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.animation.Animation;
-import android.view.animation.DecelerateInterpolator;
-import android.widget.RelativeLayout;
-
-public class AnimatedHeightLayout extends RelativeLayout {
- private static final String LOGTAG = "GeckoAnimatedHeightLayout";
- private static final int ANIMATION_DURATION = 100;
- private boolean mAnimating;
-
- public AnimatedHeightLayout(Context context) {
- super(context, null);
- }
-
- public AnimatedHeightLayout(Context context, AttributeSet attrs) {
- super(context, attrs, 0);
- }
-
- public AnimatedHeightLayout(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- int oldHeight = getMeasuredHeight();
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- int newHeight = getMeasuredHeight();
-
- if (!mAnimating && oldHeight != 0 && oldHeight != newHeight) {
- mAnimating = true;
- setMeasuredDimension(getMeasuredWidth(), oldHeight);
-
- // Animate the difference of suggestion row height
- Animation anim = new HeightChangeAnimation(this, oldHeight, newHeight);
- anim.setDuration(ANIMATION_DURATION);
- anim.setInterpolator(new DecelerateInterpolator());
- anim.setAnimationListener(new Animation.AnimationListener() {
- @Override
- public void onAnimationStart(Animation animation) {}
- @Override
- public void onAnimationRepeat(Animation animation) {}
- @Override
- public void onAnimationEnd(Animation animation) {
- post(new Runnable() {
- @Override
- public void run() {
- finishAnimation();
- }
- });
- }
- });
- startAnimation(anim);
- }
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- finishAnimation();
- }
-
- void finishAnimation() {
- if (mAnimating) {
- getLayoutParams().height = LayoutParams.WRAP_CONTENT;
- mAnimating = false;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/BasicColorPicker.java b/mobile/android/base/java/org/mozilla/gecko/widget/BasicColorPicker.java
deleted file mode 100644
index 4f1468203..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/BasicColorPicker.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import org.mozilla.gecko.R;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Color;
-import android.graphics.PorterDuff;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.widget.ArrayAdapter;
-import android.widget.AdapterView;
-import android.widget.CheckedTextView;
-import android.widget.ListView;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
-import android.util.TypedValue;
-
-public class BasicColorPicker extends ListView {
- private final static String LOGTAG = "GeckoBasicColorPicker";
- private final static List<Integer> DEFAULT_COLORS = Arrays.asList(Color.rgb(215, 57, 32),
- Color.rgb(255, 134, 5),
- Color.rgb(255, 203, 19),
- Color.rgb(95, 173, 71),
- Color.rgb(84, 201, 168),
- Color.rgb(33, 161, 222),
- Color.rgb(16, 36, 87),
- Color.rgb(91, 32, 103),
- Color.rgb(212, 221, 228),
- Color.BLACK);
-
- private static Drawable mCheckDrawable;
- int mSelected;
- final ColorPickerListAdapter mAdapter;
-
- public BasicColorPicker(Context context) {
- this(context, null);
- }
-
- public BasicColorPicker(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public BasicColorPicker(Context context, AttributeSet attrs, int style) {
- this(context, attrs, style, DEFAULT_COLORS);
- }
-
- public BasicColorPicker(Context context, AttributeSet attrs, int style, List<Integer> colors) {
- super(context, attrs, style);
- mAdapter = new ColorPickerListAdapter(context, new ArrayList<Integer>(colors));
- setAdapter(mAdapter);
-
- setOnItemClickListener(new AdapterView.OnItemClickListener() {
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- mSelected = position;
- mAdapter.notifyDataSetChanged();
- }
- });
- }
-
- public int getColor() {
- return mAdapter.getItem(mSelected);
- }
-
- public void setColor(int color) {
- if (!DEFAULT_COLORS.contains(color)) {
- mSelected = mAdapter.getCount();
- mAdapter.add(color);
- } else {
- mSelected = DEFAULT_COLORS.indexOf(color);
- }
-
- setSelection(mSelected);
- mAdapter.notifyDataSetChanged();
- }
-
- Drawable getCheckDrawable() {
- if (mCheckDrawable == null) {
- Resources res = getContext().getResources();
-
- TypedValue typedValue = new TypedValue();
- getContext().getTheme().resolveAttribute(android.R.attr.listPreferredItemHeight, typedValue, true);
- DisplayMetrics metrics = new android.util.DisplayMetrics();
- ((WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(metrics);
- int height = (int) typedValue.getDimension(metrics);
-
- Drawable background = res.getDrawable(R.drawable.color_picker_row_bg);
- Rect r = new Rect();
- background.getPadding(r);
- height -= r.top + r.bottom;
-
- mCheckDrawable = res.getDrawable(R.drawable.color_picker_checkmark);
- mCheckDrawable.setBounds(0, 0, height, height);
- }
-
- return mCheckDrawable;
- }
-
- private class ColorPickerListAdapter extends ArrayAdapter<Integer> {
- private final List<Integer> mColors;
-
- public ColorPickerListAdapter(Context context, List<Integer> colors) {
- super(context, R.layout.color_picker_row, colors);
- mColors = colors;
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- View v = super.getView(position, convertView, parent);
-
- Drawable d = v.getBackground();
- d.setColorFilter(getItem(position), PorterDuff.Mode.MULTIPLY);
- v.setBackgroundDrawable(d);
-
- Drawable check = null;
- CheckedTextView checked = ((CheckedTextView) v);
- if (mSelected == position) {
- check = getCheckDrawable();
- }
-
- checked.setCompoundDrawables(check, null, null, null);
- checked.setText("");
-
- return v;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/CheckableLinearLayout.java b/mobile/android/base/java/org/mozilla/gecko/widget/CheckableLinearLayout.java
deleted file mode 100644
index b740592fe..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/CheckableLinearLayout.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.widget.CheckBox;
-import android.widget.Checkable;
-import android.widget.LinearLayout;
-
-
-public class CheckableLinearLayout extends LinearLayout implements Checkable {
-
- private CheckBox mCheckBox;
-
- public CheckableLinearLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public boolean isChecked() {
- return mCheckBox != null && mCheckBox.isChecked();
- }
-
- @Override
- public void setChecked(boolean isChecked) {
- if (mCheckBox != null) {
- mCheckBox.setChecked(isChecked);
- }
- }
-
- @Override
- public void toggle() {
- if (mCheckBox != null) {
- mCheckBox.toggle();
- }
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
-
- mCheckBox = (CheckBox) findViewById(R.id.checkbox);
- mCheckBox.setClickable(false);
- }
-}
-
-
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/ClickableWhenDisabledEditText.java b/mobile/android/base/java/org/mozilla/gecko/widget/ClickableWhenDisabledEditText.java
deleted file mode 100644
index 206341212..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/ClickableWhenDisabledEditText.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.widget.EditText;
-
-public class ClickableWhenDisabledEditText extends EditText {
- public ClickableWhenDisabledEditText(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- if (!isEnabled() && event.getAction() == MotionEvent.ACTION_UP) {
- return performClick();
- }
- return super.onTouchEvent(event);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/ContentSecurityDoorHanger.java b/mobile/android/base/java/org/mozilla/gecko/widget/ContentSecurityDoorHanger.java
deleted file mode 100644
index 96b20a6c3..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/ContentSecurityDoorHanger.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import android.support.v4.content.ContextCompat;
-import android.util.Log;
-import android.widget.Button;
-import android.widget.TextView;
-import org.mozilla.gecko.R;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.content.Context;
-import android.view.View;
-
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.toolbar.SiteIdentityPopup;
-
-import java.util.Locale;
-
-public class ContentSecurityDoorHanger extends DoorHanger {
- private static final String LOGTAG = "GeckoSecurityDoorHanger";
-
- private final TextView mTitle;
- private final TextView mSecurityState;
- private final TextView mMessage;
-
- public ContentSecurityDoorHanger(Context context, DoorhangerConfig config, Type type) {
- super(context, config, type);
-
- mTitle = (TextView) findViewById(R.id.security_title);
- mSecurityState = (TextView) findViewById(R.id.security_state);
- mMessage = (TextView) findViewById(R.id.security_message);
-
- loadConfig(config);
- }
-
- @Override
- protected void loadConfig(DoorhangerConfig config) {
- final String message = config.getMessage();
- if (message != null) {
- mMessage.setText(message);
- }
-
- final JSONObject options = config.getOptions();
- if (options != null) {
- setOptions(options);
- }
-
- final DoorhangerConfig.Link link = config.getLink();
- if (link != null) {
- addLink(link.label, link.url);
- }
-
- addButtonsToLayout(config);
- }
-
- @Override
- protected int getContentResource() {
- return R.layout.doorhanger_security;
- }
-
- @Override
- public void setOptions(final JSONObject options) {
- super.setOptions(options);
- final JSONObject link = options.optJSONObject("link");
- if (link != null) {
- try {
- final String linkLabel = link.getString("label");
- final String linkUrl = link.getString("url");
- addLink(linkLabel, linkUrl);
- } catch (JSONException e) { }
- }
-
- final JSONObject trackingProtection = options.optJSONObject("tracking_protection");
- if (trackingProtection != null) {
- mTitle.setVisibility(VISIBLE);
- mTitle.setText(R.string.doorhanger_tracking_title);
- try {
- final boolean enabled = trackingProtection.getBoolean("enabled");
- if (enabled) {
- mMessage.setText(R.string.doorhanger_tracking_message_enabled);
- mSecurityState.setText(R.string.doorhanger_tracking_state_enabled);
- mSecurityState.setTextColor(ContextCompat.getColor(getContext(), R.color.affirmative_green));
- } else {
- mMessage.setText(R.string.doorhanger_tracking_message_disabled);
- mSecurityState.setText(R.string.doorhanger_tracking_state_disabled);
- mSecurityState.setTextColor(ContextCompat.getColor(getContext(), R.color.rejection_red));
- }
- mMessage.setVisibility(VISIBLE);
- mSecurityState.setVisibility(VISIBLE);
- } catch (JSONException e) { }
- }
- }
-
- @Override
- protected OnClickListener makeOnButtonClickListener(final int id, final String telemetryExtra) {
- return new Button.OnClickListener() {
- @Override
- public void onClick(View v) {
- final String expandedExtra = mType.toString().toLowerCase(Locale.US) + "-" + telemetryExtra;
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.DOORHANGER, expandedExtra);
-
- final JSONObject response = new JSONObject();
- try {
- switch (mType) {
- case TRACKING:
- response.put("allowContent", (id == SiteIdentityPopup.ButtonType.DISABLE.ordinal()));
- response.put("contentType", ("tracking"));
- break;
- default:
- Log.w(LOGTAG, "Unknown doorhanger type " + mType.toString());
- }
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error creating onClick response", e);
- }
-
- mOnButtonClickListener.onButtonClick(response, ContentSecurityDoorHanger.this);
- }
- };
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/CropImageView.java b/mobile/android/base/java/org/mozilla/gecko/widget/CropImageView.java
deleted file mode 100644
index 63cb84c5a..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/CropImageView.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Matrix;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.widget.ImageView;
-
-import org.mozilla.gecko.widget.themed.ThemedImageView;
-
-/**
- * An ImageView which will always display at the given width and calculated height (based on the width and
- * the supplied aspect ratio), drawn starting from the top left hand corner. A supplied drawable will be resized to fit
- * the width of the view; if the resized drawable is too tall for the view then the drawable will be cropped at the
- * bottom, however if the resized drawable is too short for the view to display whilst honouring it's given width and
- * height then the drawable will be displayed at full height with the right hand side cropped.
- */
-public abstract class CropImageView extends ThemedImageView {
- public static final String LOGTAG = "Gecko" + CropImageView.class.getSimpleName();
-
- private int viewWidth;
- private int viewHeight;
- private int drawableWidth;
- private int drawableHeight;
-
- private boolean resize = true;
- private Matrix layoutCurrentMatrix = new Matrix();
- private Matrix layoutNextMatrix = new Matrix();
-
-
- public CropImageView(final Context context) {
- this(context, null);
- }
-
- public CropImageView(final Context context, final AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public CropImageView(final Context context, final AttributeSet attrs, final int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- init();
- }
-
- protected abstract float getAspectRatio();
-
- protected void init() {
- // Setting the pivots means that the image will be drawn from the top left hand corner. There are
- // issues in Android 4.1 (16) which mean setting these values to 0 may not work.
- // http://stackoverflow.com/questions/26658124/setpivotx-doesnt-work-on-android-4-1-1-nineoldandroids
- setPivotX(1);
- setPivotY(1);
- }
-
- /**
- * Measure the view to determine the measured width and height.
- * The height is constrained by the measured width.
- *
- * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
- * @param heightMeasureSpec vertical space requirements as imposed by the parent, but ignored.
- */
- @Override
- protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
- // Default measuring.
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- // Force the height based on the aspect ratio.
- viewWidth = getMeasuredWidth();
- viewHeight = (int) (viewWidth * getAspectRatio());
-
- setMeasuredDimension(viewWidth, viewHeight);
-
- updateImageMatrix();
- }
-
- protected void updateImageMatrix() {
- if (!resize || getDrawable() == null) {
- return;
- }
-
- setScaleType(ImageView.ScaleType.MATRIX);
-
- getDrawable().setBounds(0, 0, viewWidth, viewHeight);
-
- final float horizontalScaleValue = (float) viewWidth / (float) drawableWidth;
- final float verticalScaleValue = (float) viewHeight / (float) drawableHeight;
-
- final float scale = Math.max(verticalScaleValue, horizontalScaleValue);
-
- layoutNextMatrix.reset();
- layoutNextMatrix.setScale(scale, scale);
- setImageMatrix(layoutNextMatrix);
-
- // You can't modify the matrix in place and we want to avoid allocation, so let's keep two references to two
- // different matrix objects that we can swap when the values need to change
- final Matrix swapReferenceMatrix = layoutCurrentMatrix;
- layoutCurrentMatrix = layoutNextMatrix;
- layoutNextMatrix = swapReferenceMatrix;
- }
-
- public void setImageBitmap(final Bitmap bm, final boolean resize) {
- super.setImageBitmap(bm);
-
- this.resize = resize;
- updateImageMatrix();
- }
-
- @Override
- public void setImageResource(final int resId) {
- super.setImageResource(resId);
- setImageMatrix(null);
- resize = false;
- }
-
- @Override
- public void setImageDrawable(final Drawable drawable) {
- this.setImageDrawable(drawable, false);
- }
-
- public void setImageDrawable(final Drawable drawable, final boolean resize) {
- super.setImageDrawable(drawable);
-
- if (drawable != null) {
- // Reset the matrix to ensure that any previous changes aren't carried through.
- setImageMatrix(null);
-
- drawableWidth = drawable.getIntrinsicWidth();
- drawableHeight = drawable.getIntrinsicHeight();
- } else {
- drawableWidth = -1;
- drawableHeight = -1;
- }
-
- this.resize = resize;
-
- updateImageMatrix();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/DateTimePicker.java b/mobile/android/base/java/org/mozilla/gecko/widget/DateTimePicker.java
deleted file mode 100644
index 67f1bcd1d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/DateTimePicker.java
+++ /dev/null
@@ -1,665 +0,0 @@
-/*
- * Copyright (C) 2007 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.mozilla.gecko.widget;
-
-import java.text.SimpleDateFormat;
-import java.util.Arrays;
-import java.util.Calendar;
-import java.util.Locale;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.text.format.DateFormat;
-import android.text.format.DateUtils;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.Display;
-import android.view.LayoutInflater;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.CalendarView;
-import android.widget.EditText;
-import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-import android.widget.NumberPicker;
-
-public class DateTimePicker extends FrameLayout {
- private static final boolean DEBUG = true;
- private static final String LOGTAG = "GeckoDateTimePicker";
- private static final int DEFAULT_START_YEAR = 1;
- private static final int DEFAULT_END_YEAR = 9999;
- private static final char DATE_FORMAT_DAY = 'd';
- private static final char DATE_FORMAT_MONTH = 'M';
- private static final char DATE_FORMAT_YEAR = 'y';
-
- boolean mYearEnabled = true;
- boolean mMonthEnabled = true;
- boolean mWeekEnabled;
- boolean mDayEnabled = true;
- boolean mHourEnabled = true;
- boolean mMinuteEnabled = true;
- boolean mIs12HourMode;
- private boolean mCalendarEnabled;
-
- // Size of the screen in inches;
- private final int mScreenWidth;
- private final int mScreenHeight;
- private final OnValueChangeListener mOnChangeListener;
- private final LinearLayout mPickers;
- private final LinearLayout mDateSpinners;
- private final LinearLayout mTimeSpinners;
-
- final NumberPicker mDaySpinner;
- final NumberPicker mMonthSpinner;
- final NumberPicker mWeekSpinner;
- final NumberPicker mYearSpinner;
- final NumberPicker mHourSpinner;
- final NumberPicker mMinuteSpinner;
- final NumberPicker mAMPMSpinner;
- private final CalendarView mCalendar;
- private final EditText mDaySpinnerInput;
- private final EditText mMonthSpinnerInput;
- private final EditText mWeekSpinnerInput;
- private final EditText mYearSpinnerInput;
- private final EditText mHourSpinnerInput;
- private final EditText mMinuteSpinnerInput;
- private final EditText mAMPMSpinnerInput;
- private Locale mCurrentLocale;
- private String[] mShortMonths;
- private String[] mShortAMPMs;
- private int mNumberOfMonths;
-
- Calendar mTempDate;
- Calendar mCurrentDate;
- private Calendar mMinDate;
- private Calendar mMaxDate;
- private final PickersState mState;
-
- public static enum PickersState { DATE, MONTH, WEEK, TIME, DATETIME };
-
- public class OnValueChangeListener implements NumberPicker.OnValueChangeListener {
- @Override
- public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
- updateInputState();
- mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis());
- if (DEBUG) {
- Log.d(LOGTAG, "SDK version > 10, using new behavior");
- }
-
- // The native date picker widget on these SDKs increments
- // the next field when one field reaches the maximum.
- if (picker == mDaySpinner && mDayEnabled) {
- int maxDayOfMonth = mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH);
- int old = mTempDate.get(Calendar.DAY_OF_MONTH);
- setTempDate(Calendar.DAY_OF_MONTH, old, newVal, 1, maxDayOfMonth);
- } else if (picker == mMonthSpinner && mMonthEnabled) {
- int old = mTempDate.get(Calendar.MONTH);
- setTempDate(Calendar.MONTH, old, newVal, Calendar.JANUARY, Calendar.DECEMBER);
- } else if (picker == mWeekSpinner) {
- int old = mTempDate.get(Calendar.WEEK_OF_YEAR);
- int maxWeekOfYear = mTempDate.getActualMaximum(Calendar.WEEK_OF_YEAR);
- setTempDate(Calendar.WEEK_OF_YEAR, old, newVal, 0, maxWeekOfYear);
- } else if (picker == mYearSpinner && mYearEnabled) {
- int month = mTempDate.get(Calendar.MONTH);
- mTempDate.set(Calendar.YEAR, newVal);
- // Changing the year shouldn't change the month. (in case of non-leap year a Feb 29)
- // change the day instead;
- if (month != mTempDate.get(Calendar.MONTH)) {
- mTempDate.set(Calendar.MONTH, month);
- mTempDate.set(Calendar.DAY_OF_MONTH,
- mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH));
- }
- } else if (picker == mHourSpinner && mHourEnabled) {
- if (mIs12HourMode) {
- setTempDate(Calendar.HOUR, oldVal, newVal, 1, 12);
- } else {
- setTempDate(Calendar.HOUR_OF_DAY, oldVal, newVal, 0, 23);
- }
- } else if (picker == mMinuteSpinner && mMinuteEnabled) {
- setTempDate(Calendar.MINUTE, oldVal, newVal, 0, 59);
- } else if (picker == mAMPMSpinner && mHourEnabled) {
- mTempDate.set(Calendar.AM_PM, newVal);
- } else {
- throw new IllegalArgumentException();
- }
- setDate(mTempDate);
- if (mDayEnabled) {
- mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
- }
- if (mWeekEnabled) {
- mWeekSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.WEEK_OF_YEAR));
- }
- updateCalendar();
- updateSpinners();
- notifyDateChanged();
- }
-
- private void setTempDate(int field, int oldVal, int newVal, int min, int max) {
- if (oldVal == max && newVal == min) {
- mTempDate.add(field, 1);
- } else if (oldVal == min && newVal == max) {
- mTempDate.add(field, -1);
- } else {
- mTempDate.add(field, newVal - oldVal);
- }
- }
- }
-
- private static final NumberPicker.Formatter TWO_DIGIT_FORMATTER = new NumberPicker.Formatter() {
- final StringBuilder mBuilder = new StringBuilder();
-
- final java.util.Formatter mFmt = new java.util.Formatter(mBuilder, java.util.Locale.US);
-
- final Object[] mArgs = new Object[1];
-
- @Override
- public String format(int value) {
- mArgs[0] = value;
- mBuilder.delete(0, mBuilder.length());
- mFmt.format("%02d", mArgs);
- return mFmt.toString();
- }
- };
-
- private void displayPickers() {
- setWeekShown(false);
- set12HourShown(mIs12HourMode);
- if (mState == PickersState.DATETIME) {
- return;
- }
-
- setHourShown(false);
- setMinuteShown(false);
- if (mState == PickersState.WEEK) {
- setDayShown(false);
- setMonthShown(false);
- setWeekShown(true);
- } else if (mState == PickersState.MONTH) {
- setDayShown(false);
- }
- }
-
- public DateTimePicker(Context context) {
- this(context, "", "", PickersState.DATE, null, null);
- }
-
- public DateTimePicker(Context context, String dateFormat, String dateTimeValue, PickersState state, String minDateValue, String maxDateValue) {
- super(context);
-
- setCurrentLocale(Locale.getDefault());
-
- mState = state;
- LayoutInflater inflater = LayoutInflater.from(context);
- inflater.inflate(R.layout.datetime_picker, this, true);
-
- mOnChangeListener = new OnValueChangeListener();
-
- mDateSpinners = (LinearLayout)findViewById(R.id.date_spinners);
- mTimeSpinners = (LinearLayout)findViewById(R.id.time_spinners);
- mPickers = (LinearLayout)findViewById(R.id.datetime_picker);
-
- // We will display differently according to the screen size width.
- WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
- Display display = wm.getDefaultDisplay();
- DisplayMetrics dm = new DisplayMetrics();
- display.getMetrics(dm);
- mScreenWidth = display.getWidth() / dm.densityDpi;
- mScreenHeight = display.getHeight() / dm.densityDpi;
-
- if (DEBUG) {
- Log.d(LOGTAG, "screen width: " + mScreenWidth + " screen height: " + mScreenHeight);
- }
-
- // Set the min / max attribute.
- try {
- if (minDateValue != null && !minDateValue.equals("")) {
- mMinDate.setTime(new SimpleDateFormat(dateFormat).parse(minDateValue));
- } else {
- mMinDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1);
- }
- } catch (Exception ex) {
- Log.e(LOGTAG, "Error parsing format sting: " + ex);
- mMinDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1);
- }
-
- try {
- if (maxDateValue != null && !maxDateValue.equals("")) {
- mMaxDate.setTime(new SimpleDateFormat(dateFormat).parse(maxDateValue));
- } else {
- mMaxDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31);
- }
- } catch (Exception ex) {
- Log.e(LOGTAG, "Error parsing format string: " + ex);
- mMaxDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31);
- }
-
- // Find the initial date from the constructor arguments.
- try {
- if (!dateTimeValue.equals("")) {
- mTempDate.setTime(new SimpleDateFormat(dateFormat).parse(dateTimeValue));
- } else {
- mTempDate.setTimeInMillis(System.currentTimeMillis());
- }
- } catch (Exception ex) {
- Log.e(LOGTAG, "Error parsing format string: " + ex);
- mTempDate.setTimeInMillis(System.currentTimeMillis());
- }
-
- if (mMaxDate.before(mMinDate)) {
- // If the input date range is illogical/garbage, we should not restrict the input range (i.e. allow the
- // user to select any date). If we try to make any assumptions based on the illogical min/max date we could
- // potentially prevent the user from selecting dates that are in the developers intended range, so it's best
- // to allow anything.
- mMinDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1);
- mMaxDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31);
- }
-
- // mTempDate will either be a site-supplied value, or today's date otherwise. CalendarView implementations can
- // crash if they're supplied an invalid date (i.e. a date not in the specified range), hence we need to set
- // a sensible default date here.
- if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) {
- mTempDate.setTimeInMillis(mMinDate.getTimeInMillis());
- }
-
- // If we're displaying a date, the screen is wide enough
- // (and if we're using an SDK where the calendar view exists)
- // then display a calendar.
- if (mState == PickersState.DATE || mState == PickersState.DATETIME) {
- mCalendar = new CalendarView(context);
- mCalendar.setVisibility(GONE);
-
- mCalendar.setFocusable(true);
- mCalendar.setFocusableInTouchMode(true);
- mCalendar.setMaxDate(mMaxDate.getTimeInMillis());
- mCalendar.setMinDate(mMinDate.getTimeInMillis());
- mCalendar.setDate(mTempDate.getTimeInMillis(), false, false);
-
- mCalendar.setOnDateChangeListener(new CalendarView.OnDateChangeListener() {
- @Override
- public void onSelectedDayChange(
- CalendarView view, int year, int month, int monthDay) {
- mTempDate.set(year, month, monthDay);
- setDate(mTempDate);
- notifyDateChanged();
- }
- });
-
- final int height;
- if (Versions.preLollipop) {
- // The 4.X version of CalendarView doesn't request any height, resulting in
- // the whole dialog not appearing unless we manually request height.
- height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200, getResources().getDisplayMetrics());;
- } else {
- height = LayoutParams.WRAP_CONTENT;
- }
-
- mPickers.addView(mCalendar, LayoutParams.MATCH_PARENT, height);
-
- } else {
- // If the screen is more wide than high, we are displaying day and
- // time spinners, and if there is no calendar displayed, we should
- // display the fields in one row.
- if (mScreenWidth > mScreenHeight && mState == PickersState.DATETIME) {
- mPickers.setOrientation(LinearLayout.HORIZONTAL);
- }
- mCalendar = null;
- }
-
- // Initialize all spinners.
- mDaySpinner = setupSpinner(R.id.day, 1,
- mTempDate.get(Calendar.DAY_OF_MONTH));
- mDaySpinner.setFormatter(TWO_DIGIT_FORMATTER);
- mDaySpinnerInput = (EditText) mDaySpinner.getChildAt(1);
-
- mMonthSpinner = setupSpinner(R.id.month, 1,
- mTempDate.get(Calendar.MONTH) + 1); // Month is 0-based
- mMonthSpinner.setFormatter(TWO_DIGIT_FORMATTER);
- mMonthSpinner.setDisplayedValues(mShortMonths);
- mMonthSpinnerInput = (EditText) mMonthSpinner.getChildAt(1);
-
- mWeekSpinner = setupSpinner(R.id.week, 1,
- mTempDate.get(Calendar.WEEK_OF_YEAR));
- mWeekSpinner.setFormatter(TWO_DIGIT_FORMATTER);
- mWeekSpinnerInput = (EditText) mWeekSpinner.getChildAt(1);
-
- mYearSpinner = setupSpinner(R.id.year, DEFAULT_START_YEAR,
- DEFAULT_END_YEAR);
- mYearSpinnerInput = (EditText) mYearSpinner.getChildAt(1);
-
- mAMPMSpinner = setupSpinner(R.id.ampm, 0, 1);
- mAMPMSpinner.setFormatter(TWO_DIGIT_FORMATTER);
-
- if (mIs12HourMode) {
- mHourSpinner = setupSpinner(R.id.hour, 1, 12);
- mAMPMSpinnerInput = (EditText) mAMPMSpinner.getChildAt(1);
- mAMPMSpinner.setDisplayedValues(mShortAMPMs);
- } else {
- mHourSpinner = setupSpinner(R.id.hour, 0, 23);
- mAMPMSpinnerInput = null;
- }
-
- mHourSpinner.setFormatter(TWO_DIGIT_FORMATTER);
- mHourSpinnerInput = (EditText) mHourSpinner.getChildAt(1);
-
- mMinuteSpinner = setupSpinner(R.id.minute, 0, 59);
- mMinuteSpinner.setFormatter(TWO_DIGIT_FORMATTER);
- mMinuteSpinnerInput = (EditText) mMinuteSpinner.getChildAt(1);
-
- // The order in which the spinners are displayed are locale-dependent
- reorderDateSpinners();
-
- // Set the date to the initial date. Since this date can come from the user,
- // it can fire an exception (out-of-bound date)
- try {
- updateDate(mTempDate);
- } catch (Exception ex) {
- }
-
- // Display only the pickers needed for the current state.
- displayPickers();
- }
-
- public NumberPicker setupSpinner(int id, int min, int max) {
- NumberPicker mSpinner = (NumberPicker) findViewById(id);
- mSpinner.setMinValue(min);
- mSpinner.setMaxValue(max);
- mSpinner.setOnValueChangedListener(mOnChangeListener);
- mSpinner.setOnLongPressUpdateInterval(100);
- return mSpinner;
- }
-
- public long getTimeInMillis() {
- return mCurrentDate.getTimeInMillis();
- }
-
- private void reorderDateSpinners() {
- mDateSpinners.removeAllViews();
- char[] order = DateFormat.getDateFormatOrder(getContext());
- final int spinnerCount = order.length;
-
- for (int i = 0; i < spinnerCount; i++) {
- switch (order[i]) {
- case DATE_FORMAT_DAY:
- mDateSpinners.addView(mDaySpinner);
- break;
- case DATE_FORMAT_MONTH:
- mDateSpinners.addView(mMonthSpinner);
- break;
- case DATE_FORMAT_YEAR:
- mDateSpinners.addView(mYearSpinner);
- break;
- default:
- throw new IllegalArgumentException();
- }
- }
-
- mDateSpinners.addView(mWeekSpinner);
- }
-
- void setDate(Calendar calendar) {
- mCurrentDate = mTempDate;
- if (mCurrentDate.before(mMinDate)) {
- mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
- } else if (mCurrentDate.after(mMaxDate)) {
- mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
- }
- }
-
- void updateInputState() {
- InputMethodManager inputMethodManager = (InputMethodManager)
- getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
- if (mYearEnabled && inputMethodManager.isActive(mYearSpinnerInput)) {
- mYearSpinnerInput.clearFocus();
- inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
- } else if (mMonthEnabled && inputMethodManager.isActive(mMonthSpinnerInput)) {
- mMonthSpinnerInput.clearFocus();
- inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
- } else if (mDayEnabled && inputMethodManager.isActive(mDaySpinnerInput)) {
- mDaySpinnerInput.clearFocus();
- inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
- } else if (mHourEnabled && inputMethodManager.isActive(mHourSpinnerInput)) {
- mHourSpinnerInput.clearFocus();
- inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
- } else if (mMinuteEnabled && inputMethodManager.isActive(mMinuteSpinnerInput)) {
- mMinuteSpinnerInput.clearFocus();
- inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
- }
- }
-
- void updateSpinners() {
- if (mDayEnabled) {
- if (mCurrentDate.equals(mMinDate)) {
- mDaySpinner.setMinValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
- mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
- } else if (mCurrentDate.equals(mMaxDate)) {
- mDaySpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH));
- mDaySpinner.setMaxValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
- } else {
- mDaySpinner.setMinValue(1);
- mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
- }
- mDaySpinner.setValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
- }
-
- if (mWeekEnabled) {
- mWeekSpinner.setMinValue(1);
- mWeekSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.WEEK_OF_YEAR));
- mWeekSpinner.setValue(mCurrentDate.get(Calendar.WEEK_OF_YEAR));
- }
-
- if (mMonthEnabled) {
- mMonthSpinner.setDisplayedValues(null);
- if (mCurrentDate.equals(mMinDate)) {
- mMonthSpinner.setMinValue(mCurrentDate.get(Calendar.MONTH));
- mMonthSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH));
- } else if (mCurrentDate.equals(mMaxDate)) {
- mMonthSpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH));
- mMonthSpinner.setMaxValue(mCurrentDate.get(Calendar.MONTH));
- } else {
- mMonthSpinner.setMinValue(Calendar.JANUARY);
- mMonthSpinner.setMaxValue(Calendar.DECEMBER);
- }
-
- String[] displayedValues = Arrays.copyOfRange(mShortMonths,
- mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1);
- mMonthSpinner.setDisplayedValues(displayedValues);
- mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH));
- }
-
- if (mYearEnabled) {
- mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR));
- mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR));
- mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR));
- }
-
- if (mHourEnabled) {
- if (mIs12HourMode) {
- mHourSpinner.setValue(mCurrentDate.get(Calendar.HOUR));
- mAMPMSpinner.setValue(mCurrentDate.get(Calendar.AM_PM));
- mAMPMSpinner.setDisplayedValues(mShortAMPMs);
- } else {
- mHourSpinner.setValue(mCurrentDate.get(Calendar.HOUR_OF_DAY));
- }
- }
- if (mMinuteEnabled) {
- mMinuteSpinner.setValue(mCurrentDate.get(Calendar.MINUTE));
- }
- }
-
- void updateCalendar() {
- if (mCalendarEnabled) {
- mCalendar.setDate(mCurrentDate.getTimeInMillis(), false, false);
- }
- }
-
- void notifyDateChanged() {
- sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
- }
-
- public void toggleCalendar(boolean shown) {
- if ((mState != PickersState.DATE && mState != PickersState.DATETIME)) {
- return;
- }
-
- if (shown) {
- mCalendarEnabled = true;
- mCalendar.setVisibility(VISIBLE);
- setYearShown(false);
- setWeekShown(false);
- setMonthShown(false);
- setDayShown(false);
- } else {
- mCalendar.setVisibility(GONE);
- setYearShown(true);
- setMonthShown(true);
- setDayShown(true);
- mPickers.setOrientation(LinearLayout.HORIZONTAL);
- mCalendarEnabled = false;
- }
- }
-
- private void setYearShown(boolean shown) {
- if (shown) {
- toggleCalendar(false);
- mYearSpinner.setVisibility(VISIBLE);
- mYearEnabled = true;
- } else {
- mYearSpinner.setVisibility(GONE);
- mYearEnabled = false;
- }
- }
-
- private void setWeekShown(boolean shown) {
- if (shown) {
- toggleCalendar(false);
- mWeekSpinner.setVisibility(VISIBLE);
- mWeekEnabled = true;
- } else {
- mWeekSpinner.setVisibility(GONE);
- mWeekEnabled = false;
- }
- }
-
- private void setMonthShown(boolean shown) {
- if (shown) {
- toggleCalendar(false);
- mMonthSpinner.setVisibility(VISIBLE);
- mMonthEnabled = true;
- } else {
- mMonthSpinner.setVisibility(GONE);
- mMonthEnabled = false;
- }
- }
-
- private void setDayShown(boolean shown) {
- if (shown) {
- toggleCalendar(false);
- mDaySpinner.setVisibility(VISIBLE);
- mDayEnabled = true;
- } else {
- mDaySpinner.setVisibility(GONE);
- mDayEnabled = false;
- }
- }
-
- private void set12HourShown(boolean shown) {
- if (shown) {
- mAMPMSpinner.setVisibility(VISIBLE);
- } else {
- mAMPMSpinner.setVisibility(GONE);
- }
- }
-
- private void setHourShown(boolean shown) {
- if (shown) {
- mHourSpinner.setVisibility(VISIBLE);
- mHourEnabled = true;
- } else {
- mHourSpinner.setVisibility(GONE);
- mAMPMSpinner.setVisibility(GONE);
- mTimeSpinners.setVisibility(GONE);
- mHourEnabled = false;
- }
- }
-
- private void setMinuteShown(boolean shown) {
- if (shown) {
- mMinuteSpinner.setVisibility(VISIBLE);
- mTimeSpinners.findViewById(R.id.mincolon).setVisibility(VISIBLE);
- mMinuteEnabled = true;
- } else {
- mMinuteSpinner.setVisibility(GONE);
- mTimeSpinners.findViewById(R.id.mincolon).setVisibility(GONE);
- mMinuteEnabled = false;
- }
- }
-
- private void setCurrentLocale(Locale locale) {
- if (locale.equals(mCurrentLocale)) {
- return;
- }
-
- mCurrentLocale = locale;
- mIs12HourMode = !DateFormat.is24HourFormat(getContext());
- mTempDate = getCalendarForLocale(mTempDate, locale);
- mMinDate = getCalendarForLocale(mMinDate, locale);
- mMaxDate = getCalendarForLocale(mMaxDate, locale);
- mCurrentDate = getCalendarForLocale(mCurrentDate, locale);
-
- mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1;
-
- mShortAMPMs = new String[2];
- mShortAMPMs[0] = DateUtils.getAMPMString(Calendar.AM);
- mShortAMPMs[1] = DateUtils.getAMPMString(Calendar.PM);
-
- mShortMonths = new String[mNumberOfMonths];
- for (int i = 0; i < mNumberOfMonths; i++) {
- mShortMonths[i] = DateUtils.getMonthString(Calendar.JANUARY + i,
- DateUtils.LENGTH_MEDIUM);
- }
- }
-
- private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
- if (oldCalendar == null) {
- return Calendar.getInstance(locale);
- }
-
- final long currentTimeMillis = oldCalendar.getTimeInMillis();
- Calendar newCalendar = Calendar.getInstance(locale);
- newCalendar.setTimeInMillis(currentTimeMillis);
- return newCalendar;
- }
-
- public void updateDate(Calendar calendar) {
- if (mCurrentDate.equals(calendar)) {
- return;
- }
- mCurrentDate.setTimeInMillis(calendar.getTimeInMillis());
- if (mCurrentDate.before(mMinDate)) {
- mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
- } else if (mCurrentDate.after(mMaxDate)) {
- mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
- }
- updateSpinners();
- notifyDateChanged();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/DefaultDoorHanger.java b/mobile/android/base/java/org/mozilla/gecko/widget/DefaultDoorHanger.java
deleted file mode 100644
index cb8716af7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/DefaultDoorHanger.java
+++ /dev/null
@@ -1,190 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import android.support.v4.content.ContextCompat;
-import android.text.Html;
-import android.text.Spanned;
-import android.util.Log;
-import android.widget.Button;
-import android.widget.TextView;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.prompts.PromptInput;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.content.Context;
-import android.text.TextUtils;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CheckBox;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-
-public class DefaultDoorHanger extends DoorHanger {
- private static final String LOGTAG = "GeckoDefaultDoorHanger";
-
- private static int sSpinnerTextColor = -1;
-
- private final TextView mMessage;
- private List<PromptInput> mInputs;
- private CheckBox mCheckBox;
-
- public DefaultDoorHanger(Context context, DoorhangerConfig config, Type type) {
- super(context, config, type);
-
- mMessage = (TextView) findViewById(R.id.doorhanger_message);
-
- if (sSpinnerTextColor == -1) {
- sSpinnerTextColor = ContextCompat.getColor(context, R.color.text_color_primary_disable_only);
- }
-
- switch (mType) {
- case GEOLOCATION:
- mIcon.setImageResource(R.drawable.location);
- mIcon.setVisibility(VISIBLE);
- break;
-
- case DESKTOPNOTIFICATION2:
- mIcon.setImageResource(R.drawable.push_notification);
- mIcon.setVisibility(VISIBLE);
- break;
- }
-
- loadConfig(config);
- }
-
- @Override
- protected void loadConfig(DoorhangerConfig config) {
- final String message = config.getMessage();
- if (message != null) {
- setMessage(message);
- }
-
- final JSONObject options = config.getOptions();
- if (options != null) {
- setOptions(options);
- }
-
- final DoorhangerConfig.Link link = config.getLink();
- if (link != null) {
- addLink(link.label, link.url);
- }
-
- addButtonsToLayout(config);
- }
-
- @Override
- protected int getContentResource() {
- return R.layout.default_doorhanger;
- }
-
- private List<PromptInput> getInputs() {
- return mInputs;
- }
-
- private CheckBox getCheckBox() {
- return mCheckBox;
- }
-
- @Override
- public void setOptions(final JSONObject options) {
- super.setOptions(options);
-
- final JSONArray inputs = options.optJSONArray("inputs");
- if (inputs != null) {
- mInputs = new ArrayList<PromptInput>();
-
- final ViewGroup group = (ViewGroup) findViewById(R.id.doorhanger_inputs);
- group.setVisibility(VISIBLE);
-
- for (int i = 0; i < inputs.length(); i++) {
- try {
- PromptInput input = PromptInput.getInput(inputs.getJSONObject(i));
- mInputs.add(input);
-
- final int padding = mResources.getDimensionPixelSize(R.dimen.doorhanger_section_padding_medium);
- View v = input.getView(getContext());
- styleInput(input, v);
- v.setPadding(0, 0, 0, padding);
- group.addView(v);
- } catch (JSONException ex) { }
- }
- }
-
- final String checkBoxText = options.optString("checkbox");
- if (!TextUtils.isEmpty(checkBoxText)) {
- mCheckBox = (CheckBox) findViewById(R.id.doorhanger_checkbox);
- mCheckBox.setText(checkBoxText);
- if (options.has("checkboxState")) {
- final boolean checkBoxState = options.optBoolean("checkboxState");
- mCheckBox.setChecked(checkBoxState);
- }
- mCheckBox.setVisibility(VISIBLE);
- }
- }
-
- @Override
- protected OnClickListener makeOnButtonClickListener(final int id, final String telemetryExtra) {
- return new Button.OnClickListener() {
- @Override
- public void onClick(View v) {
- final String expandedExtra = mType.toString().toLowerCase(Locale.US) + "-" + telemetryExtra;
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.DOORHANGER, expandedExtra);
-
- final JSONObject response = new JSONObject();
- try {
- response.put("callback", id);
-
- CheckBox checkBox = getCheckBox();
- // If the checkbox is being used, pass its value
- if (checkBox != null) {
- response.put("checked", checkBox.isChecked());
- }
-
- List<PromptInput> doorHangerInputs = getInputs();
- if (doorHangerInputs != null) {
- JSONObject inputs = new JSONObject();
- for (PromptInput input : doorHangerInputs) {
- inputs.put(input.getId(), input.getValue());
- }
- response.put("inputs", inputs);
- }
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error creating onClick response", e);
- }
-
- mOnButtonClickListener.onButtonClick(response, DefaultDoorHanger.this);
- }
- };
- }
-
- private void setMessage(String message) {
- Spanned markupMessage = Html.fromHtml(message);
- mMessage.setText(markupMessage);
- }
-
- private void styleInput(PromptInput input, View view) {
- if (input instanceof PromptInput.MenulistInput) {
- styleDropdownInputs(input, view);
- }
- view.setPadding(0, 0, 0, mResources.getDimensionPixelSize(R.dimen.doorhanger_subsection_padding));
- }
-
- private void styleDropdownInputs(PromptInput input, View view) {
- PromptInput.MenulistInput spinInput = (PromptInput.MenulistInput) input;
-
- if (spinInput.textView != null) {
- spinInput.textView.setTextColor(sSpinnerTextColor);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/DefaultItemAnimatorBase.java b/mobile/android/base/java/org/mozilla/gecko/widget/DefaultItemAnimatorBase.java
deleted file mode 100644
index 5beec3a5c..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/DefaultItemAnimatorBase.java
+++ /dev/null
@@ -1,685 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import android.support.annotation.NonNull;
-import android.support.v4.animation.AnimatorCompatHelper;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.ViewPropertyAnimatorCompat;
-import android.support.v4.view.ViewPropertyAnimatorListener;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.SimpleItemAnimator;
-import android.view.View;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * This basically follows the approach taken by Wasabeef:
- * <a href="https://github.com/wasabeef/recyclerview-animators">https://github.com/wasabeef/recyclerview-animators</a>
- * based off of Android's DefaultItemAnimator from October 2016:
- * <a href="https://github.com/android/platform_frameworks_support/blob/432f3317f8a9b8cf98277938ea5df4021e983055/v7/recyclerview/src/android/support/v7/widget/DefaultItemAnimator.java">
- * https://github.com/android/platform_frameworks_support/blob/432f3317f8a9b8cf98277938ea5df4021e983055/v7/recyclerview/src/android/support/v7/widget/DefaultItemAnimator.java
- * </a>
- * <p>
- * Usage Notes:
- * </p>
- * <ul>
- * <li>You <strong>must</strong> add a Default*VpaListener to your animate*Impl animation - the
- * listener takes care of animation bookkeeping.</li>
- * <li>You should call {@link #resetAnimation(RecyclerView.ViewHolder)} at some point in
- * preAnimate*Impl if you choose to proceed with the animation. Some animations will want to
- * know some or all of the current animation values for initializing their own animation
- * values before resetting the current animation, so this class does not provide the reset
- * service itself.</li>
- * <li>{@link #resetViewProperties(View)} is used to reset a view any time an animation ends or
- * gets canceled - you should redefine resetViewProperties if the version here doesn't reset
- * all of the properties you're animating.</li>
- * </ul>
- */
-public class DefaultItemAnimatorBase extends SimpleItemAnimator {
- private List<RecyclerView.ViewHolder> pendingRemovals = new ArrayList<>();
- private List<RecyclerView.ViewHolder> pendingAdditions = new ArrayList<>();
- private List<MoveInfo> pendingMoves = new ArrayList<>();
- private List<ChangeInfo> pendingChanges = new ArrayList<>();
-
- private List<List<RecyclerView.ViewHolder>> additionsList = new ArrayList<>();
- private List<List<MoveInfo>> movesList = new ArrayList<>();
- private List<List<ChangeInfo>> changesList = new ArrayList<>();
-
- private List<RecyclerView.ViewHolder> addAnimations = new ArrayList<>();
- private List<RecyclerView.ViewHolder> moveAnimations = new ArrayList<>();
- private List<RecyclerView.ViewHolder> removeAnimations = new ArrayList<>();
- private List<RecyclerView.ViewHolder> changeAnimations = new ArrayList<>();
-
- protected static class MoveInfo {
- public RecyclerView.ViewHolder holder;
- public int fromX, fromY, toX, toY;
-
- public MoveInfo(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
- this.holder = holder;
- this.fromX = fromX;
- this.fromY = fromY;
- this.toX = toX;
- this.toY = toY;
- }
- }
-
- protected static class ChangeInfo {
- public RecyclerView.ViewHolder oldHolder, newHolder;
- public int fromX, fromY, toX, toY;
-
- public ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder) {
- this.oldHolder = oldHolder;
- this.newHolder = newHolder;
- }
-
- public ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
- int fromX, int fromY, int toX, int toY) {
- this(oldHolder, newHolder);
- this.fromX = fromX;
- this.fromY = fromY;
- this.toX = toX;
- this.toY = toY;
- }
-
- @Override
- public String toString() {
- return "ChangeInfo{" +
- "oldHolder=" + oldHolder +
- ", newHolder=" + newHolder +
- ", fromX=" + fromX +
- ", fromY=" + fromY +
- ", toX=" + toX +
- ", toY=" + toY +
- '}';
- }
- }
-
- @Override
- public void runPendingAnimations() {
- final boolean removalsPending = !pendingRemovals.isEmpty();
- final boolean movesPending = !pendingMoves.isEmpty();
- final boolean changesPending = !pendingChanges.isEmpty();
- final boolean additionsPending = !pendingAdditions.isEmpty();
- if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
- return;
- }
- // First, remove stuff.
- for (final RecyclerView.ViewHolder holder : pendingRemovals) {
- animateRemoveImpl(holder);
- }
- pendingRemovals.clear();
- // Next, move stuff.
- if (movesPending) {
- final List<MoveInfo> moves = new ArrayList<>();
- moves.addAll(pendingMoves);
- movesList.add(moves);
- pendingMoves.clear();
- final Runnable mover = new Runnable() {
- @Override
- public void run() {
- for (final MoveInfo moveInfo : moves) {
- animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
- moveInfo.toX, moveInfo.toY);
- }
- moves.clear();
- movesList.remove(moves);
- }
- };
- if (removalsPending) {
- final View view = moves.get(0).holder.itemView;
- ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
- } else {
- mover.run();
- }
- }
- // Next, change stuff, to run in parallel with move animations.
- if (changesPending) {
- final List<ChangeInfo> changes = new ArrayList<>();
- changes.addAll(pendingChanges);
- changesList.add(changes);
- pendingChanges.clear();
- final Runnable changer = new Runnable() {
- @Override
- public void run() {
- for (final ChangeInfo change : changes) {
- animateChangeImpl(change);
- }
- changes.clear();
- changesList.remove(changes);
- }
- };
- if (removalsPending) {
- RecyclerView.ViewHolder holder = changes.get(0).oldHolder;
- ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
- } else {
- changer.run();
- }
- }
- // Next, add stuff.
- if (additionsPending) {
- final List<RecyclerView.ViewHolder> additions = new ArrayList<>();
- additions.addAll(pendingAdditions);
- additionsList.add(additions);
- pendingAdditions.clear();
- final Runnable adder = new Runnable() {
- public void run() {
- for (final RecyclerView.ViewHolder holder : additions) {
- animateAddImpl(holder);
- }
- additions.clear();
- additionsList.remove(additions);
- }
- };
- if (removalsPending || movesPending || changesPending) {
- final long removeDuration = removalsPending ? getRemoveDuration() : 0;
- final long moveDuration = movesPending ? getMoveDuration() : 0;
- final long changeDuration = changesPending ? getChangeDuration() : 0;
- final long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
- final View view = additions.get(0).itemView;
- ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
- } else {
- adder.run();
- }
- }
- }
-
- @Override
- public boolean animateRemove(final RecyclerView.ViewHolder holder) {
- if (!preAnimateRemoveImpl(holder)) {
- dispatchRemoveFinished(holder);
- return false;
- }
- pendingRemovals.add(holder);
- return true;
- }
-
- protected boolean preAnimateRemoveImpl(final RecyclerView.ViewHolder holder) {
- resetAnimation(holder);
- return true;
- }
-
- protected void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
- ViewCompat.animate(holder.itemView)
- .setDuration(getRemoveDuration())
- .alpha(0)
- .setListener(new DefaultRemoveVpaListener(holder))
- .start();
- }
-
- @Override
- public boolean animateAdd(final RecyclerView.ViewHolder holder) {
- if (!preAnimateAddImpl(holder)) {
- dispatchAddFinished(holder);
- return false;
- }
- pendingAdditions.add(holder);
- return true;
- }
-
- protected boolean preAnimateAddImpl(RecyclerView.ViewHolder holder) {
- resetAnimation(holder);
- holder.itemView.setAlpha(0);
- return true;
- }
-
- protected void animateAddImpl(final RecyclerView.ViewHolder holder) {
- ViewCompat.animate(holder.itemView)
- .setDuration(getAddDuration())
- .alpha(1)
- .setListener(new DefaultAddVpaListener(holder))
- .start();
- }
-
- @Override
- public boolean animateMove(final RecyclerView.ViewHolder holder,
- int fromX, int fromY, int toX, int toY) {
- final View view = holder.itemView;
- fromX += ViewCompat.getTranslationX(holder.itemView);
- fromY += ViewCompat.getTranslationY(holder.itemView);
- final int deltaX = toX - fromX;
- final int deltaY = toY - fromY;
- if (deltaX == 0 && deltaY == 0) {
- dispatchMoveFinished(holder);
- return false;
- }
- resetAnimation(holder);
- if (deltaX != 0) {
- ViewCompat.setTranslationX(view, -deltaX);
- }
- if (deltaY != 0) {
- ViewCompat.setTranslationY(view, -deltaY);
- }
- pendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
- return true;
- }
-
- protected void animateMoveImpl(final RecyclerView.ViewHolder holder,
- int fromX, int fromY, int toX, int toY) {
- final View view = holder.itemView;
- final int deltaX = toX - fromX;
- final int deltaY = toY - fromY;
- if (deltaX != 0) {
- ViewCompat.animate(view).translationX(0);
- }
- if (deltaY != 0) {
- ViewCompat.animate(view).translationY(0);
- }
- // TODO: make EndActions end listeners instead, since end actions aren't called when
- // vpas are canceled (and can't end them. why?)
- // need listener functionality in VPACompat for this. Ick.
- final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
- moveAnimations.add(holder);
- animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() {
- @Override
- public void onAnimationStart(View view) {
- dispatchMoveStarting(holder);
- }
- @Override
- public void onAnimationCancel(View view) {
- resetViewProperties(view);
- }
- @Override
- public void onAnimationEnd(View view) {
- animation.setListener(null);
- dispatchMoveFinished(holder);
- moveAnimations.remove(holder);
- dispatchFinishedWhenDone();
- }
- }).start();
- }
-
- @Override
- public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
- int fromX, int fromY, int toX, int toY) {
- if (oldHolder == newHolder) {
- // Don't know how to run change animations when the same view holder is re-used.
- // Run a move animation to handle position changes (if there are any).
- if (fromX != toX || fromY != toY) {
- // *Don't* call dispatchChangeFinished here, it leads to unbalanced isRecyclable calls.
- return animateMove(oldHolder, fromX, fromY, toX, toY);
- }
- dispatchChangeFinished(oldHolder, true);
- return false;
- }
- final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView);
- final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView);
- final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
- resetAnimation(oldHolder);
- final int deltaX = (int) (toX - fromX - prevTranslationX);
- final int deltaY = (int) (toY - fromY - prevTranslationY);
- // Recover previous translation state after ending animation.
- ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX);
- ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY);
- ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
- if (newHolder != null) {
- // Carry over translation values.
- resetAnimation(newHolder);
- ViewCompat.setTranslationX(newHolder.itemView, -deltaX);
- ViewCompat.setTranslationY(newHolder.itemView, -deltaY);
- ViewCompat.setAlpha(newHolder.itemView, 0);
- }
- pendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
- return true;
- }
-
- protected void animateChangeImpl(final ChangeInfo changeInfo) {
- final RecyclerView.ViewHolder holder = changeInfo.oldHolder;
- final View view = holder == null ? null : holder.itemView;
- final RecyclerView.ViewHolder newHolder = changeInfo.newHolder;
- final View newView = newHolder != null ? newHolder.itemView : null;
- if (view != null) {
- final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration(
- getChangeDuration());
- changeAnimations.add(changeInfo.oldHolder);
- oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
- oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
- oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() {
- @Override
- public void onAnimationStart(View view) {
- dispatchChangeStarting(changeInfo.oldHolder, true);
- }
-
- @Override
- public void onAnimationEnd(View view) {
- oldViewAnim.setListener(null);
- resetViewProperties(view);
- dispatchChangeFinished(changeInfo.oldHolder, true);
- changeAnimations.remove(changeInfo.oldHolder);
- dispatchFinishedWhenDone();
- }
- }).start();
- }
- if (newView != null) {
- final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView);
- changeAnimations.add(changeInfo.newHolder);
- newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()).
- alpha(1).setListener(new VpaListenerAdapter() {
- @Override
- public void onAnimationStart(View view) {
- dispatchChangeStarting(changeInfo.newHolder, false);
- }
- @Override
- public void onAnimationEnd(View view) {
- newViewAnimation.setListener(null);
- resetViewProperties(view);
- dispatchChangeFinished(changeInfo.newHolder, false);
- changeAnimations.remove(changeInfo.newHolder);
- dispatchFinishedWhenDone();
- }
- }).start();
-}
- }
-
- private void endChangeAnimation(List<ChangeInfo> infoList, RecyclerView.ViewHolder item) {
- for (int i = infoList.size() - 1; i >= 0; i--) {
- final ChangeInfo changeInfo = infoList.get(i);
- if (endChangeAnimationIfNecessary(changeInfo, item)) {
- if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
- infoList.remove(changeInfo);
- }
- }
- }
- }
-
- private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
- if (changeInfo.oldHolder != null) {
- endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
- }
- if (changeInfo.newHolder != null) {
- endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
- }
- }
-
- private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, RecyclerView.ViewHolder item) {
- boolean oldItem = false;
- if (changeInfo.newHolder == item) {
- changeInfo.newHolder = null;
- } else if (changeInfo.oldHolder == item) {
- changeInfo.oldHolder = null;
- oldItem = true;
- } else {
- return false;
- }
- resetViewProperties(item.itemView);
- dispatchChangeFinished(item, oldItem);
- return true;
- }
-
- /**
- * Called to reset all properties possibly animated by any and all defined animations.
- */
- protected void resetViewProperties(View view) {
- view.setTranslationX(0);
- view.setTranslationY(0);
- view.setAlpha(1);
- }
-
- @Override
- public void endAnimation(RecyclerView.ViewHolder item) {
-
- final View view = item.itemView;
- // This calls dispatch*Finished, resets view properties, and removes item from current
- // animations list if the view is currently being animated.
- ViewCompat.animate(view).cancel();
- // TODO if some other animations are chained to end, how do we cancel them as well?
- for (int i = pendingMoves.size() - 1; i >= 0; i--) {
- final MoveInfo moveInfo = pendingMoves.get(i);
- if (moveInfo.holder == item) {
- resetViewProperties(view);
- dispatchMoveFinished(item);
- pendingMoves.remove(i);
- }
- }
- endChangeAnimation(pendingChanges, item);
- if (pendingRemovals.remove(item)) {
- resetViewProperties(view);
- dispatchRemoveFinished(item);
- }
- if (pendingAdditions.remove(item)) {
- resetViewProperties(view);
- dispatchAddFinished(item);
- }
-
- for (int i = changesList.size() - 1; i >= 0; i--) {
- final List<ChangeInfo> changes = changesList.get(i);
- endChangeAnimation(changes, item);
- if (changes.isEmpty()) {
- changesList.remove(i);
- }
- }
- for (int i = movesList.size() - 1; i >= 0; i--) {
- final List<MoveInfo> moves = movesList.get(i);
- for (int j = moves.size() - 1; j >= 0; j--) {
- final MoveInfo moveInfo = moves.get(j);
- if (moveInfo.holder == item) {
- resetViewProperties(view);
- dispatchMoveFinished(item);
- moves.remove(j);
- if (moves.isEmpty()) {
- movesList.remove(i);
- }
- break;
- }
- }
- }
- for (int i = additionsList.size() - 1; i >= 0; i--) {
- final List<RecyclerView.ViewHolder> additions = additionsList.get(i);
- if (additions.remove(item)) {
- resetViewProperties(view);
- dispatchAddFinished(item);
- if (additions.isEmpty()) {
- additionsList.remove(i);
- }
- }
- }
- dispatchFinishedWhenDone();
- }
-
- protected void resetAnimation(RecyclerView.ViewHolder holder) {
- AnimatorCompatHelper.clearInterpolator(holder.itemView);
- endAnimation(holder);
- }
-
- @Override
- public boolean isRunning() {
- return (!pendingAdditions.isEmpty() ||
- !pendingChanges.isEmpty() ||
- !pendingMoves.isEmpty() ||
- !pendingRemovals.isEmpty() ||
- !moveAnimations.isEmpty() ||
- !removeAnimations.isEmpty() ||
- !addAnimations.isEmpty() ||
- !changeAnimations.isEmpty() ||
- !movesList.isEmpty() ||
- !additionsList.isEmpty() ||
- !changesList.isEmpty());
- }
-
- /**
- * Check the state of currently pending and running animations. If there are none
- * pending/running, call {@link #dispatchAnimationsFinished()} to notify any
- * listeners.
- */
- protected void dispatchFinishedWhenDone() {
- if (!isRunning()) {
- dispatchAnimationsFinished();
- }
- }
-
- @Override
- public void endAnimations() {
- int count = pendingMoves.size();
- for (int i = count - 1; i >= 0; i--) {
- final MoveInfo item = pendingMoves.get(i);
- resetViewProperties(item.holder.itemView);
- dispatchMoveFinished(item.holder);
- pendingMoves.remove(i);
- }
- count = pendingRemovals.size();
- for (int i = count - 1; i >= 0; i--) {
- final RecyclerView.ViewHolder item = pendingRemovals.get(i);
- resetViewProperties(item.itemView);
- dispatchRemoveFinished(item);
- pendingRemovals.remove(i);
- }
- count = pendingAdditions.size();
- for (int i = count - 1; i >= 0; i--) {
- final RecyclerView.ViewHolder item = pendingAdditions.get(i);
- resetViewProperties(item.itemView);
- dispatchAddFinished(item);
- pendingAdditions.remove(i);
- }
- count = pendingChanges.size();
- for (int i = count - 1; i >= 0; i--) {
- endChangeAnimationIfNecessary(pendingChanges.get(i));
- }
- pendingChanges.clear();
- if (!isRunning()) {
- return;
- }
-
- int listCount = movesList.size();
- for (int i = listCount - 1; i >= 0; i--) {
- final List<MoveInfo> moves = movesList.get(i);
- count = moves.size();
- for (int j = count - 1; j >= 0; j--) {
- final MoveInfo moveInfo = moves.get(j);
- final RecyclerView.ViewHolder item = moveInfo.holder;
- resetViewProperties(item.itemView);
- dispatchMoveFinished(item);
- moves.remove(j);
- if (moves.isEmpty()) {
- movesList.remove(moves);
- }
- }
- }
- listCount = additionsList.size();
- for (int i = listCount - 1; i >= 0; i--) {
- final List<RecyclerView.ViewHolder> additions = additionsList.get(i);
- count = additions.size();
- for (int j = count - 1; j >= 0; j--) {
- final RecyclerView.ViewHolder item = additions.get(j);
- resetViewProperties(item.itemView);
- dispatchAddFinished(item);
- additions.remove(j);
- if (additions.isEmpty()) {
- additionsList.remove(additions);
- }
- }
- }
- listCount = changesList.size();
- for (int i = listCount - 1; i >= 0; i--) {
- final List<ChangeInfo> changes = changesList.get(i);
- count = changes.size();
- for (int j = count - 1; j >= 0; j--) {
- endChangeAnimationIfNecessary(changes.get(j));
- if (changes.isEmpty()) {
- changesList.remove(changes);
- }
- }
- }
-
- cancelAll(removeAnimations);
- cancelAll(moveAnimations);
- cancelAll(addAnimations);
- cancelAll(changeAnimations);
-
- dispatchAnimationsFinished();
- }
-
- public void cancelAll(List<RecyclerView.ViewHolder> viewHolders) {
- for (int i = viewHolders.size() - 1; i >= 0; i--) {
- ViewCompat.animate(viewHolders.get(i).itemView).cancel();
- }
- }
-
- /**
- * {@inheritDoc}
- * <p>
- * If the payload list is not empty, DefaultItemAnimator returns <code>true</code>.
- * When this is the case:
- * <ul>
- * <li>If you override
- * {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)},
- * both ViewHolder arguments will be the same instance.
- * </li>
- * <li>
- * If you are not overriding
- * {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)},
- * then DefaultItemAnimator will call
- * {@link #animateMove(RecyclerView.ViewHolder, int, int, int, int)} and run a move animation
- * instead.
- * </li>
- * </ul>
- */
- @Override
- public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder,
- @NonNull List<Object> payloads) {
- return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
- }
-
- private class VpaListenerAdapter implements ViewPropertyAnimatorListener {
- @Override
- public void onAnimationStart(View view) {}
-
- // Note that onAnimationEnd is called (in addition to OnAnimationCancel) whenever an
- // animation is canceled.
- @Override
- public void onAnimationEnd(View view) {
- resetViewProperties(view);
- view.animate().setListener(null);
- }
-
- @Override
- public void onAnimationCancel(View view) {}
- }
-
- protected class DefaultRemoveVpaListener extends VpaListenerAdapter {
- private final RecyclerView.ViewHolder viewHolder;
-
- public DefaultRemoveVpaListener(final RecyclerView.ViewHolder holder) {
- viewHolder = holder;
- }
-
- @Override
- public void onAnimationStart(View view) {
- removeAnimations.add(viewHolder);
- dispatchRemoveStarting(viewHolder);
- }
-
- @Override
- public void onAnimationEnd(View view) {
- removeAnimations.remove(viewHolder);
- dispatchRemoveFinished(viewHolder);
- dispatchFinishedWhenDone();
- super.onAnimationEnd(view);
- }
- }
-
- protected class DefaultAddVpaListener extends VpaListenerAdapter {
- private final RecyclerView.ViewHolder viewHolder;
-
- public DefaultAddVpaListener(final RecyclerView.ViewHolder holder) {
- viewHolder = holder;
- }
-
- @Override
- public void onAnimationStart(View view) {
- addAnimations.add(viewHolder);
- dispatchAddStarting(viewHolder);
- }
-
- @Override
- public void onAnimationEnd(View view) {
- addAnimations.remove(viewHolder);
- dispatchAddFinished(viewHolder);
- dispatchFinishedWhenDone();
- super.onAnimationEnd(view);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/DoorHanger.java b/mobile/android/base/java/org/mozilla/gecko/widget/DoorHanger.java
deleted file mode 100644
index 7d32278e4..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/DoorHanger.java
+++ /dev/null
@@ -1,220 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.support.v4.content.ContextCompat;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewStub;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-import org.json.JSONObject;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-
-import java.util.Locale;
-
-public abstract class DoorHanger extends LinearLayout {
-
- public static DoorHanger Get(Context context, DoorhangerConfig config) {
- final Type type = config.getType();
- switch (type) {
- case LOGIN:
- return new LoginDoorHanger(context, config);
- case TRACKING:
- return new ContentSecurityDoorHanger(context, config, type);
- }
- return new DefaultDoorHanger(context, config, type);
- }
-
- // Doorhanger types created from Gecko are checked against enum strings to determine type.
- public static enum Type { DEFAULT, LOGIN, TRACKING, GEOLOCATION, DESKTOPNOTIFICATION2, WEBRTC, VIBRATION }
-
- public interface OnButtonClickListener {
- public void onButtonClick(JSONObject response, DoorHanger doorhanger);
- }
-
- private static final String LOGTAG = "GeckoDoorHanger";
-
- // Divider between doorhangers.
- private final View mDivider;
-
- private final Button mNegativeButton;
- private final Button mPositiveButton;
- protected final OnButtonClickListener mOnButtonClickListener;
-
- // The tab this doorhanger is associated with.
- private final int mTabId;
-
- // DoorHanger identifier.
- private final String mIdentifier;
-
- protected final Type mType;
-
- protected final ImageView mIcon;
- protected final TextView mLink;
- protected final TextView mDoorhangerTitle;
-
- protected final Context mContext;
- protected final Resources mResources;
-
- protected int mDividerColor;
-
- protected boolean mPersistWhileVisible;
- protected int mPersistenceCount;
- protected long mTimeout;
-
- protected DoorHanger(Context context, DoorhangerConfig config, Type type) {
- super(context);
-
- mContext = context;
- mResources = context.getResources();
- mTabId = config.getTabId();
- mIdentifier = config.getId();
- mType = type;
-
- LayoutInflater.from(context).inflate(R.layout.doorhanger, this);
- setOrientation(VERTICAL);
-
- mDivider = findViewById(R.id.divider_doorhanger);
- mIcon = (ImageView) findViewById(R.id.doorhanger_icon);
- mLink = (TextView) findViewById(R.id.doorhanger_link);
- mDoorhangerTitle = (TextView) findViewById(R.id.doorhanger_title);
-
- mNegativeButton = (Button) findViewById(R.id.doorhanger_button_negative);
- mPositiveButton = (Button) findViewById(R.id.doorhanger_button_positive);
- mOnButtonClickListener = config.getButtonClickListener();
-
- mDividerColor = ContextCompat.getColor(context, R.color.toolbar_divider_grey);
-
- final ViewStub contentStub = (ViewStub) findViewById(R.id.content);
- contentStub.setLayoutResource(getContentResource());
- contentStub.inflate();
-
- final String typeExtra = mType.toString().toLowerCase(Locale.US);
- Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.DOORHANGER, typeExtra);
- }
-
- protected abstract int getContentResource();
-
- protected abstract void loadConfig(DoorhangerConfig config);
-
- protected void setOptions(final JSONObject options) {
- final int persistence = options.optInt("persistence");
- if (persistence > 0) {
- mPersistenceCount = persistence;
- }
-
- mPersistWhileVisible = options.optBoolean("persistWhileVisible");
-
- final long timeout = options.optLong("timeout");
- if (timeout > 0) {
- mTimeout = timeout;
- }
- }
-
- protected void addButtonsToLayout(DoorhangerConfig config) {
- final DoorhangerConfig.ButtonConfig negativeButtonConfig = config.getNegativeButtonConfig();
- final DoorhangerConfig.ButtonConfig positiveButtonConfig = config.getPositiveButtonConfig();
-
- if (negativeButtonConfig != null) {
- mNegativeButton.setText(negativeButtonConfig.label);
- mNegativeButton.setOnClickListener(makeOnButtonClickListener(negativeButtonConfig.callback, "negative"));
- mNegativeButton.setVisibility(VISIBLE);
- }
-
- if (positiveButtonConfig != null) {
- mPositiveButton.setText(positiveButtonConfig.label);
- mPositiveButton.setOnClickListener(makeOnButtonClickListener(positiveButtonConfig.callback, "positive"));
- mPositiveButton.setVisibility(VISIBLE);
- }
- }
-
- public int getTabId() {
- return mTabId;
- }
-
- public String getIdentifier() {
- return mIdentifier;
- }
-
- public void showDivider() {
- mDivider.setVisibility(View.VISIBLE);
- }
-
- public void hideDivider() {
- mDivider.setVisibility(View.GONE);
- }
-
- public void setIcon(int resId) {
- mIcon.setImageResource(resId);
- mIcon.setVisibility(View.VISIBLE);
- }
-
- protected void addLink(String label, final String url) {
- mLink.setText(label);
- mLink.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View view) {
- final String typeExtra = mType.toString().toLowerCase(Locale.US);
- Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.DOORHANGER, typeExtra);
- Tabs.getInstance().loadUrlInTab(url);
- }
- });
- mLink.setVisibility(VISIBLE);
- }
-
- protected abstract OnClickListener makeOnButtonClickListener(final int id, final String telemetryExtra);
-
- /*
- * Checks with persistence and timeout options to see if it's okay to remove a doorhanger.
- *
- * @param isShowing Whether or not this doorhanger is currently visible to the user.
- * (e.g. the DoorHanger view might be VISIBLE, but its parent could be hidden)
- */
- public boolean shouldRemove(boolean isShowing) {
- if (mPersistWhileVisible && isShowing) {
- // We still want to decrement mPersistence, even if the popup is showing
- if (mPersistenceCount != 0)
- mPersistenceCount--;
- return false;
- }
-
- // If persistence is set to -1, the doorhanger will never be
- // automatically removed.
- if (mPersistenceCount != 0) {
- mPersistenceCount--;
- return false;
- }
-
- if (System.currentTimeMillis() <= mTimeout) {
- return false;
- }
-
- return true;
- }
-
- public void showTitle(Bitmap favicon, String title) {
- mDoorhangerTitle.setText(title);
- mDoorhangerTitle.setCompoundDrawablesWithIntrinsicBounds(new BitmapDrawable(getResources(), favicon), null, null, null);
- if (favicon != null) {
- mDoorhangerTitle.setCompoundDrawablePadding((int) mContext.getResources().getDimension(R.dimen.doorhanger_drawable_padding));
- }
- mDoorhangerTitle.setVisibility(VISIBLE);
- }
-
- public void hideTitle() {
- mDoorhangerTitle.setVisibility(GONE);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/DoorhangerConfig.java b/mobile/android/base/java/org/mozilla/gecko/widget/DoorhangerConfig.java
deleted file mode 100644
index 98f1e57e1..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/DoorhangerConfig.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import android.util.Log;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import org.mozilla.gecko.widget.DoorHanger.Type;
-
-public class DoorhangerConfig {
-
- public static class Link {
- public final String label;
- public final String url;
-
- private Link(String label, String url) {
- this.label = label;
- this.url = url;
- }
- }
-
- public static class ButtonConfig {
- public final String label;
- public final int callback;
-
- public ButtonConfig(String label, int callback) {
- this.label = label;
- this.callback = callback;
- }
- }
- private static final String LOGTAG = "DoorhangerConfig";
-
- private final int tabId;
- private final String id;
- private final DoorHanger.OnButtonClickListener buttonClickListener;
- private final DoorHanger.Type type;
- private String message;
- private JSONObject options;
- private Link link;
- private ButtonConfig positiveButtonConfig;
- private ButtonConfig negativeButtonConfig;
-
- public DoorhangerConfig(Type type, DoorHanger.OnButtonClickListener listener) {
- // XXX: This should only be used by SiteIdentityPopup doorhangers which
- // don't need tab or id references, until bug 1141904 unifies doorhangers.
-
- this(-1, null, type, listener);
- }
-
- public DoorhangerConfig(int tabId, String id, DoorHanger.Type type, DoorHanger.OnButtonClickListener buttonClickListener) {
- this.tabId = tabId;
- this.id = id;
- this.type = type;
- this.buttonClickListener = buttonClickListener;
- }
-
- public int getTabId() {
- return tabId;
- }
-
- public String getId() {
- return id;
- }
-
- public Type getType() {
- return type;
- }
-
- public void setMessage(String message) {
- this.message = message;
- }
-
- public String getMessage() {
- return message;
- }
-
- public void setOptions(JSONObject options) {
- this.options = options;
-
- // Set link if there is a link provided in options.
- final JSONObject linkObj = options.optJSONObject("link");
- if (linkObj != null) {
- try {
- setLink(linkObj.getString("label"), linkObj.getString("url"));
- } catch (JSONException e) {
- Log.e(LOGTAG, "Malformed link object in options");
- }
- }
- }
-
- public JSONObject getOptions() {
- return options;
- }
-
- public void setButton(String label, int callbackId, boolean isPositive) {
- final ButtonConfig buttonConfig = new ButtonConfig(label, callbackId);
- if (isPositive) {
- positiveButtonConfig = buttonConfig;
- } else {
- negativeButtonConfig = buttonConfig;
- }
- }
-
- public ButtonConfig getPositiveButtonConfig() {
- return positiveButtonConfig;
- }
-
- public ButtonConfig getNegativeButtonConfig() {
- return negativeButtonConfig;
- }
-
- public DoorHanger.OnButtonClickListener getButtonClickListener() {
- return this.buttonClickListener;
- }
-
- public void setLink(String label, String url) {
- this.link = new Link(label, url);
- }
-
- public Link getLink() {
- return link;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/EllipsisTextView.java b/mobile/android/base/java/org/mozilla/gecko/widget/EllipsisTextView.java
deleted file mode 100644
index 44f88e668..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/EllipsisTextView.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.util.AttributeSet;
-import android.widget.TextView;
-
-/**
- * Text view that correctly handles maxLines and ellipsizing for Android < 2.3.
- */
-public class EllipsisTextView extends TextView {
- private final String ellipsis;
-
- private final int maxLines;
- private CharSequence originalText;
-
- public EllipsisTextView(Context context) {
- this(context, null);
- }
-
- public EllipsisTextView(Context context, AttributeSet attrs) {
- this(context, attrs, android.R.attr.textViewStyle);
- }
-
- public EllipsisTextView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
-
- ellipsis = getResources().getString(R.string.ellipsis);
-
- TypedArray a = context.getTheme()
- .obtainStyledAttributes(attrs, R.styleable.EllipsisTextView, 0, 0);
- maxLines = a.getInteger(R.styleable.EllipsisTextView_ellipsizeAtLine, 1);
- a.recycle();
- }
-
- public void setOriginalText(CharSequence text) {
- originalText = text;
- setText(text);
- }
-
- @Override
- public void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
-
- // There is extra space, start over with the original text
- if (getLineCount() < maxLines) {
- setText(originalText);
- }
-
- // If we are over the max line attribute, ellipsize
- if (getLineCount() > maxLines) {
- final int endIndex = getLayout().getLineEnd(maxLines - 1) - 1 - ellipsis.length();
- final String text = getText().subSequence(0, endIndex) + ellipsis;
- // Make sure that we don't change originalText
- setText(text);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/ExternalIntentDuringPrivateBrowsingPromptFragment.java b/mobile/android/base/java/org/mozilla/gecko/widget/ExternalIntentDuringPrivateBrowsingPromptFragment.java
deleted file mode 100644
index b4d1e13d9..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/ExternalIntentDuringPrivateBrowsingPromptFragment.java
+++ /dev/null
@@ -1,106 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-package org.mozilla.gecko.widget;
-
-import org.mozilla.gecko.ActivityHandlerHelper;
-import org.mozilla.gecko.GeckoApplication;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-
-import android.app.Dialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.os.Bundle;
-import android.support.v4.app.DialogFragment;
-import android.support.v4.app.FragmentManager;
-import android.support.v7.app.AlertDialog;
-import android.util.Log;
-
-import java.util.List;
-
-/**
- * A DialogFragment to contain a dialog that appears when the user clicks an Intent:// URI during private browsing. The
- * dialog appears to notify the user that a clicked link will open in an external application, potentially leaking their
- * browsing history.
- */
-public class ExternalIntentDuringPrivateBrowsingPromptFragment extends DialogFragment {
- private static final String LOGTAG = ExternalIntentDuringPrivateBrowsingPromptFragment.class.getSimpleName();
- private static final String FRAGMENT_TAG = "ExternalIntentPB";
-
- private static final String KEY_APPLICATION_NAME = "matchingApplicationName";
- private static final String KEY_INTENT = "intent";
-
- @Override
- public Dialog onCreateDialog(final Bundle savedInstanceState) {
- final Bundle args = getArguments();
- final CharSequence matchingApplicationName = args.getCharSequence(KEY_APPLICATION_NAME);
- final Intent intent = args.getParcelable(KEY_INTENT);
-
- final Context context = getActivity();
- final String promptMessage = context.getString(R.string.intent_uri_private_browsing_prompt, matchingApplicationName);
-
- final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
- builder.setMessage(promptMessage)
- .setTitle(intent.getDataString())
- .setPositiveButton(R.string.button_yes, new DialogInterface.OnClickListener() {
- public void onClick(final DialogInterface dialog, final int id) {
- context.startActivity(intent);
- }
- })
- .setNegativeButton(R.string.button_no, null /* we do nothing if the user rejects */ );
- return builder.create();
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
-
- GeckoApplication.watchReference(getActivity(), this);
- }
-
- /**
- * @return true if the Activity is started or a dialog is shown. false if the Activity fails to start.
- */
- public static boolean showDialogOrAndroidChooser(final Context context, final FragmentManager fragmentManager,
- final Intent intent) {
- final Tab selectedTab = Tabs.getInstance().getSelectedTab();
- if (selectedTab == null || !selectedTab.isPrivate()) {
- return ActivityHandlerHelper.startIntentAndCatch(LOGTAG, context, intent);
- }
-
- final PackageManager pm = context.getPackageManager();
- final List<ResolveInfo> matchingActivities = pm.queryIntentActivities(intent, 0);
- if (matchingActivities.size() == 1) {
- final ExternalIntentDuringPrivateBrowsingPromptFragment fragment = new ExternalIntentDuringPrivateBrowsingPromptFragment();
-
- final Bundle args = new Bundle(2);
- args.putCharSequence(KEY_APPLICATION_NAME, matchingActivities.get(0).loadLabel(pm));
- args.putParcelable(KEY_INTENT, intent);
- fragment.setArguments(args);
-
- fragment.show(fragmentManager, FRAGMENT_TAG);
- // We don't know the results of the user interaction with the fragment so just return true.
- return true;
- } else if (matchingActivities.size() > 1) {
- // We want to show the Android Intent Chooser. However, we have no way of distinguishing regular tabs from
- // private tabs to the chooser. Thus, if a user chooses "Always" in regular browsing mode, the chooser will
- // not be shown and the URL will be opened. Therefore we explicitly show the chooser (which notably does not
- // have an "Always" option).
- final String androidChooserTitle =
- context.getResources().getString(R.string.intent_uri_private_browsing_multiple_match_title);
- final Intent chooserIntent = Intent.createChooser(intent, androidChooserTitle);
- return ActivityHandlerHelper.startIntentAndCatch(LOGTAG, context, chooserIntent);
- } else {
- // Normally, we show about:neterror when an Intent does not resolve
- // but we don't have the references here to do that so log instead.
- Log.w(LOGTAG, "showDialogOrAndroidChooser unexpectedly called with Intent that does not resolve");
- return false;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/FadedMultiColorTextView.java b/mobile/android/base/java/org/mozilla/gecko/widget/FadedMultiColorTextView.java
deleted file mode 100644
index 08bb55ef6..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/FadedMultiColorTextView.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.LinearGradient;
-import android.graphics.Paint;
-import android.graphics.Shader;
-import android.util.AttributeSet;
-
-/**
- * Fades the end of the text by gecko:fadeWidth amount,
- * if the text is too long and requires an ellipsis.
- *
- * This implementation is an improvement over Android's built-in fadingEdge
- * but potentially slower than the {@link org.mozilla.gecko.widget.FadedSingleColorTextView}.
- * It works for text of multiple colors but only one background color. It works by
- * drawing a gradient rectangle with the background color over the text, fading it out.
- */
-public class FadedMultiColorTextView extends FadedTextView {
- private final ColorStateList fadeBackgroundColorList;
-
- private final Paint fadePaint;
- private FadedTextGradient backgroundGradient;
-
- public FadedMultiColorTextView(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- fadePaint = new Paint();
-
- final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FadedMultiColorTextView);
- fadeBackgroundColorList =
- a.getColorStateList(R.styleable.FadedMultiColorTextView_fadeBackgroundColor);
- a.recycle();
- }
-
- @Override
- public void onDraw(Canvas canvas) {
- super.onDraw(canvas);
-
- final boolean needsEllipsis = needsEllipsis();
- if (needsEllipsis) {
- final int right = getWidth() - getCompoundPaddingRight();
- final float left = right - fadeWidth;
-
- updateGradientShader(needsEllipsis, right);
-
- // Shrink height of gradient to prevent it overlaying parent view border.
- // The shrunk size just nee to cover the text itself.
- final float density = getResources().getDisplayMetrics().density;
- final float h = Math.abs(fadePaint.getFontMetrics().top) + 1;
- final float l = fadePaint.getFontMetrics().bottom + 1;
- final float top = getBaseline() - h * density;
- final float bottom = getBaseline() + l * density;
-
- canvas.drawRect(left, top, right, bottom, fadePaint);
- }
- }
-
- private void updateGradientShader(final boolean needsEllipsis, final int gradientEndRight) {
- final int backgroundColor =
- fadeBackgroundColorList.getColorForState(getDrawableState(), Color.RED);
-
- final boolean needsNewGradient = (backgroundGradient == null ||
- backgroundGradient.getBackgroundColor() != backgroundColor ||
- backgroundGradient.getEndRight() != gradientEndRight);
-
- if (needsEllipsis && needsNewGradient) {
- backgroundGradient = new FadedTextGradient(gradientEndRight, fadeWidth, backgroundColor);
- fadePaint.setShader(backgroundGradient);
- }
- }
-
- private static class FadedTextGradient extends LinearGradient {
- private final int endRight;
- private final int backgroundColor;
-
- public FadedTextGradient(final int gradientEndRight, final int fadeWidth,
- final int backgroundColor) {
- super(gradientEndRight - fadeWidth, 0, gradientEndRight, 0,
- getColorWithZeroedAlpha(backgroundColor), backgroundColor, Shader.TileMode.CLAMP);
-
- this.endRight = gradientEndRight;
- this.backgroundColor = backgroundColor;
- }
-
- private static int getColorWithZeroedAlpha(final int color) {
- return Color.argb(0, Color.red(color), Color.green(color), Color.blue(color));
- }
-
- public int getEndRight() {
- return endRight;
- }
-
- public int getBackgroundColor() {
- return backgroundColor;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/FadedSingleColorTextView.java b/mobile/android/base/java/org/mozilla/gecko/widget/FadedSingleColorTextView.java
deleted file mode 100644
index 866b7ecbd..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/FadedSingleColorTextView.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.LinearGradient;
-import android.graphics.Shader;
-import android.util.AttributeSet;
-
-/**
- * Fades the end of the text by gecko:fadeWidth amount,
- * if the text is too long and requires an ellipsis.
- *
- * This implementation is an improvement over Android's built-in fadingEdge
- * and the fastest of Fennec's implementations. However, it only works for
- * text of one color. It works by applying a linear gradient directly to the text.
- */
-public class FadedSingleColorTextView extends FadedTextView {
- // Shader for the fading edge.
- private FadedTextGradient mTextGradient;
-
- public FadedSingleColorTextView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- private void updateGradientShader() {
- final int color = getCurrentTextColor();
- final int width = getAvailableWidth();
-
- final boolean needsNewGradient = (mTextGradient == null ||
- mTextGradient.getColor() != color ||
- mTextGradient.getWidth() != width);
-
- final boolean needsEllipsis = needsEllipsis();
- if (needsEllipsis && needsNewGradient) {
- mTextGradient = new FadedTextGradient(width, fadeWidth, color);
- }
-
- getPaint().setShader(needsEllipsis ? mTextGradient : null);
- }
-
- @Override
- public void onDraw(Canvas canvas) {
- updateGradientShader();
- super.onDraw(canvas);
- }
-
- private static class FadedTextGradient extends LinearGradient {
- private final int mWidth;
- private final int mColor;
-
- public FadedTextGradient(int width, int fadeWidth, int color) {
- super(0, 0, width, 0,
- new int[] { color, color, 0x0 },
- new float[] { 0, ((float) (width - fadeWidth) / width), 1.0f },
- Shader.TileMode.CLAMP);
-
- mWidth = width;
- mColor = color;
- }
-
- public int getWidth() {
- return mWidth;
- }
-
- public int getColor() {
- return mColor;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/FadedTextView.java b/mobile/android/base/java/org/mozilla/gecko/widget/FadedTextView.java
deleted file mode 100644
index e10433083..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/FadedTextView.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.text.Layout;
-import android.util.AttributeSet;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.widget.themed.ThemedTextView;
-
-/**
- * An implementation of FadedTextView should fade the end of the text
- * by gecko:fadeWidth amount, if the text is too long and requires an ellipsis.
- */
-public abstract class FadedTextView extends ThemedTextView {
- // Width of the fade effect from end of the view.
- protected final int fadeWidth;
-
- public FadedTextView(final Context context, final AttributeSet attrs) {
- super(context, attrs);
-
- setSingleLine(true);
- setEllipsize(null);
-
- final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FadedTextView);
- fadeWidth = a.getDimensionPixelSize(R.styleable.FadedTextView_fadeWidth, 0);
- a.recycle();
- }
-
- protected int getAvailableWidth() {
- return getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight();
- }
-
- protected boolean needsEllipsis() {
- final int width = getAvailableWidth();
- if (width <= 0) {
- return false;
- }
-
- final Layout layout = getLayout();
- return (layout != null && layout.getLineWidth(0) > width);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/FaviconView.java b/mobile/android/base/java/org/mozilla/gecko/widget/FaviconView.java
deleted file mode 100644
index 4652345b4..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/FaviconView.java
+++ /dev/null
@@ -1,268 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.icons.IconCallback;
-import org.mozilla.gecko.icons.IconResponse;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.RectF;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
-import android.util.TypedValue;
-import android.widget.ImageView;
-
-import java.lang.ref.WeakReference;
-
-/**
- * Special version of ImageView for favicons.
- * Displays solid colour background around Favicon to fill space not occupied by the icon. Colour
- * selected is the dominant colour of the provided Favicon.
- */
-public class FaviconView extends ImageView {
- private static final String LOGTAG = "GeckoFaviconView";
-
- private static String DEFAULT_FAVICON_KEY = FaviconView.class.getSimpleName() + "DefaultFavicon";
-
- // Default x/y-radius of the oval used to round the corners of the background (dp)
- private static final int DEFAULT_CORNER_RADIUS_DP = 4;
-
- private Bitmap mIconBitmap;
-
- // Reference to the unscaled bitmap, if any, to prevent repeated assignments of the same bitmap
- // to the view from causing repeated rescalings (Some of the callers do this)
- private Bitmap mUnscaledBitmap;
-
- private int mActualWidth;
- private int mActualHeight;
-
- // Flag indicating if the most recently assigned image is considered likely to need scaling.
- private boolean mScalingExpected;
-
- // Dominant color of the favicon.
- private int mDominantColor;
-
- // Paint for drawing the background.
- private static final Paint sBackgroundPaint;
-
- // Size of the background rectangle.
- private final RectF mBackgroundRect;
-
- // The x/y-radius of the oval used to round the corners of the background (pixels)
- private final float mBackgroundCornerRadius;
-
- // Type of the border whose value is defined in attrs.xml .
- private final boolean isDominantBorderEnabled;
-
- // boolean switch for overriding scaletype, whose value is defined in attrs.xml .
- private final boolean isOverrideScaleTypeEnabled;
-
- // boolean switch for disabling rounded corners, value defined in attrs.xml .
- private final boolean areRoundCornersEnabled;
-
- // Initializing the static paints.
- static {
- sBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- sBackgroundPaint.setStyle(Paint.Style.FILL);
- }
-
- public FaviconView(Context context, AttributeSet attrs) {
- super(context, attrs);
- TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.FaviconView, 0, 0);
-
- try {
- isDominantBorderEnabled = a.getBoolean(R.styleable.FaviconView_dominantBorderEnabled, true);
- isOverrideScaleTypeEnabled = a.getBoolean(R.styleable.FaviconView_overrideScaleType, true);
- areRoundCornersEnabled = a.getBoolean(R.styleable.FaviconView_enableRoundCorners, true);
- } finally {
- a.recycle();
- }
-
- if (isOverrideScaleTypeEnabled) {
- setScaleType(ImageView.ScaleType.CENTER);
- }
-
- final DisplayMetrics metrics = getResources().getDisplayMetrics();
-
- mBackgroundRect = new RectF(0, 0, 0, 0);
- mBackgroundCornerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_CORNER_RADIUS_DP, metrics);
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
-
- // No point rechecking the image if there hasn't really been any change.
- if (w == mActualWidth && h == mActualHeight) {
- return;
- }
-
- mActualWidth = w;
- mActualHeight = h;
-
- mBackgroundRect.right = w;
- mBackgroundRect.bottom = h;
-
- formatImage();
- }
-
- @Override
- public void onDraw(Canvas canvas) {
- if (isDominantBorderEnabled) {
- sBackgroundPaint.setColor(mDominantColor & 0x7FFFFFFF);
-
- if (areRoundCornersEnabled) {
- canvas.drawRoundRect(mBackgroundRect, mBackgroundCornerRadius, mBackgroundCornerRadius, sBackgroundPaint);
- } else {
- canvas.drawRect(mBackgroundRect, sBackgroundPaint);
- }
- }
-
- super.onDraw(canvas);
- }
-
- /**
- * Formats the image for display, if the prerequisite data are available. Upscales tiny Favicons to
- * normal sized ones, replaces null bitmaps with the default Favicon, and fills all remaining space
- * in this view with the coloured background.
- */
- private void formatImage() {
- // We're waiting for both onSizeChanged and updateImage to be called before scaling.
- if (mIconBitmap == null || mActualWidth == 0 || mActualHeight == 0) {
- showNoImage();
- return;
- }
-
- if (mScalingExpected && mActualWidth != mIconBitmap.getWidth()) {
- scaleBitmap();
- // Don't scale the image every time something changes.
- mScalingExpected = false;
- }
-
- setImageBitmap(mIconBitmap);
-
- // After scaling, determine if we have empty space around the scaled image which we need to
- // fill with the coloured background. If applicable, show it.
- // We assume Favicons are still squares and only bother with the background if more than 3px
- // of it would be displayed.
- if (Math.abs(mIconBitmap.getWidth() - mActualWidth) < 3) {
- mDominantColor = 0;
- }
- }
-
- private void scaleBitmap() {
- // If the Favicon can be resized to fill the view exactly without an enlargment of more than
- // a factor of two, do so.
- int doubledSize = mIconBitmap.getWidth() * 2;
- if (mActualWidth > doubledSize) {
- // If the view is more than twice the size of the image, just double the image size
- // and do the rest with padding.
- mIconBitmap = Bitmap.createScaledBitmap(mIconBitmap, doubledSize, doubledSize, true);
- } else {
- // Otherwise, scale the image to fill the view.
- mIconBitmap = Bitmap.createScaledBitmap(mIconBitmap, mActualWidth, mActualWidth, true);
- }
- }
-
- /**
- * Sets the icon displayed in this Favicon view to the bitmap provided. If the size of the view
- * has been set, the display will be updated right away, otherwise the update will be deferred
- * until then. The key provided is used to cache the result of the calculation of the dominant
- * colour of the provided image - this value is used to draw the coloured background in this view
- * if the icon is not large enough to fill it.
- *
- * @param allowScaling If true, allows the provided bitmap to be scaled by this FaviconView.
- * Typically, you should prefer using Favicons obtained via the caching system
- * (Favicons class), so as to exploit caching.
- */
- private void updateImageInternal(IconResponse response, boolean allowScaling) {
- // Reassigning the same bitmap? Don't bother.
- if (mUnscaledBitmap == response.getBitmap()) {
- return;
- }
- mUnscaledBitmap = response.getBitmap();
- mIconBitmap = response.getBitmap();
- mDominantColor = response.getColor();
- mScalingExpected = allowScaling;
-
- // Possibly update the display.
- formatImage();
- }
-
- private void showNoImage() {
- setImageDrawable(null);
- mDominantColor = 0;
- }
-
- /**
- * Clear image and background shown by this view.
- */
- public void clearImage() {
- showNoImage();
- mUnscaledBitmap = null;
- mIconBitmap = null;
- mDominantColor = 0;
- mScalingExpected = false;
- }
-
- /**
- * Update the displayed image and apply the scaling logic.
- * The scaling logic will attempt to resize the image to fit correctly inside the view in a way
- * that avoids unreasonable levels of loss of quality.
- * Scaling is necessary only when the icon being provided is not drawn from the Favicon cache
- * introduced in Bug 914296.
- *
- * Due to Bug 913746, icons bundled for search engines are not available to the cache, so must
- * always have the scaling logic applied here. At the time of writing, this is the only case in
- * which the scaling logic here is applied.
- */
- public void updateAndScaleImage(IconResponse response) {
- updateImageInternal(response, true);
- }
-
- /**
- * Update the image displayed in the Favicon view without scaling. Images larger than the view
- * will be centrally cropped. Images smaller than the view will be placed centrally and the
- * extra space filled with the dominant colour of the provided image.
- */
- public void updateImage(IconResponse response) {
- updateImageInternal(response, false);
- }
-
- public Bitmap getBitmap() {
- return mIconBitmap;
- }
-
- /**
- * Create an IconCallback implementation that will update this view after an icon has been loaded.
- */
- public IconCallback createIconCallback() {
- return new Callback(this);
- }
-
- private static class Callback implements IconCallback {
- private final WeakReference<FaviconView> viewReference;
-
- private Callback(FaviconView view) {
- this.viewReference = new WeakReference<FaviconView>(view);
- }
-
- @Override
- public void onIconResponse(IconResponse response) {
- final FaviconView view = viewReference.get();
- if (view == null) {
- return;
- }
-
- view.updateImage(response);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/FilledCardView.java b/mobile/android/base/java/org/mozilla/gecko/widget/FilledCardView.java
deleted file mode 100644
index f1662896e..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/FilledCardView.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-package org.mozilla.gecko.widget;
-
-import android.content.Context;
-import android.support.v7.widget.CardView;
-import android.util.AttributeSet;
-
-import org.mozilla.gecko.AppConstants;
-
-/**
- * CardView that ensures its content can fill the entire card. Use this instead of CardView
- * if you want to fill the card with e.g. images, backgrounds, etc.
- *
- * On API < 21, CardView content isn't clipped for performance reasons. We work around this by disabling
- * rounded corners on those devices.
- */
-public class FilledCardView extends CardView {
-
- public FilledCardView(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- // Disable corners on < lollipop:
- // CardView only supports clipping content on API >= 21 (for performance reasons). Without
- // content clipping, any cards that provide their own content that fills the card will look
- // ugly: by default there is a 2px white edge along the top and sides (i.e. an inset corresponding
- // to the corner radius), if we disable the inset then the corners overlap.
- // It's possible to implement custom clipping, however given that the support library
- // chose not to support this for performance reasons, we too have chosen to just disable
- // corners on < 21, see Bug 1271428.
- if (AppConstants.Versions.preLollipop) {
- setRadius(0);
- }
-
- setUseCompatPadding(true);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/FlowLayout.java b/mobile/android/base/java/org/mozilla/gecko/widget/FlowLayout.java
deleted file mode 100644
index 042e74851..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/FlowLayout.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewGroup;
-
-public class FlowLayout extends ViewGroup {
- private int mSpacing;
-
- public FlowLayout(Context context) {
- super(context);
- }
-
- public FlowLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- TypedArray a = context.obtainStyledAttributes(attrs, org.mozilla.gecko.R.styleable.FlowLayout);
- mSpacing = a.getDimensionPixelSize(R.styleable.FlowLayout_spacing, (int) context.getResources().getDimension(R.dimen.flow_layout_spacing));
- a.recycle();
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- final int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
- final int childCount = getChildCount();
- int rowWidth = 0;
- int totalWidth = 0;
- int totalHeight = 0;
- boolean firstChild = true;
-
- for (int i = 0; i < childCount; i++) {
- final View child = getChildAt(i);
- if (child.getVisibility() == GONE)
- continue;
-
- measureChild(child, widthMeasureSpec, heightMeasureSpec);
-
- final int childWidth = child.getMeasuredWidth();
- final int childHeight = child.getMeasuredHeight();
-
- if (firstChild || (rowWidth + childWidth > parentWidth)) {
- rowWidth = 0;
- totalHeight += childHeight;
- if (!firstChild)
- totalHeight += mSpacing;
- firstChild = false;
- }
-
- rowWidth += childWidth;
-
- if (rowWidth > totalWidth)
- totalWidth = rowWidth;
-
- rowWidth += mSpacing;
- }
-
- setMeasuredDimension(totalWidth, totalHeight);
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- final int childCount = getChildCount();
- final int totalWidth = r - l;
- int x = 0;
- int y = 0;
- int prevChildHeight = 0;
-
- for (int i = 0; i < childCount; i++) {
- final View child = getChildAt(i);
- if (child.getVisibility() == GONE)
- continue;
-
- final int childWidth = child.getMeasuredWidth();
- final int childHeight = child.getMeasuredHeight();
- if (x + childWidth > totalWidth) {
- x = 0;
- y += prevChildHeight + mSpacing;
- }
- prevChildHeight = childHeight;
- child.layout(x, y, x + childWidth, y + childHeight);
- x += childWidth + mSpacing;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/GeckoActionProvider.java b/mobile/android/base/java/org/mozilla/gecko/widget/GeckoActionProvider.java
deleted file mode 100644
index d864792a6..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/GeckoActionProvider.java
+++ /dev/null
@@ -1,360 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import android.app.Activity;
-import android.net.Uri;
-import android.support.design.widget.Snackbar;
-import android.util.Base64;
-import android.view.Menu;
-
-import org.mozilla.gecko.GeckoApp;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.SnackbarBuilder;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.overlays.ui.ShareDialog;
-import org.mozilla.gecko.menu.MenuItemSwitcherLayout;
-import org.mozilla.gecko.util.IOUtils;
-import org.mozilla.gecko.util.IntentUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.view.MenuItem;
-import android.view.MenuItem.OnMenuItemClickListener;
-import android.view.SubMenu;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.text.TextUtils;
-import android.webkit.MimeTypeMap;
-import android.webkit.URLUtil;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.HashMap;
-
-public class GeckoActionProvider {
- private static final int MAX_HISTORY_SIZE_DEFAULT = 2;
-
- /**
- * A listener to know when a target was selected.
- * When setting a provider, the activity can listen to this,
- * to close the menu.
- */
- public interface OnTargetSelectedListener {
- public void onTargetSelected();
- }
-
- final Context mContext;
-
- public final static String DEFAULT_MIME_TYPE = "text/plain";
-
- public static final String DEFAULT_HISTORY_FILE_NAME = "history.xml";
-
- // History file.
- String mHistoryFileName = DEFAULT_HISTORY_FILE_NAME;
-
- OnTargetSelectedListener mOnTargetListener;
-
- private final Callbacks mCallbacks = new Callbacks();
-
- private static final HashMap<String, GeckoActionProvider> mProviders = new HashMap<String, GeckoActionProvider>();
-
- private static String getFilenameFromMimeType(String mimeType) {
- String[] mime = mimeType.split("/");
-
- // All text mimetypes use the default provider
- if ("text".equals(mime[0])) {
- return DEFAULT_HISTORY_FILE_NAME;
- }
-
- return "history-" + mime[0] + ".xml";
- }
-
- // Gets the action provider for a particular mimetype
- public static GeckoActionProvider getForType(String mimeType, Context context) {
- if (!mProviders.keySet().contains(mimeType)) {
- GeckoActionProvider provider = new GeckoActionProvider(context);
-
- // For empty types, we just return a default provider
- if (TextUtils.isEmpty(mimeType)) {
- return provider;
- }
-
- provider.setHistoryFileName(getFilenameFromMimeType(mimeType));
- mProviders.put(mimeType, provider);
- }
- return mProviders.get(mimeType);
- }
-
- public GeckoActionProvider(Context context) {
- mContext = context;
- }
-
- /**
- * Creates the action view using the default history size.
- */
- public View onCreateActionView(final ActionViewType viewType) {
- return onCreateActionView(MAX_HISTORY_SIZE_DEFAULT, viewType);
- }
-
- public View onCreateActionView(final int maxHistorySize, final ActionViewType viewType) {
- // Create the view and set its data model.
- ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
- final MenuItemSwitcherLayout view;
- switch (viewType) {
- case DEFAULT:
- view = new MenuItemSwitcherLayout(mContext, null);
- break;
-
- case CONTEXT_MENU:
- view = new MenuItemSwitcherLayout(mContext, null);
- view.initContextMenuStyles();
- break;
-
- default:
- throw new IllegalArgumentException(
- "Unknown " + ActionViewType.class.getSimpleName() + ": " + viewType);
- }
- view.addActionButtonClickListener(mCallbacks);
-
- final PackageManager packageManager = mContext.getPackageManager();
- int historySize = dataModel.getDistinctActivityCountInHistory();
- if (historySize > maxHistorySize) {
- historySize = maxHistorySize;
- }
-
- // Historical data is dependent on past selection of activities.
- // Activity count is determined by the number of activities that can handle
- // the particular intent. When no intent is set, the activity count is 0,
- // while the history count can be a valid number.
- if (historySize > dataModel.getActivityCount()) {
- return view;
- }
-
- for (int i = 0; i < historySize; i++) {
- view.addActionButton(dataModel.getActivity(i).loadIcon(packageManager),
- dataModel.getActivity(i).loadLabel(packageManager));
- }
-
- return view;
- }
-
- public boolean hasSubMenu() {
- return true;
- }
-
- public void onPrepareSubMenu(SubMenu subMenu) {
- // Clear since the order of items may change.
- subMenu.clear();
-
- ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
- PackageManager packageManager = mContext.getPackageManager();
-
- // Populate the sub-menu with a sub set of the activities.
- final String shareDialogClassName = ShareDialog.class.getCanonicalName();
- final String sendTabLabel = mContext.getResources().getString(R.string.overlay_share_send_other);
- final int count = dataModel.getActivityCount();
- for (int i = 0; i < count; i++) {
- ResolveInfo activity = dataModel.getActivity(i);
- final CharSequence activityLabel = activity.loadLabel(packageManager);
-
- // Pin internal actions to the top. Note:
- // the order here does not affect quick share.
- final int order;
- if (shareDialogClassName.equals(activity.activityInfo.name) &&
- sendTabLabel.equals(activityLabel)) {
- order = Menu.FIRST + i;
- } else {
- order = Menu.FIRST + (i | Menu.CATEGORY_SECONDARY);
- }
-
- subMenu.add(0, i, order, activityLabel)
- .setIcon(activity.loadIcon(packageManager))
- .setOnMenuItemClickListener(mCallbacks);
- }
- }
-
- public void setHistoryFileName(String historyFile) {
- mHistoryFileName = historyFile;
- }
-
- public Intent getIntent() {
- ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
- return dataModel.getIntent();
- }
-
- public void setIntent(Intent intent) {
- ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
- dataModel.setIntent(intent);
-
- // Inform the target listener to refresh it's UI, if needed.
- if (mOnTargetListener != null) {
- mOnTargetListener.onTargetSelected();
- }
- }
-
- public void setOnTargetSelectedListener(OnTargetSelectedListener listener) {
- mOnTargetListener = listener;
- }
-
- public ArrayList<ResolveInfo> getSortedActivities() {
- ArrayList<ResolveInfo> infos = new ArrayList<ResolveInfo>();
-
- ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
-
- // Populate the sub-menu with a sub set of the activities.
- final int count = dataModel.getActivityCount();
- for (int i = 0; i < count; i++) {
- infos.add(dataModel.getActivity(i));
- }
-
- return infos;
- }
-
- public void chooseActivity(int position) {
- mCallbacks.chooseActivity(position);
- }
-
- /**
- * Listener for handling default activity / menu item clicks.
- */
- private class Callbacks implements OnMenuItemClickListener,
- OnClickListener {
- void chooseActivity(int index) {
- final ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
- final Intent launchIntent = dataModel.chooseActivity(index);
- if (launchIntent != null) {
- // This may cause a download to happen. Make sure we're on the background thread.
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- // Share image downloads the image before sharing it.
- String type = launchIntent.getType();
- if (Intent.ACTION_SEND.equals(launchIntent.getAction()) && type != null && type.startsWith("image/")) {
- downloadImageForIntent(launchIntent);
- }
-
- launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
- mContext.startActivity(launchIntent);
- }
- });
- }
-
- if (mOnTargetListener != null) {
- mOnTargetListener.onTargetSelected();
- }
- }
-
- @Override
- public boolean onMenuItemClick(MenuItem item) {
- chooseActivity(item.getItemId());
-
- // Context: Sharing via chrome mainmenu list (no explicit session is active)
- Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "actionprovider");
- return true;
- }
-
- @Override
- public void onClick(View view) {
- Integer index = (Integer) view.getTag();
- chooseActivity(index);
-
- // Context: Sharing via chrome mainmenu and content contextmenu quickshare (no explicit session is active)
- Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.BUTTON, "actionprovider");
- }
- }
-
- public enum ActionViewType {
- DEFAULT,
- CONTEXT_MENU,
- }
-
-
- /**
- * Downloads the URI pointed to by a share intent, and alters the intent to point to the
- * locally stored file.
- *
- * @param intent share intent to alter in place.
- */
- public void downloadImageForIntent(final Intent intent) {
- final String src = IntentUtils.getStringExtraSafe(intent, Intent.EXTRA_TEXT);
- final File dir = GeckoApp.getTempDirectory();
-
- if (src == null || dir == null) {
- // We should be, but currently aren't, statically guaranteed an Activity context.
- // Try our best.
- if (mContext instanceof Activity) {
- SnackbarBuilder.builder((Activity) mContext)
- .message(mContext.getApplicationContext().getString(R.string.share_image_failed))
- .duration(Snackbar.LENGTH_LONG)
- .buildAndShow();
- }
- return;
- }
-
- GeckoApp.deleteTempFiles();
-
- String type = intent.getType();
- OutputStream os = null;
- try {
- // Create a temporary file for the image
- if (src.startsWith("data:")) {
- final int dataStart = src.indexOf(",");
-
- String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type);
-
- // If we weren't given an explicit mimetype, try to dig one out of the data uri.
- if (TextUtils.isEmpty(extension) && dataStart > 5) {
- type = src.substring(5, dataStart).replace(";base64", "");
- extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type);
- }
-
- final File imageFile = File.createTempFile("image", "." + extension, dir);
- os = new FileOutputStream(imageFile);
-
- byte[] buf = Base64.decode(src.substring(dataStart + 1), Base64.DEFAULT);
- os.write(buf);
-
- // Only alter the intent when we're sure everything has worked
- intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(imageFile));
- } else {
- InputStream is = null;
- try {
- final byte[] buf = new byte[2048];
- final URL url = new URL(src);
- final String filename = URLUtil.guessFileName(src, null, type);
- is = url.openStream();
-
- final File imageFile = new File(dir, filename);
- os = new FileOutputStream(imageFile);
-
- int length;
- while ((length = is.read(buf)) != -1) {
- os.write(buf, 0, length);
- }
-
- // Only alter the intent when we're sure everything has worked
- intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(imageFile));
- } finally {
- IOUtils.safeStreamClose(is);
- }
- }
- } catch (IOException ex) {
- // If something went wrong, we'll just leave the intent un-changed
- } finally {
- IOUtils.safeStreamClose(os);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/GeckoPopupMenu.java b/mobile/android/base/java/org/mozilla/gecko/widget/GeckoPopupMenu.java
deleted file mode 100644
index 7e7f50662..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/GeckoPopupMenu.java
+++ /dev/null
@@ -1,189 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import org.mozilla.gecko.menu.GeckoMenu;
-import org.mozilla.gecko.menu.GeckoMenuInflater;
-import org.mozilla.gecko.menu.MenuPanel;
-import org.mozilla.gecko.menu.MenuPopup;
-
-import android.content.Context;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-
-/**
- * A PopupMenu that uses the custom GeckoMenu. This menu is
- * usually tied to an anchor, and show as a dropdrown from the anchor.
- */
-public class GeckoPopupMenu implements GeckoMenu.Callback,
- GeckoMenu.MenuPresenter {
-
- // An interface for listeners for dismissal.
- public static interface OnDismissListener {
- public boolean onDismiss(GeckoMenu menu);
- }
-
- // An interface for listeners for menu item click events.
- public static interface OnMenuItemClickListener {
- public boolean onMenuItemClick(MenuItem item);
- }
-
- // An interface for listeners for menu item long click events.
- public static interface OnMenuItemLongClickListener {
- public boolean onMenuItemLongClick(MenuItem item);
- }
-
- private View mAnchor;
-
- private MenuPopup mMenuPopup;
- private MenuPanel mMenuPanel;
-
- private GeckoMenu mMenu;
- private GeckoMenuInflater mMenuInflater;
-
- private OnDismissListener mDismissListener;
- private OnMenuItemClickListener mClickListener;
- private OnMenuItemLongClickListener mLongClickListener;
-
- public GeckoPopupMenu(Context context) {
- initialize(context, null);
- }
-
- public GeckoPopupMenu(Context context, View anchor) {
- initialize(context, anchor);
- }
-
- /**
- * This method creates an empty menu and attaches the necessary listeners.
- * If an anchor is supplied, it is stored as well.
- */
- private void initialize(Context context, View anchor) {
- mMenu = new GeckoMenu(context, null);
- mMenu.setCallback(this);
- mMenu.setMenuPresenter(this);
- mMenuInflater = new GeckoMenuInflater(context);
-
- mMenuPopup = new MenuPopup(context);
- mMenuPanel = new MenuPanel(context, null);
-
- mMenuPanel.addView(mMenu);
- mMenuPopup.setPanelView(mMenuPanel);
-
- setAnchor(anchor);
- }
-
- /**
- * Returns the menu that is current being shown.
- *
- * @return The menu being shown.
- */
- public GeckoMenu getMenu() {
- return mMenu;
- }
-
- /**
- * Returns the menu inflater that was used to create the menu.
- *
- * @return The menu inflater used.
- */
- public MenuInflater getMenuInflater() {
- return mMenuInflater;
- }
-
- /**
- * Inflates a menu resource to the menu using the menu inflater.
- *
- * @param menuRes The menu resource to be inflated.
- */
- public void inflate(int menuRes) {
- if (menuRes > 0) {
- mMenuInflater.inflate(menuRes, mMenu);
- }
- }
-
- /**
- * Set a different anchor after the menu is inflated.
- *
- * @param anchor The new anchor for the popup.
- */
- public void setAnchor(View anchor) {
- mAnchor = anchor;
-
- // Reposition the popup if the anchor changes while it's showing.
- if (mMenuPopup.isShowing()) {
- mMenuPopup.dismiss();
- mMenuPopup.showAsDropDown(mAnchor);
- }
- }
-
- public void setOnDismissListener(OnDismissListener listener) {
- mDismissListener = listener;
- }
-
- public void setOnMenuItemClickListener(OnMenuItemClickListener listener) {
- mClickListener = listener;
- }
-
- public void setOnMenuItemLongClickListener(OnMenuItemLongClickListener listener) {
- mLongClickListener = listener;
- }
-
- /**
- * Show the inflated menu.
- */
- public void show() {
- if (!mMenuPopup.isShowing())
- mMenuPopup.showAsDropDown(mAnchor);
- }
-
- /**
- * Hide the inflated menu.
- */
- public void dismiss() {
- if (mMenuPopup.isShowing()) {
- mMenuPopup.dismiss();
-
- if (mDismissListener != null)
- mDismissListener.onDismiss(mMenu);
- }
- }
-
- @Override
- public boolean onMenuItemClick(MenuItem item) {
- if (mClickListener != null) {
- return mClickListener.onMenuItemClick(item);
- }
- return false;
- }
-
- @Override
- public boolean onMenuItemLongClick(MenuItem item) {
- if (mLongClickListener != null) {
- return mLongClickListener.onMenuItemLongClick(item);
- }
- return false;
- }
-
- @Override
- public void openMenu() {
- show();
- }
-
- @Override
- public void showMenu(View menu) {
- mMenuPanel.removeAllViews();
- mMenuPanel.addView(menu);
-
- openMenu();
- }
-
- @Override
- public void closeMenu() {
- dismiss();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/HistoryDividerItemDecoration.java b/mobile/android/base/java/org/mozilla/gecko/widget/HistoryDividerItemDecoration.java
deleted file mode 100644
index 9c98e8a0d..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/HistoryDividerItemDecoration.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.support.v4.content.ContextCompat;
-import android.support.v7.widget.RecyclerView;
-import android.view.View;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.home.CombinedHistoryItem;
-
-public class HistoryDividerItemDecoration extends RecyclerView.ItemDecoration {
- private final int mDividerHeight;
- private final Paint mDividerPaint;
-
- public HistoryDividerItemDecoration(Context context) {
- mDividerHeight = (int) context.getResources().getDimension(R.dimen.page_row_divider_height);
-
- mDividerPaint = new Paint();
- mDividerPaint.setColor(ContextCompat.getColor(context, R.color.toolbar_divider_grey));
- mDividerPaint.setStyle(Paint.Style.FILL_AND_STROKE);
- }
-
- @Override
- public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
- final int position = parent.getChildAdapterPosition(view);
- if (position == RecyclerView.NO_POSITION) {
- // This view is no longer corresponds to an adapter position (pending changes).
- return;
- }
-
- if (parent.getAdapter().getItemViewType(position) !=
- CombinedHistoryItem.ItemType.itemTypeToViewType(CombinedHistoryItem.ItemType.SECTION_HEADER)) {
- outRect.set(0, 0, 0, mDividerHeight);
- }
- }
-
- @Override
- public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
- if (parent.getChildCount() == 0) {
- return;
- }
-
- for (int i = 0; i < parent.getChildCount(); i++) {
- final View child = parent.getChildAt(i);
- final int position = parent.getChildAdapterPosition(child);
-
- if (position == RecyclerView.NO_POSITION) {
- // This view is no longer corresponds to an adapter position (pending changes).
- continue;
- }
-
- if (parent.getAdapter().getItemViewType(position) !=
- CombinedHistoryItem.ItemType.itemTypeToViewType(CombinedHistoryItem.ItemType.SECTION_HEADER)) {
- final float bottom = child.getBottom() + child.getTranslationY();
- c.drawRect(0, bottom, parent.getWidth(), bottom + mDividerHeight, mDividerPaint);
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/IconTabWidget.java b/mobile/android/base/java/org/mozilla/gecko/widget/IconTabWidget.java
deleted file mode 100644
index 71987bf8c..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/IconTabWidget.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.ImageButton;
-import android.widget.TabWidget;
-import android.widget.TextView;
-
-public class IconTabWidget extends TabWidget {
- OnTabChangedListener mListener;
- private final int mButtonLayoutId;
- private final boolean mIsIcon;
-
- public static interface OnTabChangedListener {
- public void onTabChanged(int tabIndex);
- }
-
- public IconTabWidget(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.IconTabWidget);
- mButtonLayoutId = a.getResourceId(R.styleable.IconTabWidget_android_layout, 0);
- mIsIcon = (a.getInt(R.styleable.IconTabWidget_display, 0x00) == 0x00);
- a.recycle();
-
- if (mButtonLayoutId == 0) {
- throw new RuntimeException("You must supply layout attribute");
- }
- }
-
- public View addTab(final int imageResId, final int stringResId) {
- View button = LayoutInflater.from(getContext()).inflate(mButtonLayoutId, this, false);
- if (mIsIcon) {
- ((ImageButton) button).setImageResource(imageResId);
- button.setContentDescription(getContext().getString(stringResId));
- } else {
- ((TextView) button).setText(getContext().getString(stringResId));
- }
-
- addView(button);
- button.setOnClickListener(new TabClickListener(getTabCount() - 1));
- button.setOnFocusChangeListener(this);
- return button;
- }
-
- public void setTabSelectionListener(OnTabChangedListener listener) {
- mListener = listener;
- }
-
- @Override
- public void onFocusChange(View view, boolean hasFocus) {
- }
-
- private class TabClickListener implements OnClickListener {
- private final int mIndex;
-
- public TabClickListener(int index) {
- mIndex = index;
- }
-
- @Override
- public void onClick(View view) {
- if (mListener != null)
- mListener.onTabChanged(mIndex);
- }
- }
-
- /**
- * Fetch the Drawable icon corresponding to the given panel.
- * @param panel to fetch icon for.
- * @return Drawable instance, or null if no icon is being displayed, or the icon does not exist.
- */
- public Drawable getIconDrawable(int index) {
- if (!mIsIcon) {
- return null;
- }
- // We can have multiple views in the tabs panel for each child. This finds the
- // first view corresponding to the given tab. This varies by Android
- // version. The first view should always be our ImageButton, but let's
- // be safe.
- final View view = getChildTabViewAt(index);
- if (view instanceof ImageButton) {
- return ((ImageButton) view).getDrawable();
- }
- return null;
- }
-
- public void setIconDrawable(int index, int resource) {
- if (!mIsIcon) {
- return;
- }
- // We can have multiple views in the tabs panel for each child. This finds the
- // first view corresponding to the given tab. This varies by Android
- // version. The first view should always be our ImageButton, but let's
- // be safe.
- final View view = getChildTabViewAt(index);
- if (view instanceof ImageButton) {
- ((ImageButton) view).setImageResource(resource);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/LoginDoorHanger.java b/mobile/android/base/java/org/mozilla/gecko/widget/LoginDoorHanger.java
deleted file mode 100644
index 232674813..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/LoginDoorHanger.java
+++ /dev/null
@@ -1,228 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.text.Html;
-import android.text.Spanned;
-import android.text.TextUtils;
-import android.text.method.PasswordTransformationMethod;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.Button;
-import android.widget.CheckBox;
-import android.widget.CompoundButton;
-import android.widget.EditText;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-
-import java.util.Locale;
-
-public class LoginDoorHanger extends DoorHanger {
- private static final String LOGTAG = "LoginDoorHanger";
- private enum ActionType { EDIT, SELECT }
-
- private final TextView mMessage;
- private final DoorhangerConfig.ButtonConfig mButtonConfig;
-
- public LoginDoorHanger(Context context, DoorhangerConfig config) {
- super(context, config, Type.LOGIN);
-
- mMessage = (TextView) findViewById(R.id.doorhanger_message);
- mIcon.setImageResource(R.drawable.icon_key);
- mIcon.setVisibility(View.VISIBLE);
-
- mButtonConfig = config.getPositiveButtonConfig();
-
- loadConfig(config);
- }
-
- private void setMessage(String message) {
- Spanned markupMessage = Html.fromHtml(message);
- mMessage.setText(markupMessage);
- }
-
- @Override
- protected void loadConfig(DoorhangerConfig config) {
- setOptions(config.getOptions());
- setMessage(config.getMessage());
- // Store the positive callback id for nested dialogs that need the same callback id.
- addButtonsToLayout(config);
- }
-
- @Override
- protected int getContentResource() {
- return R.layout.login_doorhanger;
- }
-
- @Override
- protected void setOptions(final JSONObject options) {
- super.setOptions(options);
-
- final JSONObject actionText = options.optJSONObject("actionText");
- addActionText(actionText);
- }
-
- @Override
- protected OnClickListener makeOnButtonClickListener(final int id, final String telemetryExtra) {
- return new Button.OnClickListener() {
- @Override
- public void onClick(View v) {
- final String expandedExtra = mType.toString().toLowerCase(Locale.US) + "-" + telemetryExtra;
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.DOORHANGER, expandedExtra);
-
- final JSONObject response = new JSONObject();
- try {
- response.put("callback", id);
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error making doorhanger response message", e);
- }
- mOnButtonClickListener.onButtonClick(response, LoginDoorHanger.this);
- }
- };
- }
-
- /**
- * Add sub-text to the doorhanger and add the click action.
- *
- * If the parsing the action from the JSON throws, the text is left visible, but there is no
- * click action.
- * @param actionTextObj JSONObject containing blob for making an action.
- */
- private void addActionText(JSONObject actionTextObj) {
- if (actionTextObj == null) {
- mLink.setVisibility(View.GONE);
- return;
- }
-
- // Make action.
- try {
- final JSONObject bundle = actionTextObj.getJSONObject("bundle");
- final ActionType type = ActionType.valueOf(actionTextObj.getString("type"));
- final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
-
- switch (type) {
- case EDIT:
- builder.setTitle(mResources.getString(R.string.doorhanger_login_edit_title));
-
- final View view = LayoutInflater.from(mContext).inflate(R.layout.login_edit_dialog, null);
- final EditText username = (EditText) view.findViewById(R.id.username_edit);
- username.setText(bundle.getString("username"));
- final EditText password = (EditText) view.findViewById(R.id.password_edit);
- password.setText(bundle.getString("password"));
- final CheckBox passwordCheckbox = (CheckBox) view.findViewById(R.id.checkbox_toggle_password);
- passwordCheckbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
- if (isChecked) {
- password.setTransformationMethod(null);
- } else {
- password.setTransformationMethod(PasswordTransformationMethod.getInstance());
- }
- }
- });
- builder.setView(view);
-
- builder.setPositiveButton(mButtonConfig.label, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- JSONObject response = new JSONObject();
- try {
- response.put("callback", mButtonConfig.callback);
- final JSONObject inputs = new JSONObject();
- inputs.put("username", username.getText());
- inputs.put("password", password.getText());
- response.put("inputs", inputs);
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error creating doorhanger reply message");
- response = null;
- Toast.makeText(mContext, mResources.getString(R.string.doorhanger_login_edit_toast_error), Toast.LENGTH_SHORT).show();
- }
- mOnButtonClickListener.onButtonClick(response, LoginDoorHanger.this);
- }
- });
- builder.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- dialog.dismiss();
- }
- });
- String text = actionTextObj.optString("text");
- if (TextUtils.isEmpty(text)) {
- text = mResources.getString(R.string.doorhanger_login_no_username);
- }
- mLink.setText(text);
- mLink.setVisibility(View.VISIBLE);
- break;
-
- case SELECT:
- try {
- builder.setTitle(mResources.getString(R.string.doorhanger_login_select_title));
- final JSONArray logins = bundle.getJSONArray("logins");
- final int numLogins = logins.length();
- final CharSequence[] usernames = new CharSequence[numLogins];
- final String[] passwords = new String[numLogins];
- final String noUser = mResources.getString(R.string.doorhanger_login_no_username);
- for (int i = 0; i < numLogins; i++) {
- final JSONObject login = (JSONObject) logins.get(i);
- String user = login.getString("username");
- if (TextUtils.isEmpty(user)) {
- user = noUser;
- }
- usernames[i] = user;
- passwords[i] = login.getString("password");
- }
- builder.setItems(usernames, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- final JSONObject response = new JSONObject();
- try {
- response.put("callback", mButtonConfig.callback);
- response.put("password", passwords[which]);
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error making login select dialog JSON", e);
- }
- mOnButtonClickListener.onButtonClick(response, LoginDoorHanger.this);
- }
- });
- builder.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- dialog.dismiss();
- }
- });
- mLink.setText(R.string.doorhanger_login_select_action_text);
- mLink.setVisibility(View.VISIBLE);
- } catch (JSONException e) {
- Log.e(LOGTAG, "Problem creating list of logins");
- }
- break;
- }
-
- final Dialog dialog = builder.create();
- mLink.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- dialog.show();
- }
- });
-
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error fetching actionText from JSON", e);
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/RecyclerViewClickSupport.java b/mobile/android/base/java/org/mozilla/gecko/widget/RecyclerViewClickSupport.java
deleted file mode 100644
index a0c6049c5..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/RecyclerViewClickSupport.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import android.support.v7.widget.RecyclerView;
-import android.view.View;
-
-import org.mozilla.gecko.R;
-
-/**
- * {@link RecyclerViewClickSupport} implementation that will notify an OnClickListener about clicks and long clicks
- * on items displayed by the RecyclerView.
- * @see <a href="http://www.littlerobots.nl/blog/Handle-Android-RecyclerView-Clicks/">littlerobots.nl</a>
- */
-public class RecyclerViewClickSupport {
- private final RecyclerView mRecyclerView;
- private OnItemClickListener mOnItemClickListener;
- private OnItemLongClickListener mOnItemLongClickListener;
- private View.OnClickListener mOnClickListener = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- if (mOnItemClickListener != null) {
- RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
- mOnItemClickListener.onItemClicked(mRecyclerView, holder.getAdapterPosition(), v);
- }
- }
- };
- private View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() {
- @Override
- public boolean onLongClick(View v) {
- if (mOnItemLongClickListener != null) {
- RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
- return mOnItemLongClickListener.onItemLongClicked(mRecyclerView, holder.getAdapterPosition(), v);
- }
- return false;
- }
- };
- private RecyclerView.OnChildAttachStateChangeListener mAttachListener
- = new RecyclerView.OnChildAttachStateChangeListener() {
- @Override
- public void onChildViewAttachedToWindow(View view) {
- if (mOnItemClickListener != null) {
- view.setOnClickListener(mOnClickListener);
- }
- if (mOnItemLongClickListener != null) {
- view.setOnLongClickListener(mOnLongClickListener);
- }
- }
-
- @Override
- public void onChildViewDetachedFromWindow(View view) {
-
- }
- };
-
- private RecyclerViewClickSupport(RecyclerView recyclerView) {
- mRecyclerView = recyclerView;
- mRecyclerView.setTag(R.id.recycler_view_click_support, this);
- mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener);
- }
-
- public static RecyclerViewClickSupport addTo(RecyclerView view) {
- RecyclerViewClickSupport support = (RecyclerViewClickSupport) view.getTag(R.id.recycler_view_click_support);
- if (support == null) {
- support = new RecyclerViewClickSupport(view);
- }
- return support;
- }
-
- public static RecyclerViewClickSupport removeFrom(RecyclerView view) {
- RecyclerViewClickSupport support = (RecyclerViewClickSupport) view.getTag(R.id.recycler_view_click_support);
- if (support != null) {
- support.detach(view);
- }
- return support;
- }
-
- public RecyclerViewClickSupport setOnItemClickListener(OnItemClickListener listener) {
- mOnItemClickListener = listener;
- return this;
- }
-
- public RecyclerViewClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) {
- mOnItemLongClickListener = listener;
- return this;
- }
-
- private void detach(RecyclerView view) {
- view.removeOnChildAttachStateChangeListener(mAttachListener);
- view.setTag(R.id.recycler_view_click_support, null);
- }
-
- public interface OnItemClickListener {
-
- void onItemClicked(RecyclerView recyclerView, int position, View v);
- }
-
- public interface OnItemLongClickListener {
-
- boolean onItemLongClicked(RecyclerView recyclerView, int position, View v);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/ResizablePathDrawable.java b/mobile/android/base/java/org/mozilla/gecko/widget/ResizablePathDrawable.java
deleted file mode 100644
index ff0709cb7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/ResizablePathDrawable.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import android.content.res.ColorStateList;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.drawable.ShapeDrawable;
-import android.graphics.drawable.shapes.Shape;
-
-public class ResizablePathDrawable extends ShapeDrawable {
- // An attribute mirroring the super class' value. getAlpha() is only
- // available in API 19+ so to use that alpha value, we have to mirror it.
- private int alpha = 255;
-
- private final ColorStateList colorStateList;
- private int currentColor;
-
- public ResizablePathDrawable(NonScaledPathShape shape, int color) {
- this(shape, ColorStateList.valueOf(color));
- }
-
- public ResizablePathDrawable(NonScaledPathShape shape, ColorStateList colorStateList) {
- super(shape);
- this.colorStateList = colorStateList;
- updateColor(getState());
- }
-
- private boolean updateColor(int[] stateSet) {
- int newColor = colorStateList.getColorForState(stateSet, Color.WHITE);
- if (newColor != currentColor) {
- currentColor = newColor;
- alpha = Color.alpha(currentColor);
- invalidateSelf();
- return true;
- }
-
- return false;
- }
-
- public Path getPath() {
- final NonScaledPathShape shape = (NonScaledPathShape) getShape();
- return shape.path;
- }
-
- @Override
- public boolean isStateful() {
- return true;
- }
-
- @Override
- protected void onDraw(Shape shape, Canvas canvas, Paint paint) {
- paint.setColor(currentColor);
- // setAlpha overrides the alpha value in set color. Since we just set the color,
- // the alpha value is reset: override the alpha value with the old value. We don't
- // set alpha if the color is transparent.
- //
- // Note: We *should* be able to call Shape.setAlpha, rather than Paint.setAlpha, but
- // then the opacity doesn't change - dunno why but probably not worth the time.
- if (currentColor != Color.TRANSPARENT) {
- paint.setAlpha(alpha);
- }
-
- super.onDraw(shape, canvas, paint);
- }
-
- @Override
- public void setAlpha(final int alpha) {
- super.setAlpha(alpha);
- this.alpha = alpha;
- }
-
- @Override
- protected boolean onStateChange(int[] stateSet) {
- return updateColor(stateSet);
- }
-
- /**
- * Path-based shape implementation that re-creates the path
- * when it gets resized as opposed to PathShape's scaling
- * behaviour.
- */
- public static class NonScaledPathShape extends Shape {
- private Path path;
-
- public NonScaledPathShape() {
- path = new Path();
- }
-
- @Override
- public void draw(Canvas canvas, Paint paint) {
- // No point in drawing the shape if it's not
- // going to be visible.
- if (paint.getColor() == Color.TRANSPARENT) {
- return;
- }
-
- canvas.drawPath(path, paint);
- }
-
- protected Path getPath() {
- return path;
- }
-
- @Override
- public NonScaledPathShape clone() throws CloneNotSupportedException {
- final NonScaledPathShape clonedShape = (NonScaledPathShape) super.clone();
- clonedShape.path = new Path(path);
- return clonedShape;
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/RoundedCornerLayout.java b/mobile/android/base/java/org/mozilla/gecko/widget/RoundedCornerLayout.java
deleted file mode 100644
index a102981ee..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/RoundedCornerLayout.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Path;
-import android.graphics.RectF;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
-import android.util.TypedValue;
-import android.widget.LinearLayout;
-
-public class RoundedCornerLayout extends LinearLayout {
- private static final String LOGTAG = "Gecko" + RoundedCornerLayout.class.getSimpleName();
- private float cornerRadius;
-
- private Path path;
- boolean cannotClipPath;
-
- public RoundedCornerLayout(Context context) {
- super(context);
- init(context);
- }
-
- public RoundedCornerLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- init(context);
- }
-
- public RoundedCornerLayout(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- init(context);
- }
-
- private void init(Context context) {
- // Bug 1201081 - clipPath with hardware acceleration crashes on r11-18.
- cannotClipPath = !AppConstants.Versions.feature19Plus;
-
- final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
-
- cornerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,
- getResources().getDimensionPixelSize(R.dimen.doorhanger_rounded_corner_radius), metrics);
-
- setWillNotDraw(false);
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- if (cannotClipPath) {
- return;
- }
-
- final RectF r = new RectF(0, 0, w, h);
- path = new Path();
- path.addRoundRect(r, cornerRadius, cornerRadius, Path.Direction.CW);
- path.close();
- }
-
- @Override
- public void draw(Canvas canvas) {
- if (cannotClipPath) {
- super.draw(canvas);
- return;
- }
-
- canvas.save();
- canvas.clipPath(path);
- super.draw(canvas);
- canvas.restore();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/SiteLogins.java b/mobile/android/base/java/org/mozilla/gecko/widget/SiteLogins.java
deleted file mode 100644
index 4d4a92275..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/SiteLogins.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package org.mozilla.gecko.widget;
-
-import org.json.JSONArray;
-import org.json.JSONObject;
-
-public class SiteLogins {
- private final JSONArray logins;
-
- public SiteLogins(JSONArray logins) {
- this.logins = logins;
- }
-
- public JSONArray getLogins() {
- return logins;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/SquaredImageView.java b/mobile/android/base/java/org/mozilla/gecko/widget/SquaredImageView.java
deleted file mode 100644
index 0b77e9d1c..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/SquaredImageView.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package org.mozilla.gecko.widget;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.widget.ImageView;
-
-final class SquaredImageView extends ImageView {
- public SquaredImageView(Context context) {
- super(context);
- }
-
- public SquaredImageView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth());
- }
-} \ No newline at end of file
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/SquaredRelativeLayout.java b/mobile/android/base/java/org/mozilla/gecko/widget/SquaredRelativeLayout.java
deleted file mode 100644
index c0dca0bec..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/SquaredRelativeLayout.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.widget.RelativeLayout;
-
-public class SquaredRelativeLayout extends RelativeLayout {
- public SquaredRelativeLayout(Context context) {
- super(context);
- }
-
- public SquaredRelativeLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public SquaredRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- int squareMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY);
-
- super.onMeasure(squareMeasureSpec, squareMeasureSpec);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/SwipeDismissListViewTouchListener.java b/mobile/android/base/java/org/mozilla/gecko/widget/SwipeDismissListViewTouchListener.java
deleted file mode 100644
index 8267fe8a3..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/SwipeDismissListViewTouchListener.java
+++ /dev/null
@@ -1,356 +0,0 @@
-/*
- * Copyright 2012 Roman Nurik
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.mozilla.gecko.widget;
-
-import android.graphics.Rect;
-import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.view.ViewGroup;
-import android.widget.AbsListView;
-import android.widget.AbsListView.RecyclerListener;
-import android.widget.ListView;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.view.ViewPropertyAnimator;
-
-import org.mozilla.gecko.R;
-
-/**
- * This code is based off of Jake Wharton's NOA port (https://github.com/JakeWharton/SwipeToDismissNOA)
- * of Roman Nurik's SwipeToDismiss library. It has been modified for better support with async
- * adapters.
- *
- * A {@link android.view.View.OnTouchListener} that makes the list items in a {@link ListView}
- * dismissable. {@link ListView} is given special treatment because by default it handles touches
- * for its list items... i.e. it's in charge of drawing the pressed state (the list selector),
- * handling list item clicks, etc.
- *
- * <p>After creating the listener, the caller should also call
- * {@link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}, passing
- * in the scroll listener returned by {@link #makeScrollListener()}. If a scroll listener is
- * already assigned, the caller should still pass scroll changes through to this listener. This will
- * ensure that this {@link SwipeDismissListViewTouchListener} is paused during list view
- * scrolling.</p>
- *
- * <p>Example usage:</p>
- *
- * <pre>
- * SwipeDismissListViewTouchListener touchListener =
- * new SwipeDismissListViewTouchListener(
- * listView,
- * new SwipeDismissListViewTouchListener.OnDismissCallback() {
- * public void onDismiss(ListView listView, int[] reverseSortedPositions) {
- * for (int position : reverseSortedPositions) {
- * adapter.remove(adapter.getItem(position));
- * }
- * adapter.notifyDataSetChanged();
- * }
- * });
- * listView.setOnTouchListener(touchListener);
- * listView.setOnScrollListener(touchListener.makeScrollListener());
- * </pre>
- *
- * <p>For a generalized {@link android.view.View.OnTouchListener} that makes any view dismissable,
- * see {@link SwipeDismissTouchListener}.</p>
- *
- * @see SwipeDismissTouchListener
- */
-public class SwipeDismissListViewTouchListener implements View.OnTouchListener {
- // Cached ViewConfiguration and system-wide constant values
- private final int mSlop;
- private final int mMinFlingVelocity;
- private final int mMaxFlingVelocity;
- private final long mAnimationTime;
-
- // Fixed properties
- private final ListView mListView;
- private final OnDismissCallback mCallback;
- private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero
-
- // Transient properties
- private float mDownX;
- private boolean mSwiping;
- private VelocityTracker mVelocityTracker;
- private int mDownPosition;
- private View mDownView;
- private boolean mPaused;
- private boolean mDismissing;
-
- /**
- * The callback interface used by {@link SwipeDismissListViewTouchListener} to inform its client
- * about a successful dismissal of a list item.
- */
- public interface OnDismissCallback {
- /**
- * Called when the user has indicated they she would like to dismiss one or more list item
- * positions.
- *
- * @param listView The originating {@link ListView}.
- * @param position The position being dismissed.
- */
- void onDismiss(ListView listView, int position);
- }
-
- /**
- * Constructs a new swipe-to-dismiss touch listener for the given list view.
- *
- * @param listView The list view whose items should be dismissable.
- * @param callback The callback to trigger when the user has indicated that she would like to
- * dismiss one or more list items.
- */
- public SwipeDismissListViewTouchListener(ListView listView, OnDismissCallback callback) {
- ViewConfiguration vc = ViewConfiguration.get(listView.getContext());
- mSlop = vc.getScaledTouchSlop();
- mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
- mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
- mAnimationTime = listView.getContext().getResources().getInteger(
- android.R.integer.config_shortAnimTime);
- mListView = listView;
- mCallback = callback;
- }
-
- /**
- * Enables or disables (pauses or resumes) watching for swipe-to-dismiss gestures.
- *
- * @param enabled Whether or not to watch for gestures.
- */
- public void setEnabled(boolean enabled) {
- mPaused = !enabled;
- }
-
- /**
- * Returns an {@link android.widget.AbsListView.OnScrollListener} to be added to the
- * {@link ListView} using
- * {@link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}.
- * If a scroll listener is already assigned, the caller should still pass scroll changes
- * through to this listener. This will ensure that this
- * {@link SwipeDismissListViewTouchListener} is paused during list view scrolling.</p>
- *
- * @see {@link SwipeDismissListViewTouchListener}
- */
- public AbsListView.OnScrollListener makeScrollListener() {
- return new AbsListView.OnScrollListener() {
- @Override
- public void onScrollStateChanged(AbsListView absListView, int scrollState) {
- setEnabled(scrollState != AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
- }
-
- @Override
- public void onScroll(AbsListView absListView, int i, int i1, int i2) {
- }
- };
- }
-
- /**
- * Returns a {@link android.widget.AbsListView.RecyclerListener} to be added to the
- * {@link ListView} using {@link ListView#setRecyclerListener(RecyclerListener)}.
- */
- public AbsListView.RecyclerListener makeRecyclerListener() {
- return new AbsListView.RecyclerListener() {
- @Override
- public void onMovedToScrapHeap(View view) {
- final Object tag = view.getTag(R.id.original_height);
-
- // To reset the view to the correct height after its animation, the view's height
- // is stored in its tag. Reset the view here.
- if (tag instanceof Integer) {
- view.setAlpha(1f);
- view.setTranslationX(0);
- final ViewGroup.LayoutParams lp = view.getLayoutParams();
- lp.height = (int) tag;
- view.setLayoutParams(lp);
- view.setTag(R.id.original_height, null);
- }
- }
- };
- }
-
- @Override
- public boolean onTouch(View view, MotionEvent motionEvent) {
- if (mViewWidth < 2) {
- mViewWidth = mListView.getWidth();
- }
-
- switch (motionEvent.getActionMasked()) {
- case MotionEvent.ACTION_DOWN: {
- if (mPaused) {
- return false;
- }
-
- if (mDismissing) {
- return true;
- }
-
- // TODO: ensure this is a finger, and set a flag
-
- // Find the child view that was touched (perform a hit test)
- Rect rect = new Rect();
- int childCount = mListView.getChildCount();
- int[] listViewCoords = new int[2];
- mListView.getLocationOnScreen(listViewCoords);
- int x = (int) motionEvent.getRawX() - listViewCoords[0];
- int y = (int) motionEvent.getRawY() - listViewCoords[1];
- View child;
- for (int i = 0; i < childCount; i++) {
- child = mListView.getChildAt(i);
- child.getHitRect(rect);
- if (rect.contains(x, y)) {
- mDownView = child;
- break;
- }
- }
-
- if (mDownView != null) {
- mDownX = motionEvent.getRawX();
- mDownPosition = mListView.getPositionForView(mDownView);
-
- mVelocityTracker = VelocityTracker.obtain();
- mVelocityTracker.addMovement(motionEvent);
- }
- view.onTouchEvent(motionEvent);
- return true;
- }
-
- case MotionEvent.ACTION_UP: {
- if (mVelocityTracker == null) {
- break;
- }
-
- float deltaX = motionEvent.getRawX() - mDownX;
- mVelocityTracker.addMovement(motionEvent);
- mVelocityTracker.computeCurrentVelocity(1000);
- float velocityX = Math.abs(mVelocityTracker.getXVelocity());
- float velocityY = Math.abs(mVelocityTracker.getYVelocity());
- boolean dismiss = false;
- boolean dismissRight = false;
- if (Math.abs(deltaX) > mViewWidth / 2) {
- dismiss = true;
- dismissRight = deltaX > 0;
- } else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity
- && velocityY < velocityX) {
- dismiss = true;
- dismissRight = mVelocityTracker.getXVelocity() > 0;
- }
- if (dismiss) {
- // dismiss
- mDismissing = true;
- final View downView = mDownView; // mDownView gets null'd before animation ends
- final int downPosition = mDownPosition;
- mDownView.animate()
- .translationX(dismissRight ? mViewWidth : -mViewWidth)
- .alpha(0)
- .setDuration(mAnimationTime)
- .setListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- performDismiss(downView, downPosition);
- }
- });
- } else {
- // cancel
- mDownView.animate()
- .translationX(0)
- .alpha(1)
- .setDuration(mAnimationTime)
- .setListener(null);
- }
-
- if (mVelocityTracker != null) {
- mVelocityTracker.recycle();
- mVelocityTracker = null;
- }
-
- mDownX = 0;
- mDownView = null;
- mDownPosition = ListView.INVALID_POSITION;
- mSwiping = false;
- break;
- }
-
- case MotionEvent.ACTION_MOVE: {
- if (mVelocityTracker == null || mPaused) {
- break;
- }
-
- mVelocityTracker.addMovement(motionEvent);
- float deltaX = motionEvent.getRawX() - mDownX;
- if (Math.abs(deltaX) > mSlop) {
- mSwiping = true;
- mListView.requestDisallowInterceptTouchEvent(true);
-
- // Cancel ListView's touch (un-highlighting the item)
- MotionEvent cancelEvent = MotionEvent.obtain(motionEvent);
- cancelEvent.setAction(MotionEvent.ACTION_CANCEL |
- (motionEvent.getActionIndex()
- << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
- mListView.onTouchEvent(cancelEvent);
- cancelEvent.recycle();
- }
-
- if (mSwiping) {
- mDownView.setTranslationX(deltaX);
- mDownView.setAlpha(Math.max(0f, Math.min(1f, 1f - 2f * Math.abs(deltaX) / mViewWidth)));
- return true;
- }
- break;
- }
- }
- return false;
- }
-
- /**
- * Animate the dismissed list item to zero-height and fire the dismiss callback when it finishes.
- *
- * @param dismissView ListView item to dismiss
- * @param dismissPosition Position of dismissed item
- */
- private void performDismiss(final View dismissView, final int dismissPosition) {
- final ViewGroup.LayoutParams lp = dismissView.getLayoutParams();
- final int originalHeight = lp.height;
-
- ValueAnimator animator = ValueAnimator.ofInt(dismissView.getHeight(), 1).setDuration(mAnimationTime);
-
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- // Since the view is still a part of the ListView, we can't reset the animated
- // properties yet; otherwise, the view would briefly reappear. Store the original
- // height in the view's tag to flag it for the recycler. This is racy since the user
- // could scroll the dismissed view off the screen, then back on the screen, before
- // it's removed from the adapter, causing the dismissed view to briefly reappear.
- dismissView.setTag(R.id.original_height, originalHeight);
-
- mCallback.onDismiss(mListView, dismissPosition);
- mDismissing = false;
- }
- });
-
- animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator valueAnimator) {
- lp.height = (Integer) valueAnimator.getAnimatedValue();
- dismissView.setLayoutParams(lp);
- }
- });
-
- animator.start();
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/TabThumbnailWrapper.java b/mobile/android/base/java/org/mozilla/gecko/widget/TabThumbnailWrapper.java
deleted file mode 100644
index 848e2f6ed..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/TabThumbnailWrapper.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package org.mozilla.gecko.widget;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.widget.themed.ThemedRelativeLayout;
-
-
-public class TabThumbnailWrapper extends ThemedRelativeLayout {
- private boolean mRecording;
- private static final int[] STATE_RECORDING = { R.attr.state_recording };
-
- public TabThumbnailWrapper(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-
- public TabThumbnailWrapper(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public int[] onCreateDrawableState(int extraSpace) {
- final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
-
- if (mRecording) {
- mergeDrawableStates(drawableState, STATE_RECORDING);
- }
- return drawableState;
- }
-
- public void setRecording(boolean recording) {
- if (mRecording != recording) {
- mRecording = recording;
- refreshDrawableState();
- }
- }
-
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/ThumbnailView.java b/mobile/android/base/java/org/mozilla/gecko/widget/ThumbnailView.java
deleted file mode 100644
index 5ab00ea7f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/ThumbnailView.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import org.mozilla.gecko.R;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Matrix;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.widget.themed.ThemedImageView;
-
-/* Special version of ImageView for thumbnails. Scales a thumbnail so that it maintains its aspect
- * ratio and so that the images width and height are the same size or greater than the view size
- */
-public class ThumbnailView extends ThemedImageView {
- private static final String LOGTAG = "GeckoThumbnailView";
-
- final private Matrix mMatrix;
- private int mWidthSpec = -1;
- private int mHeightSpec = -1;
- private boolean mLayoutChanged;
- private boolean mScale = false;
-
- public ThumbnailView(Context context, AttributeSet attrs) {
- super(context, attrs);
- mMatrix = new Matrix();
- mLayoutChanged = true;
- }
-
- @Override
- public void onDraw(Canvas canvas) {
- if (!mScale) {
- super.onDraw(canvas);
- return;
- }
-
- Drawable d = getDrawable();
- if (mLayoutChanged) {
- int w1 = d.getIntrinsicWidth();
- int h1 = d.getIntrinsicHeight();
- int w2 = getWidth();
- int h2 = getHeight();
-
- float scale = ((w2 / h2) < (w1 / h1)) ? (float) h2 / h1 : (float) w2 / w1;
- mMatrix.setScale(scale, scale);
- }
-
- int saveCount = canvas.save();
- canvas.concat(mMatrix);
- d.draw(canvas);
- canvas.restoreToCount(saveCount);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- // OnLayout.changed isn't a reliable measure of whether or not the size of this view has changed
- // neither is onSizeChanged called often enough. Instead, we track changes in size ourselves, and
- // only invalidate this matrix if we have a new width/height spec
- if (widthMeasureSpec != mWidthSpec || heightMeasureSpec != mHeightSpec) {
- mWidthSpec = widthMeasureSpec;
- mHeightSpec = heightMeasureSpec;
- mLayoutChanged = true;
- }
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-
- @Override
- public void setImageDrawable(Drawable drawable) {
- if (drawable == null) {
- drawable = ContextCompat.getDrawable(getContext(), R.drawable.tab_panel_tab_background);
- setScaleType(ScaleType.FIT_XY);
- mScale = false;
- } else {
- mScale = true;
- setScaleType(ScaleType.FIT_CENTER);
- }
-
- super.setImageDrawable(drawable);
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/TouchDelegateWithReset.java b/mobile/android/base/java/org/mozilla/gecko/widget/TouchDelegateWithReset.java
deleted file mode 100644
index 52e0b1fd0..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/TouchDelegateWithReset.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget;
-
-import android.graphics.Rect;
-import android.view.MotionEvent;
-import android.view.TouchDelegate;
-import android.view.View;
-import android.view.ViewConfiguration;
-
-/**
- * This is a copy of TouchDelegate from
- * https://github.com/android/platform_frameworks_base/blob/4b1a8f46d6ec55796bf77fd8921a5a242a219278/core/java/android/view/TouchDelegate.java
- * with a fix to reset mDelegateTargeted on each new gesture - the sole substantive change is a new
- * else leg in the ACTION_DOWN case of onTouchEvent marked by "START|END BUG FIX" comments.
- */
-
-/**
- * Helper class to handle situations where you want a view to have a larger touch area than its
- * actual view bounds. The view whose touch area is changed is called the delegate view. This
- * class should be used by an ancestor of the delegate. To use a TouchDelegate, first create an
- * instance that specifies the bounds that should be mapped to the delegate and the delegate
- * view itself.
- * <p>
- * The ancestor should then forward all of its touch events received in its
- * {@link android.view.View#onTouchEvent(MotionEvent)} to {@link #onTouchEvent(MotionEvent)}.
- * </p>
- */
-public class TouchDelegateWithReset extends TouchDelegate {
-
- /**
- * View that should receive forwarded touch events
- */
- private View mDelegateView;
-
- /**
- * Bounds in local coordinates of the containing view that should be mapped to the delegate
- * view. This rect is used for initial hit testing.
- */
- private Rect mBounds;
-
- /**
- * mBounds inflated to include some slop. This rect is to track whether the motion events
- * should be considered to be be within the delegate view.
- */
- private Rect mSlopBounds;
-
- /**
- * True if the delegate had been targeted on a down event (intersected mBounds).
- */
- private boolean mDelegateTargeted;
-
- private int mSlop;
-
- /**
- * Constructor
- *
- * @param bounds Bounds in local coordinates of the containing view that should be mapped to
- * the delegate view
- * @param delegateView The view that should receive motion events
- */
- public TouchDelegateWithReset(Rect bounds, View delegateView) {
- super(bounds, delegateView);
-
- mBounds = bounds;
-
- mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
- mSlopBounds = new Rect(bounds);
- mSlopBounds.inset(-mSlop, -mSlop);
- mDelegateView = delegateView;
- }
-
- /**
- * Will forward touch events to the delegate view if the event is within the bounds
- * specified in the constructor.
- *
- * @param event The touch event to forward
- * @return True if the event was forwarded to the delegate, false otherwise.
- */
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- int x = (int)event.getX();
- int y = (int)event.getY();
- boolean sendToDelegate = false;
- boolean hit = true;
- boolean handled = false;
-
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- Rect bounds = mBounds;
-
- if (bounds.contains(x, y)) {
- mDelegateTargeted = true;
- sendToDelegate = true;
- } /* START BUG FIX */
- else {
- mDelegateTargeted = false;
- }
- /* END BUG FIX */
- break;
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_MOVE:
- sendToDelegate = mDelegateTargeted;
- if (sendToDelegate) {
- Rect slopBounds = mSlopBounds;
- if (!slopBounds.contains(x, y)) {
- hit = false;
- }
- }
- break;
- case MotionEvent.ACTION_CANCEL:
- sendToDelegate = mDelegateTargeted;
- mDelegateTargeted = false;
- break;
- }
- if (sendToDelegate) {
- final View delegateView = mDelegateView;
-
- if (hit) {
- // Offset event coordinates to be inside the target view
- event.setLocation(delegateView.getWidth() / 2, delegateView.getHeight() / 2);
- } else {
- // Offset event coordinates to be outside the target view (in case it does
- // something like tracking pressed state)
- int slop = mSlop;
- event.setLocation(-(slop * 2), -(slop * 2));
- }
- handled = delegateView.dispatchTouchEvent(event);
- }
- return handled;
- }
-} \ No newline at end of file
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/TwoWayView.java b/mobile/android/base/java/org/mozilla/gecko/widget/TwoWayView.java
deleted file mode 100644
index b5ad36ab7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/TwoWayView.java
+++ /dev/null
@@ -1,7191 +0,0 @@
-/*
- * Copyright (C) 2013 Lucas Rocha
- *
- * This code is based on bits and pieces of Android's AbsListView,
- * Listview, and StaggeredGridView.
- *
- * 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 org.mozilla.gecko.widget;
-
-import org.mozilla.gecko.R;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.database.DataSetObserver;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.TransitionDrawable;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.os.SystemClock;
-import android.support.v4.util.LongSparseArray;
-import android.support.v4.util.SparseArrayCompat;
-import android.support.v4.view.AccessibilityDelegateCompat;
-import android.support.v4.view.KeyEventCompat;
-import android.support.v4.view.MotionEventCompat;
-import android.support.v4.view.VelocityTrackerCompat;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
-import android.support.v4.widget.EdgeEffectCompat;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.util.SparseBooleanArray;
-import android.view.ContextMenu.ContextMenuInfo;
-import android.view.FocusFinder;
-import android.view.HapticFeedbackConstants;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.SoundEffectConstants;
-import android.view.VelocityTracker;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-import android.view.ViewTreeObserver;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.widget.AdapterView;
-import android.widget.Checkable;
-import android.widget.ListAdapter;
-import android.widget.Scroller;
-
-import static android.os.Build.VERSION_CODES.HONEYCOMB;
-
-/*
- * Implementation Notes:
- *
- * Some terminology:
- *
- * index - index of the items that are currently visible
- * position - index of the items in the cursor
- *
- * Given the bi-directional nature of this view, the source code
- * usually names variables with 'start' to mean 'top' or 'left'; and
- * 'end' to mean 'bottom' or 'right', depending on the current
- * orientation of the widget.
- */
-
-/**
- * A view that shows items in a vertical or horizontal scrolling list.
- * The items come from the {@link ListAdapter} associated with this view.
- */
-public class TwoWayView extends AdapterView<ListAdapter> implements
- ViewTreeObserver.OnTouchModeChangeListener {
- private static final String LOGTAG = "TwoWayView";
-
- private static final int NO_POSITION = -1;
- private static final int INVALID_POINTER = -1;
-
- public static final int[] STATE_NOTHING = new int[] { 0 };
-
- private static final int TOUCH_MODE_REST = -1;
- private static final int TOUCH_MODE_DOWN = 0;
- private static final int TOUCH_MODE_TAP = 1;
- private static final int TOUCH_MODE_DONE_WAITING = 2;
- private static final int TOUCH_MODE_DRAGGING = 3;
- private static final int TOUCH_MODE_FLINGING = 4;
- private static final int TOUCH_MODE_OVERSCROLL = 5;
-
- private static final int TOUCH_MODE_UNKNOWN = -1;
- private static final int TOUCH_MODE_ON = 0;
- private static final int TOUCH_MODE_OFF = 1;
-
- private static final int LAYOUT_NORMAL = 0;
- private static final int LAYOUT_FORCE_TOP = 1;
- private static final int LAYOUT_SET_SELECTION = 2;
- private static final int LAYOUT_FORCE_BOTTOM = 3;
- private static final int LAYOUT_SPECIFIC = 4;
- private static final int LAYOUT_SYNC = 5;
- private static final int LAYOUT_MOVE_SELECTION = 6;
-
- private static final int SYNC_SELECTED_POSITION = 0;
- private static final int SYNC_FIRST_POSITION = 1;
-
- private static final int SYNC_MAX_DURATION_MILLIS = 100;
-
- private static final int CHECK_POSITION_SEARCH_DISTANCE = 20;
-
- private static final float MAX_SCROLL_FACTOR = 0.33f;
-
- private static final int MIN_SCROLL_PREVIEW_PIXELS = 10;
-
- public static enum ChoiceMode {
- NONE,
- SINGLE,
- MULTIPLE
- }
-
- public static enum Orientation {
- HORIZONTAL,
- VERTICAL
- }
-
- private final Context mContext;
-
- private ListAdapter mAdapter;
-
- private boolean mIsVertical;
-
- private int mItemMargin;
-
- private boolean mInLayout;
- private boolean mBlockLayoutRequests;
-
- private boolean mIsAttached;
-
- private final RecycleBin mRecycler;
- private AdapterDataSetObserver mDataSetObserver;
-
- private boolean mItemsCanFocus;
-
- final boolean[] mIsScrap = new boolean[1];
-
- private boolean mDataChanged;
- private int mItemCount;
- private int mOldItemCount;
- private boolean mHasStableIds;
- private boolean mAreAllItemsSelectable;
-
- private int mFirstPosition;
- private int mSpecificStart;
-
- private SavedState mPendingSync;
-
- private PositionScroller mPositionScroller;
- private Runnable mPositionScrollAfterLayout;
-
- private final int mTouchSlop;
- private final int mMaximumVelocity;
- private final int mFlingVelocity;
- private float mLastTouchPos;
- private float mTouchRemainderPos;
- private int mActivePointerId;
-
- private final Rect mTempRect;
-
- private final ArrowScrollFocusResult mArrowScrollFocusResult;
-
- private Rect mTouchFrame;
- private int mMotionPosition;
- private CheckForTap mPendingCheckForTap;
- private CheckForLongPress mPendingCheckForLongPress;
- private CheckForKeyLongPress mPendingCheckForKeyLongPress;
- private PerformClick mPerformClick;
- private Runnable mTouchModeReset;
- private int mResurrectToPosition;
-
- private boolean mIsChildViewEnabled;
-
- private boolean mDrawSelectorOnTop;
- private Drawable mSelector;
- private int mSelectorPosition;
- private final Rect mSelectorRect;
-
- private int mOverScroll;
- private final int mOverscrollDistance;
-
- private boolean mDesiredFocusableState;
- private boolean mDesiredFocusableInTouchModeState;
-
- private SelectionNotifier mSelectionNotifier;
-
- private boolean mNeedSync;
- private int mSyncMode;
- private int mSyncPosition;
- private long mSyncRowId;
- private long mSyncSize;
- private int mSelectedStart;
-
- private int mNextSelectedPosition;
- private long mNextSelectedRowId;
- private int mSelectedPosition;
- private long mSelectedRowId;
- private int mOldSelectedPosition;
- private long mOldSelectedRowId;
-
- private ChoiceMode mChoiceMode;
- private int mCheckedItemCount;
- private SparseBooleanArray mCheckStates;
- LongSparseArray<Integer> mCheckedIdStates;
-
- private ContextMenuInfo mContextMenuInfo;
-
- private int mLayoutMode;
- private int mTouchMode;
- private int mLastTouchMode;
- private VelocityTracker mVelocityTracker;
- private final Scroller mScroller;
-
- private EdgeEffectCompat mStartEdge;
- private EdgeEffectCompat mEndEdge;
-
- private OnScrollListener mOnScrollListener;
- private int mLastScrollState;
-
- private View mEmptyView;
-
- private ListItemAccessibilityDelegate mAccessibilityDelegate;
-
- private int mLastAccessibilityScrollEventFromIndex;
- private int mLastAccessibilityScrollEventToIndex;
-
- public interface OnScrollListener {
-
- /**
- * The view is not scrolling. Note navigating the list using the trackball counts as
- * being in the idle state since these transitions are not animated.
- */
- public static int SCROLL_STATE_IDLE = 0;
-
- /**
- * The user is scrolling using touch, and their finger is still on the screen
- */
- public static int SCROLL_STATE_TOUCH_SCROLL = 1;
-
- /**
- * The user had previously been scrolling using touch and had performed a fling. The
- * animation is now coasting to a stop
- */
- public static int SCROLL_STATE_FLING = 2;
-
- /**
- * Callback method to be invoked while the list view or grid view is being scrolled. If the
- * view is being scrolled, this method will be called before the next frame of the scroll is
- * rendered. In particular, it will be called before any calls to
- * {@link android.widget.Adapter#getView(int, View, ViewGroup)}.
- *
- * @param view The view whose scroll state is being reported
- *
- * @param scrollState The current scroll state. One of {@link #SCROLL_STATE_IDLE},
- * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}.
- */
- public void onScrollStateChanged(TwoWayView view, int scrollState);
-
- /**
- * Callback method to be invoked when the list or grid has been scrolled. This will be
- * called after the scroll has completed
- * @param view The view whose scroll state is being reported
- * @param firstVisibleItem the index of the first visible cell (ignore if
- * visibleItemCount == 0)
- * @param visibleItemCount the number of visible cells
- * @param totalItemCount the number of items in the list adaptor
- */
- public void onScroll(TwoWayView view, int firstVisibleItem, int visibleItemCount,
- int totalItemCount);
- }
-
- /**
- * A RecyclerListener is used to receive a notification whenever a View is placed
- * inside the RecycleBin's scrap heap. This listener is used to free resources
- * associated to Views placed in the RecycleBin.
- *
- * @see TwoWayView.RecycleBin
- * @see TwoWayView#setRecyclerListener(TwoWayView.RecyclerListener)
- */
- public static interface RecyclerListener {
- /**
- * Indicates that the specified View was moved into the recycler's scrap heap.
- * The view is not displayed on screen any more and any expensive resource
- * associated with the view should be discarded.
- *
- * @param view
- */
- void onMovedToScrapHeap(View view);
- }
-
- public TwoWayView(Context context) {
- this(context, null);
- }
-
- public TwoWayView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public TwoWayView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
-
- mContext = context;
-
- mLayoutMode = LAYOUT_NORMAL;
- mTouchMode = TOUCH_MODE_REST;
- mLastTouchMode = TOUCH_MODE_UNKNOWN;
-
- mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE;
-
- final ViewConfiguration vc = ViewConfiguration.get(context);
- mTouchSlop = vc.getScaledTouchSlop();
- mMaximumVelocity = vc.getScaledMaximumFlingVelocity();
- mFlingVelocity = vc.getScaledMinimumFlingVelocity();
- mOverscrollDistance = getScaledOverscrollDistance(vc);
-
- mScroller = new Scroller(context);
-
- mIsVertical = true;
-
- mTempRect = new Rect();
-
- mArrowScrollFocusResult = new ArrowScrollFocusResult();
-
- mSelectorPosition = INVALID_POSITION;
-
- mSelectorRect = new Rect();
-
- mResurrectToPosition = INVALID_POSITION;
-
- mNextSelectedPosition = INVALID_POSITION;
- mNextSelectedRowId = INVALID_ROW_ID;
- mSelectedPosition = INVALID_POSITION;
- mSelectedRowId = INVALID_ROW_ID;
- mOldSelectedPosition = INVALID_POSITION;
- mOldSelectedRowId = INVALID_ROW_ID;
-
- mChoiceMode = ChoiceMode.NONE;
-
- mRecycler = new RecycleBin();
-
- mAreAllItemsSelectable = true;
-
- setClickable(true);
- setFocusableInTouchMode(true);
- setWillNotDraw(false);
- setAlwaysDrawnWithCacheEnabled(false);
- setWillNotDraw(false);
- setClipToPadding(false);
-
- ViewCompat.setOverScrollMode(this, ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS);
-
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TwoWayView, defStyle, 0);
-
- mDrawSelectorOnTop = a.getBoolean(
- R.styleable.TwoWayView_android_drawSelectorOnTop, false);
-
- Drawable d = a.getDrawable(R.styleable.TwoWayView_android_listSelector);
- if (d != null) {
- setSelector(d);
- }
-
- int orientation = a.getInt(R.styleable.TwoWayView_android_orientation, -1);
- if (orientation >= 0) {
- setOrientation(Orientation.values()[orientation]);
- }
-
- int choiceMode = a.getInt(R.styleable.TwoWayView_android_choiceMode, -1);
- if (choiceMode >= 0) {
- setChoiceMode(ChoiceMode.values()[choiceMode]);
- }
-
- a.recycle();
- }
-
- public void setOrientation(Orientation orientation) {
- final boolean isVertical = (orientation == Orientation.VERTICAL);
- if (mIsVertical == isVertical) {
- return;
- }
-
- mIsVertical = isVertical;
-
- resetState();
- mRecycler.clear();
-
- requestLayout();
- }
-
- public Orientation getOrientation() {
- return (mIsVertical ? Orientation.VERTICAL : Orientation.HORIZONTAL);
- }
-
- public void setItemMargin(int itemMargin) {
- if (mItemMargin == itemMargin) {
- return;
- }
-
- mItemMargin = itemMargin;
- requestLayout();
- }
-
- @SuppressWarnings("unused")
- public int getItemMargin() {
- return mItemMargin;
- }
-
- /**
- * Indicates that the views created by the ListAdapter can contain focusable
- * items.
- *
- * @param itemsCanFocus true if items can get focus, false otherwise
- */
- @SuppressWarnings("unused")
- public void setItemsCanFocus(boolean itemsCanFocus) {
- mItemsCanFocus = itemsCanFocus;
- if (!itemsCanFocus) {
- setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
- }
- }
-
- /**
- * @return Whether the views created by the ListAdapter can contain focusable
- * items.
- */
- @SuppressWarnings("unused")
- public boolean getItemsCanFocus() {
- return mItemsCanFocus;
- }
-
- /**
- * Set the listener that will receive notifications every time the list scrolls.
- *
- * @param l the scroll listener
- */
- public void setOnScrollListener(OnScrollListener l) {
- mOnScrollListener = l;
- invokeOnItemScrollListener();
- }
-
- /**
- * Sets the recycler listener to be notified whenever a View is set aside in
- * the recycler for later reuse. This listener can be used to free resources
- * associated to the View.
- *
- * @param l The recycler listener to be notified of views set aside
- * in the recycler.
- *
- * @see TwoWayView.RecycleBin
- * @see TwoWayView.RecyclerListener
- */
- public void setRecyclerListener(RecyclerListener l) {
- mRecycler.mRecyclerListener = l;
- }
-
- /**
- * Controls whether the selection highlight drawable should be drawn on top of the item or
- * behind it.
- *
- * @param drawSelectorOnTop If true, the selector will be drawn on the item it is highlighting.
- * The default is false.
- *
- * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop
- */
- @SuppressWarnings("unused")
- public void setDrawSelectorOnTop(boolean drawSelectorOnTop) {
- mDrawSelectorOnTop = drawSelectorOnTop;
- }
-
- /**
- * Set a Drawable that should be used to highlight the currently selected item.
- *
- * @param resID A Drawable resource to use as the selection highlight.
- *
- * @attr ref android.R.styleable#AbsListView_listSelector
- */
- @SuppressWarnings("unused")
- public void setSelector(int resID) {
- setSelector(getResources().getDrawable(resID));
- }
-
- /**
- * Set a Drawable that should be used to highlight the currently selected item.
- *
- * @param selector A Drawable to use as the selection highlight.
- *
- * @attr ref android.R.styleable#AbsListView_listSelector
- */
- public void setSelector(Drawable selector) {
- if (mSelector != null) {
- mSelector.setCallback(null);
- unscheduleDrawable(mSelector);
- }
-
- mSelector = selector;
- Rect padding = new Rect();
- selector.getPadding(padding);
-
- selector.setCallback(this);
- updateSelectorState();
- }
-
- /**
- * Returns the selector {@link android.graphics.drawable.Drawable} that is used to draw the
- * selection in the list.
- *
- * @return the drawable used to display the selector
- */
- @SuppressWarnings("unused")
- public Drawable getSelector() {
- return mSelector;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int getSelectedItemPosition() {
- return mNextSelectedPosition;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public long getSelectedItemId() {
- return mNextSelectedRowId;
- }
-
- /**
- * Returns the number of items currently selected. This will only be valid
- * if the choice mode is not {@link ChoiceMode#NONE} (default).
- *
- * <p>To determine the specific items that are currently selected, use one of
- * the <code>getChecked*</code> methods.
- *
- * @return The number of items currently selected
- *
- * @see #getCheckedItemPosition()
- * @see #getCheckedItemPositions()
- * @see #getCheckedItemIds()
- */
- @SuppressWarnings("unused")
- public int getCheckedItemCount() {
- return mCheckedItemCount;
- }
-
- /**
- * Returns the checked state of the specified position. The result is only
- * valid if the choice mode has been set to {@link ChoiceMode#SINGLE}
- * or {@link ChoiceMode#MULTIPLE}.
- *
- * @param position The item whose checked state to return
- * @return The item's checked state or <code>false</code> if choice mode
- * is invalid
- *
- * @see #setChoiceMode(ChoiceMode)
- */
- public boolean isItemChecked(int position) {
- if (mChoiceMode == ChoiceMode.NONE && mCheckStates != null) {
- return mCheckStates.get(position);
- }
-
- return false;
- }
-
- /**
- * Returns the currently checked item. The result is only valid if the choice
- * mode has been set to {@link ChoiceMode#SINGLE}.
- *
- * @return The position of the currently checked item or
- * {@link #INVALID_POSITION} if nothing is selected
- *
- * @see #setChoiceMode(ChoiceMode)
- */
- public int getCheckedItemPosition() {
- if (mChoiceMode == ChoiceMode.SINGLE && mCheckStates != null && mCheckStates.size() == 1) {
- return mCheckStates.keyAt(0);
- }
-
- return INVALID_POSITION;
- }
-
- /**
- * Returns the set of checked items in the list. The result is only valid if
- * the choice mode has not been set to {@link ChoiceMode#NONE}.
- *
- * @return A SparseBooleanArray which will return true for each call to
- * get(int position) where position is a position in the list,
- * or <code>null</code> if the choice mode is set to
- * {@link ChoiceMode#NONE}.
- */
- public SparseBooleanArray getCheckedItemPositions() {
- if (mChoiceMode != ChoiceMode.NONE) {
- return mCheckStates;
- }
-
- return null;
- }
-
- /**
- * Returns the set of checked items ids. The result is only valid if the
- * choice mode has not been set to {@link ChoiceMode#NONE} and the adapter
- * has stable IDs. ({@link ListAdapter#hasStableIds()} == {@code true})
- *
- * @return A new array which contains the id of each checked item in the
- * list.
- */
- public long[] getCheckedItemIds() {
- if (mChoiceMode == ChoiceMode.NONE || mCheckedIdStates == null || mAdapter == null) {
- return new long[0];
- }
-
- final LongSparseArray<Integer> idStates = mCheckedIdStates;
- final int count = idStates.size();
- final long[] ids = new long[count];
-
- for (int i = 0; i < count; i++) {
- ids[i] = idStates.keyAt(i);
- }
-
- return ids;
- }
-
- /**
- * Sets the checked state of the specified position. The is only valid if
- * the choice mode has been set to {@link ChoiceMode#SINGLE} or
- * {@link ChoiceMode#MULTIPLE}.
- *
- * @param position The item whose checked state is to be checked
- * @param value The new checked state for the item
- */
- @SuppressWarnings("unused")
- public void setItemChecked(int position, boolean value) {
- if (mChoiceMode == ChoiceMode.NONE) {
- return;
- }
-
- if (mChoiceMode == ChoiceMode.MULTIPLE) {
- boolean oldValue = mCheckStates.get(position);
- mCheckStates.put(position, value);
-
- if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
- if (value) {
- mCheckedIdStates.put(mAdapter.getItemId(position), position);
- } else {
- mCheckedIdStates.delete(mAdapter.getItemId(position));
- }
- }
-
- if (oldValue != value) {
- if (value) {
- mCheckedItemCount++;
- } else {
- mCheckedItemCount--;
- }
- }
- } else {
- boolean updateIds = mCheckedIdStates != null && mAdapter.hasStableIds();
-
- // Clear all values if we're checking something, or unchecking the currently
- // selected item
- if (value || isItemChecked(position)) {
- mCheckStates.clear();
-
- if (updateIds) {
- mCheckedIdStates.clear();
- }
- }
-
- // This may end up selecting the value we just cleared but this way
- // we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on
- if (value) {
- mCheckStates.put(position, true);
-
- if (updateIds) {
- mCheckedIdStates.put(mAdapter.getItemId(position), position);
- }
-
- mCheckedItemCount = 1;
- } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
- mCheckedItemCount = 0;
- }
- }
-
- // Do not generate a data change while we are in the layout phase
- if (!mInLayout && !mBlockLayoutRequests) {
- mDataChanged = true;
- rememberSyncState();
- requestLayout();
- }
- }
-
- /**
- * Clear any choices previously set
- */
- @SuppressWarnings("unused")
- public void clearChoices() {
- if (mCheckStates != null) {
- mCheckStates.clear();
- }
-
- if (mCheckedIdStates != null) {
- mCheckedIdStates.clear();
- }
-
- mCheckedItemCount = 0;
- }
-
- /**
- * @see #setChoiceMode(ChoiceMode)
- *
- * @return The current choice mode
- */
- @SuppressWarnings("unused")
- public ChoiceMode getChoiceMode() {
- return mChoiceMode;
- }
-
- /**
- * Defines the choice behavior for the List. By default, Lists do not have any choice behavior
- * ({@link ChoiceMode#NONE}). By setting the choiceMode to {@link ChoiceMode#SINGLE}, the
- * List allows up to one item to be in a chosen state. By setting the choiceMode to
- * {@link ChoiceMode#MULTIPLE}, the list allows any number of items to be chosen.
- *
- * @param choiceMode One of {@link ChoiceMode#NONE}, {@link ChoiceMode#SINGLE}, or
- * {@link ChoiceMode#MULTIPLE}
- */
- public void setChoiceMode(ChoiceMode choiceMode) {
- mChoiceMode = choiceMode;
-
- if (mChoiceMode != ChoiceMode.NONE) {
- if (mCheckStates == null) {
- mCheckStates = new SparseBooleanArray();
- }
-
- if (mCheckedIdStates == null && mAdapter != null && mAdapter.hasStableIds()) {
- mCheckedIdStates = new LongSparseArray<Integer>();
- }
- }
- }
-
- @Override
- public ListAdapter getAdapter() {
- return mAdapter;
- }
-
- @Override
- public void setAdapter(ListAdapter adapter) {
- if (mAdapter != null && mDataSetObserver != null) {
- mAdapter.unregisterDataSetObserver(mDataSetObserver);
- }
-
- resetState();
- mRecycler.clear();
-
- mAdapter = adapter;
- mDataChanged = true;
-
- mOldSelectedPosition = INVALID_POSITION;
- mOldSelectedRowId = INVALID_ROW_ID;
-
- if (mCheckStates != null) {
- mCheckStates.clear();
- }
-
- if (mCheckedIdStates != null) {
- mCheckedIdStates.clear();
- }
-
- if (mAdapter != null) {
- mOldItemCount = mItemCount;
- mItemCount = adapter.getCount();
-
- mDataSetObserver = new AdapterDataSetObserver();
- mAdapter.registerDataSetObserver(mDataSetObserver);
-
- mRecycler.setViewTypeCount(adapter.getViewTypeCount());
-
- mHasStableIds = adapter.hasStableIds();
- mAreAllItemsSelectable = adapter.areAllItemsEnabled();
-
- if (mChoiceMode != ChoiceMode.NONE && mHasStableIds && mCheckedIdStates == null) {
- mCheckedIdStates = new LongSparseArray<Integer>();
- }
-
- final int position = lookForSelectablePosition(0);
- setSelectedPositionInt(position);
- setNextSelectedPositionInt(position);
-
- if (mItemCount == 0) {
- checkSelectionChanged();
- }
- } else {
- mItemCount = 0;
- mHasStableIds = false;
- mAreAllItemsSelectable = true;
-
- checkSelectionChanged();
- }
-
- checkFocus();
- requestLayout();
- }
-
- @Override
- public int getFirstVisiblePosition() {
- return mFirstPosition;
- }
-
- @Override
- public int getLastVisiblePosition() {
- return mFirstPosition + getChildCount() - 1;
- }
-
- @Override
- public int getCount() {
- return mItemCount;
- }
-
- @Override
- public int getPositionForView(View view) {
- View child = view;
- try {
- View v;
- while (!(v = (View) child.getParent()).equals(this)) {
- child = v;
- }
- } catch (ClassCastException e) {
- // We made it up to the window without find this list view
- return INVALID_POSITION;
- }
-
- // Search the children for the list item
- final int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- if (getChildAt(i).equals(child)) {
- return mFirstPosition + i;
- }
- }
-
- // Child not found!
- return INVALID_POSITION;
- }
-
- @Override
- public void getFocusedRect(Rect r) {
- View view = getSelectedView();
-
- if (view != null && view.getParent() == this) {
- // The focused rectangle of the selected view offset into the
- // coordinate space of this view.
- view.getFocusedRect(r);
- offsetDescendantRectToMyCoords(view, r);
- } else {
- super.getFocusedRect(r);
- }
- }
-
- @Override
- protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
- super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
-
- if (gainFocus && mSelectedPosition < 0 && !isInTouchMode()) {
- if (!mIsAttached && mAdapter != null) {
- // Data may have changed while we were detached and it's valid
- // to change focus while detached. Refresh so we don't die.
- mDataChanged = true;
- mOldItemCount = mItemCount;
- mItemCount = mAdapter.getCount();
- }
-
- resurrectSelection();
- }
-
- final ListAdapter adapter = mAdapter;
- int closetChildIndex = INVALID_POSITION;
- int closestChildStart = 0;
-
- if (adapter != null && gainFocus && previouslyFocusedRect != null) {
- previouslyFocusedRect.offset(getScrollX(), getScrollY());
-
- // Don't cache the result of getChildCount or mFirstPosition here,
- // it could change in layoutChildren.
- if (adapter.getCount() < getChildCount() + mFirstPosition) {
- mLayoutMode = LAYOUT_NORMAL;
- layoutChildren();
- }
-
- // Figure out which item should be selected based on previously
- // focused rect.
- Rect otherRect = mTempRect;
- int minDistance = Integer.MAX_VALUE;
- final int childCount = getChildCount();
- final int firstPosition = mFirstPosition;
-
- for (int i = 0; i < childCount; i++) {
- // Only consider selectable views
- if (!adapter.isEnabled(firstPosition + i)) {
- continue;
- }
-
- View other = getChildAt(i);
- other.getDrawingRect(otherRect);
- offsetDescendantRectToMyCoords(other, otherRect);
- int distance = getDistance(previouslyFocusedRect, otherRect, direction);
-
- if (distance < minDistance) {
- minDistance = distance;
- closetChildIndex = i;
- closestChildStart = getChildStartEdge(other);
- }
- }
- }
-
- if (closetChildIndex >= 0) {
- setSelectionFromOffset(closetChildIndex + mFirstPosition, closestChildStart);
- } else {
- requestLayout();
- }
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- final ViewTreeObserver treeObserver = getViewTreeObserver();
- treeObserver.addOnTouchModeChangeListener(this);
-
- if (mAdapter != null && mDataSetObserver == null) {
- mDataSetObserver = new AdapterDataSetObserver();
- mAdapter.registerDataSetObserver(mDataSetObserver);
-
- // Data may have changed while we were detached. Refresh.
- mDataChanged = true;
- mOldItemCount = mItemCount;
- mItemCount = mAdapter.getCount();
- }
-
- mIsAttached = true;
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- // Detach any view left in the scrap heap
- mRecycler.clear();
-
- final ViewTreeObserver treeObserver = getViewTreeObserver();
- treeObserver.removeOnTouchModeChangeListener(this);
-
- if (mAdapter != null) {
- mAdapter.unregisterDataSetObserver(mDataSetObserver);
- mDataSetObserver = null;
- }
-
- if (mPerformClick != null) {
- removeCallbacks(mPerformClick);
- }
-
- if (mTouchModeReset != null) {
- removeCallbacks(mTouchModeReset);
- mTouchModeReset.run();
- }
-
- finishSmoothScrolling();
-
- mIsAttached = false;
- }
-
- @Override
- public void onWindowFocusChanged(boolean hasWindowFocus) {
- super.onWindowFocusChanged(hasWindowFocus);
-
- final int touchMode = isInTouchMode() ? TOUCH_MODE_ON : TOUCH_MODE_OFF;
-
- if (!hasWindowFocus) {
- if (!mScroller.isFinished()) {
- finishSmoothScrolling();
- if (mOverScroll != 0) {
- mOverScroll = 0;
- finishEdgeGlows();
- invalidate();
- }
- }
-
- if (touchMode == TOUCH_MODE_OFF) {
- // Remember the last selected element
- mResurrectToPosition = mSelectedPosition;
- }
- } else {
- // If we changed touch mode since the last time we had focus
- if (touchMode != mLastTouchMode && mLastTouchMode != TOUCH_MODE_UNKNOWN) {
- // If we come back in trackball mode, we bring the selection back
- if (touchMode == TOUCH_MODE_OFF) {
- // This will trigger a layout
- resurrectSelection();
-
- // If we come back in touch mode, then we want to hide the selector
- } else {
- hideSelector();
- mLayoutMode = LAYOUT_NORMAL;
- layoutChildren();
- }
- }
- }
-
- mLastTouchMode = touchMode;
- }
-
- @Override
- protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
- boolean needsInvalidate = false;
-
- if (mIsVertical && mOverScroll != scrollY) {
- onScrollChanged(getScrollX(), scrollY, getScrollX(), mOverScroll);
- mOverScroll = scrollY;
- needsInvalidate = true;
- } else if (!mIsVertical && mOverScroll != scrollX) {
- onScrollChanged(scrollX, getScrollY(), mOverScroll, getScrollY());
- mOverScroll = scrollX;
- needsInvalidate = true;
- }
-
- if (needsInvalidate) {
- invalidate();
- awakenScrollbarsInternal();
- }
- }
-
- @TargetApi(9)
- private boolean overScrollByInternal(int deltaX, int deltaY,
- int scrollX, int scrollY,
- int scrollRangeX, int scrollRangeY,
- int maxOverScrollX, int maxOverScrollY,
- boolean isTouchEvent) {
- if (Build.VERSION.SDK_INT < 9) {
- return false;
- }
-
- return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
- scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
- }
-
- @Override
- @TargetApi(9)
- public void setOverScrollMode(int mode) {
- if (Build.VERSION.SDK_INT < 9) {
- return;
- }
-
- if (mode != ViewCompat.OVER_SCROLL_NEVER) {
- if (mStartEdge == null) {
- Context context = getContext();
-
- mStartEdge = new EdgeEffectCompat(context);
- mEndEdge = new EdgeEffectCompat(context);
- }
- } else {
- mStartEdge = null;
- mEndEdge = null;
- }
-
- super.setOverScrollMode(mode);
- }
-
- public int pointToPosition(int x, int y) {
- Rect frame = mTouchFrame;
- if (frame == null) {
- mTouchFrame = new Rect();
- frame = mTouchFrame;
- }
-
- final int count = getChildCount();
- for (int i = count - 1; i >= 0; i--) {
- final View child = getChildAt(i);
-
- if (child.getVisibility() == View.VISIBLE) {
- child.getHitRect(frame);
-
- if (frame.contains(x, y)) {
- return mFirstPosition + i;
- }
- }
- }
- return INVALID_POSITION;
- }
-
- @Override
- protected float getTopFadingEdgeStrength() {
- if (!mIsVertical) {
- return 0f;
- }
-
- final float fadingEdge = super.getTopFadingEdgeStrength();
-
- final int childCount = getChildCount();
- if (childCount == 0) {
- return fadingEdge;
- } else {
- if (mFirstPosition > 0) {
- return 1.0f;
- }
-
- final int top = getChildAt(0).getTop();
- final int paddingTop = getPaddingTop();
-
- final float length = (float) getVerticalFadingEdgeLength();
-
- return (top < paddingTop ? (float) -(top - paddingTop) / length : fadingEdge);
- }
- }
-
- @Override
- protected float getBottomFadingEdgeStrength() {
- if (!mIsVertical) {
- return 0f;
- }
-
- final float fadingEdge = super.getBottomFadingEdgeStrength();
-
- final int childCount = getChildCount();
- if (childCount == 0) {
- return fadingEdge;
- } else {
- if (mFirstPosition + childCount - 1 < mItemCount - 1) {
- return 1.0f;
- }
-
- final int bottom = getChildAt(childCount - 1).getBottom();
- final int paddingBottom = getPaddingBottom();
-
- final int height = getHeight();
- final float length = (float) getVerticalFadingEdgeLength();
-
- return (bottom > height - paddingBottom ?
- (float) (bottom - height + paddingBottom) / length : fadingEdge);
- }
- }
-
- @Override
- protected float getLeftFadingEdgeStrength() {
- if (mIsVertical) {
- return 0f;
- }
-
- final float fadingEdge = super.getLeftFadingEdgeStrength();
-
- final int childCount = getChildCount();
- if (childCount == 0) {
- return fadingEdge;
- } else {
- if (mFirstPosition > 0) {
- return 1.0f;
- }
-
- final int left = getChildAt(0).getLeft();
- final int paddingLeft = getPaddingLeft();
-
- final float length = (float) getHorizontalFadingEdgeLength();
-
- return (left < paddingLeft ? (float) -(left - paddingLeft) / length : fadingEdge);
- }
- }
-
- @Override
- protected float getRightFadingEdgeStrength() {
- if (mIsVertical) {
- return 0f;
- }
-
- final float fadingEdge = super.getRightFadingEdgeStrength();
-
- final int childCount = getChildCount();
- if (childCount == 0) {
- return fadingEdge;
- } else {
- if (mFirstPosition + childCount - 1 < mItemCount - 1) {
- return 1.0f;
- }
-
- final int right = getChildAt(childCount - 1).getRight();
- final int paddingRight = getPaddingRight();
-
- final int width = getWidth();
- final float length = (float) getHorizontalFadingEdgeLength();
-
- return (right > width - paddingRight ?
- (float) (right - width + paddingRight) / length : fadingEdge);
- }
- }
-
- @Override
- protected int computeVerticalScrollExtent() {
- final int count = getChildCount();
- if (count == 0) {
- return 0;
- }
-
- int extent = count * 100;
-
- View child = getChildAt(0);
- final int childTop = child.getTop();
-
- int childHeight = child.getHeight();
- if (childHeight > 0) {
- extent += (childTop * 100) / childHeight;
- }
-
- child = getChildAt(count - 1);
- final int childBottom = child.getBottom();
-
- childHeight = child.getHeight();
- if (childHeight > 0) {
- extent -= ((childBottom - getHeight()) * 100) / childHeight;
- }
-
- return extent;
- }
-
- @Override
- protected int computeHorizontalScrollExtent() {
- final int count = getChildCount();
- if (count == 0) {
- return 0;
- }
-
- int extent = count * 100;
-
- View child = getChildAt(0);
- final int childLeft = child.getLeft();
-
- int childWidth = child.getWidth();
- if (childWidth > 0) {
- extent += (childLeft * 100) / childWidth;
- }
-
- child = getChildAt(count - 1);
- final int childRight = child.getRight();
-
- childWidth = child.getWidth();
- if (childWidth > 0) {
- extent -= ((childRight - getWidth()) * 100) / childWidth;
- }
-
- return extent;
- }
-
- @Override
- protected int computeVerticalScrollOffset() {
- final int firstPosition = mFirstPosition;
- final int childCount = getChildCount();
-
- if (firstPosition < 0 || childCount == 0) {
- return 0;
- }
-
- final View child = getChildAt(0);
- final int childTop = child.getTop();
-
- int childHeight = child.getHeight();
- if (childHeight > 0) {
- return Math.max(firstPosition * 100 - (childTop * 100) / childHeight, 0);
- }
-
- return 0;
- }
-
- @Override
- protected int computeHorizontalScrollOffset() {
- final int firstPosition = mFirstPosition;
- final int childCount = getChildCount();
-
- if (firstPosition < 0 || childCount == 0) {
- return 0;
- }
-
- final View child = getChildAt(0);
- final int childLeft = child.getLeft();
-
- int childWidth = child.getWidth();
- if (childWidth > 0) {
- return Math.max(firstPosition * 100 - (childLeft * 100) / childWidth, 0);
- }
-
- return 0;
- }
-
- @Override
- protected int computeVerticalScrollRange() {
- int result = Math.max(mItemCount * 100, 0);
-
- if (mIsVertical && mOverScroll != 0) {
- // Compensate for overscroll
- result += Math.abs((int) ((float) mOverScroll / getHeight() * mItemCount * 100));
- }
-
- return result;
- }
-
- @Override
- protected int computeHorizontalScrollRange() {
- int result = Math.max(mItemCount * 100, 0);
-
- if (!mIsVertical && mOverScroll != 0) {
- // Compensate for overscroll
- result += Math.abs((int) ((float) mOverScroll / getWidth() * mItemCount * 100));
- }
-
- return result;
- }
-
- @Override
- public boolean showContextMenuForChild(View originalView) {
- final int longPressPosition = getPositionForView(originalView);
- if (longPressPosition >= 0) {
- final long longPressId = mAdapter.getItemId(longPressPosition);
- boolean handled = false;
-
- OnItemLongClickListener listener = getOnItemLongClickListener();
- if (listener != null) {
- handled = listener.onItemLongClick(TwoWayView.this, originalView,
- longPressPosition, longPressId);
- }
-
- if (!handled) {
- mContextMenuInfo = createContextMenuInfo(
- getChildAt(longPressPosition - mFirstPosition),
- longPressPosition, longPressId);
-
- handled = super.showContextMenuForChild(originalView);
- }
-
- return handled;
- }
-
- return false;
- }
-
- @Override
- public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
- if (disallowIntercept) {
- recycleVelocityTracker();
- }
-
- super.requestDisallowInterceptTouchEvent(disallowIntercept);
- }
-
- @Override
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- if (!mIsAttached || mAdapter == null) {
- return false;
- }
-
- final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- initOrResetVelocityTracker();
- mVelocityTracker.addMovement(ev);
-
- mScroller.abortAnimation();
- if (mPositionScroller != null) {
- mPositionScroller.stop();
- }
-
- final float x = ev.getX();
- final float y = ev.getY();
-
- mLastTouchPos = (mIsVertical ? y : x);
-
- final int motionPosition = findMotionRowOrColumn((int) mLastTouchPos);
-
- mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
- mTouchRemainderPos = 0;
-
- if (mTouchMode == TOUCH_MODE_FLINGING) {
- return true;
- } else if (motionPosition >= 0) {
- mMotionPosition = motionPosition;
- mTouchMode = TOUCH_MODE_DOWN;
- }
-
- break;
-
- case MotionEvent.ACTION_MOVE: {
- if (mTouchMode != TOUCH_MODE_DOWN) {
- break;
- }
-
- initVelocityTrackerIfNotExists();
- mVelocityTracker.addMovement(ev);
-
- final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
- if (index < 0) {
- Log.e(LOGTAG, "onInterceptTouchEvent could not find pointer with id " +
- mActivePointerId + " - did TwoWayView receive an inconsistent " +
- "event stream?");
- return false;
- }
-
- final float pos;
- if (mIsVertical) {
- pos = MotionEventCompat.getY(ev, index);
- } else {
- pos = MotionEventCompat.getX(ev, index);
- }
-
- final float diff = pos - mLastTouchPos + mTouchRemainderPos;
- final int delta = (int) diff;
- mTouchRemainderPos = diff - delta;
-
- if (maybeStartScrolling(delta)) {
- return true;
- }
-
- break;
- }
-
- case MotionEvent.ACTION_CANCEL:
- case MotionEvent.ACTION_UP:
- mActivePointerId = INVALID_POINTER;
- mTouchMode = TOUCH_MODE_REST;
- recycleVelocityTracker();
- reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
-
- break;
- }
-
- return false;
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- if (!isEnabled()) {
- // A disabled view that is clickable still consumes the touch
- // events, it just doesn't respond to them.
- return isClickable() || isLongClickable();
- }
-
- if (!mIsAttached || mAdapter == null) {
- return false;
- }
-
- boolean needsInvalidate = false;
-
- initVelocityTrackerIfNotExists();
- mVelocityTracker.addMovement(ev);
-
- final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
- switch (action) {
- case MotionEvent.ACTION_DOWN: {
- if (mDataChanged) {
- break;
- }
-
- mVelocityTracker.clear();
- mScroller.abortAnimation();
- if (mPositionScroller != null) {
- mPositionScroller.stop();
- }
-
- final float x = ev.getX();
- final float y = ev.getY();
-
- mLastTouchPos = (mIsVertical ? y : x);
-
- int motionPosition = pointToPosition((int) x, (int) y);
-
- mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
- mTouchRemainderPos = 0;
-
- if (mDataChanged) {
- break;
- }
-
- if (mTouchMode == TOUCH_MODE_FLINGING) {
- mTouchMode = TOUCH_MODE_DRAGGING;
- reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
- motionPosition = findMotionRowOrColumn((int) mLastTouchPos);
- } else if (mMotionPosition >= 0 && mAdapter.isEnabled(mMotionPosition)) {
- mTouchMode = TOUCH_MODE_DOWN;
- triggerCheckForTap();
- }
-
- mMotionPosition = motionPosition;
-
- break;
- }
-
- case MotionEvent.ACTION_MOVE: {
- final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
- if (index < 0) {
- Log.e(LOGTAG, "onInterceptTouchEvent could not find pointer with id " +
- mActivePointerId + " - did TwoWayView receive an inconsistent " +
- "event stream?");
- return false;
- }
-
- final float pos;
- if (mIsVertical) {
- pos = MotionEventCompat.getY(ev, index);
- } else {
- pos = MotionEventCompat.getX(ev, index);
- }
-
- if (mDataChanged) {
- // Re-sync everything if data has been changed
- // since the scroll operation can query the adapter.
- layoutChildren();
- }
-
- final float diff = pos - mLastTouchPos + mTouchRemainderPos;
- final int delta = (int) diff;
- mTouchRemainderPos = diff - delta;
-
- switch (mTouchMode) {
- case TOUCH_MODE_DOWN:
- case TOUCH_MODE_TAP:
- case TOUCH_MODE_DONE_WAITING:
- // Check if we have moved far enough that it looks more like a
- // scroll than a tap
- maybeStartScrolling(delta);
- break;
-
- case TOUCH_MODE_DRAGGING:
- case TOUCH_MODE_OVERSCROLL:
- mLastTouchPos = pos;
- maybeScroll(delta);
- break;
- }
-
- break;
- }
-
- case MotionEvent.ACTION_CANCEL:
- cancelCheckForTap();
- mTouchMode = TOUCH_MODE_REST;
- reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
-
- setPressed(false);
- View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
- if (motionView != null) {
- motionView.setPressed(false);
- }
-
- if (mStartEdge != null && mEndEdge != null) {
- needsInvalidate = mStartEdge.onRelease() | mEndEdge.onRelease();
- }
-
- recycleVelocityTracker();
-
- break;
-
- case MotionEvent.ACTION_UP: {
- switch (mTouchMode) {
- case TOUCH_MODE_DOWN:
- case TOUCH_MODE_TAP:
- case TOUCH_MODE_DONE_WAITING: {
- final int motionPosition = mMotionPosition;
- final View child = getChildAt(motionPosition - mFirstPosition);
-
- final float x = ev.getX();
- final float y = ev.getY();
-
- final boolean inList;
- if (mIsVertical) {
- inList = x > getPaddingLeft() && x < getWidth() - getPaddingRight();
- } else {
- inList = y > getPaddingTop() && y < getHeight() - getPaddingBottom();
- }
-
- if (child != null && !child.hasFocusable() && inList) {
- if (mTouchMode != TOUCH_MODE_DOWN) {
- child.setPressed(false);
- }
-
- if (mPerformClick == null) {
- mPerformClick = new PerformClick();
- }
-
- final PerformClick performClick = mPerformClick;
- performClick.mClickMotionPosition = motionPosition;
- performClick.rememberWindowAttachCount();
-
- mResurrectToPosition = motionPosition;
-
- if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
- if (mTouchMode == TOUCH_MODE_DOWN) {
- cancelCheckForTap();
- } else {
- cancelCheckForLongPress();
- }
-
- mLayoutMode = LAYOUT_NORMAL;
-
- if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
- mTouchMode = TOUCH_MODE_TAP;
-
- setPressed(true);
- positionSelector(mMotionPosition, child);
- child.setPressed(true);
-
- if (mSelector != null) {
- Drawable d = mSelector.getCurrent();
- if (d != null && d instanceof TransitionDrawable) {
- ((TransitionDrawable) d).resetTransition();
- }
- }
-
- if (mTouchModeReset != null) {
- removeCallbacks(mTouchModeReset);
- }
-
- mTouchModeReset = new Runnable() {
- @Override
- public void run() {
- mTouchMode = TOUCH_MODE_REST;
-
- setPressed(false);
- child.setPressed(false);
-
- if (!mDataChanged) {
- performClick.run();
- }
-
- mTouchModeReset = null;
- }
- };
-
- postDelayed(mTouchModeReset,
- ViewConfiguration.getPressedStateDuration());
- } else {
- mTouchMode = TOUCH_MODE_REST;
- updateSelectorState();
- }
- } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
- performClick.run();
- }
- }
-
- mTouchMode = TOUCH_MODE_REST;
-
- finishSmoothScrolling();
- updateSelectorState();
-
- break;
- }
-
- case TOUCH_MODE_DRAGGING:
- if (contentFits()) {
- mTouchMode = TOUCH_MODE_REST;
- reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
- break;
- }
-
- mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
-
- final float velocity;
- if (mIsVertical) {
- velocity = VelocityTrackerCompat.getYVelocity(mVelocityTracker,
- mActivePointerId);
- } else {
- velocity = VelocityTrackerCompat.getXVelocity(mVelocityTracker,
- mActivePointerId);
- }
-
- if (Math.abs(velocity) >= mFlingVelocity) {
- mTouchMode = TOUCH_MODE_FLINGING;
- reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
-
- mScroller.fling(0, 0,
- (int) (mIsVertical ? 0 : velocity),
- (int) (mIsVertical ? velocity : 0),
- (mIsVertical ? 0 : Integer.MIN_VALUE),
- (mIsVertical ? 0 : Integer.MAX_VALUE),
- (mIsVertical ? Integer.MIN_VALUE : 0),
- (mIsVertical ? Integer.MAX_VALUE : 0));
-
- mLastTouchPos = 0;
- needsInvalidate = true;
- } else {
- mTouchMode = TOUCH_MODE_REST;
- reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
- }
-
- break;
-
- case TOUCH_MODE_OVERSCROLL:
- mTouchMode = TOUCH_MODE_REST;
- reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
- break;
- }
-
- cancelCheckForTap();
- cancelCheckForLongPress();
- setPressed(false);
-
- if (mStartEdge != null && mEndEdge != null) {
- needsInvalidate |= mStartEdge.onRelease() | mEndEdge.onRelease();
- }
-
- recycleVelocityTracker();
-
- break;
- }
- }
-
- if (needsInvalidate) {
- ViewCompat.postInvalidateOnAnimation(this);
- }
-
- return true;
- }
-
- @Override
- public void onTouchModeChanged(boolean isInTouchMode) {
- if (isInTouchMode) {
- // Get rid of the selection when we enter touch mode
- hideSelector();
-
- // Layout, but only if we already have done so previously.
- // (Otherwise may clobber a LAYOUT_SYNC layout that was requested to restore
- // state.)
- if (getWidth() > 0 && getHeight() > 0 && getChildCount() > 0) {
- layoutChildren();
- }
-
- updateSelectorState();
- } else {
- final int touchMode = mTouchMode;
- if (touchMode == TOUCH_MODE_OVERSCROLL) {
- finishSmoothScrolling();
- if (mOverScroll != 0) {
- mOverScroll = 0;
- finishEdgeGlows();
- invalidate();
- }
- }
- }
- }
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- return handleKeyEvent(keyCode, 1, event);
- }
-
- @Override
- public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
- return handleKeyEvent(keyCode, repeatCount, event);
- }
-
- @Override
- public boolean onKeyUp(int keyCode, KeyEvent event) {
- return handleKeyEvent(keyCode, 1, event);
- }
-
- @Override
- public void sendAccessibilityEvent(int eventType) {
- // Since this class calls onScrollChanged even if the mFirstPosition and the
- // child count have not changed we will avoid sending duplicate accessibility
- // events.
- if (eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
- final int firstVisiblePosition = getFirstVisiblePosition();
- final int lastVisiblePosition = getLastVisiblePosition();
-
- if (mLastAccessibilityScrollEventFromIndex == firstVisiblePosition
- && mLastAccessibilityScrollEventToIndex == lastVisiblePosition) {
- return;
- } else {
- mLastAccessibilityScrollEventFromIndex = firstVisiblePosition;
- mLastAccessibilityScrollEventToIndex = lastVisiblePosition;
- }
- }
-
- super.sendAccessibilityEvent(eventType);
- }
-
- @Override
- @TargetApi(14)
- public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
- super.onInitializeAccessibilityEvent(event);
- event.setClassName(TwoWayView.class.getName());
- }
-
- @Override
- @TargetApi(14)
- public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
- super.onInitializeAccessibilityNodeInfo(info);
- info.setClassName(TwoWayView.class.getName());
-
- AccessibilityNodeInfoCompat infoCompat = new AccessibilityNodeInfoCompat(info);
-
- if (isEnabled()) {
- if (getFirstVisiblePosition() > 0) {
- infoCompat.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
- }
-
- if (getLastVisiblePosition() < getCount() - 1) {
- infoCompat.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
- }
- }
- }
-
- @Override
- @TargetApi(16)
- public boolean performAccessibilityAction(int action, Bundle arguments) {
- if (super.performAccessibilityAction(action, arguments)) {
- return true;
- }
-
- switch (action) {
- case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
- if (isEnabled() && getLastVisiblePosition() < getCount() - 1) {
- // TODO: Use some form of smooth scroll instead
- scrollListItemsBy(getAvailableSize());
- return true;
- }
- return false;
-
- case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
- if (isEnabled() && mFirstPosition > 0) {
- // TODO: Use some form of smooth scroll instead
- scrollListItemsBy(-getAvailableSize());
- return true;
- }
- return false;
- }
-
- return false;
- }
-
- /**
- * Return true if child is an ancestor of parent, (or equal to the parent).
- */
- private boolean isViewAncestorOf(View child, View parent) {
- if (child == parent) {
- return true;
- }
-
- final ViewParent theParent = child.getParent();
-
- return (theParent instanceof ViewGroup) &&
- isViewAncestorOf((View) theParent, parent);
- }
-
- private void forceValidFocusDirection(int direction) {
- if (mIsVertical && direction != View.FOCUS_UP && direction != View.FOCUS_DOWN) {
- throw new IllegalArgumentException("Focus direction must be one of"
- + " {View.FOCUS_UP, View.FOCUS_DOWN} for vertical orientation");
- } else if (!mIsVertical && direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT) {
- throw new IllegalArgumentException("Focus direction must be one of"
- + " {View.FOCUS_LEFT, View.FOCUS_RIGHT} for vertical orientation");
- }
- }
-
- private void forceValidInnerFocusDirection(int direction) {
- if (mIsVertical && direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT) {
- throw new IllegalArgumentException("Direction must be one of"
- + " {View.FOCUS_LEFT, View.FOCUS_RIGHT} for vertical orientation");
- } else if (!mIsVertical && direction != View.FOCUS_UP && direction != View.FOCUS_DOWN) {
- throw new IllegalArgumentException("direction must be one of"
- + " {View.FOCUS_UP, View.FOCUS_DOWN} for horizontal orientation");
- }
- }
-
- /**
- * Scrolls up or down by the number of items currently present on screen.
- *
- * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
- * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
- * current view orientation.
- *
- * @return whether selection was moved
- */
- boolean pageScroll(int direction) {
- forceValidFocusDirection(direction);
-
- boolean forward = false;
- int nextPage = -1;
-
- if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) {
- nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1);
- } else if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
- nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1);
- forward = true;
- }
-
- if (nextPage < 0) {
- return false;
- }
-
- final int position = lookForSelectablePosition(nextPage, forward);
- if (position >= 0) {
- mLayoutMode = LAYOUT_SPECIFIC;
- mSpecificStart = getStartEdge() + getFadingEdgeLength();
-
- if (forward && position > mItemCount - getChildCount()) {
- mLayoutMode = LAYOUT_FORCE_BOTTOM;
- }
-
- if (!forward && position < getChildCount()) {
- mLayoutMode = LAYOUT_FORCE_TOP;
- }
-
- setSelectionInt(position);
- invokeOnItemScrollListener();
-
- if (!awakenScrollbarsInternal()) {
- invalidate();
- }
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Go to the last or first item if possible (not worrying about panning across or navigating
- * within the internal focus of the currently selected item.)
- *
- * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
- * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
- * current view orientation.
- *
- * @return whether selection was moved
- */
- boolean fullScroll(int direction) {
- forceValidFocusDirection(direction);
-
- boolean moved = false;
- if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) {
- if (mSelectedPosition != 0) {
- int position = lookForSelectablePosition(0, true);
- if (position >= 0) {
- mLayoutMode = LAYOUT_FORCE_TOP;
- setSelectionInt(position);
- invokeOnItemScrollListener();
- }
-
- moved = true;
- }
- } else if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
- if (mSelectedPosition < mItemCount - 1) {
- int position = lookForSelectablePosition(mItemCount - 1, true);
- if (position >= 0) {
- mLayoutMode = LAYOUT_FORCE_BOTTOM;
- setSelectionInt(position);
- invokeOnItemScrollListener();
- }
-
- moved = true;
- }
- }
-
- if (moved && !awakenScrollbarsInternal()) {
- awakenScrollbarsInternal();
- invalidate();
- }
-
- return moved;
- }
-
- /**
- * To avoid horizontal/vertical focus searches changing the selected item,
- * we manually focus search within the selected item (as applicable), and
- * prevent focus from jumping to something within another item.
- *
- * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
- * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
- * current view orientation.
- *
- * @return Whether this consumes the key event.
- */
- private boolean handleFocusWithinItem(int direction) {
- forceValidInnerFocusDirection(direction);
-
- final int numChildren = getChildCount();
-
- if (mItemsCanFocus && numChildren > 0 && mSelectedPosition != INVALID_POSITION) {
- final View selectedView = getSelectedView();
-
- if (selectedView != null && selectedView.hasFocus() &&
- selectedView instanceof ViewGroup) {
-
- final View currentFocus = selectedView.findFocus();
- final View nextFocus = FocusFinder.getInstance().findNextFocus(
- (ViewGroup) selectedView, currentFocus, direction);
-
- if (nextFocus != null) {
- // Do the math to get interesting rect in next focus' coordinates
- currentFocus.getFocusedRect(mTempRect);
- offsetDescendantRectToMyCoords(currentFocus, mTempRect);
- offsetRectIntoDescendantCoords(nextFocus, mTempRect);
-
- if (nextFocus.requestFocus(direction, mTempRect)) {
- return true;
- }
- }
-
- // We are blocking the key from being handled (by returning true)
- // if the global result is going to be some other view within this
- // list. This is to achieve the overall goal of having horizontal/vertical
- // d-pad navigation remain in the current item depending on the current
- // orientation in this view.
- final View globalNextFocus = FocusFinder.getInstance().findNextFocus(
- (ViewGroup) getRootView(), currentFocus, direction);
-
- if (globalNextFocus != null) {
- return isViewAncestorOf(globalNextFocus, this);
- }
- }
- }
-
- return false;
- }
-
- /**
- * Scrolls to the next or previous item if possible.
- *
- * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
- * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
- * current view orientation.
- *
- * @return whether selection was moved
- */
- private boolean arrowScroll(int direction) {
- forceValidFocusDirection(direction);
-
- try {
- mInLayout = true;
-
- final boolean handled = arrowScrollImpl(direction);
- if (handled) {
- playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
- }
-
- return handled;
- } finally {
- mInLayout = false;
- }
- }
-
- /**
- * When selection changes, it is possible that the previously selected or the
- * next selected item will change its size. If so, we need to offset some folks,
- * and re-layout the items as appropriate.
- *
- * @param selectedView The currently selected view (before changing selection).
- * should be <code>null</code> if there was no previous selection.
- * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
- * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
- * current view orientation.
- * @param newSelectedPosition The position of the next selection.
- * @param newFocusAssigned whether new focus was assigned. This matters because
- * when something has focus, we don't want to show selection (ugh).
- */
- private void handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition,
- boolean newFocusAssigned) {
- forceValidFocusDirection(direction);
-
- if (newSelectedPosition == INVALID_POSITION) {
- throw new IllegalArgumentException("newSelectedPosition needs to be valid");
- }
-
- // Whether or not we are moving down/right or up/left, we want to preserve the
- // top/left of whatever view is at the start:
- // - moving down/right: the view that had selection
- // - moving up/left: the view that is getting selection
- final int selectedIndex = mSelectedPosition - mFirstPosition;
- final int nextSelectedIndex = newSelectedPosition - mFirstPosition;
- int startViewIndex, endViewIndex;
- boolean topSelected = false;
- View startView;
- View endView;
-
- if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) {
- startViewIndex = nextSelectedIndex;
- endViewIndex = selectedIndex;
- startView = getChildAt(startViewIndex);
- endView = selectedView;
- topSelected = true;
- } else {
- startViewIndex = selectedIndex;
- endViewIndex = nextSelectedIndex;
- startView = selectedView;
- endView = getChildAt(endViewIndex);
- }
-
- final int numChildren = getChildCount();
-
- // start with top view: is it changing size?
- if (startView != null) {
- startView.setSelected(!newFocusAssigned && topSelected);
- measureAndAdjustDown(startView, startViewIndex, numChildren);
- }
-
- // is the bottom view changing size?
- if (endView != null) {
- endView.setSelected(!newFocusAssigned && !topSelected);
- measureAndAdjustDown(endView, endViewIndex, numChildren);
- }
- }
-
- /**
- * Re-measure a child, and if its height changes, lay it out preserving its
- * top, and adjust the children below it appropriately.
- *
- * @param child The child
- * @param childIndex The view group index of the child.
- * @param numChildren The number of children in the view group.
- */
- private void measureAndAdjustDown(View child, int childIndex, int numChildren) {
- int oldSize = getChildSize(child);
- measureChild(child);
-
- if (getChildMeasuredSize(child) == oldSize) {
- return;
- }
-
- // lay out the view, preserving its top
- relayoutMeasuredChild(child);
-
- // adjust views below appropriately
- final int sizeDelta = getChildMeasuredSize(child) - oldSize;
- for (int i = childIndex + 1; i < numChildren; i++) {
- getChildAt(i).offsetTopAndBottom(sizeDelta);
- }
- }
-
- /**
- * Do an arrow scroll based on focus searching. If a new view is
- * given focus, return the selection delta and amount to scroll via
- * an {@link ArrowScrollFocusResult}, otherwise, return null.
- *
- * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
- * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
- * current view orientation.
- *
- * @return The result if focus has changed, or <code>null</code>.
- */
- private ArrowScrollFocusResult arrowScrollFocused(final int direction) {
- forceValidFocusDirection(direction);
-
- final View selectedView = getSelectedView();
- final View newFocus;
- final int searchPoint;
-
- if (selectedView != null && selectedView.hasFocus()) {
- View oldFocus = selectedView.findFocus();
- newFocus = FocusFinder.getInstance().findNextFocus(this, oldFocus, direction);
- } else {
- if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
- boolean fadingEdgeShowing = (mFirstPosition > 0);
- final int start = getStartEdge() +
- (fadingEdgeShowing ? getArrowScrollPreviewLength() : 0);
-
- final int selectedStart;
- if (selectedView != null) {
- selectedStart = getChildStartEdge(selectedView);
- } else {
- selectedStart = start;
- }
-
- searchPoint = Math.max(selectedStart, start);
- } else {
- final boolean fadingEdgeShowing =
- (mFirstPosition + getChildCount() - 1) < mItemCount;
- final int end = getEndEdge() - (fadingEdgeShowing ? getArrowScrollPreviewLength() : 0);
-
- final int selectedEnd;
- if (selectedView != null) {
- selectedEnd = getChildEndEdge(selectedView);
- } else {
- selectedEnd = end;
- }
-
- searchPoint = Math.min(selectedEnd, end);
- }
-
- final int x = (mIsVertical ? 0 : searchPoint);
- final int y = (mIsVertical ? searchPoint : 0);
- mTempRect.set(x, y, x, y);
-
- newFocus = FocusFinder.getInstance().findNextFocusFromRect(this, mTempRect, direction);
- }
-
- if (newFocus != null) {
- final int positionOfNewFocus = positionOfNewFocus(newFocus);
-
- // If the focus change is in a different new position, make sure
- // we aren't jumping over another selectable position.
- if (mSelectedPosition != INVALID_POSITION && positionOfNewFocus != mSelectedPosition) {
- final int selectablePosition = lookForSelectablePositionOnScreen(direction);
-
- final boolean movingForward =
- (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT);
- final boolean movingBackward =
- (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT);
-
- if (selectablePosition != INVALID_POSITION &&
- ((movingForward && selectablePosition < positionOfNewFocus) ||
- (movingBackward && selectablePosition > positionOfNewFocus))) {
- return null;
- }
- }
-
- int focusScroll = amountToScrollToNewFocus(direction, newFocus, positionOfNewFocus);
-
- final int maxScrollAmount = getMaxScrollAmount();
- if (focusScroll < maxScrollAmount) {
- // Not moving too far, safe to give next view focus
- newFocus.requestFocus(direction);
- mArrowScrollFocusResult.populate(positionOfNewFocus, focusScroll);
- return mArrowScrollFocusResult;
- } else if (distanceToView(newFocus) < maxScrollAmount) {
- // Case to consider:
- // Too far to get entire next focusable on screen, but by going
- // max scroll amount, we are getting it at least partially in view,
- // so give it focus and scroll the max amount.
- newFocus.requestFocus(direction);
- mArrowScrollFocusResult.populate(positionOfNewFocus, maxScrollAmount);
- return mArrowScrollFocusResult;
- }
- }
-
- return null;
- }
-
- /**
- * @return The maximum amount a list view will scroll in response to
- * an arrow event.
- */
- public int getMaxScrollAmount() {
- return (int) (MAX_SCROLL_FACTOR * getSize());
- }
-
- /**
- * @return The amount to preview next items when arrow scrolling.
- */
- private int getArrowScrollPreviewLength() {
- return mItemMargin + Math.max(MIN_SCROLL_PREVIEW_PIXELS, getFadingEdgeLength());
- }
-
- /**
- * @param newFocus The view that would have focus.
- * @return the position that contains newFocus
- */
- private int positionOfNewFocus(View newFocus) {
- final int numChildren = getChildCount();
-
- for (int i = 0; i < numChildren; i++) {
- final View child = getChildAt(i);
- if (isViewAncestorOf(newFocus, child)) {
- return mFirstPosition + i;
- }
- }
-
- throw new IllegalArgumentException("newFocus is not a child of any of the"
- + " children of the list!");
- }
-
- /**
- * Handle an arrow scroll going up or down. Take into account whether items are selectable,
- * whether there are focusable items, etc.
- *
- * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
- * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
- * current view orientation.
- *
- * @return Whether any scrolling, selection or focus change occurred.
- */
- private boolean arrowScrollImpl(int direction) {
- forceValidFocusDirection(direction);
-
- if (getChildCount() <= 0) {
- return false;
- }
-
- View selectedView = getSelectedView();
- int selectedPos = mSelectedPosition;
-
- int nextSelectedPosition = lookForSelectablePositionOnScreen(direction);
- int amountToScroll = amountToScroll(direction, nextSelectedPosition);
-
- // If we are moving focus, we may OVERRIDE the default behaviour
- final ArrowScrollFocusResult focusResult = (mItemsCanFocus ? arrowScrollFocused(direction) : null);
- if (focusResult != null) {
- nextSelectedPosition = focusResult.getSelectedPosition();
- amountToScroll = focusResult.getAmountToScroll();
- }
-
- boolean needToRedraw = (focusResult != null);
- if (nextSelectedPosition != INVALID_POSITION) {
- handleNewSelectionChange(selectedView, direction, nextSelectedPosition, focusResult != null);
-
- setSelectedPositionInt(nextSelectedPosition);
- setNextSelectedPositionInt(nextSelectedPosition);
-
- selectedView = getSelectedView();
- selectedPos = nextSelectedPosition;
-
- if (mItemsCanFocus && focusResult == null) {
- // There was no new view found to take focus, make sure we
- // don't leave focus with the old selection.
- final View focused = getFocusedChild();
- if (focused != null) {
- focused.clearFocus();
- }
- }
-
- needToRedraw = true;
- checkSelectionChanged();
- }
-
- if (amountToScroll > 0) {
- scrollListItemsBy(direction == View.FOCUS_UP || direction == View.FOCUS_LEFT ?
- amountToScroll : -amountToScroll);
- needToRedraw = true;
- }
-
- // If we didn't find a new focusable, make sure any existing focused
- // item that was panned off screen gives up focus.
- if (mItemsCanFocus && focusResult == null &&
- selectedView != null && selectedView.hasFocus()) {
- final View focused = selectedView.findFocus();
- if (!isViewAncestorOf(focused, this) || distanceToView(focused) > 0) {
- focused.clearFocus();
- }
- }
-
- // If the current selection is panned off, we need to remove the selection
- if (nextSelectedPosition == INVALID_POSITION && selectedView != null
- && !isViewAncestorOf(selectedView, this)) {
- selectedView = null;
- hideSelector();
-
- // But we don't want to set the ressurect position (that would make subsequent
- // unhandled key events bring back the item we just scrolled off)
- mResurrectToPosition = INVALID_POSITION;
- }
-
- if (needToRedraw) {
- if (selectedView != null) {
- positionSelector(selectedPos, selectedView);
- mSelectedStart = getChildStartEdge(selectedView);
- }
-
- if (!awakenScrollbarsInternal()) {
- invalidate();
- }
-
- invokeOnItemScrollListener();
- return true;
- }
-
- return false;
- }
-
- /**
- * Determine how much we need to scroll in order to get the next selected view
- * visible. The amount is capped at {@link #getMaxScrollAmount()}.
- *
- * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
- * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
- * current view orientation.
- * @param nextSelectedPosition The position of the next selection, or
- * {@link #INVALID_POSITION} if there is no next selectable position
- *
- * @return The amount to scroll. Note: this is always positive! Direction
- * needs to be taken into account when actually scrolling.
- */
- private int amountToScroll(int direction, int nextSelectedPosition) {
- forceValidFocusDirection(direction);
-
- final int numChildren = getChildCount();
-
- if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
- final int end = getEndEdge();
-
- int indexToMakeVisible = numChildren - 1;
- if (nextSelectedPosition != INVALID_POSITION) {
- indexToMakeVisible = nextSelectedPosition - mFirstPosition;
- }
-
- final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
- final View viewToMakeVisible = getChildAt(indexToMakeVisible);
-
- int goalEnd = end;
- if (positionToMakeVisible < mItemCount - 1) {
- goalEnd -= getArrowScrollPreviewLength();
- }
-
- final int viewToMakeVisibleStart = getChildStartEdge(viewToMakeVisible);
- final int viewToMakeVisibleEnd = getChildEndEdge(viewToMakeVisible);
-
- if (viewToMakeVisibleEnd <= goalEnd) {
- // Target item is fully visible
- return 0;
- }
-
- if (nextSelectedPosition != INVALID_POSITION &&
- (goalEnd - viewToMakeVisibleStart) >= getMaxScrollAmount()) {
- // Item already has enough of it visible, changing selection is good enough
- return 0;
- }
-
- int amountToScroll = (viewToMakeVisibleEnd - goalEnd);
-
- if (mFirstPosition + numChildren == mItemCount) {
- final int lastChildEnd = getChildEndEdge(getChildAt(numChildren - 1));
-
- // Last is last in list -> Make sure we don't scroll past it
- final int max = lastChildEnd - end;
- amountToScroll = Math.min(amountToScroll, max);
- }
-
- return Math.min(amountToScroll, getMaxScrollAmount());
- } else {
- final int start = getStartEdge();
-
- int indexToMakeVisible = 0;
- if (nextSelectedPosition != INVALID_POSITION) {
- indexToMakeVisible = nextSelectedPosition - mFirstPosition;
- }
-
- final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
- final View viewToMakeVisible = getChildAt(indexToMakeVisible);
-
- int goalStart = start;
- if (positionToMakeVisible > 0) {
- goalStart += getArrowScrollPreviewLength();
- }
-
- final int viewToMakeVisibleStart = getChildStartEdge(viewToMakeVisible);
- final int viewToMakeVisibleEnd = getChildEndEdge(viewToMakeVisible);
-
- if (viewToMakeVisibleStart >= goalStart) {
- // Item is fully visible
- return 0;
- }
-
- if (nextSelectedPosition != INVALID_POSITION &&
- (viewToMakeVisibleEnd - goalStart) >= getMaxScrollAmount()) {
- // Item already has enough of it visible, changing selection is good enough
- return 0;
- }
-
- int amountToScroll = (goalStart - viewToMakeVisibleStart);
-
- if (mFirstPosition == 0) {
- final int firstChildStart = getChildStartEdge(getChildAt(0));
-
- // First is first in list -> make sure we don't scroll past it
- final int max = start - firstChildStart;
- amountToScroll = Math.min(amountToScroll, max);
- }
-
- return Math.min(amountToScroll, getMaxScrollAmount());
- }
- }
-
- /**
- * Determine how much we need to scroll in order to get newFocus in view.
- *
- * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
- * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
- * current view orientation.
- * @param newFocus The view that would take focus.
- * @param positionOfNewFocus The position of the list item containing newFocus
- *
- * @return The amount to scroll. Note: this is always positive! Direction
- * needs to be taken into account when actually scrolling.
- */
- private int amountToScrollToNewFocus(int direction, View newFocus, int positionOfNewFocus) {
- forceValidFocusDirection(direction);
-
- int amountToScroll = 0;
-
- newFocus.getDrawingRect(mTempRect);
- offsetDescendantRectToMyCoords(newFocus, mTempRect);
-
- if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) {
- final int start = getStartEdge();
- final int newFocusStart = (mIsVertical ? mTempRect.top : mTempRect.left);
-
- if (newFocusStart < start) {
- amountToScroll = start - newFocusStart;
- if (positionOfNewFocus > 0) {
- amountToScroll += getArrowScrollPreviewLength();
- }
- }
- } else {
- final int end = getEndEdge();
- final int newFocusEnd = (mIsVertical ? mTempRect.bottom : mTempRect.right);
-
- if (newFocusEnd > end) {
- amountToScroll = newFocusEnd - end;
- if (positionOfNewFocus < mItemCount - 1) {
- amountToScroll += getArrowScrollPreviewLength();
- }
- }
- }
-
- return amountToScroll;
- }
-
- /**
- * Determine the distance to the nearest edge of a view in a particular
- * direction.
- *
- * @param descendant A descendant of this list.
- * @return The distance, or 0 if the nearest edge is already on screen.
- */
- private int distanceToView(View descendant) {
- descendant.getDrawingRect(mTempRect);
- offsetDescendantRectToMyCoords(descendant, mTempRect);
-
- final int start = getStartEdge();
- final int end = getEndEdge();
-
- final int viewStart = (mIsVertical ? mTempRect.top : mTempRect.left);
- final int viewEnd = (mIsVertical ? mTempRect.bottom : mTempRect.right);
-
- int distance = 0;
- if (viewEnd < start) {
- distance = start - viewEnd;
- } else if (viewStart > end) {
- distance = viewStart - end;
- }
-
- return distance;
- }
-
- private boolean handleKeyScroll(KeyEvent event, int count, int direction) {
- boolean handled = false;
-
- if (KeyEventCompat.hasNoModifiers(event)) {
- handled = resurrectSelectionIfNeeded();
- if (!handled) {
- while (count-- > 0) {
- if (arrowScroll(direction)) {
- handled = true;
- } else {
- break;
- }
- }
- }
- } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) {
- handled = resurrectSelectionIfNeeded() || fullScroll(direction);
- }
-
- return handled;
- }
-
- private boolean handleKeyEvent(int keyCode, int count, KeyEvent event) {
- if (mAdapter == null || !mIsAttached) {
- return false;
- }
-
- if (mDataChanged) {
- layoutChildren();
- }
-
- boolean handled = false;
- final int action = event.getAction();
-
- if (action != KeyEvent.ACTION_UP) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_DPAD_UP:
- if (mIsVertical) {
- handled = handleKeyScroll(event, count, View.FOCUS_UP);
- } else if (KeyEventCompat.hasNoModifiers(event)) {
- handled = handleFocusWithinItem(View.FOCUS_UP);
- }
- break;
-
- case KeyEvent.KEYCODE_DPAD_DOWN: {
- if (mIsVertical) {
- handled = handleKeyScroll(event, count, View.FOCUS_DOWN);
- } else if (KeyEventCompat.hasNoModifiers(event)) {
- handled = handleFocusWithinItem(View.FOCUS_DOWN);
- }
- break;
- }
-
- case KeyEvent.KEYCODE_DPAD_LEFT:
- if (!mIsVertical) {
- handled = handleKeyScroll(event, count, View.FOCUS_LEFT);
- } else if (KeyEventCompat.hasNoModifiers(event)) {
- handled = handleFocusWithinItem(View.FOCUS_LEFT);
- }
- break;
-
- case KeyEvent.KEYCODE_DPAD_RIGHT:
- if (!mIsVertical) {
- handled = handleKeyScroll(event, count, View.FOCUS_RIGHT);
- } else if (KeyEventCompat.hasNoModifiers(event)) {
- handled = handleFocusWithinItem(View.FOCUS_RIGHT);
- }
- break;
-
- case KeyEvent.KEYCODE_DPAD_CENTER:
- case KeyEvent.KEYCODE_ENTER:
- if (KeyEventCompat.hasNoModifiers(event)) {
- handled = resurrectSelectionIfNeeded();
- if (!handled
- && event.getRepeatCount() == 0 && getChildCount() > 0) {
- keyPressed();
- handled = true;
- }
- }
- break;
-
- case KeyEvent.KEYCODE_SPACE:
- if (KeyEventCompat.hasNoModifiers(event)) {
- handled = resurrectSelectionIfNeeded() ||
- pageScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT);
- } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) {
- handled = resurrectSelectionIfNeeded() ||
- fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT);
- }
-
- handled = true;
- break;
-
- case KeyEvent.KEYCODE_PAGE_UP:
- if (KeyEventCompat.hasNoModifiers(event)) {
- handled = resurrectSelectionIfNeeded() ||
- pageScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT);
- } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) {
- handled = resurrectSelectionIfNeeded() ||
- fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT);
- }
- break;
-
- case KeyEvent.KEYCODE_PAGE_DOWN:
- if (KeyEventCompat.hasNoModifiers(event)) {
- handled = resurrectSelectionIfNeeded() ||
- pageScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT);
- } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) {
- handled = resurrectSelectionIfNeeded() ||
- fullScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT);
- }
- break;
-
- case KeyEvent.KEYCODE_MOVE_HOME:
- if (KeyEventCompat.hasNoModifiers(event)) {
- handled = resurrectSelectionIfNeeded() ||
- fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT);
- }
- break;
-
- case KeyEvent.KEYCODE_MOVE_END:
- if (KeyEventCompat.hasNoModifiers(event)) {
- handled = resurrectSelectionIfNeeded() ||
- fullScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT);
- }
- break;
- }
- }
-
- if (handled) {
- return true;
- }
-
- switch (action) {
- case KeyEvent.ACTION_DOWN:
- return super.onKeyDown(keyCode, event);
-
- case KeyEvent.ACTION_UP:
- if (!isEnabled()) {
- return true;
- }
-
- if (isClickable() && isPressed() &&
- mSelectedPosition >= 0 && mAdapter != null &&
- mSelectedPosition < mAdapter.getCount()) {
-
- final View child = getChildAt(mSelectedPosition - mFirstPosition);
- if (child != null) {
- performItemClick(child, mSelectedPosition, mSelectedRowId);
- child.setPressed(false);
- }
-
- setPressed(false);
- return true;
- }
-
- return false;
-
- case KeyEvent.ACTION_MULTIPLE:
- return super.onKeyMultiple(keyCode, count, event);
-
- default:
- 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;
- }
- }
-
- /**
- * Notify our scroll listener (if there is one) of a change in scroll state
- */
- private void invokeOnItemScrollListener() {
- if (mOnScrollListener != null) {
- mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount);
- }
-
- // Dummy values, View's implementation does not use these.
- onScrollChanged(0, 0, 0, 0);
- }
-
- private void reportScrollStateChange(int newState) {
- if (newState == mLastScrollState) {
- return;
- }
-
- if (mOnScrollListener != null) {
- mLastScrollState = newState;
- mOnScrollListener.onScrollStateChanged(this, newState);
- }
- }
-
- private boolean maybeStartScrolling(int delta) {
- final boolean isOverScroll = (mOverScroll != 0);
- if (Math.abs(delta) <= mTouchSlop && !isOverScroll) {
- return false;
- }
-
- if (isOverScroll) {
- mTouchMode = TOUCH_MODE_OVERSCROLL;
- } else {
- mTouchMode = TOUCH_MODE_DRAGGING;
- }
-
- // Time to start stealing events! Once we've stolen them, don't
- // let anyone steal from us.
- final ViewParent parent = getParent();
- if (parent != null) {
- parent.requestDisallowInterceptTouchEvent(true);
- }
-
- cancelCheckForLongPress();
-
- setPressed(false);
- View motionView = getChildAt(mMotionPosition - mFirstPosition);
- if (motionView != null) {
- motionView.setPressed(false);
- }
-
- reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
-
- return true;
- }
-
- private void maybeScroll(int delta) {
- if (mTouchMode == TOUCH_MODE_DRAGGING) {
- handleDragChange(delta);
- } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) {
- handleOverScrollChange(delta);
- }
- }
-
- private void handleDragChange(int delta) {
- // Time to start stealing events! Once we've stolen them, don't
- // let anyone steal from us.
- final ViewParent parent = getParent();
- if (parent != null) {
- parent.requestDisallowInterceptTouchEvent(true);
- }
-
- final int motionIndex;
- if (mMotionPosition >= 0) {
- motionIndex = mMotionPosition - mFirstPosition;
- } else {
- // If we don't have a motion position that we can reliably track,
- // pick something in the middle to make a best guess at things below.
- motionIndex = getChildCount() / 2;
- }
-
- int motionViewPrevStart = 0;
- View motionView = this.getChildAt(motionIndex);
- if (motionView != null) {
- motionViewPrevStart = getChildStartEdge(motionView);
- }
-
- boolean atEdge = scrollListItemsBy(delta);
-
- motionView = this.getChildAt(motionIndex);
- if (motionView != null) {
- final int motionViewRealStart = getChildStartEdge(motionView);
-
- if (atEdge) {
- final int overscroll = -delta - (motionViewRealStart - motionViewPrevStart);
- updateOverScrollState(delta, overscroll);
- }
- }
- }
-
- private void updateOverScrollState(int delta, int overscroll) {
- overScrollByInternal((mIsVertical ? 0 : overscroll),
- (mIsVertical ? overscroll : 0),
- (mIsVertical ? 0 : mOverScroll),
- (mIsVertical ? mOverScroll : 0),
- 0, 0,
- (mIsVertical ? 0 : mOverscrollDistance),
- (mIsVertical ? mOverscrollDistance : 0),
- true);
-
- if (Math.abs(mOverscrollDistance) == Math.abs(mOverScroll)) {
- // Break fling velocity if we impacted an edge
- if (mVelocityTracker != null) {
- mVelocityTracker.clear();
- }
- }
-
- final int overscrollMode = ViewCompat.getOverScrollMode(this);
- if (overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
- (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits())) {
- mTouchMode = TOUCH_MODE_OVERSCROLL;
-
- float pull = (float) overscroll / getSize();
- if (delta > 0) {
- mStartEdge.onPull(pull);
-
- if (!mEndEdge.isFinished()) {
- mEndEdge.onRelease();
- }
- } else if (delta < 0) {
- mEndEdge.onPull(pull);
-
- if (!mStartEdge.isFinished()) {
- mStartEdge.onRelease();
- }
- }
-
- if (delta != 0) {
- ViewCompat.postInvalidateOnAnimation(this);
- }
- }
- }
-
- private void handleOverScrollChange(int delta) {
- final int oldOverScroll = mOverScroll;
- final int newOverScroll = oldOverScroll - delta;
-
- int overScrollDistance = -delta;
- if ((newOverScroll < 0 && oldOverScroll >= 0) ||
- (newOverScroll > 0 && oldOverScroll <= 0)) {
- overScrollDistance = -oldOverScroll;
- delta += overScrollDistance;
- } else {
- delta = 0;
- }
-
- if (overScrollDistance != 0) {
- updateOverScrollState(delta, overScrollDistance);
- }
-
- if (delta != 0) {
- if (mOverScroll != 0) {
- mOverScroll = 0;
- ViewCompat.postInvalidateOnAnimation(this);
- }
-
- scrollListItemsBy(delta);
- mTouchMode = TOUCH_MODE_DRAGGING;
-
- // We did not scroll the full amount. Treat this essentially like the
- // start of a new touch scroll
- mMotionPosition = findClosestMotionRowOrColumn((int) mLastTouchPos);
- mTouchRemainderPos = 0;
- }
- }
-
- /**
- * What is the distance between the source and destination rectangles given the direction of
- * focus navigation between them? The direction basically helps figure out more quickly what is
- * self evident by the relationship between the rects...
- *
- * @param source the source rectangle
- * @param dest the destination rectangle
- * @param direction the direction
- * @return the distance between the rectangles
- */
- private static int getDistance(Rect source, Rect dest, int direction) {
- int sX, sY; // source x, y
- int dX, dY; // dest x, y
-
- switch (direction) {
- case View.FOCUS_RIGHT:
- sX = source.right;
- sY = source.top + source.height() / 2;
- dX = dest.left;
- dY = dest.top + dest.height() / 2;
- break;
-
- case View.FOCUS_DOWN:
- sX = source.left + source.width() / 2;
- sY = source.bottom;
- dX = dest.left + dest.width() / 2;
- dY = dest.top;
- break;
-
- case View.FOCUS_LEFT:
- sX = source.left;
- sY = source.top + source.height() / 2;
- dX = dest.right;
- dY = dest.top + dest.height() / 2;
- break;
-
- case View.FOCUS_UP:
- sX = source.left + source.width() / 2;
- sY = source.top;
- dX = dest.left + dest.width() / 2;
- dY = dest.bottom;
- break;
-
- case View.FOCUS_FORWARD:
- case View.FOCUS_BACKWARD:
- sX = source.right + source.width() / 2;
- sY = source.top + source.height() / 2;
- dX = dest.left + dest.width() / 2;
- dY = dest.top + dest.height() / 2;
- break;
-
- default:
- throw new IllegalArgumentException("direction must be one of "
- + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, "
- + "FOCUS_FORWARD, FOCUS_BACKWARD}.");
- }
-
- int deltaX = dX - sX;
- int deltaY = dY - sY;
-
- return deltaY * deltaY + deltaX * deltaX;
- }
-
- private int findMotionRowOrColumn(int motionPos) {
- int childCount = getChildCount();
- if (childCount == 0) {
- return INVALID_POSITION;
- }
-
- for (int i = 0; i < childCount; i++) {
- final View v = getChildAt(i);
- if (motionPos <= getChildEndEdge(v)) {
- return mFirstPosition + i;
- }
- }
-
- return INVALID_POSITION;
- }
-
- private int findClosestMotionRowOrColumn(int motionPos) {
- final int childCount = getChildCount();
- if (childCount == 0) {
- return INVALID_POSITION;
- }
-
- final int motionRow = findMotionRowOrColumn(motionPos);
- if (motionRow != INVALID_POSITION) {
- return motionRow;
- } else {
- return mFirstPosition + childCount - 1;
- }
- }
-
- @TargetApi(9)
- private int getScaledOverscrollDistance(ViewConfiguration vc) {
- if (Build.VERSION.SDK_INT < 9) {
- return 0;
- }
-
- return vc.getScaledOverscrollDistance();
- }
-
- private int getStartEdge() {
- return (mIsVertical ? getPaddingTop() : getPaddingLeft());
- }
-
- private int getEndEdge() {
- if (mIsVertical) {
- return (getHeight() - getPaddingBottom());
- } else {
- return (getWidth() - getPaddingRight());
- }
- }
-
- private int getSize() {
- return (mIsVertical ? getHeight() : getWidth());
- }
-
- private int getAvailableSize() {
- if (mIsVertical) {
- return getHeight() - getPaddingBottom() - getPaddingTop();
- } else {
- return getWidth() - getPaddingRight() - getPaddingLeft();
- }
- }
-
- private int getChildStartEdge(View child) {
- return (mIsVertical ? child.getTop() : child.getLeft());
- }
-
- private int getChildEndEdge(View child) {
- return (mIsVertical ? child.getBottom() : child.getRight());
- }
-
- private int getChildSize(View child) {
- return (mIsVertical ? child.getHeight() : child.getWidth());
- }
-
- private int getChildMeasuredSize(View child) {
- return (mIsVertical ? child.getMeasuredHeight() : child.getMeasuredWidth());
- }
-
- private int getFadingEdgeLength() {
- return (mIsVertical ? getVerticalFadingEdgeLength() : getHorizontalFadingEdgeLength());
- }
-
- private int getMinSelectionPixel(int start, int fadingEdgeLength, int selectedPosition) {
- // First pixel we can draw the selection into.
- int selectionPixelStart = start;
- if (selectedPosition > 0) {
- selectionPixelStart += fadingEdgeLength;
- }
-
- return selectionPixelStart;
- }
-
- private int getMaxSelectionPixel(int end, int fadingEdgeLength,
- int selectedPosition) {
- int selectionPixelEnd = end;
- if (selectedPosition != mItemCount - 1) {
- selectionPixelEnd -= fadingEdgeLength;
- }
-
- return selectionPixelEnd;
- }
-
- private boolean contentFits() {
- final int childCount = getChildCount();
- if (childCount == 0) {
- return true;
- }
-
- if (childCount != mItemCount) {
- return false;
- }
-
- View first = getChildAt(0);
- View last = getChildAt(childCount - 1);
-
- return (getChildStartEdge(first) >= getStartEdge() &&
- getChildEndEdge(last) <= getEndEdge());
- }
-
- private void triggerCheckForTap() {
- if (mPendingCheckForTap == null) {
- mPendingCheckForTap = new CheckForTap();
- }
-
- postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
- }
-
- private void cancelCheckForTap() {
- if (mPendingCheckForTap == null) {
- return;
- }
-
- removeCallbacks(mPendingCheckForTap);
- }
-
- private void triggerCheckForLongPress() {
- if (mPendingCheckForLongPress == null) {
- mPendingCheckForLongPress = new CheckForLongPress();
- }
-
- mPendingCheckForLongPress.rememberWindowAttachCount();
-
- postDelayed(mPendingCheckForLongPress,
- ViewConfiguration.getLongPressTimeout());
- }
-
- private void cancelCheckForLongPress() {
- if (mPendingCheckForLongPress == null) {
- return;
- }
-
- removeCallbacks(mPendingCheckForLongPress);
- }
-
- private boolean scrollListItemsBy(int incrementalDelta) {
- final int childCount = getChildCount();
- if (childCount == 0) {
- return true;
- }
-
- final int firstStart = getChildStartEdge(getChildAt(0));
- final int lastEnd = getChildEndEdge(getChildAt(childCount - 1));
-
- final int paddingTop = getPaddingTop();
- final int paddingLeft = getPaddingLeft();
-
- final int paddingStart = (mIsVertical ? paddingTop : paddingLeft);
-
- final int spaceBefore = paddingStart - firstStart;
- final int end = getEndEdge();
- final int spaceAfter = lastEnd - end;
-
- final int size = getAvailableSize();
-
- if (incrementalDelta < 0) {
- incrementalDelta = Math.max(-(size - 1), incrementalDelta);
- } else {
- incrementalDelta = Math.min(size - 1, incrementalDelta);
- }
-
- final int firstPosition = mFirstPosition;
-
- final boolean cannotScrollDown = (firstPosition == 0 &&
- firstStart >= paddingStart && incrementalDelta >= 0);
- final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&
- lastEnd <= end && incrementalDelta <= 0);
-
- if (cannotScrollDown || cannotScrollUp) {
- return incrementalDelta != 0;
- }
-
- final boolean inTouchMode = isInTouchMode();
- if (inTouchMode) {
- hideSelector();
- }
-
- int start = 0;
- int count = 0;
-
- final boolean down = (incrementalDelta < 0);
- if (down) {
- int childrenStart = -incrementalDelta + paddingStart;
-
- for (int i = 0; i < childCount; i++) {
- final View child = getChildAt(i);
- final int childEnd = getChildEndEdge(child);
-
- if (childEnd >= childrenStart) {
- break;
- }
-
- count++;
- mRecycler.addScrapView(child, firstPosition + i);
- }
- } else {
- int childrenEnd = end - incrementalDelta;
-
- for (int i = childCount - 1; i >= 0; i--) {
- final View child = getChildAt(i);
- final int childStart = getChildStartEdge(child);
-
- if (childStart <= childrenEnd) {
- break;
- }
-
- start = i;
- count++;
- mRecycler.addScrapView(child, firstPosition + i);
- }
- }
-
- mBlockLayoutRequests = true;
-
- if (count > 0) {
- detachViewsFromParent(start, count);
- }
-
- // invalidate before moving the children to avoid unnecessary invalidate
- // calls to bubble up from the children all the way to the top
- if (!awakenScrollbarsInternal()) {
- invalidate();
- }
-
- offsetChildren(incrementalDelta);
-
- if (down) {
- mFirstPosition += count;
- }
-
- final int absIncrementalDelta = Math.abs(incrementalDelta);
- if (spaceBefore < absIncrementalDelta || spaceAfter < absIncrementalDelta) {
- fillGap(down);
- }
-
- if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
- final int childIndex = mSelectedPosition - mFirstPosition;
- if (childIndex >= 0 && childIndex < getChildCount()) {
- positionSelector(mSelectedPosition, getChildAt(childIndex));
- }
- } else if (mSelectorPosition != INVALID_POSITION) {
- final int childIndex = mSelectorPosition - mFirstPosition;
- if (childIndex >= 0 && childIndex < getChildCount()) {
- positionSelector(INVALID_POSITION, getChildAt(childIndex));
- }
- } else {
- mSelectorRect.setEmpty();
- }
-
- mBlockLayoutRequests = false;
-
- invokeOnItemScrollListener();
-
- return false;
- }
-
- @TargetApi(14)
- private final float getCurrVelocity() {
- if (Build.VERSION.SDK_INT >= 14) {
- return mScroller.getCurrVelocity();
- }
-
- return 0;
- }
-
- @TargetApi(5)
- private boolean awakenScrollbarsInternal() {
- return (Build.VERSION.SDK_INT >= 5) && super.awakenScrollBars();
- }
-
- @Override
- public void computeScroll() {
- if (!mScroller.computeScrollOffset()) {
- return;
- }
-
- final int pos;
- if (mIsVertical) {
- pos = mScroller.getCurrY();
- } else {
- pos = mScroller.getCurrX();
- }
-
- final int diff = (int) (pos - mLastTouchPos);
- mLastTouchPos = pos;
-
- final boolean stopped = scrollListItemsBy(diff);
-
- if (!stopped && !mScroller.isFinished()) {
- ViewCompat.postInvalidateOnAnimation(this);
- } else {
- if (stopped) {
- final int overScrollMode = ViewCompat.getOverScrollMode(this);
- if (overScrollMode != ViewCompat.OVER_SCROLL_NEVER) {
- final EdgeEffectCompat edge =
- (diff > 0 ? mStartEdge : mEndEdge);
-
- boolean needsInvalidate =
- edge.onAbsorb(Math.abs((int) getCurrVelocity()));
-
- if (needsInvalidate) {
- ViewCompat.postInvalidateOnAnimation(this);
- }
- }
-
- finishSmoothScrolling();
- }
-
- mTouchMode = TOUCH_MODE_REST;
- reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
- }
- }
-
- private void finishEdgeGlows() {
- if (mStartEdge != null) {
- mStartEdge.finish();
- }
-
- if (mEndEdge != null) {
- mEndEdge.finish();
- }
- }
-
- private boolean drawStartEdge(Canvas canvas) {
- if (mStartEdge.isFinished()) {
- return false;
- }
-
- if (mIsVertical) {
- return mStartEdge.draw(canvas);
- }
-
- final int restoreCount = canvas.save();
- final int height = getHeight();
-
- canvas.translate(0, height);
- canvas.rotate(270);
-
- final boolean needsInvalidate = mStartEdge.draw(canvas);
- canvas.restoreToCount(restoreCount);
- return needsInvalidate;
- }
-
- private boolean drawEndEdge(Canvas canvas) {
- if (mEndEdge.isFinished()) {
- return false;
- }
-
- final int restoreCount = canvas.save();
- final int width = getWidth();
- final int height = getHeight();
-
- if (mIsVertical) {
- canvas.translate(-width, height);
- canvas.rotate(180, width, 0);
- } else {
- canvas.translate(width, 0);
- canvas.rotate(90);
- }
-
- final boolean needsInvalidate = mEndEdge.draw(canvas);
- canvas.restoreToCount(restoreCount);
- return needsInvalidate;
- }
-
- private void finishSmoothScrolling() {
- mTouchMode = TOUCH_MODE_REST;
- reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
-
- mScroller.abortAnimation();
- if (mPositionScroller != null) {
- mPositionScroller.stop();
- }
- }
-
- private void drawSelector(Canvas canvas) {
- if (!mSelectorRect.isEmpty()) {
- final Drawable selector = mSelector;
- selector.setBounds(mSelectorRect);
- selector.draw(canvas);
- }
- }
-
- private void useDefaultSelector() {
- setSelector(getResources().getDrawable(
- android.R.drawable.list_selector_background));
- }
-
- private boolean shouldShowSelector() {
- return (hasFocus() && !isInTouchMode()) || touchModeDrawsInPressedState();
- }
-
- private void positionSelector(int position, View selected) {
- if (position != INVALID_POSITION) {
- mSelectorPosition = position;
- }
-
- mSelectorRect.set(selected.getLeft(), selected.getTop(), selected.getRight(),
- selected.getBottom());
-
- final boolean isChildViewEnabled = mIsChildViewEnabled;
- if (selected.isEnabled() != isChildViewEnabled) {
- mIsChildViewEnabled = !isChildViewEnabled;
-
- if (getSelectedItemPosition() != INVALID_POSITION) {
- refreshDrawableState();
- }
- }
- }
-
- private void hideSelector() {
- if (mSelectedPosition != INVALID_POSITION) {
- if (mLayoutMode != LAYOUT_SPECIFIC) {
- mResurrectToPosition = mSelectedPosition;
- }
-
- if (mNextSelectedPosition >= 0 && mNextSelectedPosition != mSelectedPosition) {
- mResurrectToPosition = mNextSelectedPosition;
- }
-
- setSelectedPositionInt(INVALID_POSITION);
- setNextSelectedPositionInt(INVALID_POSITION);
-
- mSelectedStart = 0;
- }
- }
-
- private void setSelectedPositionInt(int position) {
- mSelectedPosition = position;
- mSelectedRowId = getItemIdAtPosition(position);
- }
-
- private void setSelectionInt(int position) {
- setNextSelectedPositionInt(position);
- boolean awakeScrollbars = false;
-
- final int selectedPosition = mSelectedPosition;
- if (selectedPosition >= 0) {
- if (position == selectedPosition - 1) {
- awakeScrollbars = true;
- } else if (position == selectedPosition + 1) {
- awakeScrollbars = true;
- }
- }
-
- layoutChildren();
-
- if (awakeScrollbars) {
- awakenScrollbarsInternal();
- }
- }
-
- private void setNextSelectedPositionInt(int position) {
- mNextSelectedPosition = position;
- mNextSelectedRowId = getItemIdAtPosition(position);
-
- // If we are trying to sync to the selection, update that too
- if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) {
- mSyncPosition = position;
- mSyncRowId = mNextSelectedRowId;
- }
- }
-
- private boolean touchModeDrawsInPressedState() {
- switch (mTouchMode) {
- case TOUCH_MODE_TAP:
- case TOUCH_MODE_DONE_WAITING:
- return true;
- default:
- return false;
- }
- }
-
- /**
- * Sets the selector state to "pressed" and posts a CheckForKeyLongPress to see if
- * this is a long press.
- */
- private void keyPressed() {
- if (!isEnabled() || !isClickable()) {
- return;
- }
-
- final Drawable selector = mSelector;
- final Rect selectorRect = mSelectorRect;
-
- if (selector != null && (isFocused() || touchModeDrawsInPressedState())
- && !selectorRect.isEmpty()) {
-
- final View child = getChildAt(mSelectedPosition - mFirstPosition);
-
- if (child != null) {
- if (child.hasFocusable()) {
- return;
- }
-
- child.setPressed(true);
- }
-
- setPressed(true);
-
- final boolean longClickable = isLongClickable();
- final Drawable d = selector.getCurrent();
- if (d != null && d instanceof TransitionDrawable) {
- if (longClickable) {
- ((TransitionDrawable) d).startTransition(
- ViewConfiguration.getLongPressTimeout());
- } else {
- ((TransitionDrawable) d).resetTransition();
- }
- }
-
- if (longClickable && !mDataChanged) {
- if (mPendingCheckForKeyLongPress == null) {
- mPendingCheckForKeyLongPress = new CheckForKeyLongPress();
- }
-
- mPendingCheckForKeyLongPress.rememberWindowAttachCount();
- postDelayed(mPendingCheckForKeyLongPress, ViewConfiguration.getLongPressTimeout());
- }
- }
- }
-
- private void updateSelectorState() {
- if (mSelector != null) {
- if (shouldShowSelector()) {
- mSelector.setState(getDrawableState());
- } else {
- mSelector.setState(STATE_NOTHING);
- }
- }
- }
-
- private void checkSelectionChanged() {
- if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) {
- selectionChanged();
- mOldSelectedPosition = mSelectedPosition;
- mOldSelectedRowId = mSelectedRowId;
- }
- }
-
- private void selectionChanged() {
- OnItemSelectedListener listener = getOnItemSelectedListener();
- if (listener == null) {
- return;
- }
-
- if (mInLayout || mBlockLayoutRequests) {
- // If we are in a layout traversal, defer notification
- // by posting. This ensures that the view tree is
- // in a consistent state and is able to accommodate
- // new layout or invalidate requests.
- if (mSelectionNotifier == null) {
- mSelectionNotifier = new SelectionNotifier();
- }
-
- post(mSelectionNotifier);
- } else {
- fireOnSelected();
- performAccessibilityActionsOnSelected();
- }
- }
-
- private void fireOnSelected() {
- OnItemSelectedListener listener = getOnItemSelectedListener();
- if (listener == null) {
- return;
- }
-
- final int selection = getSelectedItemPosition();
- if (selection >= 0) {
- View v = getSelectedView();
- listener.onItemSelected(this, v, selection,
- mAdapter.getItemId(selection));
- } else {
- listener.onNothingSelected(this);
- }
- }
-
- private void performAccessibilityActionsOnSelected() {
- final int position = getSelectedItemPosition();
- if (position >= 0) {
- // We fire selection events here not in View
- sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
- }
- }
-
- private int lookForSelectablePosition(int position) {
- return lookForSelectablePosition(position, true);
- }
-
- private int lookForSelectablePosition(int position, boolean lookDown) {
- final ListAdapter adapter = mAdapter;
- if (adapter == null || isInTouchMode()) {
- return INVALID_POSITION;
- }
-
- final int itemCount = mItemCount;
- if (!mAreAllItemsSelectable) {
- if (lookDown) {
- position = Math.max(0, position);
- while (position < itemCount && !adapter.isEnabled(position)) {
- position++;
- }
- } else {
- position = Math.min(position, itemCount - 1);
- while (position >= 0 && !adapter.isEnabled(position)) {
- position--;
- }
- }
-
- if (position < 0 || position >= itemCount) {
- return INVALID_POSITION;
- }
-
- return position;
- } else {
- if (position < 0 || position >= itemCount) {
- return INVALID_POSITION;
- }
-
- return position;
- }
- }
-
- /**
- * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
- * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
- * current view orientation.
- *
- * @return The position of the next selectable position of the views that
- * are currently visible, taking into account the fact that there might
- * be no selection. Returns {@link #INVALID_POSITION} if there is no
- * selectable view on screen in the given direction.
- */
- private int lookForSelectablePositionOnScreen(int direction) {
- forceValidFocusDirection(direction);
-
- final int firstPosition = mFirstPosition;
- final ListAdapter adapter = getAdapter();
-
- if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
- int startPos = (mSelectedPosition != INVALID_POSITION ?
- mSelectedPosition + 1 : firstPosition);
-
- if (startPos >= adapter.getCount()) {
- return INVALID_POSITION;
- }
-
- if (startPos < firstPosition) {
- startPos = firstPosition;
- }
-
- final int lastVisiblePos = getLastVisiblePosition();
-
- for (int pos = startPos; pos <= lastVisiblePos; pos++) {
- if (adapter.isEnabled(pos)
- && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) {
- return pos;
- }
- }
- } else {
- final int last = firstPosition + getChildCount() - 1;
-
- int startPos = (mSelectedPosition != INVALID_POSITION) ?
- mSelectedPosition - 1 : firstPosition + getChildCount() - 1;
-
- if (startPos < 0 || startPos >= adapter.getCount()) {
- return INVALID_POSITION;
- }
-
- if (startPos > last) {
- startPos = last;
- }
-
- for (int pos = startPos; pos >= firstPosition; pos--) {
- if (adapter.isEnabled(pos)
- && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) {
- return pos;
- }
- }
- }
-
- return INVALID_POSITION;
- }
-
- @Override
- protected void drawableStateChanged() {
- super.drawableStateChanged();
- updateSelectorState();
- }
-
- @Override
- protected int[] onCreateDrawableState(int extraSpace) {
- // If the child view is enabled then do the default behavior.
- if (mIsChildViewEnabled) {
- // Common case
- return super.onCreateDrawableState(extraSpace);
- }
-
- // The selector uses this View's drawable state. The selected child view
- // is disabled, so we need to remove the enabled state from the drawable
- // states.
- final int enabledState = ENABLED_STATE_SET[0];
-
- // If we don't have any extra space, it will return one of the static state arrays,
- // and clearing the enabled state on those arrays is a bad thing! If we specify
- // we need extra space, it will create+copy into a new array that safely mutable.
- int[] state = super.onCreateDrawableState(extraSpace + 1);
- int enabledPos = -1;
- for (int i = state.length - 1; i >= 0; i--) {
- if (state[i] == enabledState) {
- enabledPos = i;
- break;
- }
- }
-
- // Remove the enabled state
- if (enabledPos >= 0) {
- System.arraycopy(state, enabledPos + 1, state, enabledPos,
- state.length - enabledPos - 1);
- }
-
- return state;
- }
-
- @Override
- protected boolean canAnimate() {
- return (super.canAnimate() && mItemCount > 0);
- }
-
- @Override
- protected void dispatchDraw(Canvas canvas) {
- final boolean drawSelectorOnTop = mDrawSelectorOnTop;
- if (!drawSelectorOnTop) {
- drawSelector(canvas);
- }
-
- super.dispatchDraw(canvas);
-
- if (drawSelectorOnTop) {
- drawSelector(canvas);
- }
- }
-
- @Override
- public void draw(Canvas canvas) {
- super.draw(canvas);
-
- boolean needsInvalidate = false;
-
- if (mStartEdge != null) {
- needsInvalidate |= drawStartEdge(canvas);
- }
-
- if (mEndEdge != null) {
- needsInvalidate |= drawEndEdge(canvas);
- }
-
- if (needsInvalidate) {
- ViewCompat.postInvalidateOnAnimation(this);
- }
- }
-
- @Override
- public void requestLayout() {
- if (!mInLayout && !mBlockLayoutRequests) {
- super.requestLayout();
- }
- }
-
- @Override
- public View getSelectedView() {
- if (mItemCount > 0 && mSelectedPosition >= 0) {
- return getChildAt(mSelectedPosition - mFirstPosition);
- } else {
- return null;
- }
- }
-
- @Override
- public void setSelection(int position) {
- setSelectionFromOffset(position, 0);
- }
-
- public void setSelectionFromOffset(int position, int offset) {
- if (mAdapter == null) {
- return;
- }
-
- if (!isInTouchMode()) {
- position = lookForSelectablePosition(position);
- if (position >= 0) {
- setNextSelectedPositionInt(position);
- }
- } else {
- mResurrectToPosition = position;
- }
-
- if (position >= 0) {
- mLayoutMode = LAYOUT_SPECIFIC;
-
- if (mIsVertical) {
- mSpecificStart = getPaddingTop() + offset;
- } else {
- mSpecificStart = getPaddingLeft() + offset;
- }
-
- if (mNeedSync) {
- mSyncPosition = position;
- mSyncRowId = mAdapter.getItemId(position);
- }
-
- requestLayout();
- }
- }
-
- public void scrollBy(int offset) {
- scrollListItemsBy(-offset);
- }
-
- /**
- * Smoothly scroll to the specified adapter position. The view will
- * scroll such that the indicated position is displayed.
- * @param position Scroll to this adapter position.
- */
- public void smoothScrollToPosition(int position) {
- if (mPositionScroller == null) {
- mPositionScroller = new PositionScroller();
- }
- mPositionScroller.start(position);
- }
-
- /**
- * Smoothly scroll to the specified adapter position. The view will scroll
- * such that the indicated position is displayed <code>offset</code> pixels from
- * the top/left edge of the view, according to the orientation. If this is
- * impossible, (e.g. the offset would scroll the first or last item beyond the boundaries
- * of the list) it will get as close as possible. The scroll will take
- * <code>duration</code> milliseconds to complete.
- *
- * @param position Position to scroll to
- * @param offset Desired distance in pixels of <code>position</code> from the top/left
- * of the view when scrolling is finished
- * @param duration Number of milliseconds to use for the scroll
- */
- public void smoothScrollToPositionFromOffset(int position, int offset, int duration) {
- if (mPositionScroller == null) {
- mPositionScroller = new PositionScroller();
- }
- mPositionScroller.startWithOffset(position, offset, duration);
- }
-
- /**
- * Smoothly scroll to the specified adapter position. The view will scroll
- * such that the indicated position is displayed <code>offset</code> pixels from
- * the top edge of the view. If this is impossible, (e.g. the offset would scroll
- * the first or last item beyond the boundaries of the list) it will get as close
- * as possible.
- *
- * @param position Position to scroll to
- * @param offset Desired distance in pixels of <code>position</code> from the top
- * of the view when scrolling is finished
- */
- public void smoothScrollToPositionFromOffset(int position, int offset) {
- if (mPositionScroller == null) {
- mPositionScroller = new PositionScroller();
- }
- mPositionScroller.startWithOffset(position, offset);
- }
-
- /**
- * Smoothly scroll to the specified adapter position. The view will
- * scroll such that the indicated position is displayed, but it will
- * stop early if scrolling further would scroll boundPosition out of
- * view.
- *
- * @param position Scroll to this adapter position.
- * @param boundPosition Do not scroll if it would move this adapter
- * position out of view.
- */
- public void smoothScrollToPosition(int position, int boundPosition) {
- if (mPositionScroller == null) {
- mPositionScroller = new PositionScroller();
- }
- mPositionScroller.start(position, boundPosition);
- }
-
- /**
- * Smoothly scroll by distance pixels over duration milliseconds.
- * @param distance Distance to scroll in pixels.
- * @param duration Duration of the scroll animation in milliseconds.
- */
- public void smoothScrollBy(int distance, int duration) {
- // No sense starting to scroll if we're not going anywhere
- final int firstPosition = mFirstPosition;
- final int childCount = getChildCount();
- final int lastPosition = firstPosition + childCount;
- final int start = getStartEdge();
- final int end = getEndEdge();
-
- if (distance == 0 || mItemCount == 0 || childCount == 0 ||
- (firstPosition == 0 && getChildStartEdge(getChildAt(0)) == start && distance < 0) ||
- (lastPosition == mItemCount &&
- getChildEndEdge(getChildAt(childCount - 1)) == end && distance > 0)) {
- finishSmoothScrolling();
- } else {
- mScroller.startScroll(0, 0,
- mIsVertical ? 0 : -distance,
- mIsVertical ? -distance : 0,
- duration);
-
- mLastTouchPos = 0;
-
- mTouchMode = TOUCH_MODE_FLINGING;
- reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
-
- ViewCompat.postInvalidateOnAnimation(this);
- }
- }
-
- @Override
- public boolean dispatchKeyEvent(KeyEvent event) {
- // Dispatch in the normal way
- boolean handled = super.dispatchKeyEvent(event);
- if (!handled) {
- // If we didn't handle it...
- final View focused = getFocusedChild();
- if (focused != null && event.getAction() == KeyEvent.ACTION_DOWN) {
- // ... and our focused child didn't handle it
- // ... give it to ourselves so we can scroll if necessary
- handled = onKeyDown(event.getKeyCode(), event);
- }
- }
-
- return handled;
- }
-
- @Override
- protected void dispatchSetPressed(boolean pressed) {
- // Don't dispatch setPressed to our children. We call setPressed on ourselves to
- // get the selector in the right state, but we don't want to press each child.
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- if (mSelector == null) {
- useDefaultSelector();
- }
-
- int widthMode = MeasureSpec.getMode(widthMeasureSpec);
- int heightMode = MeasureSpec.getMode(heightMeasureSpec);
- int widthSize = MeasureSpec.getSize(widthMeasureSpec);
- int heightSize = MeasureSpec.getSize(heightMeasureSpec);
-
- int childWidth = 0;
- int childHeight = 0;
-
- mItemCount = (mAdapter == null ? 0 : mAdapter.getCount());
- if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED ||
- heightMode == MeasureSpec.UNSPECIFIED)) {
- final View child = obtainView(0, mIsScrap);
-
- final int secondaryMeasureSpec =
- (mIsVertical ? widthMeasureSpec : heightMeasureSpec);
-
- measureScrapChild(child, 0, secondaryMeasureSpec);
-
- childWidth = child.getMeasuredWidth();
- childHeight = child.getMeasuredHeight();
-
- if (recycleOnMeasure()) {
- mRecycler.addScrapView(child, -1);
- }
- }
-
- if (widthMode == MeasureSpec.UNSPECIFIED) {
- widthSize = getPaddingLeft() + getPaddingRight() + childWidth;
- if (mIsVertical) {
- widthSize += getVerticalScrollbarWidth();
- }
- }
-
- if (heightMode == MeasureSpec.UNSPECIFIED) {
- heightSize = getPaddingTop() + getPaddingBottom() + childHeight;
- if (!mIsVertical) {
- heightSize += getHorizontalScrollbarHeight();
- }
- }
-
- if (mIsVertical && heightMode == MeasureSpec.AT_MOST) {
- heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
- }
-
- if (!mIsVertical && widthMode == MeasureSpec.AT_MOST) {
- widthSize = measureWidthOfChildren(heightMeasureSpec, 0, NO_POSITION, widthSize, -1);
- }
-
- setMeasuredDimension(widthSize, heightSize);
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- mInLayout = true;
-
- if (changed) {
- final int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- getChildAt(i).forceLayout();
- }
-
- mRecycler.markChildrenDirty();
- }
-
- layoutChildren();
-
- mInLayout = false;
-
- final int width = r - l - getPaddingLeft() - getPaddingRight();
- final int height = b - t - getPaddingTop() - getPaddingBottom();
-
- if (mStartEdge != null && mEndEdge != null) {
- if (mIsVertical) {
- mStartEdge.setSize(width, height);
- mEndEdge.setSize(width, height);
- } else {
- mStartEdge.setSize(height, width);
- mEndEdge.setSize(height, width);
- }
- }
- }
-
- private void layoutChildren() {
- if (getWidth() == 0 || getHeight() == 0) {
- return;
- }
-
- final boolean blockLayoutRequests = mBlockLayoutRequests;
- if (!blockLayoutRequests) {
- mBlockLayoutRequests = true;
- } else {
- return;
- }
-
- try {
- invalidate();
-
- if (mAdapter == null) {
- resetState();
- return;
- }
-
- final int start = getStartEdge();
- final int end = getEndEdge();
-
- int childCount = getChildCount();
- int index = 0;
- int delta = 0;
-
- View focusLayoutRestoreView = null;
-
- View selected = null;
- View oldSelected = null;
- View newSelected = null;
- View oldFirstChild = null;
-
- switch (mLayoutMode) {
- case LAYOUT_SET_SELECTION:
- index = mNextSelectedPosition - mFirstPosition;
- if (index >= 0 && index < childCount) {
- newSelected = getChildAt(index);
- }
-
- break;
-
- case LAYOUT_FORCE_TOP:
- case LAYOUT_FORCE_BOTTOM:
- case LAYOUT_SPECIFIC:
- case LAYOUT_SYNC:
- break;
-
- case LAYOUT_MOVE_SELECTION:
- default:
- // Remember the previously selected view
- index = mSelectedPosition - mFirstPosition;
- if (index >= 0 && index < childCount) {
- oldSelected = getChildAt(index);
- }
-
- // Remember the previous first child
- oldFirstChild = getChildAt(0);
-
- if (mNextSelectedPosition >= 0) {
- delta = mNextSelectedPosition - mSelectedPosition;
- }
-
- // Caution: newSelected might be null
- newSelected = getChildAt(index + delta);
- }
-
- final boolean dataChanged = mDataChanged;
- if (dataChanged) {
- handleDataChanged();
- }
-
- // Handle the empty set by removing all views that are visible
- // and calling it a day
- if (mItemCount == 0) {
- resetState();
- return;
- } else if (mItemCount != mAdapter.getCount()) {
- throw new IllegalStateException("The content of the adapter has changed but "
- + "TwoWayView did not receive a notification. Make sure the content of "
- + "your adapter is not modified from a background thread, but only "
- + "from the UI thread. [in TwoWayView(" + getId() + ", " + getClass()
- + ") with Adapter(" + mAdapter.getClass() + ")]");
- }
-
- setSelectedPositionInt(mNextSelectedPosition);
-
- // Reset the focus restoration
- View focusLayoutRestoreDirectChild = null;
-
- // Pull all children into the RecycleBin.
- // These views will be reused if possible
- final int firstPosition = mFirstPosition;
- final RecycleBin recycleBin = mRecycler;
-
- if (dataChanged) {
- for (int i = 0; i < childCount; i++) {
- recycleBin.addScrapView(getChildAt(i), firstPosition + i);
- }
- } else {
- recycleBin.fillActiveViews(childCount, firstPosition);
- }
-
- // Take focus back to us temporarily to avoid the eventual
- // call to clear focus when removing the focused child below
- // from messing things up when ViewAncestor assigns focus back
- // to someone else.
- final View focusedChild = getFocusedChild();
- if (focusedChild != null) {
- // We can remember the focused view to restore after relayout if the
- // data hasn't changed, or if the focused position is a header or footer.
- if (!dataChanged) {
- focusLayoutRestoreDirectChild = focusedChild;
-
- // Remember the specific view that had focus
- focusLayoutRestoreView = findFocus();
- if (focusLayoutRestoreView != null) {
- // Tell it we are going to mess with it
- focusLayoutRestoreView.onStartTemporaryDetach();
- }
- }
-
- requestFocus();
- }
-
- // FIXME: We need a way to save current accessibility focus here
- // so that it can be restored after we re-attach the children on each
- // layout round.
-
- detachAllViewsFromParent();
-
- switch (mLayoutMode) {
- case LAYOUT_SET_SELECTION:
- if (newSelected != null) {
- final int newSelectedStart = getChildStartEdge(newSelected);
- selected = fillFromSelection(newSelectedStart, start, end);
- } else {
- selected = fillFromMiddle(start, end);
- }
-
- break;
-
- case LAYOUT_SYNC:
- selected = fillSpecific(mSyncPosition, mSpecificStart);
- break;
-
- case LAYOUT_FORCE_BOTTOM:
- selected = fillBefore(mItemCount - 1, end);
- adjustViewsStartOrEnd();
- break;
-
- case LAYOUT_FORCE_TOP:
- mFirstPosition = 0;
- selected = fillFromOffset(start);
- adjustViewsStartOrEnd();
- break;
-
- case LAYOUT_SPECIFIC:
- selected = fillSpecific(reconcileSelectedPosition(), mSpecificStart);
- break;
-
- case LAYOUT_MOVE_SELECTION:
- selected = moveSelection(oldSelected, newSelected, delta, start, end);
- break;
-
- default:
- if (childCount == 0) {
- final int position = lookForSelectablePosition(0);
- setSelectedPositionInt(position);
- selected = fillFromOffset(start);
- } else {
- if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
- int offset = start;
- if (oldSelected != null) {
- offset = getChildStartEdge(oldSelected);
- }
- selected = fillSpecific(mSelectedPosition, offset);
- } else if (mFirstPosition < mItemCount) {
- int offset = start;
- if (oldFirstChild != null) {
- offset = getChildStartEdge(oldFirstChild);
- }
-
- selected = fillSpecific(mFirstPosition, offset);
- } else {
- selected = fillSpecific(0, start);
- }
- }
-
- break;
-
- }
-
- recycleBin.scrapActiveViews();
-
- if (selected != null) {
- if (mItemsCanFocus && hasFocus() && !selected.hasFocus()) {
- final boolean focusWasTaken = (selected == focusLayoutRestoreDirectChild &&
- focusLayoutRestoreView != null &&
- focusLayoutRestoreView.requestFocus()) || selected.requestFocus();
-
- if (!focusWasTaken) {
- // Selected item didn't take focus, fine, but still want
- // to make sure something else outside of the selected view
- // has focus
- final View focused = getFocusedChild();
- if (focused != null) {
- focused.clearFocus();
- }
-
- positionSelector(INVALID_POSITION, selected);
- } else {
- selected.setSelected(false);
- mSelectorRect.setEmpty();
- }
- } else {
- positionSelector(INVALID_POSITION, selected);
- }
-
- mSelectedStart = getChildStartEdge(selected);
- } else {
- if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_DRAGGING) {
- View child = getChildAt(mMotionPosition - mFirstPosition);
-
- if (child != null) {
- positionSelector(mMotionPosition, child);
- }
- } else {
- mSelectedStart = 0;
- mSelectorRect.setEmpty();
- }
-
- // Even if there is not selected position, we may need to restore
- // focus (i.e. something focusable in touch mode)
- if (hasFocus() && focusLayoutRestoreView != null) {
- focusLayoutRestoreView.requestFocus();
- }
- }
-
- // Tell focus view we are done mucking with it, if it is still in
- // our view hierarchy.
- if (focusLayoutRestoreView != null
- && focusLayoutRestoreView.getWindowToken() != null) {
- focusLayoutRestoreView.onFinishTemporaryDetach();
- }
-
- mLayoutMode = LAYOUT_NORMAL;
- mDataChanged = false;
- mNeedSync = false;
-
- setNextSelectedPositionInt(mSelectedPosition);
- if (mItemCount > 0) {
- checkSelectionChanged();
- }
-
- invokeOnItemScrollListener();
- } finally {
- if (!blockLayoutRequests) {
- mBlockLayoutRequests = false;
- mDataChanged = false;
- }
- }
- }
-
- protected boolean recycleOnMeasure() {
- return true;
- }
-
- private void offsetChildren(int offset) {
- final int childCount = getChildCount();
-
- for (int i = 0; i < childCount; i++) {
- final View child = getChildAt(i);
-
- if (mIsVertical) {
- child.offsetTopAndBottom(offset);
- } else {
- child.offsetLeftAndRight(offset);
- }
- }
- }
-
- private View moveSelection(View oldSelected, View newSelected, int delta, int start,
- int end) {
- final int fadingEdgeLength = getFadingEdgeLength();
- final int selectedPosition = mSelectedPosition;
-
- final int oldSelectedStart = getChildStartEdge(oldSelected);
- final int oldSelectedEnd = getChildEndEdge(oldSelected);
-
- final int minStart = getMinSelectionPixel(start, fadingEdgeLength, selectedPosition);
- final int maxEnd = getMaxSelectionPixel(end, fadingEdgeLength, selectedPosition);
-
- View selected = null;
-
- if (delta > 0) {
- /*
- * Case 1: Scrolling down.
- */
-
- /*
- * Before After
- * | | | |
- * +-------+ +-------+
- * | A | | A |
- * | 1 | => +-------+
- * +-------+ | B |
- * | B | | 2 |
- * +-------+ +-------+
- * | | | |
- *
- * Try to keep the top of the previously selected item where it was.
- * oldSelected = A
- * selected = B
- */
-
- // Put oldSelected (A) where it belongs
- oldSelected = makeAndAddView(selectedPosition - 1, oldSelectedStart, true, false);
-
- final int itemMargin = mItemMargin;
-
- // Now put the new selection (B) below that
- selected = makeAndAddView(selectedPosition, oldSelectedEnd + itemMargin, true, true);
-
- final int selectedStart = getChildStartEdge(selected);
- final int selectedEnd = getChildEndEdge(selected);
-
- // Some of the newly selected item extends below the bottom of the list
- if (selectedEnd > end) {
- // Find space available above the selection into which we can scroll upwards
- final int spaceBefore = selectedStart - minStart;
-
- // Find space required to bring the bottom of the selected item fully into view
- final int spaceAfter = selectedEnd - maxEnd;
-
- // Don't scroll more than half the size of the list
- final int halfSpace = (end - start) / 2;
- int offset = Math.min(spaceBefore, spaceAfter);
- offset = Math.min(offset, halfSpace);
-
- if (mIsVertical) {
- oldSelected.offsetTopAndBottom(-offset);
- selected.offsetTopAndBottom(-offset);
- } else {
- oldSelected.offsetLeftAndRight(-offset);
- selected.offsetLeftAndRight(-offset);
- }
- }
-
- // Fill in views before and after
- fillBefore(mSelectedPosition - 2, selectedStart - itemMargin);
- adjustViewsStartOrEnd();
- fillAfter(mSelectedPosition + 1, selectedEnd + itemMargin);
- } else if (delta < 0) {
- /*
- * Case 2: Scrolling up.
- */
-
- /*
- * Before After
- * | | | |
- * +-------+ +-------+
- * | A | | A |
- * +-------+ => | 1 |
- * | B | +-------+
- * | 2 | | B |
- * +-------+ +-------+
- * | | | |
- *
- * Try to keep the top of the item about to become selected where it was.
- * newSelected = A
- * olSelected = B
- */
-
- if (newSelected != null) {
- // Try to position the top of newSel (A) where it was before it was selected
- final int newSelectedStart = getChildStartEdge(newSelected);
- selected = makeAndAddView(selectedPosition, newSelectedStart, true, true);
- } else {
- // If (A) was not on screen and so did not have a view, position
- // it above the oldSelected (B)
- selected = makeAndAddView(selectedPosition, oldSelectedStart, false, true);
- }
-
- final int selectedStart = getChildStartEdge(selected);
- final int selectedEnd = getChildEndEdge(selected);
-
- // Some of the newly selected item extends above the top of the list
- if (selectedStart < minStart) {
- // Find space required to bring the top of the selected item fully into view
- final int spaceBefore = minStart - selectedStart;
-
- // Find space available below the selection into which we can scroll downwards
- final int spaceAfter = maxEnd - selectedEnd;
-
- // Don't scroll more than half the height of the list
- final int halfSpace = (end - start) / 2;
- int offset = Math.min(spaceBefore, spaceAfter);
- offset = Math.min(offset, halfSpace);
-
- if (mIsVertical) {
- selected.offsetTopAndBottom(offset);
- } else {
- selected.offsetLeftAndRight(offset);
- }
- }
-
- // Fill in views above and below
- fillBeforeAndAfter(selected, selectedPosition);
- } else {
- /*
- * Case 3: Staying still
- */
-
- selected = makeAndAddView(selectedPosition, oldSelectedStart, true, true);
-
- final int selectedStart = getChildStartEdge(selected);
- final int selectedEnd = getChildEndEdge(selected);
-
- // We're staying still...
- if (oldSelectedStart < start) {
- // ... but the top of the old selection was off screen.
- // (This can happen if the data changes size out from under us)
- int newEnd = selectedEnd;
- if (newEnd < start + 20) {
- // Not enough visible -- bring it onscreen
- if (mIsVertical) {
- selected.offsetTopAndBottom(start - selectedStart);
- } else {
- selected.offsetLeftAndRight(start - selectedStart);
- }
- }
- }
-
- // Fill in views above and below
- fillBeforeAndAfter(selected, selectedPosition);
- }
-
- return selected;
- }
-
- void confirmCheckedPositionsById() {
- // Clear out the positional check states, we'll rebuild it below from IDs.
- mCheckStates.clear();
-
- for (int checkedIndex = 0; checkedIndex < mCheckedIdStates.size(); checkedIndex++) {
- final long id = mCheckedIdStates.keyAt(checkedIndex);
- final int lastPos = mCheckedIdStates.valueAt(checkedIndex);
-
- final long lastPosId = mAdapter.getItemId(lastPos);
- if (id != lastPosId) {
- // Look around to see if the ID is nearby. If not, uncheck it.
- final int start = Math.max(0, lastPos - CHECK_POSITION_SEARCH_DISTANCE);
- final int end = Math.min(lastPos + CHECK_POSITION_SEARCH_DISTANCE, mItemCount);
- boolean found = false;
-
- for (int searchPos = start; searchPos < end; searchPos++) {
- final long searchId = mAdapter.getItemId(searchPos);
- if (id == searchId) {
- found = true;
- mCheckStates.put(searchPos, true);
- mCheckedIdStates.setValueAt(checkedIndex, searchPos);
- break;
- }
- }
-
- if (!found) {
- mCheckedIdStates.delete(id);
- checkedIndex--;
- mCheckedItemCount--;
- }
- } else {
- mCheckStates.put(lastPos, true);
- }
- }
- }
-
- private void handleDataChanged() {
- if (mChoiceMode != ChoiceMode.NONE && mAdapter != null && mAdapter.hasStableIds()) {
- confirmCheckedPositionsById();
- }
-
- mRecycler.clearTransientStateViews();
-
- final int itemCount = mItemCount;
- if (itemCount > 0) {
- int newPos;
- int selectablePos;
-
- // Find the row we are supposed to sync to
- if (mNeedSync) {
- // Update this first, since setNextSelectedPositionInt inspects it
- mNeedSync = false;
- mPendingSync = null;
-
- switch (mSyncMode) {
- case SYNC_SELECTED_POSITION:
- if (isInTouchMode()) {
- // We saved our state when not in touch mode. (We know this because
- // mSyncMode is SYNC_SELECTED_POSITION.) Now we are trying to
- // restore in touch mode. Just leave mSyncPosition as it is (possibly
- // adjusting if the available range changed) and return.
- mLayoutMode = LAYOUT_SYNC;
- mSyncPosition = Math.min(Math.max(0, mSyncPosition), itemCount - 1);
-
- return;
- } else {
- // See if we can find a position in the new data with the same
- // id as the old selection. This will change mSyncPosition.
- newPos = findSyncPosition();
- if (newPos >= 0) {
- // Found it. Now verify that new selection is still selectable
- selectablePos = lookForSelectablePosition(newPos, true);
- if (selectablePos == newPos) {
- // Same row id is selected
- mSyncPosition = newPos;
-
- if (mSyncSize == getSize()) {
- // If we are at the same height as when we saved state, try
- // to restore the scroll position too.
- mLayoutMode = LAYOUT_SYNC;
- } else {
- // We are not the same height as when the selection was saved, so
- // don't try to restore the exact position
- mLayoutMode = LAYOUT_SET_SELECTION;
- }
-
- // Restore selection
- setNextSelectedPositionInt(newPos);
- return;
- }
- }
- }
- break;
-
- case SYNC_FIRST_POSITION:
- // Leave mSyncPosition as it is -- just pin to available range
- mLayoutMode = LAYOUT_SYNC;
- mSyncPosition = Math.min(Math.max(0, mSyncPosition), itemCount - 1);
-
- return;
- }
- }
-
- if (!isInTouchMode()) {
- // We couldn't find matching data -- try to use the same position
- newPos = getSelectedItemPosition();
-
- // Pin position to the available range
- if (newPos >= itemCount) {
- newPos = itemCount - 1;
- }
- if (newPos < 0) {
- newPos = 0;
- }
-
- // Make sure we select something selectable -- first look down
- selectablePos = lookForSelectablePosition(newPos, true);
-
- if (selectablePos >= 0) {
- setNextSelectedPositionInt(selectablePos);
- return;
- } else {
- // Looking down didn't work -- try looking up
- selectablePos = lookForSelectablePosition(newPos, false);
- if (selectablePos >= 0) {
- setNextSelectedPositionInt(selectablePos);
- return;
- }
- }
- } else {
- // We already know where we want to resurrect the selection
- if (mResurrectToPosition >= 0) {
- return;
- }
- }
- }
-
- // Nothing is selected. Give up and reset everything.
- mLayoutMode = LAYOUT_FORCE_TOP;
- mSelectedPosition = INVALID_POSITION;
- mSelectedRowId = INVALID_ROW_ID;
- mNextSelectedPosition = INVALID_POSITION;
- mNextSelectedRowId = INVALID_ROW_ID;
- mNeedSync = false;
- mPendingSync = null;
- mSelectorPosition = INVALID_POSITION;
-
- checkSelectionChanged();
- }
-
- private int reconcileSelectedPosition() {
- int position = mSelectedPosition;
- if (position < 0) {
- position = mResurrectToPosition;
- }
-
- position = Math.max(0, position);
- position = Math.min(position, mItemCount - 1);
-
- return position;
- }
-
- boolean resurrectSelection() {
- final int childCount = getChildCount();
- if (childCount <= 0) {
- return false;
- }
-
- int selectedStart = 0;
- int selectedPosition;
-
- int start = getStartEdge();
- int end = getEndEdge();
-
- final int firstPosition = mFirstPosition;
- final int toPosition = mResurrectToPosition;
- boolean down = true;
-
- if (toPosition >= firstPosition && toPosition < firstPosition + childCount) {
- selectedPosition = toPosition;
-
- final View selected = getChildAt(selectedPosition - mFirstPosition);
- selectedStart = getChildStartEdge(selected);
-
- final int selectedEnd = getChildEndEdge(selected);
-
- // We are scrolled, don't get in the fade
- if (selectedStart < start) {
- selectedStart = start + getFadingEdgeLength();
- } else if (selectedEnd > end) {
- selectedStart = end - getChildMeasuredSize(selected) - getFadingEdgeLength();
- }
- } else if (toPosition < firstPosition) {
- // Default to selecting whatever is first
- selectedPosition = firstPosition;
-
- for (int i = 0; i < childCount; i++) {
- final View child = getChildAt(i);
- final int childStart = getChildStartEdge(child);
-
- if (i == 0) {
- // Remember the position of the first item
- selectedStart = childStart;
-
- // See if we are scrolled at all
- if (firstPosition > 0 || childStart < start) {
- // If we are scrolled, don't select anything that is
- // in the fade region
- start += getFadingEdgeLength();
- }
- }
-
- if (childStart >= start) {
- // Found a view whose top is fully visible
- selectedPosition = firstPosition + i;
- selectedStart = childStart;
- break;
- }
- }
- } else {
- final int itemCount = mItemCount;
- selectedPosition = firstPosition + childCount - 1;
- down = false;
-
- for (int i = childCount - 1; i >= 0; i--) {
- final View child = getChildAt(i);
- final int childStart = getChildStartEdge(child);
- final int childEnd = getChildEndEdge(child);
-
- if (i == childCount - 1) {
- selectedStart = childStart;
-
- if (firstPosition + childCount < itemCount || childEnd > end) {
- end -= getFadingEdgeLength();
- }
- }
-
- if (childEnd <= end) {
- selectedPosition = firstPosition + i;
- selectedStart = childStart;
- break;
- }
- }
- }
-
- mResurrectToPosition = INVALID_POSITION;
-
- finishSmoothScrolling();
-
- mTouchMode = TOUCH_MODE_REST;
- reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
-
- mSpecificStart = selectedStart;
-
- selectedPosition = lookForSelectablePosition(selectedPosition, down);
- if (selectedPosition >= firstPosition && selectedPosition <= getLastVisiblePosition()) {
- mLayoutMode = LAYOUT_SPECIFIC;
- updateSelectorState();
- setSelectionInt(selectedPosition);
- invokeOnItemScrollListener();
- } else {
- selectedPosition = INVALID_POSITION;
- }
-
- return selectedPosition >= 0;
- }
-
- /**
- * If there is a selection returns false.
- * Otherwise resurrects the selection and returns true if resurrected.
- */
- boolean resurrectSelectionIfNeeded() {
- if (mSelectedPosition < 0 && resurrectSelection()) {
- updateSelectorState();
- return true;
- }
-
- return false;
- }
-
- private int getChildWidthMeasureSpec(LayoutParams lp) {
- if (!mIsVertical && lp.width == LayoutParams.WRAP_CONTENT) {
- return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
- } else if (mIsVertical) {
- final int maxWidth = getWidth() - getPaddingLeft() - getPaddingRight();
- return MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY);
- } else {
- return MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
- }
- }
-
- private int getChildHeightMeasureSpec(LayoutParams lp) {
- if (mIsVertical && lp.height == LayoutParams.WRAP_CONTENT) {
- return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
- } else if (!mIsVertical) {
- final int maxHeight = getHeight() - getPaddingTop() - getPaddingBottom();
- return MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY);
- } else {
- return MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
- }
- }
-
- private void measureChild(View child) {
- measureChild(child, (LayoutParams) child.getLayoutParams());
- }
-
- private void measureChild(View child, LayoutParams lp) {
- final int widthSpec = getChildWidthMeasureSpec(lp);
- final int heightSpec = getChildHeightMeasureSpec(lp);
- child.measure(widthSpec, heightSpec);
- }
-
- private void relayoutMeasuredChild(View child) {
- final int w = child.getMeasuredWidth();
- final int h = child.getMeasuredHeight();
-
- final int childLeft = getPaddingLeft();
- final int childRight = childLeft + w;
- final int childTop = child.getTop();
- final int childBottom = childTop + h;
-
- child.layout(childLeft, childTop, childRight, childBottom);
- }
-
- private void measureScrapChild(View scrapChild, int position, int secondaryMeasureSpec) {
- LayoutParams lp = (LayoutParams) scrapChild.getLayoutParams();
- if (lp == null) {
- lp = generateDefaultLayoutParams();
- scrapChild.setLayoutParams(lp);
- }
-
- lp.viewType = mAdapter.getItemViewType(position);
- lp.forceAdd = true;
-
- final int widthMeasureSpec;
- final int heightMeasureSpec;
- if (mIsVertical) {
- widthMeasureSpec = secondaryMeasureSpec;
- heightMeasureSpec = getChildHeightMeasureSpec(lp);
- } else {
- widthMeasureSpec = getChildWidthMeasureSpec(lp);
- heightMeasureSpec = secondaryMeasureSpec;
- }
-
- scrapChild.measure(widthMeasureSpec, heightMeasureSpec);
- }
-
- /**
- * Measures the height of the given range of children (inclusive) and
- * returns the height with this TwoWayView's padding and item margin heights
- * included. If maxHeight is provided, the measuring will stop when the
- * current height reaches maxHeight.
- *
- * @param widthMeasureSpec The width measure spec to be given to a child's
- * {@link View#measure(int, int)}.
- * @param startPosition The position of the first child to be shown.
- * @param endPosition The (inclusive) position of the last child to be
- * shown. Specify {@link #NO_POSITION} if the last child should be
- * the last available child from the adapter.
- * @param maxHeight The maximum height that will be returned (if all the
- * children don't fit in this value, this value will be
- * returned).
- * @param disallowPartialChildPosition In general, whether the returned
- * height should only contain entire children. This is more
- * powerful--it is the first inclusive position at which partial
- * children will not be allowed. Example: it looks nice to have
- * at least 3 completely visible children, and in portrait this
- * will most likely fit; but in landscape there could be times
- * when even 2 children can not be completely shown, so a value
- * of 2 (remember, inclusive) would be good (assuming
- * startPosition is 0).
- * @return The height of this TwoWayView with the given children.
- */
- private int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
- final int maxHeight, int disallowPartialChildPosition) {
-
- final int paddingTop = getPaddingTop();
- final int paddingBottom = getPaddingBottom();
-
- final ListAdapter adapter = mAdapter;
- if (adapter == null) {
- return paddingTop + paddingBottom;
- }
-
- // Include the padding of the list
- int returnedHeight = paddingTop + paddingBottom;
- final int itemMargin = mItemMargin;
-
- // The previous height value that was less than maxHeight and contained
- // no partial children
- int prevHeightWithoutPartialChild = 0;
- int i;
- View child;
-
- // mItemCount - 1 since endPosition parameter is inclusive
- endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
- final RecycleBin recycleBin = mRecycler;
- final boolean shouldRecycle = recycleOnMeasure();
- final boolean[] isScrap = mIsScrap;
-
- for (i = startPosition; i <= endPosition; ++i) {
- child = obtainView(i, isScrap);
-
- measureScrapChild(child, i, widthMeasureSpec);
-
- if (i > 0) {
- // Count the item margin for all but one child
- returnedHeight += itemMargin;
- }
-
- // Recycle the view before we possibly return from the method
- if (shouldRecycle) {
- recycleBin.addScrapView(child, -1);
- }
-
- returnedHeight += child.getMeasuredHeight();
-
- if (returnedHeight >= maxHeight) {
- // We went over, figure out which height to return. If returnedHeight > maxHeight,
- // then the i'th position did not fit completely.
- return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
- && (i > disallowPartialChildPosition) // We've past the min pos
- && (prevHeightWithoutPartialChild > 0) // We have a prev height
- && (returnedHeight != maxHeight) // i'th child did not fit completely
- ? prevHeightWithoutPartialChild
- : maxHeight;
- }
-
- if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
- prevHeightWithoutPartialChild = returnedHeight;
- }
- }
-
- // At this point, we went through the range of children, and they each
- // completely fit, so return the returnedHeight
- return returnedHeight;
- }
-
- /**
- * Measures the width of the given range of children (inclusive) and
- * returns the width with this TwoWayView's padding and item margin widths
- * included. If maxWidth is provided, the measuring will stop when the
- * current width reaches maxWidth.
- *
- * @param heightMeasureSpec The height measure spec to be given to a child's
- * {@link View#measure(int, int)}.
- * @param startPosition The position of the first child to be shown.
- * @param endPosition The (inclusive) position of the last child to be
- * shown. Specify {@link #NO_POSITION} if the last child should be
- * the last available child from the adapter.
- * @param maxWidth The maximum width that will be returned (if all the
- * children don't fit in this value, this value will be
- * returned).
- * @param disallowPartialChildPosition In general, whether the returned
- * width should only contain entire children. This is more
- * powerful--it is the first inclusive position at which partial
- * children will not be allowed. Example: it looks nice to have
- * at least 3 completely visible children, and in portrait this
- * will most likely fit; but in landscape there could be times
- * when even 2 children can not be completely shown, so a value
- * of 2 (remember, inclusive) would be good (assuming
- * startPosition is 0).
- * @return The width of this TwoWayView with the given children.
- */
- private int measureWidthOfChildren(int heightMeasureSpec, int startPosition, int endPosition,
- final int maxWidth, int disallowPartialChildPosition) {
-
- final int paddingLeft = getPaddingLeft();
- final int paddingRight = getPaddingRight();
-
- final ListAdapter adapter = mAdapter;
- if (adapter == null) {
- return paddingLeft + paddingRight;
- }
-
- // Include the padding of the list
- int returnedWidth = paddingLeft + paddingRight;
- final int itemMargin = mItemMargin;
-
- // The previous height value that was less than maxHeight and contained
- // no partial children
- int prevWidthWithoutPartialChild = 0;
- int i;
- View child;
-
- // mItemCount - 1 since endPosition parameter is inclusive
- endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
- final RecycleBin recycleBin = mRecycler;
- final boolean shouldRecycle = recycleOnMeasure();
- final boolean[] isScrap = mIsScrap;
-
- for (i = startPosition; i <= endPosition; ++i) {
- child = obtainView(i, isScrap);
-
- measureScrapChild(child, i, heightMeasureSpec);
-
- if (i > 0) {
- // Count the item margin for all but one child
- returnedWidth += itemMargin;
- }
-
- // Recycle the view before we possibly return from the method
- if (shouldRecycle) {
- recycleBin.addScrapView(child, -1);
- }
-
- returnedWidth += child.getMeasuredWidth();
-
- if (returnedWidth >= maxWidth) {
- // We went over, figure out which width to return. If returnedWidth > maxWidth,
- // then the i'th position did not fit completely.
- return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
- && (i > disallowPartialChildPosition) // We've past the min pos
- && (prevWidthWithoutPartialChild > 0) // We have a prev width
- && (returnedWidth != maxWidth) // i'th child did not fit completely
- ? prevWidthWithoutPartialChild
- : maxWidth;
- }
-
- if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
- prevWidthWithoutPartialChild = returnedWidth;
- }
- }
-
- // At this point, we went through the range of children, and they each
- // completely fit, so return the returnedWidth
- return returnedWidth;
- }
-
- private View makeAndAddView(int position, int offset, boolean flow, boolean selected) {
- final int top;
- final int left;
-
- // Compensate item margin on the first item of the list if the item margin
- // is negative to avoid incorrect offset for the very first child.
- if (mIsVertical) {
- top = offset;
- left = getPaddingLeft();
- } else {
- top = getPaddingTop();
- left = offset;
- }
-
- if (!mDataChanged) {
- // Try to use an existing view for this position
- final View activeChild = mRecycler.getActiveView(position);
- if (activeChild != null) {
- // Found it -- we're using an existing child
- // This just needs to be positioned
- setupChild(activeChild, position, top, left, flow, selected, true);
-
- return activeChild;
- }
- }
-
- // Make a new view for this position, or convert an unused view if possible
- final View child = obtainView(position, mIsScrap);
-
- // This needs to be positioned and measured
- setupChild(child, position, top, left, flow, selected, mIsScrap[0]);
-
- return child;
- }
-
- @TargetApi(11)
- private void setupChild(View child, int position, int top, int left,
- boolean flow, boolean selected, boolean recycled) {
- final boolean isSelected = selected && shouldShowSelector();
- final boolean updateChildSelected = isSelected != child.isSelected();
- final int touchMode = mTouchMode;
-
- final boolean isPressed = touchMode > TOUCH_MODE_DOWN && touchMode < TOUCH_MODE_DRAGGING &&
- mMotionPosition == position;
-
- final boolean updateChildPressed = isPressed != child.isPressed();
- final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
-
- // Respect layout params that are already in the view. Otherwise make some up...
- LayoutParams lp = (LayoutParams) child.getLayoutParams();
- if (lp == null) {
- lp = generateDefaultLayoutParams();
- }
-
- lp.viewType = mAdapter.getItemViewType(position);
-
- if (recycled && !lp.forceAdd) {
- attachViewToParent(child, (flow ? -1 : 0), lp);
- } else {
- lp.forceAdd = false;
- addViewInLayout(child, (flow ? -1 : 0), lp, true);
- }
-
- if (updateChildSelected) {
- child.setSelected(isSelected);
- }
-
- if (updateChildPressed) {
- child.setPressed(isPressed);
- }
-
- if (mChoiceMode != ChoiceMode.NONE && mCheckStates != null) {
- if (child instanceof Checkable) {
- ((Checkable) child).setChecked(mCheckStates.get(position));
- } else if (Build.VERSION.SDK_INT >= HONEYCOMB) {
- child.setActivated(mCheckStates.get(position));
- }
- }
-
- if (needToMeasure) {
- measureChild(child, lp);
- } else {
- cleanupLayoutState(child);
- }
-
- final int w = child.getMeasuredWidth();
- final int h = child.getMeasuredHeight();
-
- final int childTop = (mIsVertical && !flow ? top - h : top);
- final int childLeft = (!mIsVertical && !flow ? left - w : left);
-
- if (needToMeasure) {
- final int childRight = childLeft + w;
- final int childBottom = childTop + h;
-
- child.layout(childLeft, childTop, childRight, childBottom);
- } else {
- child.offsetLeftAndRight(childLeft - child.getLeft());
- child.offsetTopAndBottom(childTop - child.getTop());
- }
- }
-
- void fillGap(boolean down) {
- final int childCount = getChildCount();
-
- if (down) {
- final int start = getStartEdge();
- final int lastEnd = getChildEndEdge(getChildAt(childCount - 1));
- final int offset = (childCount > 0 ? lastEnd + mItemMargin : start);
- fillAfter(mFirstPosition + childCount, offset);
- correctTooHigh(getChildCount());
- } else {
- final int end = getEndEdge();
- final int firstStart = getChildStartEdge(getChildAt(0));
- final int offset = (childCount > 0 ? firstStart - mItemMargin : end);
- fillBefore(mFirstPosition - 1, offset);
- correctTooLow(getChildCount());
- }
- }
-
- private View fillBefore(int pos, int nextOffset) {
- View selectedView = null;
-
- final int start = getStartEdge();
-
- while (nextOffset > start && pos >= 0) {
- boolean isSelected = (pos == mSelectedPosition);
-
- View child = makeAndAddView(pos, nextOffset, false, isSelected);
- nextOffset = getChildStartEdge(child) - mItemMargin;
-
- if (isSelected) {
- selectedView = child;
- }
-
- pos--;
- }
-
- mFirstPosition = pos + 1;
-
- return selectedView;
- }
-
- private View fillAfter(int pos, int nextOffset) {
- View selectedView = null;
-
- final int end = getEndEdge();
-
- while (nextOffset < end && pos < mItemCount) {
- boolean selected = (pos == mSelectedPosition);
-
- View child = makeAndAddView(pos, nextOffset, true, selected);
- nextOffset = getChildEndEdge(child) + mItemMargin;
-
- if (selected) {
- selectedView = child;
- }
-
- pos++;
- }
-
- return selectedView;
- }
-
- private View fillSpecific(int position, int offset) {
- final boolean tempIsSelected = (position == mSelectedPosition);
- View temp = makeAndAddView(position, offset, true, tempIsSelected);
-
- // Possibly changed again in fillBefore if we add rows above this one.
- mFirstPosition = position;
-
- final int offsetBefore = getChildStartEdge(temp) - mItemMargin;
- final View before = fillBefore(position - 1, offsetBefore);
-
- // This will correct for the top of the first view not touching the top of the list
- adjustViewsStartOrEnd();
-
- final int offsetAfter = getChildEndEdge(temp) + mItemMargin;
- final View after = fillAfter(position + 1, offsetAfter);
-
- final int childCount = getChildCount();
- if (childCount > 0) {
- correctTooHigh(childCount);
- }
-
- if (tempIsSelected) {
- return temp;
- } else if (before != null) {
- return before;
- } else {
- return after;
- }
- }
-
- private View fillFromOffset(int nextOffset) {
- mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
- mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
-
- if (mFirstPosition < 0) {
- mFirstPosition = 0;
- }
-
- return fillAfter(mFirstPosition, nextOffset);
- }
-
- private View fillFromMiddle(int start, int end) {
- final int size = end - start;
- int position = reconcileSelectedPosition();
-
- View selected = makeAndAddView(position, start, true, true);
- mFirstPosition = position;
-
- if (mIsVertical) {
- int selectedHeight = selected.getMeasuredHeight();
- if (selectedHeight <= size) {
- selected.offsetTopAndBottom((size - selectedHeight) / 2);
- }
- } else {
- int selectedWidth = selected.getMeasuredWidth();
- if (selectedWidth <= size) {
- selected.offsetLeftAndRight((size - selectedWidth) / 2);
- }
- }
-
- fillBeforeAndAfter(selected, position);
- correctTooHigh(getChildCount());
-
- return selected;
- }
-
- private void fillBeforeAndAfter(View selected, int position) {
- final int offsetBefore = getChildStartEdge(selected) + mItemMargin;
- fillBefore(position - 1, offsetBefore);
-
- adjustViewsStartOrEnd();
-
- final int offsetAfter = getChildEndEdge(selected) + mItemMargin;
- fillAfter(position + 1, offsetAfter);
- }
-
- private View fillFromSelection(int selectedTop, int start, int end) {
- int fadingEdgeLength = getFadingEdgeLength();
- final int selectedPosition = mSelectedPosition;
-
- final int minStart = getMinSelectionPixel(start, fadingEdgeLength, selectedPosition);
- final int maxEnd = getMaxSelectionPixel(end, fadingEdgeLength, selectedPosition);
-
- View selected = makeAndAddView(selectedPosition, selectedTop, true, true);
-
- final int selectedStart = getChildStartEdge(selected);
- final int selectedEnd = getChildEndEdge(selected);
-
- // Some of the newly selected item extends below the bottom of the list
- if (selectedEnd > maxEnd) {
- // Find space available above the selection into which we can scroll
- // upwards
- final int spaceAbove = selectedStart - minStart;
-
- // Find space required to bring the bottom of the selected item
- // fully into view
- final int spaceBelow = selectedEnd - maxEnd;
-
- final int offset = Math.min(spaceAbove, spaceBelow);
-
- // Now offset the selected item to get it into view
- selected.offsetTopAndBottom(-offset);
- } else if (selectedStart < minStart) {
- // Find space required to bring the top of the selected item fully
- // into view
- final int spaceAbove = minStart - selectedStart;
-
- // Find space available below the selection into which we can scroll
- // downwards
- final int spaceBelow = maxEnd - selectedEnd;
-
- final int offset = Math.min(spaceAbove, spaceBelow);
-
- // Offset the selected item to get it into view
- selected.offsetTopAndBottom(offset);
- }
-
- // Fill in views above and below
- fillBeforeAndAfter(selected, selectedPosition);
- correctTooHigh(getChildCount());
-
- return selected;
- }
-
- private void correctTooHigh(int childCount) {
- // First see if the last item is visible. If it is not, it is OK for the
- // top of the list to be pushed up.
- final int lastPosition = mFirstPosition + childCount - 1;
- if (lastPosition != mItemCount - 1 || childCount == 0) {
- return;
- }
-
- // Get the last child end edge
- final int lastEnd = getChildEndEdge(getChildAt(childCount - 1));
-
- // This is bottom of our drawable area
- final int start = getStartEdge();
- final int end = getEndEdge();
-
- // This is how far the end edge of the last view is from the end of the
- // drawable area
- int endOffset = end - lastEnd;
-
- View firstChild = getChildAt(0);
- int firstStart = getChildStartEdge(firstChild);
-
- // Make sure we are 1) Too high, and 2) Either there are more rows above the
- // first row or the first row is scrolled off the top of the drawable area
- if (endOffset > 0 && (mFirstPosition > 0 || firstStart < start)) {
- if (mFirstPosition == 0) {
- // Don't pull the top too far down
- endOffset = Math.min(endOffset, start - firstStart);
- }
-
- // Move everything down
- offsetChildren(endOffset);
-
- if (mFirstPosition > 0) {
- firstStart = getChildStartEdge(firstChild);
-
- // Fill the gap that was opened above mFirstPosition with more rows, if
- // possible
- fillBefore(mFirstPosition - 1, firstStart - mItemMargin);
-
- // Close up the remaining gap
- adjustViewsStartOrEnd();
- }
- }
- }
-
- private void correctTooLow(int childCount) {
- // First see if the first item is visible. If it is not, it is OK for the
- // bottom of the list to be pushed down.
- if (mFirstPosition != 0 || childCount == 0) {
- return;
- }
-
- final int firstStart = getChildStartEdge(getChildAt(0));
-
- final int start = getStartEdge();
- final int end = getEndEdge();
-
- // This is how far the start edge of the first view is from the start of the
- // drawable area
- int startOffset = firstStart - start;
-
- View last = getChildAt(childCount - 1);
- int lastEnd = getChildEndEdge(last);
-
- int lastPosition = mFirstPosition + childCount - 1;
-
- // Make sure we are 1) Too low, and 2) Either there are more columns/rows below the
- // last column/row or the last column/row is scrolled off the end of the
- // drawable area
- if (startOffset > 0) {
- if (lastPosition < mItemCount - 1 || lastEnd > end) {
- if (lastPosition == mItemCount - 1) {
- // Don't pull the bottom too far up
- startOffset = Math.min(startOffset, lastEnd - end);
- }
-
- // Move everything up
- offsetChildren(-startOffset);
-
- if (lastPosition < mItemCount - 1) {
- lastEnd = getChildEndEdge(last);
-
- // Fill the gap that was opened below the last position with more rows, if
- // possible
- fillAfter(lastPosition + 1, lastEnd + mItemMargin);
-
- // Close up the remaining gap
- adjustViewsStartOrEnd();
- }
- } else if (lastPosition == mItemCount - 1) {
- adjustViewsStartOrEnd();
- }
- }
- }
-
- private void adjustViewsStartOrEnd() {
- if (getChildCount() == 0) {
- return;
- }
-
- int delta = getChildStartEdge(getChildAt(0)) - getStartEdge();
-
- // If item margin is negative we shouldn't apply it in the
- // first item of the list to avoid offsetting it incorrectly.
- if (mItemMargin >= 0 || mFirstPosition != 0) {
- delta -= mItemMargin;
- }
-
- if (delta < 0) {
- // We only are looking to see if we are too low, not too high
- delta = 0;
- }
-
- if (delta != 0) {
- offsetChildren(-delta);
- }
- }
-
- @TargetApi(14)
- private SparseBooleanArray cloneCheckStates() {
- if (mCheckStates == null) {
- return null;
- }
-
- SparseBooleanArray checkedStates;
-
- if (Build.VERSION.SDK_INT >= 14) {
- checkedStates = mCheckStates.clone();
- } else {
- checkedStates = new SparseBooleanArray();
-
- for (int i = 0; i < mCheckStates.size(); i++) {
- checkedStates.put(mCheckStates.keyAt(i), mCheckStates.valueAt(i));
- }
- }
-
- return checkedStates;
- }
-
- private int findSyncPosition() {
- int itemCount = mItemCount;
-
- if (itemCount == 0) {
- return INVALID_POSITION;
- }
-
- final long idToMatch = mSyncRowId;
-
- // If there isn't a selection don't hunt for it
- if (idToMatch == INVALID_ROW_ID) {
- return INVALID_POSITION;
- }
-
- // Pin seed to reasonable values
- int seed = mSyncPosition;
- seed = Math.max(0, seed);
- seed = Math.min(itemCount - 1, seed);
-
- long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS;
-
- long rowId;
-
- // first position scanned so far
- int first = seed;
-
- // last position scanned so far
- int last = seed;
-
- // True if we should move down on the next iteration
- boolean next = false;
-
- // True when we have looked at the first item in the data
- boolean hitFirst;
-
- // True when we have looked at the last item in the data
- boolean hitLast;
-
- // Get the item ID locally (instead of getItemIdAtPosition), so
- // we need the adapter
- final ListAdapter adapter = mAdapter;
- if (adapter == null) {
- return INVALID_POSITION;
- }
-
- while (SystemClock.uptimeMillis() <= endTime) {
- rowId = adapter.getItemId(seed);
- if (rowId == idToMatch) {
- // Found it!
- return seed;
- }
-
- hitLast = (last == itemCount - 1);
- hitFirst = (first == 0);
-
- if (hitLast && hitFirst) {
- // Looked at everything
- break;
- }
-
- if (hitFirst || (next && !hitLast)) {
- // Either we hit the top, or we are trying to move down
- last++;
- seed = last;
-
- // Try going up next time
- next = false;
- } else if (hitLast || (!next && !hitFirst)) {
- // Either we hit the bottom, or we are trying to move up
- first--;
- seed = first;
-
- // Try going down next time
- next = true;
- }
- }
-
- return INVALID_POSITION;
- }
-
- @TargetApi(16)
- private View obtainView(int position, boolean[] isScrap) {
- isScrap[0] = false;
-
- View scrapView = mRecycler.getTransientStateView(position);
- if (scrapView != null) {
- return scrapView;
- }
-
- scrapView = mRecycler.getScrapView(position);
-
- final View child;
- if (scrapView != null) {
- child = mAdapter.getView(position, scrapView, this);
-
- if (child != scrapView) {
- mRecycler.addScrapView(scrapView, position);
- } else {
- isScrap[0] = true;
- }
- } else {
- child = mAdapter.getView(position, null, this);
- }
-
- if (ViewCompat.getImportantForAccessibility(child) == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
- ViewCompat.setImportantForAccessibility(child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
- }
-
- if (mHasStableIds) {
- LayoutParams lp = (LayoutParams) child.getLayoutParams();
-
- if (lp == null) {
- lp = generateDefaultLayoutParams();
- } else if (!checkLayoutParams(lp)) {
- lp = generateLayoutParams(lp);
- }
-
- lp.id = mAdapter.getItemId(position);
-
- child.setLayoutParams(lp);
- }
-
- if (mAccessibilityDelegate == null) {
- mAccessibilityDelegate = new ListItemAccessibilityDelegate();
- }
-
- ViewCompat.setAccessibilityDelegate(child, mAccessibilityDelegate);
-
- return child;
- }
-
- void resetState() {
- mScroller.forceFinished(true);
-
- removeAllViewsInLayout();
-
- mSelectedStart = 0;
- mFirstPosition = 0;
- mDataChanged = false;
- mNeedSync = false;
- mPendingSync = null;
- mOldSelectedPosition = INVALID_POSITION;
- mOldSelectedRowId = INVALID_ROW_ID;
-
- mOverScroll = 0;
-
- setSelectedPositionInt(INVALID_POSITION);
- setNextSelectedPositionInt(INVALID_POSITION);
-
- mSelectorPosition = INVALID_POSITION;
- mSelectorRect.setEmpty();
-
- invalidate();
- }
-
- private void rememberSyncState() {
- if (getChildCount() == 0) {
- return;
- }
-
- mNeedSync = true;
-
- if (mSelectedPosition >= 0) {
- View child = getChildAt(mSelectedPosition - mFirstPosition);
-
- mSyncRowId = mNextSelectedRowId;
- mSyncPosition = mNextSelectedPosition;
-
- if (child != null) {
- mSpecificStart = getChildStartEdge(child);
- }
-
- mSyncMode = SYNC_SELECTED_POSITION;
- } else {
- // Sync the based on the offset of the first view
- View child = getChildAt(0);
- ListAdapter adapter = getAdapter();
-
- if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) {
- mSyncRowId = adapter.getItemId(mFirstPosition);
- } else {
- mSyncRowId = NO_ID;
- }
-
- mSyncPosition = mFirstPosition;
-
- if (child != null) {
- mSpecificStart = getChildStartEdge(child);
- }
-
- mSyncMode = SYNC_FIRST_POSITION;
- }
- }
-
- private ContextMenuInfo createContextMenuInfo(View view, int position, long id) {
- return new AdapterContextMenuInfo(view, position, id);
- }
-
- @TargetApi(11)
- private void updateOnScreenCheckedViews() {
- final int firstPos = mFirstPosition;
- final int count = getChildCount();
-
- for (int i = 0; i < count; i++) {
- final View child = getChildAt(i);
- final int position = firstPos + i;
-
- if (child instanceof Checkable) {
- ((Checkable) child).setChecked(mCheckStates.get(position));
- } else if (Build.VERSION.SDK_INT >= HONEYCOMB) {
- child.setActivated(mCheckStates.get(position));
- }
- }
- }
-
- @Override
- public boolean performItemClick(View view, int position, long id) {
- boolean checkedStateChanged = false;
-
- if (mChoiceMode == ChoiceMode.MULTIPLE) {
- boolean checked = !mCheckStates.get(position, false);
- mCheckStates.put(position, checked);
-
- if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
- if (checked) {
- mCheckedIdStates.put(mAdapter.getItemId(position), position);
- } else {
- mCheckedIdStates.delete(mAdapter.getItemId(position));
- }
- }
-
- if (checked) {
- mCheckedItemCount++;
- } else {
- mCheckedItemCount--;
- }
-
- checkedStateChanged = true;
- } else if (mChoiceMode == ChoiceMode.SINGLE) {
- boolean checked = !mCheckStates.get(position, false);
- if (checked) {
- mCheckStates.clear();
- mCheckStates.put(position, true);
-
- if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
- mCheckedIdStates.clear();
- mCheckedIdStates.put(mAdapter.getItemId(position), position);
- }
-
- mCheckedItemCount = 1;
- } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
- mCheckedItemCount = 0;
- }
-
- checkedStateChanged = true;
- }
-
- if (checkedStateChanged) {
- updateOnScreenCheckedViews();
- }
-
- return super.performItemClick(view, position, id);
- }
-
- private boolean performLongPress(final View child,
- final int longPressPosition, final long longPressId) {
- // CHOICE_MODE_MULTIPLE_MODAL takes over long press.
- boolean handled = false;
-
- OnItemLongClickListener listener = getOnItemLongClickListener();
- if (listener != null) {
- handled = listener.onItemLongClick(TwoWayView.this, child,
- longPressPosition, longPressId);
- }
-
- if (!handled) {
- mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId);
- handled = super.showContextMenuForChild(TwoWayView.this);
- }
-
- if (handled) {
- performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
- }
-
- return handled;
- }
-
- @Override
- protected LayoutParams generateDefaultLayoutParams() {
- if (mIsVertical) {
- return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
- } else {
- return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
- }
- }
-
- @Override
- protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
- return new LayoutParams(lp);
- }
-
- @Override
- protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) {
- return lp instanceof LayoutParams;
- }
-
- @Override
- public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
- return new LayoutParams(getContext(), attrs);
- }
-
- @Override
- protected ContextMenuInfo getContextMenuInfo() {
- return mContextMenuInfo;
- }
-
- @Override
- public Parcelable onSaveInstanceState() {
- Parcelable superState = super.onSaveInstanceState();
- SavedState ss = new SavedState(superState);
-
- if (mPendingSync != null) {
- ss.selectedId = mPendingSync.selectedId;
- ss.firstId = mPendingSync.firstId;
- ss.viewStart = mPendingSync.viewStart;
- ss.position = mPendingSync.position;
- ss.size = mPendingSync.size;
-
- return ss;
- }
-
- boolean haveChildren = (getChildCount() > 0 && mItemCount > 0);
- long selectedId = getSelectedItemId();
- ss.selectedId = selectedId;
- ss.size = getSize();
-
- if (selectedId >= 0) {
- ss.viewStart = mSelectedStart;
- ss.position = getSelectedItemPosition();
- ss.firstId = INVALID_POSITION;
- } else if (haveChildren && mFirstPosition > 0) {
- // Remember the position of the first child.
- // We only do this if we are not currently at the top of
- // the list, for two reasons:
- //
- // (1) The list may be in the process of becoming empty, in
- // which case mItemCount may not be 0, but if we try to
- // ask for any information about position 0 we will crash.
- //
- // (2) Being "at the top" seems like a special case, anyway,
- // and the user wouldn't expect to end up somewhere else when
- // they revisit the list even if its content has changed.
-
- ss.viewStart = getChildStartEdge(getChildAt(0));
-
- int firstPos = mFirstPosition;
- if (firstPos >= mItemCount) {
- firstPos = mItemCount - 1;
- }
-
- ss.position = firstPos;
- ss.firstId = mAdapter.getItemId(firstPos);
- } else {
- ss.viewStart = 0;
- ss.firstId = INVALID_POSITION;
- ss.position = 0;
- }
-
- if (mCheckStates != null) {
- ss.checkState = cloneCheckStates();
- }
-
- if (mCheckedIdStates != null) {
- final LongSparseArray<Integer> idState = new LongSparseArray<Integer>();
-
- final int count = mCheckedIdStates.size();
- for (int i = 0; i < count; i++) {
- idState.put(mCheckedIdStates.keyAt(i), mCheckedIdStates.valueAt(i));
- }
-
- ss.checkIdState = idState;
- }
-
- ss.checkedItemCount = mCheckedItemCount;
-
- return ss;
- }
-
- @Override
- public void onRestoreInstanceState(Parcelable state) {
- SavedState ss = (SavedState) state;
- super.onRestoreInstanceState(ss.getSuperState());
-
- mDataChanged = true;
- mSyncSize = ss.size;
-
- if (ss.selectedId >= 0) {
- mNeedSync = true;
- mPendingSync = ss;
- mSyncRowId = ss.selectedId;
- mSyncPosition = ss.position;
- mSpecificStart = ss.viewStart;
- mSyncMode = SYNC_SELECTED_POSITION;
- } else if (ss.firstId >= 0) {
- setSelectedPositionInt(INVALID_POSITION);
-
- // Do this before setting mNeedSync since setNextSelectedPosition looks at mNeedSync
- setNextSelectedPositionInt(INVALID_POSITION);
-
- mSelectorPosition = INVALID_POSITION;
- mNeedSync = true;
- mPendingSync = ss;
- mSyncRowId = ss.firstId;
- mSyncPosition = ss.position;
- mSpecificStart = ss.viewStart;
- mSyncMode = SYNC_FIRST_POSITION;
- }
-
- if (ss.checkState != null) {
- mCheckStates = ss.checkState;
- }
-
- if (ss.checkIdState != null) {
- mCheckedIdStates = ss.checkIdState;
- }
-
- mCheckedItemCount = ss.checkedItemCount;
-
- requestLayout();
- }
-
- public static class LayoutParams extends ViewGroup.LayoutParams {
- /**
- * Type of this view as reported by the adapter
- */
- int viewType;
-
- /**
- * The stable ID of the item this view displays
- */
- long id = -1;
-
- /**
- * The position the view was removed from when pulled out of the
- * scrap heap.
- * @hide
- */
- int scrappedFromPosition;
-
- /**
- * When a TwoWayView is measured with an AT_MOST measure spec, it needs
- * to obtain children views to measure itself. When doing so, the children
- * are not attached to the window, but put in the recycler which assumes
- * they've been attached before. Setting this flag will force the reused
- * view to be attached to the window rather than just attached to the
- * parent.
- */
- boolean forceAdd;
-
- public LayoutParams(int width, int height) {
- super(width, height);
-
- if (this.width == MATCH_PARENT) {
- Log.w(LOGTAG, "Constructing LayoutParams with width FILL_PARENT " +
- "does not make much sense as the view might change orientation. " +
- "Falling back to WRAP_CONTENT");
- this.width = WRAP_CONTENT;
- }
-
- if (this.height == MATCH_PARENT) {
- Log.w(LOGTAG, "Constructing LayoutParams with height FILL_PARENT " +
- "does not make much sense as the view might change orientation. " +
- "Falling back to WRAP_CONTENT");
- this.height = WRAP_CONTENT;
- }
- }
-
- public LayoutParams(Context c, AttributeSet attrs) {
- super(c, attrs);
-
- if (this.width == MATCH_PARENT) {
- Log.w(LOGTAG, "Inflation setting LayoutParams width to MATCH_PARENT - " +
- "does not make much sense as the view might change orientation. " +
- "Falling back to WRAP_CONTENT");
- this.width = MATCH_PARENT;
- }
-
- if (this.height == MATCH_PARENT) {
- Log.w(LOGTAG, "Inflation setting LayoutParams height to MATCH_PARENT - " +
- "does not make much sense as the view might change orientation. " +
- "Falling back to WRAP_CONTENT");
- this.height = WRAP_CONTENT;
- }
- }
-
- public LayoutParams(ViewGroup.LayoutParams other) {
- super(other);
-
- if (this.width == MATCH_PARENT) {
- Log.w(LOGTAG, "Constructing LayoutParams with width MATCH_PARENT - " +
- "does not make much sense as the view might change orientation. " +
- "Falling back to WRAP_CONTENT");
- this.width = WRAP_CONTENT;
- }
-
- if (this.height == MATCH_PARENT) {
- Log.w(LOGTAG, "Constructing LayoutParams with height MATCH_PARENT - " +
- "does not make much sense as the view might change orientation. " +
- "Falling back to WRAP_CONTENT");
- this.height = WRAP_CONTENT;
- }
- }
- }
-
- class RecycleBin {
- private RecyclerListener mRecyclerListener;
- private int mFirstActivePosition;
- private View[] mActiveViews = new View[0];
- private ArrayList<View>[] mScrapViews;
- private int mViewTypeCount;
- private ArrayList<View> mCurrentScrap;
- private SparseArrayCompat<View> mTransientStateViews;
-
- public void setViewTypeCount(int viewTypeCount) {
- if (viewTypeCount < 1) {
- throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
- }
-
- @SuppressWarnings({"unchecked", "rawtypes"})
- ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
- for (int i = 0; i < viewTypeCount; i++) {
- scrapViews[i] = new ArrayList<View>();
- }
-
- mViewTypeCount = viewTypeCount;
- mCurrentScrap = scrapViews[0];
- mScrapViews = scrapViews;
- }
-
- public void markChildrenDirty() {
- if (mViewTypeCount == 1) {
- final ArrayList<View> scrap = mCurrentScrap;
- final int scrapCount = scrap.size();
-
- for (int i = 0; i < scrapCount; i++) {
- scrap.get(i).forceLayout();
- }
- } else {
- final int typeCount = mViewTypeCount;
- for (int i = 0; i < typeCount; i++) {
- for (View scrap : mScrapViews[i]) {
- scrap.forceLayout();
- }
- }
- }
-
- if (mTransientStateViews != null) {
- final int count = mTransientStateViews.size();
- for (int i = 0; i < count; i++) {
- mTransientStateViews.valueAt(i).forceLayout();
- }
- }
- }
-
- public boolean shouldRecycleViewType(int viewType) {
- return viewType >= 0;
- }
-
- void clear() {
- if (mViewTypeCount == 1) {
- final ArrayList<View> scrap = mCurrentScrap;
- final int scrapCount = scrap.size();
-
- for (int i = 0; i < scrapCount; i++) {
- removeDetachedView(scrap.remove(scrapCount - 1 - i), false);
- }
- } else {
- final int typeCount = mViewTypeCount;
- for (int i = 0; i < typeCount; i++) {
- final ArrayList<View> scrap = mScrapViews[i];
- final int scrapCount = scrap.size();
-
- for (int j = 0; j < scrapCount; j++) {
- removeDetachedView(scrap.remove(scrapCount - 1 - j), false);
- }
- }
- }
-
- if (mTransientStateViews != null) {
- mTransientStateViews.clear();
- }
- }
-
- void fillActiveViews(int childCount, int firstActivePosition) {
- if (mActiveViews.length < childCount) {
- mActiveViews = new View[childCount];
- }
-
- mFirstActivePosition = firstActivePosition;
-
- final View[] activeViews = mActiveViews;
- for (int i = 0; i < childCount; i++) {
- View child = getChildAt(i);
-
- // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views.
- // However, we will NOT place them into scrap views.
- activeViews[i] = child;
- }
- }
-
- View getActiveView(int position) {
- final int index = position - mFirstActivePosition;
- final View[] activeViews = mActiveViews;
-
- if (index >= 0 && index < activeViews.length) {
- final View match = activeViews[index];
- activeViews[index] = null;
-
- return match;
- }
-
- return null;
- }
-
- View getTransientStateView(int position) {
- if (mTransientStateViews == null) {
- return null;
- }
-
- final int index = mTransientStateViews.indexOfKey(position);
- if (index < 0) {
- return null;
- }
-
- final View result = mTransientStateViews.valueAt(index);
- mTransientStateViews.removeAt(index);
-
- return result;
- }
-
- void clearTransientStateViews() {
- if (mTransientStateViews != null) {
- mTransientStateViews.clear();
- }
- }
-
- View getScrapView(int position) {
- if (mViewTypeCount == 1) {
- return retrieveFromScrap(mCurrentScrap, position);
- } else {
- int whichScrap = mAdapter.getItemViewType(position);
- if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
- return retrieveFromScrap(mScrapViews[whichScrap], position);
- }
- }
-
- return null;
- }
-
- @TargetApi(14)
- void addScrapView(View scrap, int position) {
- LayoutParams lp = (LayoutParams) scrap.getLayoutParams();
- if (lp == null) {
- return;
- }
-
- lp.scrappedFromPosition = position;
-
- final int viewType = lp.viewType;
- final boolean scrapHasTransientState = ViewCompat.hasTransientState(scrap);
-
- // Don't put views that should be ignored into the scrap heap
- if (!shouldRecycleViewType(viewType) || scrapHasTransientState) {
- if (scrapHasTransientState) {
- if (mTransientStateViews == null) {
- mTransientStateViews = new SparseArrayCompat<View>();
- }
-
- mTransientStateViews.put(position, scrap);
- }
-
- return;
- }
-
- if (mViewTypeCount == 1) {
- mCurrentScrap.add(scrap);
- } else {
- mScrapViews[viewType].add(scrap);
- }
-
- // FIXME: Unfortunately, ViewCompat.setAccessibilityDelegate() doesn't accept
- // null delegates.
- if (Build.VERSION.SDK_INT >= 14) {
- scrap.setAccessibilityDelegate(null);
- }
-
- if (mRecyclerListener != null) {
- mRecyclerListener.onMovedToScrapHeap(scrap);
- }
- }
-
- @TargetApi(14)
- void scrapActiveViews() {
- final View[] activeViews = mActiveViews;
- final boolean multipleScraps = (mViewTypeCount > 1);
-
- ArrayList<View> scrapViews = mCurrentScrap;
- final int count = activeViews.length;
-
- for (int i = count - 1; i >= 0; i--) {
- final View victim = activeViews[i];
- if (victim != null) {
- final LayoutParams lp = (LayoutParams) victim.getLayoutParams();
- int whichScrap = lp.viewType;
-
- activeViews[i] = null;
-
- final boolean scrapHasTransientState = ViewCompat.hasTransientState(victim);
- if (!shouldRecycleViewType(whichScrap) || scrapHasTransientState) {
- if (scrapHasTransientState) {
- removeDetachedView(victim, false);
-
- if (mTransientStateViews == null) {
- mTransientStateViews = new SparseArrayCompat<View>();
- }
-
- mTransientStateViews.put(mFirstActivePosition + i, victim);
- }
-
- continue;
- }
-
- if (multipleScraps) {
- scrapViews = mScrapViews[whichScrap];
- }
-
- lp.scrappedFromPosition = mFirstActivePosition + i;
- scrapViews.add(victim);
-
- // FIXME: Unfortunately, ViewCompat.setAccessibilityDelegate() doesn't accept
- // null delegates.
- if (Build.VERSION.SDK_INT >= 14) {
- victim.setAccessibilityDelegate(null);
- }
-
- if (mRecyclerListener != null) {
- mRecyclerListener.onMovedToScrapHeap(victim);
- }
- }
- }
-
- pruneScrapViews();
- }
-
- private void pruneScrapViews() {
- final int maxViews = mActiveViews.length;
- final int viewTypeCount = mViewTypeCount;
- final ArrayList<View>[] scrapViews = mScrapViews;
-
- for (int i = 0; i < viewTypeCount; ++i) {
- final ArrayList<View> scrapPile = scrapViews[i];
- int size = scrapPile.size();
- final int extras = size - maxViews;
-
- size--;
-
- for (int j = 0; j < extras; j++) {
- removeDetachedView(scrapPile.remove(size--), false);
- }
- }
-
- if (mTransientStateViews != null) {
- for (int i = 0; i < mTransientStateViews.size(); i++) {
- final View v = mTransientStateViews.valueAt(i);
- if (!ViewCompat.hasTransientState(v)) {
- mTransientStateViews.removeAt(i);
- i--;
- }
- }
- }
- }
-
- void reclaimScrapViews(List<View> views) {
- if (mViewTypeCount == 1) {
- views.addAll(mCurrentScrap);
- } else {
- final int viewTypeCount = mViewTypeCount;
- final ArrayList<View>[] scrapViews = mScrapViews;
-
- for (int i = 0; i < viewTypeCount; ++i) {
- final ArrayList<View> scrapPile = scrapViews[i];
- views.addAll(scrapPile);
- }
- }
- }
-
- View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
- int size = scrapViews.size();
- if (size <= 0) {
- return null;
- }
-
- for (int i = 0; i < size; i++) {
- final View scrapView = scrapViews.get(i);
- final LayoutParams lp = (LayoutParams) scrapView.getLayoutParams();
-
- if (lp.scrappedFromPosition == position) {
- scrapViews.remove(i);
- return scrapView;
- }
- }
-
- return scrapViews.remove(size - 1);
- }
- }
-
- @Override
- public void setEmptyView(View emptyView) {
- super.setEmptyView(emptyView);
- mEmptyView = emptyView;
- updateEmptyStatus();
- }
-
- @Override
- public void setFocusable(boolean focusable) {
- final ListAdapter adapter = getAdapter();
- final boolean empty = (adapter == null || adapter.getCount() == 0);
-
- mDesiredFocusableState = focusable;
- if (!focusable) {
- mDesiredFocusableInTouchModeState = false;
- }
-
- super.setFocusable(focusable && !empty);
- }
-
- @Override
- public void setFocusableInTouchMode(boolean focusable) {
- final ListAdapter adapter = getAdapter();
- final boolean empty = (adapter == null || adapter.getCount() == 0);
-
- mDesiredFocusableInTouchModeState = focusable;
- if (focusable) {
- mDesiredFocusableState = true;
- }
-
- super.setFocusableInTouchMode(focusable && !empty);
- }
-
- private void checkFocus() {
- final ListAdapter adapter = getAdapter();
- final boolean focusable = (adapter != null && adapter.getCount() > 0);
-
- // The order in which we set focusable in touch mode/focusable may matter
- // for the client, see View.setFocusableInTouchMode() comments for more
- // details
- super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);
- super.setFocusable(focusable && mDesiredFocusableState);
-
- if (mEmptyView != null) {
- updateEmptyStatus();
- }
- }
-
- private void updateEmptyStatus() {
- final boolean isEmpty = (mAdapter == null || mAdapter.isEmpty());
-
- if (isEmpty) {
- if (mEmptyView != null) {
- mEmptyView.setVisibility(View.VISIBLE);
- setVisibility(View.GONE);
- } else {
- // If the caller just removed our empty view, make sure the list
- // view is visible
- setVisibility(View.VISIBLE);
- }
-
- // We are now GONE, so pending layouts will not be dispatched.
- // Force one here to make sure that the state of the list matches
- // the state of the adapter.
- if (mDataChanged) {
- layout(getLeft(), getTop(), getRight(), getBottom());
- }
- } else {
- if (mEmptyView != null) {
- mEmptyView.setVisibility(View.GONE);
- }
-
- setVisibility(View.VISIBLE);
- }
- }
-
- private class AdapterDataSetObserver extends DataSetObserver {
- private Parcelable mInstanceState = null;
-
- @Override
- public void onChanged() {
- mDataChanged = true;
- mOldItemCount = mItemCount;
- mItemCount = getAdapter().getCount();
-
- // Detect the case where a cursor that was previously invalidated has
- // been re-populated with new data.
- if (TwoWayView.this.mHasStableIds && mInstanceState != null
- && mOldItemCount == 0 && mItemCount > 0) {
- TwoWayView.this.onRestoreInstanceState(mInstanceState);
- mInstanceState = null;
- } else {
- rememberSyncState();
- }
-
- checkFocus();
- requestLayout();
- }
-
- @Override
- public void onInvalidated() {
- mDataChanged = true;
-
- if (TwoWayView.this.mHasStableIds) {
- // Remember the current state for the case where our hosting activity is being
- // stopped and later restarted
- mInstanceState = TwoWayView.this.onSaveInstanceState();
- }
-
- // Data is invalid so we should reset our state
- mOldItemCount = mItemCount;
- mItemCount = 0;
-
- mSelectedPosition = INVALID_POSITION;
- mSelectedRowId = INVALID_ROW_ID;
-
- mNextSelectedPosition = INVALID_POSITION;
- mNextSelectedRowId = INVALID_ROW_ID;
-
- mNeedSync = false;
-
- checkFocus();
- requestLayout();
- }
- }
-
- static class SavedState extends BaseSavedState {
- long selectedId;
- long firstId;
- int viewStart;
- int position;
- int size;
- int checkedItemCount;
- SparseBooleanArray checkState;
- LongSparseArray<Integer> checkIdState;
-
- /**
- * Constructor called from {@link TwoWayView#onSaveInstanceState()}
- */
- SavedState(Parcelable superState) {
- super(superState);
- }
-
- /**
- * Constructor called from {@link #CREATOR}
- */
- private SavedState(Parcel in) {
- super(in);
-
- selectedId = in.readLong();
- firstId = in.readLong();
- viewStart = in.readInt();
- position = in.readInt();
- size = in.readInt();
-
- checkedItemCount = in.readInt();
- checkState = in.readSparseBooleanArray();
-
- final int N = in.readInt();
- if (N > 0) {
- checkIdState = new LongSparseArray<Integer>();
- for (int i = 0; i < N; i++) {
- final long key = in.readLong();
- final int value = in.readInt();
- checkIdState.put(key, value);
- }
- }
- }
-
- @Override
- public void writeToParcel(Parcel out, int flags) {
- super.writeToParcel(out, flags);
-
- out.writeLong(selectedId);
- out.writeLong(firstId);
- out.writeInt(viewStart);
- out.writeInt(position);
- out.writeInt(size);
-
- out.writeInt(checkedItemCount);
- out.writeSparseBooleanArray(checkState);
-
- final int N = checkIdState != null ? checkIdState.size() : 0;
- out.writeInt(N);
-
- for (int i = 0; i < N; i++) {
- out.writeLong(checkIdState.keyAt(i));
- out.writeInt(checkIdState.valueAt(i));
- }
- }
-
- @Override
- public String toString() {
- return "TwoWayView.SavedState{"
- + Integer.toHexString(System.identityHashCode(this))
- + " selectedId=" + selectedId
- + " firstId=" + firstId
- + " viewStart=" + viewStart
- + " size=" + size
- + " position=" + position
- + " checkState=" + checkState + "}";
- }
-
- public static final Parcelable.Creator<SavedState> CREATOR
- = new Parcelable.Creator<SavedState>() {
- @Override
- public SavedState createFromParcel(Parcel in) {
- return new SavedState(in);
- }
-
- @Override
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
- }
-
- private class SelectionNotifier implements Runnable {
- @Override
- public void run() {
- if (mDataChanged) {
- // Data has changed between when this SelectionNotifier
- // was posted and now. We need to wait until the AdapterView
- // has been synched to the new data.
- if (mAdapter != null) {
- post(this);
- }
- } else {
- fireOnSelected();
- performAccessibilityActionsOnSelected();
- }
- }
- }
-
- private class WindowRunnable {
- private int mOriginalAttachCount;
-
- public void rememberWindowAttachCount() {
- mOriginalAttachCount = getWindowAttachCount();
- }
-
- public boolean sameWindow() {
- return hasWindowFocus() && getWindowAttachCount() == mOriginalAttachCount;
- }
- }
-
- private class PerformClick extends WindowRunnable implements Runnable {
- int mClickMotionPosition;
-
- @Override
- public void run() {
- if (mDataChanged) {
- return;
- }
-
- final ListAdapter adapter = mAdapter;
- final int motionPosition = mClickMotionPosition;
-
- if (adapter != null && mItemCount > 0 &&
- motionPosition != INVALID_POSITION &&
- motionPosition < adapter.getCount() && sameWindow()) {
-
- final View child = getChildAt(motionPosition - mFirstPosition);
- if (child != null) {
- performItemClick(child, motionPosition, adapter.getItemId(motionPosition));
- }
- }
- }
- }
-
- private final class CheckForTap implements Runnable {
- @Override
- public void run() {
- if (mTouchMode != TOUCH_MODE_DOWN) {
- return;
- }
-
- mTouchMode = TOUCH_MODE_TAP;
-
- final View child = getChildAt(mMotionPosition - mFirstPosition);
- if (child != null && !child.hasFocusable()) {
- mLayoutMode = LAYOUT_NORMAL;
-
- if (!mDataChanged) {
- setPressed(true);
- child.setPressed(true);
-
- layoutChildren();
- positionSelector(mMotionPosition, child);
- refreshDrawableState();
-
- positionSelector(mMotionPosition, child);
- refreshDrawableState();
-
- final boolean longClickable = isLongClickable();
-
- if (mSelector != null) {
- Drawable d = mSelector.getCurrent();
-
- if (d != null && d instanceof TransitionDrawable) {
- if (longClickable) {
- final int longPressTimeout = ViewConfiguration.getLongPressTimeout();
- ((TransitionDrawable) d).startTransition(longPressTimeout);
- } else {
- ((TransitionDrawable) d).resetTransition();
- }
- }
- }
-
- if (longClickable) {
- triggerCheckForLongPress();
- } else {
- mTouchMode = TOUCH_MODE_DONE_WAITING;
- }
- } else {
- mTouchMode = TOUCH_MODE_DONE_WAITING;
- }
- }
- }
- }
-
- private class CheckForLongPress extends WindowRunnable implements Runnable {
- @Override
- public void run() {
- final int motionPosition = mMotionPosition;
- final View child = getChildAt(motionPosition - mFirstPosition);
-
- if (child != null) {
- final long longPressId = mAdapter.getItemId(mMotionPosition);
-
- boolean handled = false;
- if (sameWindow() && !mDataChanged) {
- handled = performLongPress(child, motionPosition, longPressId);
- }
-
- if (handled) {
- mTouchMode = TOUCH_MODE_REST;
- setPressed(false);
- child.setPressed(false);
- } else {
- mTouchMode = TOUCH_MODE_DONE_WAITING;
- }
- }
- }
- }
-
- private class CheckForKeyLongPress extends WindowRunnable implements Runnable {
- public void run() {
- if (!isPressed() || mSelectedPosition < 0) {
- return;
- }
-
- final int index = mSelectedPosition - mFirstPosition;
- final View v = getChildAt(index);
-
- if (!mDataChanged) {
- boolean handled = false;
-
- if (sameWindow()) {
- handled = performLongPress(v, mSelectedPosition, mSelectedRowId);
- }
-
- if (handled) {
- setPressed(false);
- v.setPressed(false);
- }
- } else {
- setPressed(false);
-
- if (v != null) {
- v.setPressed(false);
- }
- }
- }
- }
-
- private static class ArrowScrollFocusResult {
- private int mSelectedPosition;
- private int mAmountToScroll;
-
- /**
- * How {@link TwoWayView#arrowScrollFocused} returns its values.
- */
- void populate(int selectedPosition, int amountToScroll) {
- mSelectedPosition = selectedPosition;
- mAmountToScroll = amountToScroll;
- }
-
- public int getSelectedPosition() {
- return mSelectedPosition;
- }
-
- public int getAmountToScroll() {
- return mAmountToScroll;
- }
- }
-
- private class ListItemAccessibilityDelegate extends AccessibilityDelegateCompat {
- @Override
- public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
- super.onInitializeAccessibilityNodeInfo(host, info);
-
- final int position = getPositionForView(host);
- final ListAdapter adapter = getAdapter();
-
- // Cannot perform actions on invalid items
- if (position == INVALID_POSITION || adapter == null) {
- return;
- }
-
- // Cannot perform actions on disabled items
- if (!isEnabled() || !adapter.isEnabled(position)) {
- return;
- }
-
- if (position == getSelectedItemPosition()) {
- info.setSelected(true);
- info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION);
- } else {
- info.addAction(AccessibilityNodeInfoCompat.ACTION_SELECT);
- }
-
- if (isClickable()) {
- info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
- info.setClickable(true);
- }
-
- if (isLongClickable()) {
- info.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK);
- info.setLongClickable(true);
- }
- }
-
- @Override
- public boolean performAccessibilityAction(View host, int action, Bundle arguments) {
- if (super.performAccessibilityAction(host, action, arguments)) {
- return true;
- }
-
- final int position = getPositionForView(host);
- final ListAdapter adapter = getAdapter();
-
- // Cannot perform actions on invalid items
- if (position == INVALID_POSITION || adapter == null) {
- return false;
- }
-
- // Cannot perform actions on disabled items
- if (!isEnabled() || !adapter.isEnabled(position)) {
- return false;
- }
-
- final long id = getItemIdAtPosition(position);
-
- switch (action) {
- case AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION:
- if (getSelectedItemPosition() == position) {
- setSelection(INVALID_POSITION);
- return true;
- }
- return false;
-
- case AccessibilityNodeInfoCompat.ACTION_SELECT:
- if (getSelectedItemPosition() != position) {
- setSelection(position);
- return true;
- }
- return false;
-
- case AccessibilityNodeInfoCompat.ACTION_CLICK:
- return isClickable() && performItemClick(host, position, id);
-
- case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK:
- return isLongClickable() && performLongPress(host, position, id);
- }
-
- return false;
- }
- }
-
- private class PositionScroller implements Runnable {
- private static final int SCROLL_DURATION = 200;
-
- private static final int MOVE_AFTER_POS = 1;
- private static final int MOVE_BEFORE_POS = 2;
- private static final int MOVE_AFTER_BOUND = 3;
- private static final int MOVE_BEFORE_BOUND = 4;
- private static final int MOVE_OFFSET = 5;
-
- private int mMode;
- private int mTargetPosition;
- private int mBoundPosition;
- private int mLastSeenPosition;
- private int mScrollDuration;
- private final int mExtraScroll;
-
- private int mOffsetFromStart;
-
- PositionScroller() {
- mExtraScroll = ViewConfiguration.get(mContext).getScaledFadingEdgeLength();
- }
-
- void start(final int position) {
- stop();
-
- if (mDataChanged) {
- // Wait until we're back in a stable state to try this.
- mPositionScrollAfterLayout = new Runnable() {
- @Override public void run() {
- start(position);
- }
- };
-
- return;
- }
-
- final int childCount = getChildCount();
- if (childCount == 0) {
- // Can't scroll without children.
- return;
- }
-
- final int firstPosition = mFirstPosition;
- final int lastPosition = firstPosition + childCount - 1;
-
- final int clampedPosition = Math.max(0, Math.min(getCount() - 1, position));
-
- final int viewTravelCount;
- if (clampedPosition < firstPosition) {
- viewTravelCount = firstPosition - clampedPosition + 1;
- mMode = MOVE_BEFORE_POS;
- } else if (clampedPosition > lastPosition) {
- viewTravelCount = clampedPosition - lastPosition + 1;
- mMode = MOVE_AFTER_POS;
- } else {
- scrollToVisible(clampedPosition, INVALID_POSITION, SCROLL_DURATION);
- return;
- }
-
- if (viewTravelCount > 0) {
- mScrollDuration = SCROLL_DURATION / viewTravelCount;
- } else {
- mScrollDuration = SCROLL_DURATION;
- }
-
- mTargetPosition = clampedPosition;
- mBoundPosition = INVALID_POSITION;
- mLastSeenPosition = INVALID_POSITION;
-
- ViewCompat.postOnAnimation(TwoWayView.this, this);
- }
-
- void start(final int position, final int boundPosition) {
- stop();
-
- if (boundPosition == INVALID_POSITION) {
- start(position);
- return;
- }
-
- if (mDataChanged) {
- // Wait until we're back in a stable state to try this.
- mPositionScrollAfterLayout = new Runnable() {
- @Override public void run() {
- start(position, boundPosition);
- }
- };
-
- return;
- }
-
- final int childCount = getChildCount();
- if (childCount == 0) {
- // Can't scroll without children.
- return;
- }
-
- final int firstPosition = mFirstPosition;
- final int lastPosition = firstPosition + childCount - 1;
-
- final int clampedPosition = Math.max(0, Math.min(getCount() - 1, position));
-
- final int viewTravelCount;
- if (clampedPosition < firstPosition) {
- final int boundPositionFromLast = lastPosition - boundPosition;
- if (boundPositionFromLast < 1) {
- // Moving would shift our bound position off the screen. Abort.
- return;
- }
-
- final int positionTravel = firstPosition - clampedPosition + 1;
- final int boundTravel = boundPositionFromLast - 1;
- if (boundTravel < positionTravel) {
- viewTravelCount = boundTravel;
- mMode = MOVE_BEFORE_BOUND;
- } else {
- viewTravelCount = positionTravel;
- mMode = MOVE_BEFORE_POS;
- }
- } else if (clampedPosition > lastPosition) {
- final int boundPositionFromFirst = boundPosition - firstPosition;
- if (boundPositionFromFirst < 1) {
- // Moving would shift our bound position off the screen. Abort.
- return;
- }
-
- final int positionTravel = clampedPosition - lastPosition + 1;
- final int boundTravel = boundPositionFromFirst - 1;
- if (boundTravel < positionTravel) {
- viewTravelCount = boundTravel;
- mMode = MOVE_AFTER_BOUND;
- } else {
- viewTravelCount = positionTravel;
- mMode = MOVE_AFTER_POS;
- }
- } else {
- scrollToVisible(clampedPosition, boundPosition, SCROLL_DURATION);
- return;
- }
-
- if (viewTravelCount > 0) {
- mScrollDuration = SCROLL_DURATION / viewTravelCount;
- } else {
- mScrollDuration = SCROLL_DURATION;
- }
-
- mTargetPosition = clampedPosition;
- mBoundPosition = boundPosition;
- mLastSeenPosition = INVALID_POSITION;
-
- ViewCompat.postOnAnimation(TwoWayView.this, this);
- }
-
- void startWithOffset(int position, int offset) {
- startWithOffset(position, offset, SCROLL_DURATION);
- }
-
- void startWithOffset(final int position, int offset, final int duration) {
- stop();
-
- if (mDataChanged) {
- // Wait until we're back in a stable state to try this.
- final int postOffset = offset;
- mPositionScrollAfterLayout = new Runnable() {
- @Override public void run() {
- startWithOffset(position, postOffset, duration);
- }
- };
-
- return;
- }
-
- final int childCount = getChildCount();
- if (childCount == 0) {
- // Can't scroll without children.
- return;
- }
-
- offset += getStartEdge();
-
- mTargetPosition = Math.max(0, Math.min(getCount() - 1, position));
- mOffsetFromStart = offset;
- mBoundPosition = INVALID_POSITION;
- mLastSeenPosition = INVALID_POSITION;
- mMode = MOVE_OFFSET;
-
- final int firstPosition = mFirstPosition;
- final int lastPosition = firstPosition + childCount - 1;
-
- final int viewTravelCount;
- if (mTargetPosition < firstPosition) {
- viewTravelCount = firstPosition - mTargetPosition;
- } else if (mTargetPosition > lastPosition) {
- viewTravelCount = mTargetPosition - lastPosition;
- } else {
- // On-screen, just scroll.
- final View targetView = getChildAt(mTargetPosition - firstPosition);
- final int targetStart = getChildStartEdge(targetView);
- smoothScrollBy(targetStart - offset, duration);
- return;
- }
-
- // Estimate how many screens we should travel
- final float screenTravelCount = (float) viewTravelCount / childCount;
- mScrollDuration = screenTravelCount < 1 ?
- duration : (int) (duration / screenTravelCount);
- mLastSeenPosition = INVALID_POSITION;
-
- ViewCompat.postOnAnimation(TwoWayView.this, this);
- }
-
- /**
- * Scroll such that targetPos is in the visible padded region without scrolling
- * boundPos out of view. Assumes targetPos is onscreen.
- */
- void scrollToVisible(int targetPosition, int boundPosition, int duration) {
- final int childCount = getChildCount();
- final int firstPosition = mFirstPosition;
- final int lastPosition = firstPosition + childCount - 1;
-
- final int start = getStartEdge();
- final int end = getEndEdge();
-
- if (targetPosition < firstPosition || targetPosition > lastPosition) {
- Log.w(LOGTAG, "scrollToVisible called with targetPosition " + targetPosition +
- " not visible [" + firstPosition + ", " + lastPosition + "]");
- }
-
- if (boundPosition < firstPosition || boundPosition > lastPosition) {
- // boundPos doesn't matter, it's already offscreen.
- boundPosition = INVALID_POSITION;
- }
-
- final View targetChild = getChildAt(targetPosition - firstPosition);
- final int targetStart = getChildStartEdge(targetChild);
- final int targetEnd = getChildEndEdge(targetChild);
-
- int scrollBy = 0;
- if (targetEnd > end) {
- scrollBy = targetEnd - end;
- }
- if (targetStart < start) {
- scrollBy = targetStart - start;
- }
-
- if (scrollBy == 0) {
- return;
- }
-
- if (boundPosition >= 0) {
- final View boundChild = getChildAt(boundPosition - firstPosition);
- final int boundStart = getChildStartEdge(boundChild);
- final int boundEnd = getChildEndEdge(boundChild);
- final int absScroll = Math.abs(scrollBy);
-
- if (scrollBy < 0 && boundEnd + absScroll > end) {
- // Don't scroll the bound view off the end of the screen.
- scrollBy = Math.max(0, boundEnd - end);
- } else if (scrollBy > 0 && boundStart - absScroll < start) {
- // Don't scroll the bound view off the top of the screen.
- scrollBy = Math.min(0, boundStart - start);
- }
- }
-
- smoothScrollBy(scrollBy, duration);
- }
-
- void stop() {
- removeCallbacks(this);
- }
-
- @Override
- public void run() {
- final int size = getAvailableSize();
- final int firstPosition = mFirstPosition;
-
- final int startPadding = (mIsVertical ? getPaddingTop() : getPaddingLeft());
- final int endPadding = (mIsVertical ? getPaddingBottom() : getPaddingRight());
-
- switch (mMode) {
- case MOVE_AFTER_POS: {
- final int lastViewIndex = getChildCount() - 1;
- if (lastViewIndex < 0) {
- return;
- }
-
- final int lastPosition = firstPosition + lastViewIndex;
- if (lastPosition == mLastSeenPosition) {
- // No new views, let things keep going.
- ViewCompat.postOnAnimation(TwoWayView.this, this);
- return;
- }
-
- final View lastView = getChildAt(lastViewIndex);
- final int lastViewSize = getChildSize(lastView);
- final int lastViewStart = getChildStartEdge(lastView);
- final int lastViewPixelsShowing = size - lastViewStart;
- final int extraScroll = lastPosition < mItemCount - 1 ?
- Math.max(endPadding, mExtraScroll) : endPadding;
-
- final int scrollBy = lastViewSize - lastViewPixelsShowing + extraScroll;
- smoothScrollBy(scrollBy, mScrollDuration);
-
- mLastSeenPosition = lastPosition;
- if (lastPosition < mTargetPosition) {
- ViewCompat.postOnAnimation(TwoWayView.this, this);
- }
-
- break;
- }
-
- case MOVE_AFTER_BOUND: {
- final int nextViewIndex = 1;
- final int childCount = getChildCount();
- if (firstPosition == mBoundPosition ||
- childCount <= nextViewIndex ||
- firstPosition + childCount >= mItemCount) {
- return;
- }
-
- final int nextPosition = firstPosition + nextViewIndex;
-
- if (nextPosition == mLastSeenPosition) {
- // No new views, let things keep going.
- ViewCompat.postOnAnimation(TwoWayView.this, this);
- return;
- }
-
- final View nextView = getChildAt(nextViewIndex);
- final int nextViewSize = getChildSize(nextView);
- final int nextViewStart = getChildStartEdge(nextView);
- final int extraScroll = Math.max(endPadding, mExtraScroll);
- if (nextPosition < mBoundPosition) {
- smoothScrollBy(Math.max(0, nextViewSize + nextViewStart - extraScroll),
- mScrollDuration);
- mLastSeenPosition = nextPosition;
- ViewCompat.postOnAnimation(TwoWayView.this, this);
- } else {
- if (nextViewSize > extraScroll) {
- smoothScrollBy(nextViewSize - extraScroll, mScrollDuration);
- }
- }
-
- break;
- }
-
- case MOVE_BEFORE_POS: {
- if (firstPosition == mLastSeenPosition) {
- // No new views, let things keep going.
- ViewCompat.postOnAnimation(TwoWayView.this, this);
- return;
- }
-
- final View firstView = getChildAt(0);
- if (firstView == null) {
- return;
- }
-
- final int firstViewTop = getChildStartEdge(firstView);
- final int extraScroll = firstPosition > 0 ?
- Math.max(mExtraScroll, startPadding) : startPadding;
-
- smoothScrollBy(firstViewTop - extraScroll, mScrollDuration);
- mLastSeenPosition = firstPosition;
-
- if (firstPosition > mTargetPosition) {
- ViewCompat.postOnAnimation(TwoWayView.this, this);
- }
-
- break;
- }
-
- case MOVE_BEFORE_BOUND: {
- final int lastViewIndex = getChildCount() - 2;
- if (lastViewIndex < 0) {
- return;
- }
-
- final int lastPosition = firstPosition + lastViewIndex;
-
- if (lastPosition == mLastSeenPosition) {
- // No new views, let things keep going.
- ViewCompat.postOnAnimation(TwoWayView.this, this);
- return;
- }
-
- final View lastView = getChildAt(lastViewIndex);
- final int lastViewSize = getChildSize(lastView);
- final int lastViewStart = getChildStartEdge(lastView);
- final int lastViewPixelsShowing = size - lastViewStart;
- final int extraScroll = Math.max(startPadding, mExtraScroll);
-
- mLastSeenPosition = lastPosition;
-
- if (lastPosition > mBoundPosition) {
- smoothScrollBy(-(lastViewPixelsShowing - extraScroll), mScrollDuration);
- ViewCompat.postOnAnimation(TwoWayView.this, this);
- } else {
- final int end = size - extraScroll;
- final int lastViewEnd = lastViewStart + lastViewSize;
- if (end > lastViewEnd) {
- smoothScrollBy(-(end - lastViewEnd), mScrollDuration);
- }
- }
-
- break;
- }
-
- case MOVE_OFFSET: {
- if (mLastSeenPosition == firstPosition) {
- // No new views, let things keep going.
- ViewCompat.postOnAnimation(TwoWayView.this, this);
- return;
- }
-
- mLastSeenPosition = firstPosition;
-
- final int childCount = getChildCount();
- final int position = mTargetPosition;
- final int lastPos = firstPosition + childCount - 1;
-
- int viewTravelCount = 0;
- if (position < firstPosition) {
- viewTravelCount = firstPosition - position + 1;
- } else if (position > lastPos) {
- viewTravelCount = position - lastPos;
- }
-
- // Estimate how many screens we should travel
- final float screenTravelCount = (float) viewTravelCount / childCount;
-
- final float modifier = Math.min(Math.abs(screenTravelCount), 1.f);
- if (position < firstPosition) {
- final int distance = (int) (-getSize() * modifier);
- final int duration = (int) (mScrollDuration * modifier);
- smoothScrollBy(distance, duration);
- ViewCompat.postOnAnimation(TwoWayView.this, this);
- } else if (position > lastPos) {
- final int distance = (int) (getSize() * modifier);
- final int duration = (int) (mScrollDuration * modifier);
- smoothScrollBy(distance, duration);
- ViewCompat.postOnAnimation(TwoWayView.this, this);
- } else {
- // On-screen, just scroll.
- final View targetView = getChildAt(position - firstPosition);
- final int targetStart = getChildStartEdge(targetView);
- final int distance = targetStart - mOffsetFromStart;
- final int duration = (int) (mScrollDuration *
- ((float) Math.abs(distance) / getSize()));
- smoothScrollBy(distance, duration);
- }
-
- break;
- }
-
- default:
- break;
- }
- }
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedEditText.java b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedEditText.java
deleted file mode 100644
index c84686e90..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedEditText.java
+++ /dev/null
@@ -1,172 +0,0 @@
-// This file is generated by generate_themed_views.py; do not edit.
-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget.themed;
-
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.GeckoApplication;
-import org.mozilla.gecko.lwt.LightweightTheme;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.DrawableUtil;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.TypedArray;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-
-public class ThemedEditText extends android.widget.EditText
- implements LightweightTheme.OnChangeListener {
- private LightweightTheme theme;
-
- private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
- private static final int[] STATE_LIGHT = { R.attr.state_light };
- private static final int[] STATE_DARK = { R.attr.state_dark };
-
- protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
- protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
- protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
-
- private boolean isPrivate;
- private boolean isLight;
- private boolean isDark;
- private boolean autoUpdateTheme; // always false if there's no theme.
-
- private ColorStateList drawableColors;
-
- public ThemedEditText(Context context, AttributeSet attrs) {
- super(context, attrs);
- initialize(context, attrs, 0);
- }
-
- public ThemedEditText(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- initialize(context, attrs, defStyle);
- }
-
- private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
- // The theme can be null, particularly if we might be instantiating this
- // View in an IDE, with no ambient GeckoApplication.
- final Context applicationContext = context.getApplicationContext();
- if (applicationContext instanceof GeckoApplication) {
- theme = ((GeckoApplication) applicationContext).getLightweightTheme();
- }
-
- final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
- autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
- a.recycle();
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- if (autoUpdateTheme)
- theme.addListener(this);
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- if (autoUpdateTheme)
- theme.removeListener(this);
- }
-
- @Override
- public int[] onCreateDrawableState(int extraSpace) {
- final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
-
- if (isPrivate)
- mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
- else if (isLight)
- mergeDrawableStates(drawableState, STATE_LIGHT);
- else if (isDark)
- mergeDrawableStates(drawableState, STATE_DARK);
-
- return drawableState;
- }
-
- @Override
- public void onLightweightThemeChanged() {
- if (autoUpdateTheme && theme.isEnabled())
- setTheme(theme.isLightTheme());
- }
-
- @Override
- public void onLightweightThemeReset() {
- if (autoUpdateTheme)
- resetTheme();
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- onLightweightThemeChanged();
- }
-
- public boolean isPrivateMode() {
- return isPrivate;
- }
-
- public void setPrivateMode(boolean isPrivate) {
- if (this.isPrivate != isPrivate) {
- this.isPrivate = isPrivate;
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void setTheme(boolean isLight) {
- // Set the theme only if it is different from existing theme.
- if ((isLight && this.isLight != isLight) ||
- (!isLight && this.isDark == isLight)) {
- if (isLight) {
- this.isLight = true;
- this.isDark = false;
- } else {
- this.isLight = false;
- this.isDark = true;
- }
-
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void resetTheme() {
- if (isLight || isDark) {
- isLight = false;
- isDark = false;
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void setAutoUpdateTheme(boolean autoUpdateTheme) {
- if (theme == null) {
- return;
- }
-
- if (this.autoUpdateTheme != autoUpdateTheme) {
- this.autoUpdateTheme = autoUpdateTheme;
-
- if (autoUpdateTheme)
- theme.addListener(this);
- else
- theme.removeListener(this);
- }
- }
-
- public ColorDrawable getColorDrawable(int id) {
- return new ColorDrawable(ContextCompat.getColor(getContext(), id));
- }
-
- protected LightweightTheme getTheme() {
- return theme;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedFrameLayout.java b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedFrameLayout.java
deleted file mode 100644
index a95fe2d9f..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedFrameLayout.java
+++ /dev/null
@@ -1,172 +0,0 @@
-// This file is generated by generate_themed_views.py; do not edit.
-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget.themed;
-
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.GeckoApplication;
-import org.mozilla.gecko.lwt.LightweightTheme;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.DrawableUtil;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.TypedArray;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-
-public class ThemedFrameLayout extends android.widget.FrameLayout
- implements LightweightTheme.OnChangeListener {
- private LightweightTheme theme;
-
- private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
- private static final int[] STATE_LIGHT = { R.attr.state_light };
- private static final int[] STATE_DARK = { R.attr.state_dark };
-
- protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
- protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
- protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
-
- private boolean isPrivate;
- private boolean isLight;
- private boolean isDark;
- private boolean autoUpdateTheme; // always false if there's no theme.
-
- private ColorStateList drawableColors;
-
- public ThemedFrameLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- initialize(context, attrs, 0);
- }
-
- public ThemedFrameLayout(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- initialize(context, attrs, defStyle);
- }
-
- private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
- // The theme can be null, particularly if we might be instantiating this
- // View in an IDE, with no ambient GeckoApplication.
- final Context applicationContext = context.getApplicationContext();
- if (applicationContext instanceof GeckoApplication) {
- theme = ((GeckoApplication) applicationContext).getLightweightTheme();
- }
-
- final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
- autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
- a.recycle();
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- if (autoUpdateTheme)
- theme.addListener(this);
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- if (autoUpdateTheme)
- theme.removeListener(this);
- }
-
- @Override
- public int[] onCreateDrawableState(int extraSpace) {
- final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
-
- if (isPrivate)
- mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
- else if (isLight)
- mergeDrawableStates(drawableState, STATE_LIGHT);
- else if (isDark)
- mergeDrawableStates(drawableState, STATE_DARK);
-
- return drawableState;
- }
-
- @Override
- public void onLightweightThemeChanged() {
- if (autoUpdateTheme && theme.isEnabled())
- setTheme(theme.isLightTheme());
- }
-
- @Override
- public void onLightweightThemeReset() {
- if (autoUpdateTheme)
- resetTheme();
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- onLightweightThemeChanged();
- }
-
- public boolean isPrivateMode() {
- return isPrivate;
- }
-
- public void setPrivateMode(boolean isPrivate) {
- if (this.isPrivate != isPrivate) {
- this.isPrivate = isPrivate;
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void setTheme(boolean isLight) {
- // Set the theme only if it is different from existing theme.
- if ((isLight && this.isLight != isLight) ||
- (!isLight && this.isDark == isLight)) {
- if (isLight) {
- this.isLight = true;
- this.isDark = false;
- } else {
- this.isLight = false;
- this.isDark = true;
- }
-
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void resetTheme() {
- if (isLight || isDark) {
- isLight = false;
- isDark = false;
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void setAutoUpdateTheme(boolean autoUpdateTheme) {
- if (theme == null) {
- return;
- }
-
- if (this.autoUpdateTheme != autoUpdateTheme) {
- this.autoUpdateTheme = autoUpdateTheme;
-
- if (autoUpdateTheme)
- theme.addListener(this);
- else
- theme.removeListener(this);
- }
- }
-
- public ColorDrawable getColorDrawable(int id) {
- return new ColorDrawable(ContextCompat.getColor(getContext(), id));
- }
-
- protected LightweightTheme getTheme() {
- return theme;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedImageButton.java b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedImageButton.java
deleted file mode 100644
index 88e94c6c7..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedImageButton.java
+++ /dev/null
@@ -1,200 +0,0 @@
-// This file is generated by generate_themed_views.py; do not edit.
-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget.themed;
-
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.GeckoApplication;
-import org.mozilla.gecko.lwt.LightweightTheme;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.DrawableUtil;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.TypedArray;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-
-public class ThemedImageButton extends android.widget.ImageButton
- implements LightweightTheme.OnChangeListener {
- private LightweightTheme theme;
-
- private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
- private static final int[] STATE_LIGHT = { R.attr.state_light };
- private static final int[] STATE_DARK = { R.attr.state_dark };
-
- protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
- protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
- protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
-
- private boolean isPrivate;
- private boolean isLight;
- private boolean isDark;
- private boolean autoUpdateTheme; // always false if there's no theme.
-
- private ColorStateList drawableColors;
-
- public ThemedImageButton(Context context, AttributeSet attrs) {
- super(context, attrs);
- initialize(context, attrs, 0);
- }
-
- public ThemedImageButton(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- initialize(context, attrs, defStyle);
- }
-
- private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
- // The theme can be null, particularly if we might be instantiating this
- // View in an IDE, with no ambient GeckoApplication.
- final Context applicationContext = context.getApplicationContext();
- if (applicationContext instanceof GeckoApplication) {
- theme = ((GeckoApplication) applicationContext).getLightweightTheme();
- }
-
- final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
- autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
- a.recycle();
-
- final TypedArray themedA = context.obtainStyledAttributes(attrs, R.styleable.ThemedView, defStyle, 0);
- drawableColors = themedA.getColorStateList(R.styleable.ThemedView_drawableTintList);
- themedA.recycle();
-
- // Apply the tint initially - the Drawable is
- // initially set by XML via super's constructor.
- setTintedImageDrawable(getDrawable());
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- if (autoUpdateTheme)
- theme.addListener(this);
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- if (autoUpdateTheme)
- theme.removeListener(this);
- }
-
- @Override
- public int[] onCreateDrawableState(int extraSpace) {
- final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
-
- if (isPrivate)
- mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
- else if (isLight)
- mergeDrawableStates(drawableState, STATE_LIGHT);
- else if (isDark)
- mergeDrawableStates(drawableState, STATE_DARK);
-
- return drawableState;
- }
-
- @Override
- public void onLightweightThemeChanged() {
- if (autoUpdateTheme && theme.isEnabled())
- setTheme(theme.isLightTheme());
- }
-
- @Override
- public void onLightweightThemeReset() {
- if (autoUpdateTheme)
- resetTheme();
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- onLightweightThemeChanged();
- }
-
- public boolean isPrivateMode() {
- return isPrivate;
- }
-
- public void setPrivateMode(boolean isPrivate) {
- if (this.isPrivate != isPrivate) {
- this.isPrivate = isPrivate;
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void setTheme(boolean isLight) {
- // Set the theme only if it is different from existing theme.
- if ((isLight && this.isLight != isLight) ||
- (!isLight && this.isDark == isLight)) {
- if (isLight) {
- this.isLight = true;
- this.isDark = false;
- } else {
- this.isLight = false;
- this.isDark = true;
- }
-
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void resetTheme() {
- if (isLight || isDark) {
- isLight = false;
- isDark = false;
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void setAutoUpdateTheme(boolean autoUpdateTheme) {
- if (theme == null) {
- return;
- }
-
- if (this.autoUpdateTheme != autoUpdateTheme) {
- this.autoUpdateTheme = autoUpdateTheme;
-
- if (autoUpdateTheme)
- theme.addListener(this);
- else
- theme.removeListener(this);
- }
- }
-
- @Override
- public void setImageDrawable(final Drawable drawable) {
- setTintedImageDrawable(drawable);
- }
-
- private void setTintedImageDrawable(final Drawable drawable) {
- final Drawable tintedDrawable;
- if (drawableColors == null || R.id.bookmark == getId()) {
- // NB: The bookmarked state uses a blue star, so this is a hack to keep it untinted.
- // NB: If we tint a drawable with a null ColorStateList, it will override
- // any existing colorFilters and tint... so don't!
- tintedDrawable = drawable;
- } else if (drawable == null) {
- tintedDrawable = null;
- } else {
- tintedDrawable = DrawableUtil.tintDrawableWithStateList(drawable, drawableColors);
- }
- super.setImageDrawable(tintedDrawable);
- }
-
- public ColorDrawable getColorDrawable(int id) {
- return new ColorDrawable(ContextCompat.getColor(getContext(), id));
- }
-
- protected LightweightTheme getTheme() {
- return theme;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedImageView.java b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedImageView.java
deleted file mode 100644
index befbe6fb5..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedImageView.java
+++ /dev/null
@@ -1,199 +0,0 @@
-// This file is generated by generate_themed_views.py; do not edit.
-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget.themed;
-
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.GeckoApplication;
-import org.mozilla.gecko.lwt.LightweightTheme;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.DrawableUtil;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.TypedArray;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-
-public class ThemedImageView extends android.widget.ImageView
- implements LightweightTheme.OnChangeListener {
- private LightweightTheme theme;
-
- private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
- private static final int[] STATE_LIGHT = { R.attr.state_light };
- private static final int[] STATE_DARK = { R.attr.state_dark };
-
- protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
- protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
- protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
-
- private boolean isPrivate;
- private boolean isLight;
- private boolean isDark;
- private boolean autoUpdateTheme; // always false if there's no theme.
-
- private ColorStateList drawableColors;
-
- public ThemedImageView(Context context, AttributeSet attrs) {
- super(context, attrs);
- initialize(context, attrs, 0);
- }
-
- public ThemedImageView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- initialize(context, attrs, defStyle);
- }
-
- private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
- // The theme can be null, particularly if we might be instantiating this
- // View in an IDE, with no ambient GeckoApplication.
- final Context applicationContext = context.getApplicationContext();
- if (applicationContext instanceof GeckoApplication) {
- theme = ((GeckoApplication) applicationContext).getLightweightTheme();
- }
-
- final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
- autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
- a.recycle();
-
- final TypedArray themedA = context.obtainStyledAttributes(attrs, R.styleable.ThemedView, defStyle, 0);
- drawableColors = themedA.getColorStateList(R.styleable.ThemedView_drawableTintList);
- themedA.recycle();
-
- // Apply the tint initially - the Drawable is
- // initially set by XML via super's constructor.
- setTintedImageDrawable(getDrawable());
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- if (autoUpdateTheme)
- theme.addListener(this);
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- if (autoUpdateTheme)
- theme.removeListener(this);
- }
-
- @Override
- public int[] onCreateDrawableState(int extraSpace) {
- final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
-
- if (isPrivate)
- mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
- else if (isLight)
- mergeDrawableStates(drawableState, STATE_LIGHT);
- else if (isDark)
- mergeDrawableStates(drawableState, STATE_DARK);
-
- return drawableState;
- }
-
- @Override
- public void onLightweightThemeChanged() {
- if (autoUpdateTheme && theme.isEnabled())
- setTheme(theme.isLightTheme());
- }
-
- @Override
- public void onLightweightThemeReset() {
- if (autoUpdateTheme)
- resetTheme();
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- onLightweightThemeChanged();
- }
-
- public boolean isPrivateMode() {
- return isPrivate;
- }
-
- public void setPrivateMode(boolean isPrivate) {
- if (this.isPrivate != isPrivate) {
- this.isPrivate = isPrivate;
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void setTheme(boolean isLight) {
- // Set the theme only if it is different from existing theme.
- if ((isLight && this.isLight != isLight) ||
- (!isLight && this.isDark == isLight)) {
- if (isLight) {
- this.isLight = true;
- this.isDark = false;
- } else {
- this.isLight = false;
- this.isDark = true;
- }
-
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void resetTheme() {
- if (isLight || isDark) {
- isLight = false;
- isDark = false;
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void setAutoUpdateTheme(boolean autoUpdateTheme) {
- if (theme == null) {
- return;
- }
-
- if (this.autoUpdateTheme != autoUpdateTheme) {
- this.autoUpdateTheme = autoUpdateTheme;
-
- if (autoUpdateTheme)
- theme.addListener(this);
- else
- theme.removeListener(this);
- }
- }
-
- @Override
- public void setImageDrawable(final Drawable drawable) {
- setTintedImageDrawable(drawable);
- }
-
- private void setTintedImageDrawable(final Drawable drawable) {
- final Drawable tintedDrawable;
- if (drawableColors == null) {
- // NB: If we tint a drawable with a null ColorStateList, it will override
- // any existing colorFilters and tint... so don't!
- tintedDrawable = drawable;
- } else if (drawable == null) {
- tintedDrawable = null;
- } else {
- tintedDrawable = DrawableUtil.tintDrawableWithStateList(drawable, drawableColors);
- }
- super.setImageDrawable(tintedDrawable);
- }
-
- public ColorDrawable getColorDrawable(int id) {
- return new ColorDrawable(ContextCompat.getColor(getContext(), id));
- }
-
- protected LightweightTheme getTheme() {
- return theme;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedLinearLayout.java b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedLinearLayout.java
deleted file mode 100644
index 87ec58ce0..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedLinearLayout.java
+++ /dev/null
@@ -1,167 +0,0 @@
-// This file is generated by generate_themed_views.py; do not edit.
-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget.themed;
-
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.GeckoApplication;
-import org.mozilla.gecko.lwt.LightweightTheme;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.DrawableUtil;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.TypedArray;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-
-public class ThemedLinearLayout extends android.widget.LinearLayout
- implements LightweightTheme.OnChangeListener {
- private LightweightTheme theme;
-
- private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
- private static final int[] STATE_LIGHT = { R.attr.state_light };
- private static final int[] STATE_DARK = { R.attr.state_dark };
-
- protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
- protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
- protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
-
- private boolean isPrivate;
- private boolean isLight;
- private boolean isDark;
- private boolean autoUpdateTheme; // always false if there's no theme.
-
- private ColorStateList drawableColors;
-
- public ThemedLinearLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- initialize(context, attrs, 0);
- }
-
- private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
- // The theme can be null, particularly if we might be instantiating this
- // View in an IDE, with no ambient GeckoApplication.
- final Context applicationContext = context.getApplicationContext();
- if (applicationContext instanceof GeckoApplication) {
- theme = ((GeckoApplication) applicationContext).getLightweightTheme();
- }
-
- final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
- autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
- a.recycle();
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- if (autoUpdateTheme)
- theme.addListener(this);
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- if (autoUpdateTheme)
- theme.removeListener(this);
- }
-
- @Override
- public int[] onCreateDrawableState(int extraSpace) {
- final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
-
- if (isPrivate)
- mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
- else if (isLight)
- mergeDrawableStates(drawableState, STATE_LIGHT);
- else if (isDark)
- mergeDrawableStates(drawableState, STATE_DARK);
-
- return drawableState;
- }
-
- @Override
- public void onLightweightThemeChanged() {
- if (autoUpdateTheme && theme.isEnabled())
- setTheme(theme.isLightTheme());
- }
-
- @Override
- public void onLightweightThemeReset() {
- if (autoUpdateTheme)
- resetTheme();
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- onLightweightThemeChanged();
- }
-
- public boolean isPrivateMode() {
- return isPrivate;
- }
-
- public void setPrivateMode(boolean isPrivate) {
- if (this.isPrivate != isPrivate) {
- this.isPrivate = isPrivate;
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void setTheme(boolean isLight) {
- // Set the theme only if it is different from existing theme.
- if ((isLight && this.isLight != isLight) ||
- (!isLight && this.isDark == isLight)) {
- if (isLight) {
- this.isLight = true;
- this.isDark = false;
- } else {
- this.isLight = false;
- this.isDark = true;
- }
-
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void resetTheme() {
- if (isLight || isDark) {
- isLight = false;
- isDark = false;
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void setAutoUpdateTheme(boolean autoUpdateTheme) {
- if (theme == null) {
- return;
- }
-
- if (this.autoUpdateTheme != autoUpdateTheme) {
- this.autoUpdateTheme = autoUpdateTheme;
-
- if (autoUpdateTheme)
- theme.addListener(this);
- else
- theme.removeListener(this);
- }
- }
-
- public ColorDrawable getColorDrawable(int id) {
- return new ColorDrawable(ContextCompat.getColor(getContext(), id));
- }
-
- protected LightweightTheme getTheme() {
- return theme;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedRelativeLayout.java b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedRelativeLayout.java
deleted file mode 100644
index 14ef25c62..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedRelativeLayout.java
+++ /dev/null
@@ -1,172 +0,0 @@
-// This file is generated by generate_themed_views.py; do not edit.
-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget.themed;
-
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.GeckoApplication;
-import org.mozilla.gecko.lwt.LightweightTheme;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.DrawableUtil;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.TypedArray;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-
-public class ThemedRelativeLayout extends android.widget.RelativeLayout
- implements LightweightTheme.OnChangeListener {
- private LightweightTheme theme;
-
- private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
- private static final int[] STATE_LIGHT = { R.attr.state_light };
- private static final int[] STATE_DARK = { R.attr.state_dark };
-
- protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
- protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
- protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
-
- private boolean isPrivate;
- private boolean isLight;
- private boolean isDark;
- private boolean autoUpdateTheme; // always false if there's no theme.
-
- private ColorStateList drawableColors;
-
- public ThemedRelativeLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- initialize(context, attrs, 0);
- }
-
- public ThemedRelativeLayout(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- initialize(context, attrs, defStyle);
- }
-
- private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
- // The theme can be null, particularly if we might be instantiating this
- // View in an IDE, with no ambient GeckoApplication.
- final Context applicationContext = context.getApplicationContext();
- if (applicationContext instanceof GeckoApplication) {
- theme = ((GeckoApplication) applicationContext).getLightweightTheme();
- }
-
- final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
- autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
- a.recycle();
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- if (autoUpdateTheme)
- theme.addListener(this);
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- if (autoUpdateTheme)
- theme.removeListener(this);
- }
-
- @Override
- public int[] onCreateDrawableState(int extraSpace) {
- final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
-
- if (isPrivate)
- mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
- else if (isLight)
- mergeDrawableStates(drawableState, STATE_LIGHT);
- else if (isDark)
- mergeDrawableStates(drawableState, STATE_DARK);
-
- return drawableState;
- }
-
- @Override
- public void onLightweightThemeChanged() {
- if (autoUpdateTheme && theme.isEnabled())
- setTheme(theme.isLightTheme());
- }
-
- @Override
- public void onLightweightThemeReset() {
- if (autoUpdateTheme)
- resetTheme();
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- onLightweightThemeChanged();
- }
-
- public boolean isPrivateMode() {
- return isPrivate;
- }
-
- public void setPrivateMode(boolean isPrivate) {
- if (this.isPrivate != isPrivate) {
- this.isPrivate = isPrivate;
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void setTheme(boolean isLight) {
- // Set the theme only if it is different from existing theme.
- if ((isLight && this.isLight != isLight) ||
- (!isLight && this.isDark == isLight)) {
- if (isLight) {
- this.isLight = true;
- this.isDark = false;
- } else {
- this.isLight = false;
- this.isDark = true;
- }
-
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void resetTheme() {
- if (isLight || isDark) {
- isLight = false;
- isDark = false;
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void setAutoUpdateTheme(boolean autoUpdateTheme) {
- if (theme == null) {
- return;
- }
-
- if (this.autoUpdateTheme != autoUpdateTheme) {
- this.autoUpdateTheme = autoUpdateTheme;
-
- if (autoUpdateTheme)
- theme.addListener(this);
- else
- theme.removeListener(this);
- }
- }
-
- public ColorDrawable getColorDrawable(int id) {
- return new ColorDrawable(ContextCompat.getColor(getContext(), id));
- }
-
- protected LightweightTheme getTheme() {
- return theme;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedTextSwitcher.java b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedTextSwitcher.java
deleted file mode 100644
index 294abd9ba..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedTextSwitcher.java
+++ /dev/null
@@ -1,167 +0,0 @@
-// This file is generated by generate_themed_views.py; do not edit.
-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget.themed;
-
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.GeckoApplication;
-import org.mozilla.gecko.lwt.LightweightTheme;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.DrawableUtil;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.TypedArray;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-
-public class ThemedTextSwitcher extends android.widget.TextSwitcher
- implements LightweightTheme.OnChangeListener {
- private LightweightTheme theme;
-
- private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
- private static final int[] STATE_LIGHT = { R.attr.state_light };
- private static final int[] STATE_DARK = { R.attr.state_dark };
-
- protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
- protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
- protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
-
- private boolean isPrivate;
- private boolean isLight;
- private boolean isDark;
- private boolean autoUpdateTheme; // always false if there's no theme.
-
- private ColorStateList drawableColors;
-
- public ThemedTextSwitcher(Context context, AttributeSet attrs) {
- super(context, attrs);
- initialize(context, attrs, 0);
- }
-
- private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
- // The theme can be null, particularly if we might be instantiating this
- // View in an IDE, with no ambient GeckoApplication.
- final Context applicationContext = context.getApplicationContext();
- if (applicationContext instanceof GeckoApplication) {
- theme = ((GeckoApplication) applicationContext).getLightweightTheme();
- }
-
- final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
- autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
- a.recycle();
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- if (autoUpdateTheme)
- theme.addListener(this);
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- if (autoUpdateTheme)
- theme.removeListener(this);
- }
-
- @Override
- public int[] onCreateDrawableState(int extraSpace) {
- final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
-
- if (isPrivate)
- mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
- else if (isLight)
- mergeDrawableStates(drawableState, STATE_LIGHT);
- else if (isDark)
- mergeDrawableStates(drawableState, STATE_DARK);
-
- return drawableState;
- }
-
- @Override
- public void onLightweightThemeChanged() {
- if (autoUpdateTheme && theme.isEnabled())
- setTheme(theme.isLightTheme());
- }
-
- @Override
- public void onLightweightThemeReset() {
- if (autoUpdateTheme)
- resetTheme();
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- onLightweightThemeChanged();
- }
-
- public boolean isPrivateMode() {
- return isPrivate;
- }
-
- public void setPrivateMode(boolean isPrivate) {
- if (this.isPrivate != isPrivate) {
- this.isPrivate = isPrivate;
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void setTheme(boolean isLight) {
- // Set the theme only if it is different from existing theme.
- if ((isLight && this.isLight != isLight) ||
- (!isLight && this.isDark == isLight)) {
- if (isLight) {
- this.isLight = true;
- this.isDark = false;
- } else {
- this.isLight = false;
- this.isDark = true;
- }
-
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void resetTheme() {
- if (isLight || isDark) {
- isLight = false;
- isDark = false;
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void setAutoUpdateTheme(boolean autoUpdateTheme) {
- if (theme == null) {
- return;
- }
-
- if (this.autoUpdateTheme != autoUpdateTheme) {
- this.autoUpdateTheme = autoUpdateTheme;
-
- if (autoUpdateTheme)
- theme.addListener(this);
- else
- theme.removeListener(this);
- }
- }
-
- public ColorDrawable getColorDrawable(int id) {
- return new ColorDrawable(ContextCompat.getColor(getContext(), id));
- }
-
- protected LightweightTheme getTheme() {
- return theme;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedTextView.java b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedTextView.java
deleted file mode 100644
index 51a23a406..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedTextView.java
+++ /dev/null
@@ -1,172 +0,0 @@
-// This file is generated by generate_themed_views.py; do not edit.
-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget.themed;
-
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.GeckoApplication;
-import org.mozilla.gecko.lwt.LightweightTheme;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.DrawableUtil;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.TypedArray;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-
-public class ThemedTextView extends android.widget.TextView
- implements LightweightTheme.OnChangeListener {
- private LightweightTheme theme;
-
- private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
- private static final int[] STATE_LIGHT = { R.attr.state_light };
- private static final int[] STATE_DARK = { R.attr.state_dark };
-
- protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
- protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
- protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
-
- private boolean isPrivate;
- private boolean isLight;
- private boolean isDark;
- private boolean autoUpdateTheme; // always false if there's no theme.
-
- private ColorStateList drawableColors;
-
- public ThemedTextView(Context context, AttributeSet attrs) {
- super(context, attrs);
- initialize(context, attrs, 0);
- }
-
- public ThemedTextView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- initialize(context, attrs, defStyle);
- }
-
- private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
- // The theme can be null, particularly if we might be instantiating this
- // View in an IDE, with no ambient GeckoApplication.
- final Context applicationContext = context.getApplicationContext();
- if (applicationContext instanceof GeckoApplication) {
- theme = ((GeckoApplication) applicationContext).getLightweightTheme();
- }
-
- final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
- autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
- a.recycle();
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- if (autoUpdateTheme)
- theme.addListener(this);
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- if (autoUpdateTheme)
- theme.removeListener(this);
- }
-
- @Override
- public int[] onCreateDrawableState(int extraSpace) {
- final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
-
- if (isPrivate)
- mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
- else if (isLight)
- mergeDrawableStates(drawableState, STATE_LIGHT);
- else if (isDark)
- mergeDrawableStates(drawableState, STATE_DARK);
-
- return drawableState;
- }
-
- @Override
- public void onLightweightThemeChanged() {
- if (autoUpdateTheme && theme.isEnabled())
- setTheme(theme.isLightTheme());
- }
-
- @Override
- public void onLightweightThemeReset() {
- if (autoUpdateTheme)
- resetTheme();
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- onLightweightThemeChanged();
- }
-
- public boolean isPrivateMode() {
- return isPrivate;
- }
-
- public void setPrivateMode(boolean isPrivate) {
- if (this.isPrivate != isPrivate) {
- this.isPrivate = isPrivate;
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void setTheme(boolean isLight) {
- // Set the theme only if it is different from existing theme.
- if ((isLight && this.isLight != isLight) ||
- (!isLight && this.isDark == isLight)) {
- if (isLight) {
- this.isLight = true;
- this.isDark = false;
- } else {
- this.isLight = false;
- this.isDark = true;
- }
-
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void resetTheme() {
- if (isLight || isDark) {
- isLight = false;
- isDark = false;
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void setAutoUpdateTheme(boolean autoUpdateTheme) {
- if (theme == null) {
- return;
- }
-
- if (this.autoUpdateTheme != autoUpdateTheme) {
- this.autoUpdateTheme = autoUpdateTheme;
-
- if (autoUpdateTheme)
- theme.addListener(this);
- else
- theme.removeListener(this);
- }
- }
-
- public ColorDrawable getColorDrawable(int id) {
- return new ColorDrawable(ContextCompat.getColor(getContext(), id));
- }
-
- protected LightweightTheme getTheme() {
- return theme;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedView.java b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedView.java
deleted file mode 100644
index 77ecfd271..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedView.java
+++ /dev/null
@@ -1,172 +0,0 @@
-// This file is generated by generate_themed_views.py; do not edit.
-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget.themed;
-
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.GeckoApplication;
-import org.mozilla.gecko.lwt.LightweightTheme;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.DrawableUtil;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.TypedArray;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-
-public class ThemedView extends android.view.View
- implements LightweightTheme.OnChangeListener {
- private LightweightTheme theme;
-
- private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
- private static final int[] STATE_LIGHT = { R.attr.state_light };
- private static final int[] STATE_DARK = { R.attr.state_dark };
-
- protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
- protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
- protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
-
- private boolean isPrivate;
- private boolean isLight;
- private boolean isDark;
- private boolean autoUpdateTheme; // always false if there's no theme.
-
- private ColorStateList drawableColors;
-
- public ThemedView(Context context, AttributeSet attrs) {
- super(context, attrs);
- initialize(context, attrs, 0);
- }
-
- public ThemedView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- initialize(context, attrs, defStyle);
- }
-
- private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
- // The theme can be null, particularly if we might be instantiating this
- // View in an IDE, with no ambient GeckoApplication.
- final Context applicationContext = context.getApplicationContext();
- if (applicationContext instanceof GeckoApplication) {
- theme = ((GeckoApplication) applicationContext).getLightweightTheme();
- }
-
- final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
- autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
- a.recycle();
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- if (autoUpdateTheme)
- theme.addListener(this);
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- if (autoUpdateTheme)
- theme.removeListener(this);
- }
-
- @Override
- public int[] onCreateDrawableState(int extraSpace) {
- final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
-
- if (isPrivate)
- mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
- else if (isLight)
- mergeDrawableStates(drawableState, STATE_LIGHT);
- else if (isDark)
- mergeDrawableStates(drawableState, STATE_DARK);
-
- return drawableState;
- }
-
- @Override
- public void onLightweightThemeChanged() {
- if (autoUpdateTheme && theme.isEnabled())
- setTheme(theme.isLightTheme());
- }
-
- @Override
- public void onLightweightThemeReset() {
- if (autoUpdateTheme)
- resetTheme();
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- onLightweightThemeChanged();
- }
-
- public boolean isPrivateMode() {
- return isPrivate;
- }
-
- public void setPrivateMode(boolean isPrivate) {
- if (this.isPrivate != isPrivate) {
- this.isPrivate = isPrivate;
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void setTheme(boolean isLight) {
- // Set the theme only if it is different from existing theme.
- if ((isLight && this.isLight != isLight) ||
- (!isLight && this.isDark == isLight)) {
- if (isLight) {
- this.isLight = true;
- this.isDark = false;
- } else {
- this.isLight = false;
- this.isDark = true;
- }
-
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void resetTheme() {
- if (isLight || isDark) {
- isLight = false;
- isDark = false;
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void setAutoUpdateTheme(boolean autoUpdateTheme) {
- if (theme == null) {
- return;
- }
-
- if (this.autoUpdateTheme != autoUpdateTheme) {
- this.autoUpdateTheme = autoUpdateTheme;
-
- if (autoUpdateTheme)
- theme.addListener(this);
- else
- theme.removeListener(this);
- }
- }
-
- public ColorDrawable getColorDrawable(int id) {
- return new ColorDrawable(ContextCompat.getColor(getContext(), id));
- }
-
- protected LightweightTheme getTheme() {
- return theme;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedView.java.frag b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedView.java.frag
deleted file mode 100644
index e731a0ebe..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedView.java.frag
+++ /dev/null
@@ -1,211 +0,0 @@
-//#filter substitution
-// This file is generated by generate_themed_views.py; do not edit.
-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.widget.themed;
-
-import android.support.v4.content.ContextCompat;
-import org.mozilla.gecko.GeckoApplication;
-import org.mozilla.gecko.lwt.LightweightTheme;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.util.DrawableUtil;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.TypedArray;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-
-public class Themed@VIEW_NAME_SUFFIX@ extends @BASE_TYPE@
- implements LightweightTheme.OnChangeListener {
- private LightweightTheme theme;
-
- private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
- private static final int[] STATE_LIGHT = { R.attr.state_light };
- private static final int[] STATE_DARK = { R.attr.state_dark };
-
- protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
- protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
- protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
-
- private boolean isPrivate;
- private boolean isLight;
- private boolean isDark;
- private boolean autoUpdateTheme; // always false if there's no theme.
-
- private ColorStateList drawableColors;
-
- public Themed@VIEW_NAME_SUFFIX@(Context context, AttributeSet attrs) {
- super(context, attrs);
- initialize(context, attrs, 0);
- }
-
-//#ifdef STYLE_CONSTRUCTOR
- public Themed@VIEW_NAME_SUFFIX@(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- initialize(context, attrs, defStyle);
- }
-
-//#endif
- private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
- // The theme can be null, particularly if we might be instantiating this
- // View in an IDE, with no ambient GeckoApplication.
- final Context applicationContext = context.getApplicationContext();
- if (applicationContext instanceof GeckoApplication) {
- theme = ((GeckoApplication) applicationContext).getLightweightTheme();
- }
-
- final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
- autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
- a.recycle();
-//#if TINT_FOREGROUND_DRAWABLE
-
- final TypedArray themedA = context.obtainStyledAttributes(attrs, R.styleable.ThemedView, defStyle, 0);
- drawableColors = themedA.getColorStateList(R.styleable.ThemedView_drawableTintList);
- themedA.recycle();
-
- // Apply the tint initially - the Drawable is
- // initially set by XML via super's constructor.
- setTintedImageDrawable(getDrawable());
-//#endif
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- if (autoUpdateTheme)
- theme.addListener(this);
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- if (autoUpdateTheme)
- theme.removeListener(this);
- }
-
- @Override
- public int[] onCreateDrawableState(int extraSpace) {
- final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
-
- if (isPrivate)
- mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
- else if (isLight)
- mergeDrawableStates(drawableState, STATE_LIGHT);
- else if (isDark)
- mergeDrawableStates(drawableState, STATE_DARK);
-
- return drawableState;
- }
-
- @Override
- public void onLightweightThemeChanged() {
- if (autoUpdateTheme && theme.isEnabled())
- setTheme(theme.isLightTheme());
- }
-
- @Override
- public void onLightweightThemeReset() {
- if (autoUpdateTheme)
- resetTheme();
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- onLightweightThemeChanged();
- }
-
- public boolean isPrivateMode() {
- return isPrivate;
- }
-
- public void setPrivateMode(boolean isPrivate) {
- if (this.isPrivate != isPrivate) {
- this.isPrivate = isPrivate;
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void setTheme(boolean isLight) {
- // Set the theme only if it is different from existing theme.
- if ((isLight && this.isLight != isLight) ||
- (!isLight && this.isDark == isLight)) {
- if (isLight) {
- this.isLight = true;
- this.isDark = false;
- } else {
- this.isLight = false;
- this.isDark = true;
- }
-
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void resetTheme() {
- if (isLight || isDark) {
- isLight = false;
- isDark = false;
- refreshDrawableState();
- invalidate();
- }
- }
-
- public void setAutoUpdateTheme(boolean autoUpdateTheme) {
- if (theme == null) {
- return;
- }
-
- if (this.autoUpdateTheme != autoUpdateTheme) {
- this.autoUpdateTheme = autoUpdateTheme;
-
- if (autoUpdateTheme)
- theme.addListener(this);
- else
- theme.removeListener(this);
- }
- }
-
-//#ifdef TINT_FOREGROUND_DRAWABLE
- @Override
- public void setImageDrawable(final Drawable drawable) {
- setTintedImageDrawable(drawable);
- }
-
- private void setTintedImageDrawable(final Drawable drawable) {
- final Drawable tintedDrawable;
-//#ifdef BOOKMARK_NO_TINT
- if (drawableColors == null || R.id.bookmark == getId()) {
- // NB: The bookmarked state uses a blue star, so this is a hack to keep it untinted.
-//#else
- if (drawableColors == null) {
-//#endif
- // NB: If we tint a drawable with a null ColorStateList, it will override
- // any existing colorFilters and tint... so don't!
- tintedDrawable = drawable;
- } else if (drawable == null) {
- tintedDrawable = null;
- } else {
- tintedDrawable = DrawableUtil.tintDrawableWithStateList(drawable, drawableColors);
- }
- super.setImageDrawable(tintedDrawable);
- }
-
-//#endif
- public ColorDrawable getColorDrawable(int id) {
- return new ColorDrawable(ContextCompat.getColor(getContext(), id));
- }
-
- protected LightweightTheme getTheme() {
- return theme;
- }
-}
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/themed/generate_themed_views.py b/mobile/android/base/java/org/mozilla/gecko/widget/themed/generate_themed_views.py
deleted file mode 100644
index 3b5a00b40..000000000
--- a/mobile/android/base/java/org/mozilla/gecko/widget/themed/generate_themed_views.py
+++ /dev/null
@@ -1,72 +0,0 @@
-#!/bin/python
-
-# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-'''
-Script to generate Themed*.java source files for Fennec.
-
-This script runs the preprocessor on a input template and writes
-updated files into the source directory.
-
-To update the themed views, update the input template
-(ThemedView.java.frag) and run the script using 'mach python <script.py>'. Use version control to
-examine the differences, and don't forget to commit the changes to the
-template and the outputs.
-'''
-
-from __future__ import (
- print_function,
- unicode_literals,
-)
-
-import os
-
-from mozbuild.preprocessor import Preprocessor
-
-__DIR__ = os.path.dirname(os.path.abspath(__file__))
-
-template = os.path.join(__DIR__, 'ThemedView.java.frag')
-dest_format_string = 'Themed%(VIEW_NAME_SUFFIX)s.java'
-
-views = [
- dict(VIEW_NAME_SUFFIX='EditText',
- BASE_TYPE='android.widget.EditText',
- STYLE_CONSTRUCTOR=1),
- dict(VIEW_NAME_SUFFIX='FrameLayout',
- BASE_TYPE='android.widget.FrameLayout',
- STYLE_CONSTRUCTOR=1),
- dict(VIEW_NAME_SUFFIX='ImageButton',
- BASE_TYPE='android.widget.ImageButton',
- STYLE_CONSTRUCTOR=1,
- TINT_FOREGROUND_DRAWABLE=1,
- BOOKMARK_NO_TINT=1),
- dict(VIEW_NAME_SUFFIX='ImageView',
- BASE_TYPE='android.widget.ImageView',
- STYLE_CONSTRUCTOR=1,
- TINT_FOREGROUND_DRAWABLE=1),
- dict(VIEW_NAME_SUFFIX='LinearLayout',
- BASE_TYPE='android.widget.LinearLayout'),
- dict(VIEW_NAME_SUFFIX='RelativeLayout',
- BASE_TYPE='android.widget.RelativeLayout',
- STYLE_CONSTRUCTOR=1),
- dict(VIEW_NAME_SUFFIX='TextSwitcher',
- BASE_TYPE='android.widget.TextSwitcher'),
- dict(VIEW_NAME_SUFFIX='TextView',
- BASE_TYPE='android.widget.TextView',
- STYLE_CONSTRUCTOR=1),
- dict(VIEW_NAME_SUFFIX='View',
- BASE_TYPE='android.view.View',
- STYLE_CONSTRUCTOR=1),
-]
-
-for view in views:
- pp = Preprocessor(defines=view, marker='//#')
-
- dest = os.path.join(__DIR__, dest_format_string % view)
- with open(template, 'rU') as input:
- with open(dest, 'wt') as output:
- pp.processFile(input=input, output=output)
- print('%s' % dest)